@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/dist/index.cjs CHANGED
@@ -21,8 +21,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ApiClient: () => ApiClient,
24
- AuthClient: () => AuthClient,
25
- AuthenticationStrategyRegistry: () => AuthenticationStrategyRegistry,
26
24
  BOOK_CANON: () => BOOK_CANON,
27
25
  BOOK_IDS: () => BOOK_IDS,
28
26
  BibleClient: () => BibleClient,
@@ -33,7 +31,6 @@ __export(index_exports, {
33
31
  SignInWithYouVersionPermission: () => SignInWithYouVersionPermission,
34
32
  SignInWithYouVersionResult: () => SignInWithYouVersionResult,
35
33
  URLBuilder: () => URLBuilder,
36
- WebAuthenticationStrategy: () => WebAuthenticationStrategy,
37
34
  YouVersionAPI: () => YouVersionAPI,
38
35
  YouVersionAPIUsers: () => YouVersionAPIUsers,
39
36
  YouVersionPlatformConfiguration: () => YouVersionPlatformConfiguration,
@@ -444,10 +441,9 @@ var import_zod3 = require("zod");
444
441
  var YouVersionPlatformConfiguration = class {
445
442
  static _appKey = null;
446
443
  static _installationId = null;
447
- static _accessToken = null;
448
444
  static _apiHost = "api.youversion.com";
449
- static _isPreviewMode = false;
450
- static _previewUserInfo = null;
445
+ static _refreshTokenKey = null;
446
+ static _expiryDateKey = null;
451
447
  static getOrSetInstallationId() {
452
448
  if (typeof window === "undefined") {
453
449
  return "";
@@ -460,6 +456,44 @@ var YouVersionPlatformConfiguration = class {
460
456
  localStorage.setItem("x-yvp-installation-id", newId);
461
457
  return newId;
462
458
  }
459
+ static saveAuthData(accessToken, refreshToken, idToken, expiryDate) {
460
+ if (accessToken !== null) {
461
+ localStorage.setItem("accessToken", accessToken);
462
+ } else {
463
+ localStorage.removeItem("accessToken");
464
+ }
465
+ if (refreshToken !== null) {
466
+ localStorage.setItem("refreshToken", refreshToken);
467
+ } else {
468
+ localStorage.removeItem("refreshToken");
469
+ }
470
+ if (idToken !== null) {
471
+ localStorage.setItem("idToken", idToken);
472
+ } else {
473
+ localStorage.removeItem("idToken");
474
+ }
475
+ if (expiryDate !== null) {
476
+ localStorage.setItem("expiryDate", expiryDate.toISOString());
477
+ } else {
478
+ localStorage.removeItem("expiryDate");
479
+ }
480
+ }
481
+ static clearAuthTokens() {
482
+ this.saveAuthData(null, null, null, null);
483
+ }
484
+ static get accessToken() {
485
+ return localStorage.getItem("accessToken");
486
+ }
487
+ static get refreshToken() {
488
+ return localStorage.getItem("refreshToken");
489
+ }
490
+ static get idToken() {
491
+ return localStorage.getItem("idToken");
492
+ }
493
+ static get tokenExpiryDate() {
494
+ const dateString = localStorage.getItem("expiryDate");
495
+ return dateString ? new Date(dateString) : null;
496
+ }
463
497
  static get appKey() {
464
498
  return this._appKey;
465
499
  }
@@ -475,32 +509,23 @@ var YouVersionPlatformConfiguration = class {
475
509
  static set installationId(value) {
476
510
  this._installationId = value || this.getOrSetInstallationId();
477
511
  }
478
- static setAccessToken(token) {
479
- if (token !== null && (typeof token !== "string" || token.trim().length === 0)) {
480
- throw new Error("Access token must be a non-empty string or null");
481
- }
482
- this._accessToken = token;
483
- }
484
- static get accessToken() {
485
- return this._accessToken;
486
- }
487
512
  static get apiHost() {
488
513
  return this._apiHost;
489
514
  }
490
515
  static set apiHost(value) {
491
516
  this._apiHost = value;
492
517
  }
493
- static get isPreviewMode() {
494
- return this._isPreviewMode;
518
+ static get refreshTokenKey() {
519
+ return this._refreshTokenKey;
495
520
  }
496
- static set isPreviewMode(value) {
497
- this._isPreviewMode = value;
521
+ static set refreshTokenKey(value) {
522
+ this._refreshTokenKey = value;
498
523
  }
499
- static get previewUserInfo() {
500
- return this._previewUserInfo;
524
+ static get expiryDateKey() {
525
+ return this._expiryDateKey;
501
526
  }
502
- static set previewUserInfo(value) {
503
- this._previewUserInfo = value;
527
+ static set expiryDateKey(value) {
528
+ this._expiryDateKey = value;
504
529
  }
505
530
  };
506
531
 
@@ -625,78 +650,6 @@ var HighlightsClient = class {
625
650
  }
626
651
  };
627
652
 
628
- // src/authentication.ts
629
- var AuthClient = class {
630
- client;
631
- /**
632
- * Creates an instance of AuthClient.
633
- * @param client - The ApiClient instance to use for requests.
634
- */
635
- constructor(client) {
636
- this.client = client;
637
- }
638
- /**
639
- * Retrieves the current authenticated user.
640
- *
641
- * @param lat - The long access token (LAT) used for authentication.
642
- * @returns A promise that resolves to the authenticated User.
643
- */
644
- async getUser(lat) {
645
- return this.client.get(`/auth/me`, { lat });
646
- }
647
- };
648
-
649
- // src/AuthenticationStrategy.ts
650
- var AuthenticationStrategyRegistry = class {
651
- static strategy = null;
652
- /**
653
- * Registers a platform-specific authentication strategy
654
- *
655
- * @param strategy - The authentication strategy to register
656
- * @throws Error if strategy is null, undefined, or missing required methods
657
- */
658
- static register(strategy) {
659
- if (!strategy) {
660
- throw new Error("Authentication strategy cannot be null or undefined");
661
- }
662
- if (typeof strategy.authenticate !== "function") {
663
- throw new Error("Authentication strategy must implement authenticate method");
664
- }
665
- this.strategy = strategy;
666
- }
667
- /**
668
- * Gets the currently registered authentication strategy
669
- *
670
- * @returns The registered authentication strategy
671
- * @throws Error if no strategy has been registered
672
- */
673
- static get() {
674
- if (!this.strategy) {
675
- throw new Error(
676
- "No authentication strategy registered. Please register a platform-specific strategy using AuthenticationStrategyRegistry.register()"
677
- );
678
- }
679
- return this.strategy;
680
- }
681
- /**
682
- * Checks if a strategy is currently registered
683
- *
684
- * @returns true if a strategy is registered, false otherwise
685
- */
686
- static isRegistered() {
687
- return this.strategy !== null;
688
- }
689
- /**
690
- * Resets the registry by removing the current strategy
691
- *
692
- * This method is primarily intended for testing scenarios
693
- * where you need to clean up between test cases.
694
- */
695
- static reset() {
696
- this.strategy = null;
697
- }
698
- };
699
-
700
653
  // src/StorageStrategy.ts
701
654
  var SessionStorageStrategy = class {
702
655
  setItem(key, value) {
@@ -741,96 +694,122 @@ var MemoryStorageStrategy = class {
741
694
  }
742
695
  };
743
696
 
744
- // src/WebAuthenticationStrategy.ts
745
- var WebAuthenticationStrategy = class _WebAuthenticationStrategy {
746
- redirectUri;
747
- callbackPath;
748
- timeout;
749
- storage;
750
- static pendingAuthResolve = null;
751
- static pendingAuthReject = null;
752
- static timeoutId = null;
753
- constructor(options) {
754
- this.callbackPath = options?.callbackPath ?? "/auth/callback";
755
- this.redirectUri = options?.redirectUri ?? window.location.origin + this.callbackPath;
756
- this.timeout = options?.timeout ?? 3e5;
757
- this.storage = options?.storage ?? new SessionStorageStrategy();
758
- }
759
- async authenticate(authUrl) {
760
- authUrl.searchParams.set("redirect_uri", this.redirectUri);
761
- return this.authenticateWithRedirect(authUrl);
762
- }
763
- /**
764
- * Call this method when your app loads to handle the redirect callback
765
- */
766
- static handleCallback(callbackPath = "/auth/callback") {
767
- const currentUrl = new URL(window.location.href);
768
- if (currentUrl.pathname === callbackPath && currentUrl.searchParams.has("status")) {
769
- const callbackUrl = new URL(currentUrl.toString());
770
- if (_WebAuthenticationStrategy.pendingAuthResolve) {
771
- _WebAuthenticationStrategy.pendingAuthResolve(callbackUrl);
772
- _WebAuthenticationStrategy.cleanup();
773
- } else {
774
- const storageStrategy2 = new SessionStorageStrategy();
775
- storageStrategy2.setItem("youversion-auth-callback", callbackUrl.toString());
776
- }
777
- const storageStrategy = new SessionStorageStrategy();
778
- const returnUrl = storageStrategy.getItem("youversion-auth-return") ?? "/";
779
- storageStrategy.removeItem("youversion-auth-return");
780
- window.history.replaceState({}, "", returnUrl);
781
- return true;
697
+ // src/YouVersionUserInfo.ts
698
+ var YouVersionUserInfo = class {
699
+ name;
700
+ userId;
701
+ email;
702
+ avatarUrlFormat;
703
+ constructor(data) {
704
+ if (!data || typeof data !== "object") {
705
+ throw new Error("Invalid user data provided");
782
706
  }
783
- return false;
707
+ this.name = data.name;
708
+ this.userId = data.id;
709
+ this.email = data.email;
710
+ this.avatarUrlFormat = data.avatar_url;
784
711
  }
785
- /**
786
- * Clean up pending authentication state
787
- */
788
- static cleanup() {
789
- if (_WebAuthenticationStrategy.timeoutId) {
790
- clearTimeout(_WebAuthenticationStrategy.timeoutId);
791
- _WebAuthenticationStrategy.timeoutId = null;
712
+ getAvatarUrl(width = 200, height = 200) {
713
+ if (!this.avatarUrlFormat) {
714
+ return null;
715
+ }
716
+ let urlString = this.avatarUrlFormat;
717
+ if (urlString.startsWith("//")) {
718
+ urlString = "https:" + urlString;
719
+ }
720
+ urlString = urlString.replace("{width}", width.toString());
721
+ urlString = urlString.replace("{height}", height.toString());
722
+ try {
723
+ return new URL(urlString);
724
+ } catch {
725
+ return null;
792
726
  }
793
- _WebAuthenticationStrategy.pendingAuthResolve = null;
794
- _WebAuthenticationStrategy.pendingAuthReject = null;
795
727
  }
796
- /**
797
- * Retrieve stored callback result if available
798
- */
799
- static getStoredCallback() {
800
- const storageStrategy = new SessionStorageStrategy();
801
- const stored = storageStrategy.getItem("youversion-auth-callback");
802
- if (stored) {
803
- storageStrategy.removeItem("youversion-auth-callback");
804
- try {
805
- return new URL(stored);
806
- } catch {
807
- return null;
808
- }
728
+ get avatarUrl() {
729
+ return this.getAvatarUrl();
730
+ }
731
+ };
732
+
733
+ // src/SignInWithYouVersionPKCE.ts
734
+ var SignInWithYouVersionPKCEAuthorizationRequestBuilder = class {
735
+ static async make(appKey, permissions, redirectURL) {
736
+ const codeVerifier = this.randomURLSafeString(32);
737
+ const codeChallenge = await this.codeChallenge(codeVerifier);
738
+ const state = this.randomURLSafeString(24);
739
+ const nonce = this.randomURLSafeString(24);
740
+ const parameters = {
741
+ codeVerifier,
742
+ codeChallenge,
743
+ state,
744
+ nonce
745
+ };
746
+ const url = this.authorizeURL(appKey, permissions, redirectURL, parameters);
747
+ return { url, parameters };
748
+ }
749
+ static authorizeURL(appKey, permissions, redirectURL, parameters) {
750
+ const components = new URL(`https://${YouVersionPlatformConfiguration.apiHost}/auth/authorize`);
751
+ const redirectUrlString = redirectURL.toString().endsWith("/") ? redirectURL.toString().slice(0, -1) : redirectURL.toString();
752
+ const queryParams = new URLSearchParams({
753
+ response_type: "code",
754
+ client_id: appKey,
755
+ redirect_uri: redirectUrlString,
756
+ nonce: parameters.nonce,
757
+ state: parameters.state,
758
+ code_challenge: parameters.codeChallenge,
759
+ code_challenge_method: "S256"
760
+ });
761
+ const installId = YouVersionPlatformConfiguration.installationId;
762
+ if (installId) {
763
+ queryParams.set("x-yvp-installation-id", installId);
809
764
  }
810
- return null;
811
- }
812
- authenticateWithRedirect(authUrl) {
813
- _WebAuthenticationStrategy.cleanup();
814
- this.storage.setItem("youversion-auth-return", window.location.href);
815
- return new Promise((resolve, reject) => {
816
- _WebAuthenticationStrategy.pendingAuthResolve = resolve;
817
- _WebAuthenticationStrategy.pendingAuthReject = reject;
818
- _WebAuthenticationStrategy.timeoutId = setTimeout(() => {
819
- _WebAuthenticationStrategy.cleanup();
820
- reject(new Error("Authentication timeout"));
821
- }, this.timeout);
822
- try {
823
- window.location.href = authUrl.toString();
824
- } catch (error) {
825
- _WebAuthenticationStrategy.cleanup();
826
- reject(
827
- new Error(
828
- `Failed to navigate to auth URL: ${error instanceof Error ? error.message : "Unknown error"}`
829
- )
830
- );
765
+ const scopeValue = this.scopeValue(permissions);
766
+ if (scopeValue) {
767
+ queryParams.set("scope", scopeValue);
768
+ }
769
+ components.search = queryParams.toString();
770
+ return components;
771
+ }
772
+ static tokenURLRequest(code, codeVerifier, redirectUri) {
773
+ const apiHost = YouVersionPlatformConfiguration.apiHost;
774
+ const appKey = YouVersionPlatformConfiguration.appKey;
775
+ const url = new URL(`https://${apiHost}/auth/token`);
776
+ const parameters = new URLSearchParams({
777
+ grant_type: "authorization_code",
778
+ code,
779
+ redirect_uri: redirectUri,
780
+ client_id: appKey ?? "",
781
+ code_verifier: codeVerifier
782
+ });
783
+ return new Request(url, {
784
+ method: "POST",
785
+ body: parameters,
786
+ headers: {
787
+ "Content-Type": "application/x-www-form-urlencoded"
831
788
  }
832
789
  });
833
790
  }
791
+ static randomURLSafeString(byteCount) {
792
+ const bytes = new Uint8Array(byteCount);
793
+ crypto.getRandomValues(bytes);
794
+ return this.base64URLEncodedString(bytes);
795
+ }
796
+ static async codeChallenge(verifier) {
797
+ const data = new TextEncoder().encode(verifier);
798
+ const digest = await crypto.subtle.digest("SHA-256", data);
799
+ return this.base64URLEncodedString(new Uint8Array(digest));
800
+ }
801
+ static base64URLEncodedString(data) {
802
+ const base64 = btoa(String.fromCharCode.apply(null, Array.from(data)));
803
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
804
+ }
805
+ static scopeValue(permissions) {
806
+ const scopeArray = Array.from(permissions).sort();
807
+ let scopeWithOpenID = scopeArray.join(" ");
808
+ if (!scopeWithOpenID.split(" ").includes("openid")) {
809
+ scopeWithOpenID += (scopeWithOpenID === "" ? "" : " ") + "openid";
810
+ }
811
+ return scopeWithOpenID || null;
812
+ }
834
813
  };
835
814
 
836
815
  // src/SignInWithYouVersionResult.ts
@@ -843,72 +822,336 @@ var SignInWithYouVersionPermission = {
843
822
  };
844
823
  var SignInWithYouVersionResult = class {
845
824
  accessToken;
825
+ expiryDate;
826
+ refreshToken;
827
+ idToken;
846
828
  permissions;
847
- errorMsg;
848
829
  yvpUserId;
849
- constructor(url) {
850
- const queryParams = new URLSearchParams(url.search);
851
- const status = queryParams.get("status");
852
- const userId = queryParams.get("yvp_user_id");
853
- const latValue = queryParams.get("lat");
854
- const grants = queryParams.get("grants");
855
- const perms = grants?.split(",").map((grant) => grant.trim()).filter(
856
- (grant) => Object.values(SignInWithYouVersionPermission).includes(
857
- grant
858
- )
859
- ).map((grant) => grant) ?? [];
860
- if (status === "success" && latValue && userId) {
861
- this.accessToken = latValue;
862
- this.permissions = perms;
863
- this.errorMsg = null;
864
- this.yvpUserId = userId;
865
- } else if (status === "canceled") {
866
- this.accessToken = null;
867
- this.permissions = [];
868
- this.errorMsg = null;
869
- this.yvpUserId = null;
870
- } else {
871
- this.accessToken = null;
872
- this.permissions = [];
873
- this.errorMsg = "Authentication failed";
874
- this.yvpUserId = null;
875
- }
830
+ name;
831
+ profilePicture;
832
+ email;
833
+ constructor({
834
+ accessToken,
835
+ expiresIn,
836
+ refreshToken,
837
+ idToken,
838
+ permissions,
839
+ yvpUserId,
840
+ name,
841
+ profilePicture,
842
+ email
843
+ }) {
844
+ this.accessToken = accessToken;
845
+ this.expiryDate = expiresIn ? new Date(Date.now() + expiresIn * 1e3) : /* @__PURE__ */ new Date();
846
+ this.refreshToken = refreshToken;
847
+ this.idToken = idToken;
848
+ this.permissions = permissions;
849
+ this.yvpUserId = yvpUserId;
850
+ this.name = name;
851
+ this.profilePicture = profilePicture;
852
+ this.email = email;
876
853
  }
877
854
  };
878
855
 
879
- // src/YouVersionUserInfo.ts
880
- var YouVersionUserInfo = class {
881
- firstName;
882
- lastName;
883
- userId;
884
- avatarUrlFormat;
885
- constructor(data) {
886
- if (!data || typeof data !== "object") {
887
- throw new Error("Invalid user data provided");
856
+ // src/Users.ts
857
+ var YouVersionAPIUsers = class {
858
+ /**
859
+ * Presents the YouVersion login flow to the user and returns the login result upon completion.
860
+ *
861
+ * This function authenticates the user with YouVersion, requesting the specified required and optional permissions.
862
+ * The function redirects to the YouVersion authorization URL and expects the callback to be handled separately.
863
+ *
864
+ * @param permissions - The set of permissions that must be granted by the user for successful login.
865
+ * @param redirectURL - The URL to redirect back to after authentication.
866
+ * @throws An error if authentication fails or configuration is invalid.
867
+ */
868
+ static async signIn(permissions, redirectURL) {
869
+ const appKey = YouVersionPlatformConfiguration.appKey;
870
+ if (!appKey) {
871
+ throw new Error("YouVersionPlatformConfiguration.appKey must be set before calling signIn");
888
872
  }
889
- this.firstName = data.first_name;
890
- this.lastName = data.last_name;
891
- this.userId = data.id;
892
- this.avatarUrlFormat = data.avatar_url;
873
+ const authorizationRequest = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
874
+ appKey,
875
+ permissions,
876
+ new URL(redirectURL)
877
+ );
878
+ localStorage.setItem(
879
+ "youversion-auth-code-verifier",
880
+ authorizationRequest.parameters.codeVerifier
881
+ );
882
+ const redirectUrlString = redirectURL.toString().endsWith("/") ? redirectURL.toString().slice(0, -1) : redirectURL.toString();
883
+ localStorage.setItem("youversion-auth-redirect-uri", redirectUrlString);
884
+ localStorage.setItem("youversion-auth-state", authorizationRequest.parameters.state);
885
+ window.location.href = authorizationRequest.url.toString();
893
886
  }
894
- getAvatarUrl(width = 200, height = 200) {
895
- if (!this.avatarUrlFormat) {
887
+ /**
888
+ * Handles the OAuth callback after user authentication.
889
+ *
890
+ * Call this method when your app loads to check if the current URL contains
891
+ * an OAuth callback with authorization code. If found, it exchanges the code
892
+ * for tokens and stores them.
893
+ *
894
+ * @returns Promise<SignInWithYouVersionResult | null> - SignInWithYouVersionResult if callback was handled, null otherwise
895
+ * @throws An error if token exchange fails
896
+ */
897
+ static async handleAuthCallback() {
898
+ const urlParams = new URLSearchParams(window.location.search);
899
+ const code = urlParams.get("code");
900
+ const state = urlParams.get("state");
901
+ const error = urlParams.get("error");
902
+ if (!state && !error) {
896
903
  return null;
897
904
  }
898
- let urlString = this.avatarUrlFormat;
899
- if (urlString.startsWith("//")) {
900
- urlString = "https:" + urlString;
905
+ if (error) {
906
+ const errorDescription = urlParams.get("error_description") || error;
907
+ throw new Error(`OAuth authentication failed: ${errorDescription}`);
908
+ }
909
+ const storedState = localStorage.getItem("youversion-auth-state");
910
+ if (state !== storedState) {
911
+ throw new Error("Invalid state parameter - possible CSRF attack");
912
+ }
913
+ if (!code && state) {
914
+ this.obtainLocation(window.location.href, state);
915
+ }
916
+ const codeVerifier = localStorage.getItem("youversion-auth-code-verifier");
917
+ const redirectUri = localStorage.getItem("youversion-auth-redirect-uri");
918
+ if (!code || !codeVerifier || !redirectUri) {
919
+ throw new Error("Missing required authentication parameters");
901
920
  }
902
- urlString = urlString.replace("{width}", width.toString());
903
- urlString = urlString.replace("{height}", height.toString());
904
921
  try {
905
- return new URL(urlString);
906
- } catch {
907
- return null;
922
+ const tokenRequest = SignInWithYouVersionPKCEAuthorizationRequestBuilder.tokenURLRequest(
923
+ code,
924
+ codeVerifier,
925
+ redirectUri
926
+ );
927
+ const response = await fetch(tokenRequest);
928
+ if (!response.ok) {
929
+ throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
930
+ }
931
+ const responseText = await response.text();
932
+ const tokens = JSON.parse(responseText);
933
+ const result = this.extractSignInResult(tokens);
934
+ YouVersionPlatformConfiguration.saveAuthData(
935
+ result.accessToken || null,
936
+ result.refreshToken || null,
937
+ result.idToken || null,
938
+ result.expiryDate || null
939
+ );
940
+ localStorage.removeItem("youversion-auth-code-verifier");
941
+ localStorage.removeItem("youversion-auth-redirect-uri");
942
+ localStorage.removeItem("youversion-auth-state");
943
+ const cleanUrl = new URL(window.location.href);
944
+ cleanUrl.search = "";
945
+ window.history.replaceState({}, "", cleanUrl.toString());
946
+ return result;
947
+ } catch (error2) {
948
+ localStorage.removeItem("youversion-auth-code-verifier");
949
+ localStorage.removeItem("youversion-auth-redirect-uri");
950
+ localStorage.removeItem("youversion-auth-state");
951
+ throw error2;
908
952
  }
909
953
  }
910
- get avatarUrl() {
911
- return this.getAvatarUrl();
954
+ /**
955
+ * Redirects to the server callback endpoint to obtain authorization code
956
+ */
957
+ static obtainLocation(callbackURL, state) {
958
+ const url = new URL(callbackURL);
959
+ const params = new URLSearchParams(url.search);
960
+ if (params.get("state") !== state) {
961
+ throw new Error("Invalid state parameter");
962
+ }
963
+ const serverCallbackUrl = new URL(
964
+ `https://${YouVersionPlatformConfiguration.apiHost}/auth/callback`
965
+ );
966
+ params.forEach((value, key) => {
967
+ serverCallbackUrl.searchParams.set(key, value);
968
+ });
969
+ window.location.href = serverCallbackUrl.toString();
970
+ }
971
+ /**
972
+ * Extracts sign-in result from token response
973
+ */
974
+ static extractSignInResult(tokens) {
975
+ const idClaims = this.decodeJWT(tokens.id_token);
976
+ const permissions = tokens.scope.split(" ").map((p) => p.trim()).filter((p) => p.length > 0).filter(
977
+ (p) => Object.values(SignInWithYouVersionPermission).includes(
978
+ p
979
+ )
980
+ );
981
+ const resultData = {
982
+ accessToken: tokens.access_token,
983
+ expiresIn: tokens.expires_in,
984
+ refreshToken: tokens.refresh_token,
985
+ idToken: tokens.id_token,
986
+ permissions,
987
+ yvpUserId: idClaims.sub,
988
+ name: idClaims.name,
989
+ profilePicture: idClaims.profile_picture,
990
+ email: idClaims.email
991
+ };
992
+ return new SignInWithYouVersionResult(resultData);
993
+ }
994
+ /**
995
+ * Decodes JWT payload for UI display purposes.
996
+ *
997
+ * Note: We intentionally do not verify the JWT signature here because:
998
+ *
999
+ * 1. YouVersion's backend verifies all tokens on API requests
1000
+ * 2. This decoded data is only used for UI display
1001
+ * 3. No security decisions are made based on these claims
1002
+ *
1003
+ * @private
1004
+ */
1005
+ static decodeJWT(token) {
1006
+ const segments = token.split(".");
1007
+ if (segments.length !== 3) {
1008
+ return {};
1009
+ }
1010
+ let base64 = segments[1]?.replace(/-/g, "+").replace(/_/g, "/");
1011
+ while (base64 && base64.length % 4 !== 0) {
1012
+ base64 += "=";
1013
+ }
1014
+ try {
1015
+ if (base64) {
1016
+ const data = atob(base64);
1017
+ return JSON.parse(data);
1018
+ } else {
1019
+ return {};
1020
+ }
1021
+ } catch (error) {
1022
+ if (process.env.NODE_ENV === "development") {
1023
+ console.error("JWT decode failed:", error);
1024
+ }
1025
+ return {};
1026
+ }
1027
+ }
1028
+ static signOut() {
1029
+ YouVersionPlatformConfiguration.clearAuthTokens();
1030
+ }
1031
+ /**
1032
+ * Retrieves user information for the authenticated user by decoding the provided JWT access token.
1033
+ *
1034
+ * This function extracts the user's profile information directly from the JWT token payload.
1035
+ *
1036
+ * @param accessToken - The JWT access token obtained from the login process.
1037
+ * @returns A Promise resolving to a YouVersionUserInfo object containing the user's profile information.
1038
+ * @throws An error if the access token is invalid or cannot be decoded.
1039
+ */
1040
+ static userInfo(idToken) {
1041
+ if (!idToken || typeof idToken !== "string") {
1042
+ throw new Error("Invalid access token: must be a non-empty string");
1043
+ }
1044
+ try {
1045
+ const claims = this.decodeJWT(idToken);
1046
+ if (!claims || Object.keys(claims).length === 0) {
1047
+ throw new Error("Invalid JWT token: Unable to decode token payload");
1048
+ }
1049
+ const userInfoData = {
1050
+ id: claims.sub,
1051
+ name: claims.name,
1052
+ avatar_url: claims.profile_picture,
1053
+ email: claims.email
1054
+ };
1055
+ return new YouVersionUserInfo(userInfoData);
1056
+ } catch (error) {
1057
+ if (error instanceof Error) {
1058
+ throw new Error(`Failed to decode user information from JWT: ${error.message}`);
1059
+ } else {
1060
+ throw new Error("Failed to decode user information from JWT: Unknown error");
1061
+ }
1062
+ }
1063
+ }
1064
+ /**
1065
+ * Refreshes the access token using the stored refresh token.
1066
+ *
1067
+ * @returns Promise<SignInWithYouVersionResult | null> - New tokens if refresh succeeds, null otherwise
1068
+ * @throws An error if refresh fails or no refresh token is available
1069
+ */
1070
+ static async refreshTokens() {
1071
+ const refreshToken = YouVersionPlatformConfiguration.refreshToken;
1072
+ const appKey = YouVersionPlatformConfiguration.appKey;
1073
+ const existingIdToken = YouVersionPlatformConfiguration.idToken;
1074
+ if (!refreshToken || !existingIdToken) {
1075
+ throw new Error("No refresh token or id token available");
1076
+ }
1077
+ if (!appKey) {
1078
+ throw new Error(
1079
+ "YouVersionPlatformConfiguration.appKey must be set before refreshing tokens"
1080
+ );
1081
+ }
1082
+ try {
1083
+ const url = new URL(`https://${YouVersionPlatformConfiguration.apiHost}/auth/token`);
1084
+ const parameters = new URLSearchParams({
1085
+ grant_type: "refresh_token",
1086
+ refresh_token: refreshToken,
1087
+ client_id: appKey
1088
+ });
1089
+ const request = new Request(url, {
1090
+ method: "POST",
1091
+ body: parameters,
1092
+ headers: {
1093
+ "Content-Type": "application/x-www-form-urlencoded"
1094
+ }
1095
+ });
1096
+ const response = await fetch(request);
1097
+ if (!response.ok) {
1098
+ throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`);
1099
+ }
1100
+ const tokens = await response.json();
1101
+ const result = new SignInWithYouVersionResult({
1102
+ accessToken: tokens.access_token,
1103
+ expiresIn: tokens.expires_in,
1104
+ refreshToken: tokens.refresh_token,
1105
+ idToken: existingIdToken,
1106
+ permissions: tokens.scope.split(" ").map((p) => p.trim()).filter((p) => p.length > 0).filter(
1107
+ (p) => Object.values(SignInWithYouVersionPermission).includes(
1108
+ p
1109
+ )
1110
+ )
1111
+ });
1112
+ YouVersionPlatformConfiguration.saveAuthData(
1113
+ result.accessToken || null,
1114
+ result.refreshToken || null,
1115
+ result.idToken || null,
1116
+ result.expiryDate || null
1117
+ );
1118
+ return result;
1119
+ } catch (error) {
1120
+ if (error instanceof Error) {
1121
+ throw new Error(`Token refresh failed: ${error.message}`);
1122
+ } else {
1123
+ throw new Error("Token refresh failed: Unknown error");
1124
+ }
1125
+ }
1126
+ }
1127
+ /**
1128
+ * Checks if the current access token is expired or about to expire.
1129
+ *
1130
+ * @returns true if token is expired or about to expire
1131
+ */
1132
+ static isTokenExpired() {
1133
+ const expiryDate = YouVersionPlatformConfiguration.tokenExpiryDate;
1134
+ if (!expiryDate) {
1135
+ return true;
1136
+ }
1137
+ return (/* @__PURE__ */ new Date()).getTime() >= expiryDate.getTime();
1138
+ }
1139
+ /**
1140
+ * Refreshes the access token if it's expired or about to expire.
1141
+ *
1142
+ * @returns Promise<boolean> - true if refresh was successful or not needed, false if failed
1143
+ */
1144
+ static async refreshTokenIfNeeded() {
1145
+ if (!this.isTokenExpired()) {
1146
+ return true;
1147
+ }
1148
+ try {
1149
+ const result = await this.refreshTokens();
1150
+ return !!result;
1151
+ } catch {
1152
+ YouVersionPlatformConfiguration.clearAuthTokens();
1153
+ return false;
1154
+ }
912
1155
  }
913
1156
  };
914
1157
 
@@ -969,119 +1212,6 @@ var URLBuilder = class {
969
1212
  );
970
1213
  }
971
1214
  }
972
- static userURL(accessToken) {
973
- if (typeof accessToken !== "string" || accessToken.trim().length === 0) {
974
- throw new Error("accessToken must be a non-empty string");
975
- }
976
- try {
977
- const url = new URL(this.baseURL);
978
- url.pathname = "/auth/me";
979
- const searchParams = new URLSearchParams();
980
- searchParams.append("lat", accessToken);
981
- url.search = searchParams.toString();
982
- return url;
983
- } catch (error) {
984
- throw new Error(
985
- `Failed to construct user URL: ${error instanceof Error ? error.message : "Unknown error"}`
986
- );
987
- }
988
- }
989
- };
990
-
991
- // src/Users.ts
992
- var MAX_RETRY_ATTEMPTS = 3;
993
- var RETRY_DELAY_MS = 1e3;
994
- var YouVersionAPIUsers = class {
995
- /**
996
- * Presents the YouVersion login flow to the user and returns the login result upon completion.
997
- *
998
- * This function authenticates the user with YouVersion, requesting the specified required and optional permissions.
999
- * The function returns a promise that resolves when the user completes or cancels the login flow,
1000
- * returning the login result containing the authorization code and granted permissions.
1001
- *
1002
- * @param requiredPermissions - The set of permissions that must be granted by the user for successful login.
1003
- * @param optionalPermissions - The set of permissions that will be requested from the user but are not required for successful login.
1004
- * @returns A Promise resolving to a SignInWithYouVersionResult containing the authorization code and granted permissions upon successful login.
1005
- * @throws An error if authentication fails or is cancelled by the user.
1006
- */
1007
- static async signIn(requiredPermissions, optionalPermissions) {
1008
- if (!requiredPermissions || !(requiredPermissions instanceof Set)) {
1009
- throw new Error("Invalid requiredPermissions: must be a Set");
1010
- }
1011
- if (!optionalPermissions || !(optionalPermissions instanceof Set)) {
1012
- throw new Error("Invalid optionalPermissions: must be a Set");
1013
- }
1014
- const appKey = YouVersionPlatformConfiguration.appKey;
1015
- if (!appKey) {
1016
- throw new Error("YouVersionPlatformConfiguration.appKey must be set before calling signIn");
1017
- }
1018
- const url = URLBuilder.authURL(appKey, requiredPermissions, optionalPermissions);
1019
- const strategy = AuthenticationStrategyRegistry.get();
1020
- const callbackUrl = await strategy.authenticate(url);
1021
- const result = new SignInWithYouVersionResult(callbackUrl);
1022
- if (result.accessToken) {
1023
- YouVersionPlatformConfiguration.setAccessToken(result.accessToken);
1024
- }
1025
- return result;
1026
- }
1027
- static signOut() {
1028
- YouVersionPlatformConfiguration.setAccessToken(null);
1029
- }
1030
- /**
1031
- * Retrieves user information for the authenticated user using the provided access token.
1032
- *
1033
- * This function fetches the user's profile information from the YouVersion API, decoding it into a YouVersionUserInfo model.
1034
- *
1035
- * @param accessToken - The access token obtained from the login process.
1036
- * @returns A Promise resolving to a YouVersionUserInfo object containing the user's profile information.
1037
- * @throws An error if the URL is invalid, the network request fails, or the response cannot be decoded.
1038
- */
1039
- static async userInfo(accessToken) {
1040
- if (!accessToken || typeof accessToken !== "string") {
1041
- throw new Error("Invalid access token: must be a non-empty string");
1042
- }
1043
- if (YouVersionPlatformConfiguration.isPreviewMode && accessToken === "preview") {
1044
- return YouVersionPlatformConfiguration.previewUserInfo || new YouVersionUserInfo({
1045
- first_name: "Preview",
1046
- last_name: "User",
1047
- id: "preview-user",
1048
- avatar_url: void 0
1049
- });
1050
- }
1051
- const url = URLBuilder.userURL(accessToken);
1052
- const request = YouVersionAPI.addStandardHeaders(url);
1053
- let lastError = null;
1054
- for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
1055
- try {
1056
- const response = await fetch(request);
1057
- if (response.status === 401) {
1058
- throw new Error(
1059
- "Authentication failed: Invalid or expired access token. Please sign in again."
1060
- );
1061
- }
1062
- if (response.status === 403) {
1063
- throw new Error("Access denied: Insufficient permissions to retrieve user information");
1064
- }
1065
- if (response.status !== 200) {
1066
- throw new Error(
1067
- `Failed to retrieve user information: Server responded with status ${response.status}`
1068
- );
1069
- }
1070
- const data = await response.json();
1071
- return data;
1072
- } catch (error) {
1073
- lastError = error instanceof Error ? error : new Error("Failed to parse server response");
1074
- if (error instanceof Error && (error.message.includes("401") || error.message.includes("403"))) {
1075
- throw error;
1076
- }
1077
- if (attempt < MAX_RETRY_ATTEMPTS) {
1078
- await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS * attempt));
1079
- continue;
1080
- }
1081
- }
1082
- }
1083
- throw lastError || new Error("Failed to retrieve user information after multiple attempts");
1084
- }
1085
1215
  };
1086
1216
 
1087
1217
  // src/utils/constants.ts
@@ -1301,8 +1431,6 @@ var BOOK_CANON = {
1301
1431
  // Annotate the CommonJS export names for ESM import in node:
1302
1432
  0 && (module.exports = {
1303
1433
  ApiClient,
1304
- AuthClient,
1305
- AuthenticationStrategyRegistry,
1306
1434
  BOOK_CANON,
1307
1435
  BOOK_IDS,
1308
1436
  BibleClient,
@@ -1313,7 +1441,6 @@ var BOOK_CANON = {
1313
1441
  SignInWithYouVersionPermission,
1314
1442
  SignInWithYouVersionResult,
1315
1443
  URLBuilder,
1316
- WebAuthenticationStrategy,
1317
1444
  YouVersionAPI,
1318
1445
  YouVersionAPIUsers,
1319
1446
  YouVersionPlatformConfiguration,