@youversion/platform-core 0.5.8 → 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/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 _isPreviewMode = false;
407
- static _previewUserInfo = null;
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 isPreviewMode() {
451
- return this._isPreviewMode;
478
+ static get refreshTokenKey() {
479
+ return this._refreshTokenKey;
452
480
  }
453
- static set isPreviewMode(value) {
454
- this._isPreviewMode = value;
481
+ static set refreshTokenKey(value) {
482
+ this._refreshTokenKey = value;
455
483
  }
456
- static get previewUserInfo() {
457
- return this._previewUserInfo;
484
+ static get expiryDateKey() {
485
+ return this._expiryDateKey;
458
486
  }
459
- static set previewUserInfo(value) {
460
- this._previewUserInfo = value;
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/WebAuthenticationStrategy.ts
702
- var WebAuthenticationStrategy = class _WebAuthenticationStrategy {
703
- redirectUri;
704
- callbackPath;
705
- timeout;
706
- storage;
707
- static pendingAuthResolve = null;
708
- static pendingAuthReject = null;
709
- static timeoutId = null;
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
- return false;
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
- * Clean up pending authentication state
744
- */
745
- static cleanup() {
746
- if (_WebAuthenticationStrategy.timeoutId) {
747
- clearTimeout(_WebAuthenticationStrategy.timeoutId);
748
- _WebAuthenticationStrategy.timeoutId = null;
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
- * Retrieve stored callback result if available
755
- */
756
- static getStoredCallback() {
757
- const storageStrategy = new SessionStorageStrategy();
758
- const stored = storageStrategy.getItem("youversion-auth-callback");
759
- if (stored) {
760
- storageStrategy.removeItem("youversion-auth-callback");
761
- try {
762
- return new URL(stored);
763
- } catch {
764
- return null;
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
- return null;
768
- }
769
- authenticateWithRedirect(authUrl) {
770
- _WebAuthenticationStrategy.cleanup();
771
- this.storage.setItem("youversion-auth-return", window.location.href);
772
- return new Promise((resolve, reject) => {
773
- _WebAuthenticationStrategy.pendingAuthResolve = resolve;
774
- _WebAuthenticationStrategy.pendingAuthReject = reject;
775
- _WebAuthenticationStrategy.timeoutId = setTimeout(() => {
776
- _WebAuthenticationStrategy.cleanup();
777
- reject(new Error("Authentication timeout"));
778
- }, this.timeout);
779
- try {
780
- window.location.href = authUrl.toString();
781
- } catch (error) {
782
- _WebAuthenticationStrategy.cleanup();
783
- reject(
784
- new Error(
785
- `Failed to navigate to auth URL: ${error instanceof Error ? error.message : "Unknown error"}`
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
- constructor(url) {
807
- const queryParams = new URLSearchParams(url.search);
808
- const status = queryParams.get("status");
809
- const userId = queryParams.get("yvp_user_id");
810
- const latValue = queryParams.get("lat");
811
- const grants = queryParams.get("grants");
812
- const perms = grants?.split(",").map((grant) => grant.trim()).filter(
813
- (grant) => Object.values(SignInWithYouVersionPermission).includes(
814
- grant
815
- )
816
- ).map((grant) => grant) ?? [];
817
- if (status === "success" && latValue && userId) {
818
- this.accessToken = latValue;
819
- this.permissions = perms;
820
- this.errorMsg = null;
821
- this.yvpUserId = userId;
822
- } else if (status === "canceled") {
823
- this.accessToken = null;
824
- this.permissions = [];
825
- this.errorMsg = null;
826
- this.yvpUserId = null;
827
- } else {
828
- this.accessToken = null;
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/YouVersionUserInfo.ts
837
- var YouVersionUserInfo = class {
838
- firstName;
839
- lastName;
840
- userId;
841
- avatarUrlFormat;
842
- constructor(data) {
843
- if (!data || typeof data !== "object") {
844
- throw new Error("Invalid user data provided");
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
- this.firstName = data.first_name;
847
- this.lastName = data.last_name;
848
- this.userId = data.id;
849
- this.avatarUrlFormat = data.avatar_url;
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
- getAvatarUrl(width = 200, height = 200) {
852
- if (!this.avatarUrlFormat) {
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
- let urlString = this.avatarUrlFormat;
856
- if (urlString.startsWith("//")) {
857
- urlString = "https:" + urlString;
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
- return new URL(urlString);
863
- } catch {
864
- return null;
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
- get avatarUrl() {
868
- return this.getAvatarUrl();
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,