@pod-os/elements 0.31.1-rc.fd664af.0 → 0.32.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.
@@ -2608,6 +2608,160 @@ const requestDynamicClientRegistration = async (registration_endpoint, client_de
2608
2608
  });
2609
2609
  };
2610
2610
 
2611
+ /**
2612
+ * A simple IndexedDB wrapper
2613
+ */
2614
+ class SessionDatabase {
2615
+ dbName;
2616
+ storeName;
2617
+ dbVersion;
2618
+ db = null;
2619
+ /**
2620
+ * Creates a new instance
2621
+ * @param dbName The name of the IndexedDB database
2622
+ * @param storeName The name of the object store
2623
+ * @param dbVersion The database version
2624
+ */
2625
+ constructor(dbName = 'soidc', storeName = 'session', dbVersion = 1) {
2626
+ this.dbName = dbName;
2627
+ this.storeName = storeName;
2628
+ this.dbVersion = dbVersion;
2629
+ }
2630
+ /**
2631
+ * Initializes the IndexedDB database
2632
+ * @returns Promise that resolves when the database is ready
2633
+ */
2634
+ async init() {
2635
+ return new Promise((resolve, reject) => {
2636
+ const request = indexedDB.open(this.dbName, this.dbVersion);
2637
+ request.onerror = (event) => {
2638
+ reject(new Error(`Database error: ${event.target.error}`));
2639
+ };
2640
+ request.onsuccess = (event) => {
2641
+ this.db = event.target.result;
2642
+ resolve(this);
2643
+ };
2644
+ request.onupgradeneeded = (event) => {
2645
+ const db = event.target.result;
2646
+ // Check if the object store already exists, if not create it
2647
+ if (!db.objectStoreNames.contains(this.storeName)) {
2648
+ db.createObjectStore(this.storeName);
2649
+ }
2650
+ };
2651
+ });
2652
+ }
2653
+ /**
2654
+ * Stores any value in the database with the given ID as key
2655
+ * @param id The identifier/key for the value
2656
+ * @param value The value to store
2657
+ */
2658
+ async setItem(id, value) {
2659
+ if (!this.db) {
2660
+ await this.init();
2661
+ }
2662
+ return new Promise((resolve, reject) => {
2663
+ const transaction = this.db.transaction(this.storeName, 'readwrite');
2664
+ // Handle transation
2665
+ transaction.oncomplete = () => {
2666
+ resolve();
2667
+ };
2668
+ transaction.onerror = (event) => {
2669
+ reject(new Error(`Transaction error for setItem(${id},...): ${event.target.error}`));
2670
+ };
2671
+ transaction.onabort = (event) => {
2672
+ reject(new Error(`Transaction aborted for setItem(${id},...): ${event.target.error}`));
2673
+ };
2674
+ // Perform the request within the transaction
2675
+ const store = transaction.objectStore(this.storeName);
2676
+ store.put(value, id);
2677
+ });
2678
+ }
2679
+ /**
2680
+ * Retrieves a value from the database by ID
2681
+ * @param id The identifier/key for the value
2682
+ * @returns The stored value or null if not found
2683
+ */
2684
+ async getItem(id) {
2685
+ if (!this.db) {
2686
+ await this.init();
2687
+ }
2688
+ return new Promise((resolve, reject) => {
2689
+ const transaction = this.db.transaction(this.storeName, 'readonly');
2690
+ // Handle transation
2691
+ transaction.onerror = (event) => {
2692
+ reject(new Error(`Transaction error for getItem(${id}): ${event.target.error}`));
2693
+ };
2694
+ transaction.onabort = (event) => {
2695
+ reject(new Error(`Transaction aborted for getItem(${id}): ${event.target.error}`));
2696
+ };
2697
+ // Perform the request within the transaction
2698
+ const store = transaction.objectStore(this.storeName);
2699
+ const request = store.get(id);
2700
+ request.onsuccess = () => {
2701
+ resolve(request.result || null);
2702
+ };
2703
+ });
2704
+ }
2705
+ /**
2706
+ * Removes an item from the database
2707
+ * @param id The identifier of the item to remove
2708
+ */
2709
+ async deleteItem(id) {
2710
+ if (!this.db) {
2711
+ await this.init();
2712
+ }
2713
+ return new Promise((resolve, reject) => {
2714
+ const transaction = this.db.transaction(this.storeName, 'readwrite');
2715
+ // Handle transation
2716
+ transaction.oncomplete = () => {
2717
+ resolve();
2718
+ };
2719
+ transaction.onerror = (event) => {
2720
+ reject(new Error(`Transaction error for deleteItem(${id}): ${event.target.error}`));
2721
+ };
2722
+ transaction.onabort = (event) => {
2723
+ reject(new Error(`Transaction aborted for deleteItem(${id}): ${event.target.error}`));
2724
+ };
2725
+ // Perform the request within the transaction
2726
+ const store = transaction.objectStore(this.storeName);
2727
+ store.delete(id);
2728
+ });
2729
+ }
2730
+ /**
2731
+ * Clears all items from the database
2732
+ */
2733
+ async clear() {
2734
+ if (!this.db) {
2735
+ await this.init();
2736
+ }
2737
+ return new Promise((resolve, reject) => {
2738
+ const transaction = this.db.transaction(this.storeName, 'readwrite');
2739
+ // Handle transation
2740
+ transaction.oncomplete = () => {
2741
+ resolve();
2742
+ };
2743
+ transaction.onerror = (event) => {
2744
+ reject(new Error(`Transaction error for clear(): ${event.target.error}`));
2745
+ };
2746
+ transaction.onabort = (event) => {
2747
+ reject(new Error(`Transaction aborted for clear(): ${event.target.error}`));
2748
+ };
2749
+ // Perform the request within the transaction
2750
+ const store = transaction.objectStore(this.storeName);
2751
+ store.clear();
2752
+ });
2753
+ }
2754
+ /**
2755
+ * Closes the database connection
2756
+ */
2757
+ close() {
2758
+ if (this.db) {
2759
+ this.db.close();
2760
+ this.db = null;
2761
+ }
2762
+ }
2763
+ }
2764
+
2611
2765
  /**
2612
2766
  * Login with the idp, using a provided `client_id` or dynamic client registration if none provided.
2613
2767
  *
@@ -2631,7 +2785,7 @@ const redirectForLogin = async (idp, redirect_uri, client_details) => {
2631
2785
  const issuer = openid_configuration["issuer"];
2632
2786
  const trim_trailing_slash = (url) => (url.endsWith('/') ? url.slice(0, -1) : url);
2633
2787
  if (trim_trailing_slash(idp) !== trim_trailing_slash(issuer)) { // expected idp matches received issuer mod trailing slash?
2634
- throw new Error("RFC 9207 - iss !== idp - " + issuer + " !== " + idp);
2788
+ throw new Error("RFC 9207 - iss != idp - " + issuer + " != " + idp);
2635
2789
  }
2636
2790
  sessionStorage.setItem("idp", issuer);
2637
2791
  // remember token endpoint
@@ -2652,12 +2806,7 @@ const redirectForLogin = async (idp, redirect_uri, client_details) => {
2652
2806
  return response.json();
2653
2807
  });
2654
2808
  client_id = client_registration["client_id"];
2655
- }
2656
- // remember client_id if not URL
2657
- try {
2658
- new URL(client_id);
2659
- }
2660
- catch {
2809
+ // remember client_id
2661
2810
  sessionStorage.setItem("client_id", client_id);
2662
2811
  }
2663
2812
  // RFC 7636 PKCE, remember code verifer
@@ -2698,7 +2847,7 @@ const getPKCEcode = async () => {
2698
2847
  * URL contains authrization code, issuer (idp) and state (csrf token),
2699
2848
  * get an access token for the authrization code.
2700
2849
  */
2701
- const onIncomingRedirect = async (client_details, database) => {
2850
+ const onIncomingRedirect = async (client_details) => {
2702
2851
  const url = new URL(window.location.href);
2703
2852
  // authorization code
2704
2853
  const authorization_code = url.searchParams.get("code");
@@ -2708,12 +2857,12 @@ const onIncomingRedirect = async (client_details, database) => {
2708
2857
  }
2709
2858
  // RFC 9207 issuer check
2710
2859
  const idp = sessionStorage.getItem("idp");
2711
- if (idp === null || url.searchParams.get("iss") !== idp) {
2712
- throw new Error("RFC 9207 - iss !== idp - " + url.searchParams.get("iss") + " !== " + idp);
2860
+ if (idp === null || url.searchParams.get("iss") != idp) {
2861
+ throw new Error("RFC 9207 - iss != idp - " + url.searchParams.get("iss") + " != " + idp);
2713
2862
  }
2714
2863
  // RFC 6749 OAuth 2.0
2715
- if (url.searchParams.get("state") !== sessionStorage.getItem("csrf_token")) {
2716
- throw new Error("RFC 6749 - state !== csrf_token - " + url.searchParams.get("state") + " !== " + sessionStorage.getItem("csrf_token"));
2864
+ if (url.searchParams.get("state") != sessionStorage.getItem("csrf_token")) {
2865
+ throw new Error("RFC 6749 - state != csrf_token - " + url.searchParams.get("state") + " != " + sessionStorage.getItem("csrf_token"));
2717
2866
  }
2718
2867
  // remove redirect query parameters from URL
2719
2868
  url.searchParams.delete("iss");
@@ -2757,32 +2906,30 @@ const onIncomingRedirect = async (client_details, database) => {
2757
2906
  });
2758
2907
  // check dpop thumbprint
2759
2908
  const dpopThumbprint = await calculateJwkThumbprint(await exportJWK(key_pair.publicKey));
2760
- if (payload["cnf"]["jkt"] !== dpopThumbprint) {
2761
- throw new Error("Access Token validation failed on `jkt`: jkt !== DPoP thumbprint - " + payload["cnf"]["jkt"] + " !== " + dpopThumbprint);
2909
+ if (payload["cnf"]["jkt"] != dpopThumbprint) {
2910
+ throw new Error("Access Token validation failed on `jkt`: jkt != DPoP thumbprint - " + payload["cnf"]["jkt"] + " != " + dpopThumbprint);
2762
2911
  }
2763
2912
  // check client_id
2764
- if (payload["client_id"] !== client_id) {
2765
- throw new Error("Access Token validation failed on `client_id`: JWT payload !== client_id - " + payload["client_id"] + " !== " + client_id);
2913
+ if (payload["client_id"] != client_id) {
2914
+ throw new Error("Access Token validation failed on `client_id`: JWT payload != client_id - " + payload["client_id"] + " != " + client_id);
2766
2915
  }
2767
2916
  // summarise session info
2768
2917
  const token_details = { ...token_response, dpop_key_pair: key_pair };
2769
2918
  const idp_details = { idp, jwks_uri, token_endpoint };
2770
2919
  if (!client_details)
2771
- client_details = { redirect_uris: [url.toString()] };
2920
+ client_details = { redirect_uris: [window.location.href] };
2772
2921
  client_details.client_id = client_id;
2773
- // and persist refresh token details
2774
- if (database) {
2775
- await database.init();
2776
- await Promise.all([
2777
- database.setItem("idp", idp),
2778
- database.setItem("jwks_uri", jwks_uri),
2779
- database.setItem("token_endpoint", token_endpoint),
2780
- database.setItem("client_id", client_id),
2781
- database.setItem("dpop_keypair", key_pair),
2782
- database.setItem("refresh_token", token_response["refresh_token"])
2783
- ]);
2784
- database.close();
2785
- }
2922
+ // to remember for session restore
2923
+ const sessionDatabase = await new SessionDatabase().init();
2924
+ await Promise.all([
2925
+ sessionDatabase.setItem("idp", idp),
2926
+ sessionDatabase.setItem("jwks_uri", jwks_uri),
2927
+ sessionDatabase.setItem("token_endpoint", token_endpoint),
2928
+ sessionDatabase.setItem("client_id", client_id),
2929
+ sessionDatabase.setItem("dpop_keypair", key_pair),
2930
+ sessionDatabase.setItem("refresh_token", token_response["refresh_token"])
2931
+ ]);
2932
+ sessionDatabase.close();
2786
2933
  // clean session storage
2787
2934
  sessionStorage.removeItem("csrf_token");
2788
2935
  sessionStorage.removeItem("pkce_code_verifier");
@@ -2840,60 +2987,56 @@ const requestAccessToken = async (authorization_code, pkce_code_verifier, redire
2840
2987
  });
2841
2988
  };
2842
2989
 
2843
- const renewTokens = async (sessionDatabase) => {
2990
+ const renewTokens = async () => {
2844
2991
  // remember session details
2845
- try {
2846
- await sessionDatabase.init();
2847
- const client_id = await sessionDatabase.getItem("client_id");
2848
- const token_endpoint = await sessionDatabase.getItem("token_endpoint");
2849
- const key_pair = await sessionDatabase.getItem("dpop_keypair");
2850
- const refresh_token = await sessionDatabase.getItem("refresh_token");
2851
- if (client_id === null || token_endpoint === null || key_pair === null || refresh_token === null) {
2852
- // we can not restore the old session
2853
- throw new Error("Could not refresh tokens: details missing from database.");
2854
- }
2855
- const token_response = await requestFreshTokens(refresh_token, client_id, token_endpoint, key_pair)
2856
- .then((response) => {
2857
- if (!response.ok) {
2858
- throw new Error(`HTTP error! Status: ${response.status}`);
2859
- }
2860
- return response.json();
2861
- });
2862
- // verify access_token // ! Solid-OIDC specification says it should be a dpop-bound `id token` but implementations provide a dpop-bound `access token`
2863
- const accessToken = token_response["access_token"];
2864
- const idp = await sessionDatabase.getItem("idp");
2865
- if (idp === null) {
2866
- throw new Error("Access Token validation preparation - Could not find in sessionDatabase: idp");
2867
- }
2868
- const jwks_uri = await sessionDatabase.getItem("jwks_uri");
2869
- if (jwks_uri === null) {
2870
- throw new Error("Access Token validation preparation - Could not find in sessionDatabase: jwks_uri");
2871
- }
2872
- const jwks = createRemoteJWKSet(new URL(jwks_uri));
2873
- const { payload } = await jwtVerify(accessToken, jwks, {
2874
- issuer: idp, // RFC 9207
2875
- audience: "solid", // RFC 7519 // ! "solid" as per implementations ...
2876
- // exp, nbf, iat - handled automatically
2877
- });
2878
- // check dpop thumbprint
2879
- const dpopThumbprint = await calculateJwkThumbprint(await exportJWK(key_pair.publicKey));
2880
- if (payload["cnf"]["jkt"] !== dpopThumbprint) {
2881
- throw new Error("Access Token validation failed on `jkt`: jkt !== DPoP thumbprint - " + payload["cnf"]["jkt"] + " !== " + dpopThumbprint);
2882
- }
2883
- // check client_id
2884
- if (payload["client_id"] !== client_id) {
2885
- throw new Error("Access Token validation failed on `client_id`: JWT payload !== client_id - " + payload["client_id"] + " !== " + client_id);
2992
+ const sessionDatabase = await new SessionDatabase().init();
2993
+ const client_id = await sessionDatabase.getItem("client_id");
2994
+ const token_endpoint = await sessionDatabase.getItem("token_endpoint");
2995
+ const key_pair = await sessionDatabase.getItem("dpop_keypair");
2996
+ const refresh_token = await sessionDatabase.getItem("refresh_token");
2997
+ if (client_id === null || token_endpoint === null || key_pair === null || refresh_token === null) {
2998
+ // we can not restore the old session
2999
+ throw new Error("Could not refresh tokens: details missing from database.");
3000
+ }
3001
+ const token_response = await requestFreshTokens(refresh_token, client_id, token_endpoint, key_pair)
3002
+ .then((response) => {
3003
+ if (!response.ok) {
3004
+ throw new Error(`HTTP error! Status: ${response.status}`);
2886
3005
  }
2887
- // set new refresh token for token rotation
2888
- await sessionDatabase.setItem("refresh_token", token_response["refresh_token"]);
2889
- return {
2890
- ...token_response,
2891
- dpop_key_pair: key_pair,
2892
- };
3006
+ return response.json();
3007
+ });
3008
+ // verify access_token // ! Solid-OIDC specification says it should be a dpop-bound `id token` but implementations provide a dpop-bound `access token`
3009
+ const accessToken = token_response["access_token"];
3010
+ const idp = await sessionDatabase.getItem("idp");
3011
+ if (idp === null) {
3012
+ throw new Error("Access Token validation preparation - Could not find in sessionDatabase: idp");
2893
3013
  }
2894
- finally {
2895
- sessionDatabase.close();
3014
+ const jwks_uri = await sessionDatabase.getItem("jwks_uri");
3015
+ if (jwks_uri === null) {
3016
+ throw new Error("Access Token validation preparation - Could not find in sessionDatabase: jwks_uri");
2896
3017
  }
3018
+ const jwks = createRemoteJWKSet(new URL(jwks_uri));
3019
+ const { payload } = await jwtVerify(accessToken, jwks, {
3020
+ issuer: idp, // RFC 9207
3021
+ audience: "solid", // RFC 7519 // ! "solid" as per implementations ...
3022
+ // exp, nbf, iat - handled automatically
3023
+ });
3024
+ // check dpop thumbprint
3025
+ const dpopThumbprint = await calculateJwkThumbprint(await exportJWK(key_pair.publicKey));
3026
+ if (payload["cnf"]["jkt"] != dpopThumbprint) {
3027
+ throw new Error("Access Token validation failed on `jkt`: jkt != DPoP thumbprint - " + payload["cnf"]["jkt"] + " != " + dpopThumbprint);
3028
+ }
3029
+ // check client_id
3030
+ if (payload["client_id"] != client_id) {
3031
+ throw new Error("Access Token validation failed on `client_id`: JWT payload != client_id - " + payload["client_id"] + " != " + client_id);
3032
+ }
3033
+ // set new refresh token for token rotation
3034
+ await sessionDatabase.setItem("refresh_token", token_response["refresh_token"]);
3035
+ sessionDatabase.close();
3036
+ return {
3037
+ ...token_response,
3038
+ dpop_key_pair: key_pair,
3039
+ };
2897
3040
  };
2898
3041
  /**
2899
3042
  * Request an dpop-bound access token from a token endpoint using a refresh token
@@ -2915,7 +3058,7 @@ const requestFreshTokens = async (refresh_token, client_id, token_endpoint, key_
2915
3058
  htm: "POST",
2916
3059
  })
2917
3060
  .setIssuedAt()
2918
- .setJti(self.crypto.randomUUID())
3061
+ .setJti(window.crypto.randomUUID())
2919
3062
  .setProtectedHeader({
2920
3063
  alg: "ES256",
2921
3064
  typ: "dpop+jwt",
@@ -2936,118 +3079,113 @@ const requestFreshTokens = async (refresh_token, client_id, token_endpoint, key_
2936
3079
  });
2937
3080
  };
2938
3081
 
2939
- //
2940
- //
2941
- // Basic implementation
2942
- //
2943
- //
2944
- /**
2945
- * The SessionCore class manages session state and core logic but does not handle the refresh lifecycle.
2946
- * It receives {@link SessionOptions} with a database to be able to restore a session.
2947
- * That database can be re-used by (your!) surrounding implementation to handle the refresh lifecycle.
2948
- * If no database was provided, refresh information cannot be stored, and thus token refresh (via the refresh token grant) is not possible in this case.
2949
- *
2950
- * If you are building a web app, use the Session implementation provided in the default `/web` version of this library.
2951
- */
2952
- class SessionCore {
3082
+ class Session {
3083
+ sessionInformation;
2953
3084
  isActive_ = false;
2954
- exp_;
2955
3085
  webId_ = undefined;
2956
3086
  currentAth_ = undefined;
2957
- onSessionStateChange;
2958
- information;
2959
- database;
2960
- refreshPromise;
2961
- resolveRefresh;
2962
- rejectRefresh;
3087
+ tokenRefreshTimeout;
3088
+ sessionDeactivateTimeout;
3089
+ onSessionExpirationWarning;
3090
+ /**
3091
+ * Create a new session.
3092
+ */
2963
3093
  constructor(clientDetails, sessionOptions) {
2964
- this.information = { clientDetails };
2965
- this.database = sessionOptions?.database;
2966
- this.onSessionStateChange = sessionOptions?.onSessionStateChange;
3094
+ this.sessionInformation = { clientDetails };
3095
+ this.onSessionExpirationWarning = sessionOptions?.onSessionExpirationWarning;
2967
3096
  }
3097
+ /**
3098
+ * Redirect the user for login to their IDP.
3099
+ *
3100
+ * @throws Error if the session has not been initialized.
3101
+ */
2968
3102
  async login(idp, redirect_uri) {
2969
- await redirectForLogin(idp, redirect_uri, this.information.clientDetails);
3103
+ await redirectForLogin(idp, redirect_uri, this.sessionInformation.clientDetails);
3104
+ }
3105
+ /**
3106
+ * Clears all session-related information, including IDP details and tokens.
3107
+ * This logs the user out.
3108
+ * Client details are preserved.
3109
+ */
3110
+ async logout() {
3111
+ // clear timeouts
3112
+ if (this.sessionDeactivateTimeout)
3113
+ clearTimeout(this.sessionDeactivateTimeout);
3114
+ this.sessionDeactivateTimeout = undefined;
3115
+ if (this.tokenRefreshTimeout)
3116
+ clearTimeout(this.tokenRefreshTimeout);
3117
+ this.tokenRefreshTimeout = undefined;
3118
+ // clean session data
3119
+ this.sessionInformation.idpDetails = undefined;
3120
+ this.sessionInformation.tokenDetails = undefined;
3121
+ this.isActive_ = false;
3122
+ this.webId_ = undefined;
3123
+ // only preserve client_id if URI
3124
+ if (this.sessionInformation.clientDetails?.client_id)
3125
+ try {
3126
+ new URL(this.sessionInformation.clientDetails.client_id);
3127
+ }
3128
+ catch (_) {
3129
+ this.sessionInformation.clientDetails.client_id = undefined;
3130
+ }
3131
+ // clean session database
3132
+ const sessionDatabase = await new SessionDatabase().init();
3133
+ await sessionDatabase.clear();
3134
+ sessionDatabase.close();
2970
3135
  }
2971
3136
  /**
2972
3137
  * Handles the redirect from the identity provider after a login attempt.
2973
3138
  * It attempts to retrieve tokens using the authorization code.
2974
- * Upon success, it tries to persist information to refresh tokens in the session database.
2975
- * If no database was provided, no information is persisted.
2976
3139
  */
2977
3140
  async handleRedirectFromLogin() {
2978
3141
  // Redirect after Authorization Code Grant // memory via sessionStorage
2979
- const newSessionInfo = await onIncomingRedirect(this.information.clientDetails, this.database);
3142
+ const newSessionInfo = await onIncomingRedirect(this.sessionInformation.clientDetails);
2980
3143
  // no session - we remain unauthenticated
2981
- if (!newSessionInfo.tokenDetails)
3144
+ if (!newSessionInfo.tokenDetails) {
2982
3145
  return;
3146
+ }
2983
3147
  // we got a session
2984
- this.information.clientDetails = newSessionInfo.clientDetails;
2985
- this.information.idpDetails = newSessionInfo.idpDetails;
2986
- await this.setTokenDetails(newSessionInfo.tokenDetails);
2987
- // callback state change
2988
- this.onSessionStateChange?.(); // we logged in
3148
+ this.sessionInformation = newSessionInfo;
3149
+ await this.setSessionDetails();
2989
3150
  }
2990
3151
  /**
2991
3152
  * Handles session restoration using the refresh token grant.
2992
3153
  * Silently fails if session could not be restored (maybe there was no session in the first place).
2993
3154
  */
2994
3155
  async restore() {
2995
- if (!this.database) {
2996
- throw new Error("Could not refresh tokens: missing database. Provide database in sessionOption.");
2997
- }
2998
- if (this.refreshPromise) {
2999
- return this.refreshPromise;
3000
- }
3001
- this.refreshPromise = new Promise((resolve, reject) => {
3002
- this.resolveRefresh = resolve;
3003
- this.rejectRefresh = reject;
3004
- });
3005
- // Restore session using Refresh Token Grant
3006
- const wasActive = this.isActive;
3007
- renewTokens(this.database)
3008
- .then(tokenDetails => this.setTokenDetails(tokenDetails))
3009
- .then(() => this.resolveRefresh())
3010
- .catch(error => {
3011
- if (this.isActive) {
3012
- this.rejectRefresh(new Error(error || 'Token refresh failed'));
3013
- // do not change state (yet), let the app decide if they want to logout or if they just want to retry.
3014
- }
3015
- else {
3016
- this.rejectRefresh(new Error("No session to restore."));
3017
- }
3018
- }).finally(() => {
3019
- this.clearRefreshPromise();
3020
- if (wasActive !== this.isActive)
3021
- this.onSessionStateChange?.();
3022
- });
3023
- return this.refreshPromise;
3156
+ // Restore session using Refresh Token Grant // memory via IndexedDB
3157
+ await renewTokens()
3158
+ .then(tokenDetails => {
3159
+ // got new tokens
3160
+ this.sessionInformation.tokenDetails = tokenDetails;
3161
+ // set session information
3162
+ return this.setSessionDetails();
3163
+ })
3164
+ // anything missing or wrong => abort, could not restore session.
3165
+ .catch(_ => { }); // fail silently
3024
3166
  }
3025
3167
  /**
3026
- * This logs the user out.
3027
- * Clears all session-related information, including IDP details and tokens.
3028
- * Client ID is preserved if it is a URI.
3168
+ * Creates a signed DPoP (Demonstration of Proof-of-Possession) token.
3169
+ *
3170
+ * @param payload The payload to include in the DPoP token. By default, it includes `htu` (HTTP target URI) and `htm` (HTTP method).
3171
+ * @returns A promise that resolves to the signed DPoP token string.
3172
+ * @throws Error if the session has not been initialized - if no token details are available.
3029
3173
  */
3030
- async logout() {
3031
- // clean session data
3032
- this.isActive_ = false;
3033
- this.exp_ = undefined;
3034
- this.webId_ = undefined;
3035
- this.currentAth_ = undefined;
3036
- this.information.idpDetails = undefined;
3037
- this.information.tokenDetails = undefined;
3038
- // client details are preserved
3039
- if (this.refreshPromise && this.rejectRefresh) {
3040
- this.rejectRefresh(new Error('Logout during token refresh.'));
3041
- this.clearRefreshPromise();
3042
- }
3043
- // clean session database
3044
- if (this.database) {
3045
- await this.database.init();
3046
- await this.database.clear();
3047
- this.database.close();
3174
+ async createSignedDPoPToken(payload) {
3175
+ if (!this.sessionInformation.tokenDetails || !this.currentAth_) {
3176
+ throw new Error("Session not established.");
3048
3177
  }
3049
- // callback state change
3050
- this.onSessionStateChange?.(); // we logged out
3178
+ payload.ath = this.currentAth_;
3179
+ const jwk_public_key = await exportJWK(this.sessionInformation.tokenDetails.dpop_key_pair.publicKey);
3180
+ return new SignJWT(payload)
3181
+ .setIssuedAt()
3182
+ .setJti(window.crypto.randomUUID())
3183
+ .setProtectedHeader({
3184
+ alg: "ES256",
3185
+ typ: "dpop+jwt",
3186
+ jwk: jwk_public_key,
3187
+ })
3188
+ .sign(this.sessionInformation.tokenDetails.dpop_key_pair.privateKey);
3051
3189
  }
3052
3190
  /**
3053
3191
  * Makes an HTTP fetch request.
@@ -3059,18 +3197,10 @@ class SessionCore {
3059
3197
  * @returns A promise that resolves to the fetch Response.
3060
3198
  */
3061
3199
  async authFetch(input, init, dpopPayload) {
3062
- // if there is not session established, just delegate to the default fetch
3063
- if (!this.isActive) {
3064
- return fetch(input, init);
3065
- }
3066
- // TODO
3067
- // TODO do HEAD request to check if authentication is actually required, only then include tokens
3068
- // TODO
3069
3200
  // prepare authenticated call using a DPoP token (either provided payload, or default)
3070
3201
  let url;
3071
3202
  let method;
3072
3203
  let headers;
3073
- // wrangle fetch input parameters into place
3074
3204
  if (input instanceof Request) {
3075
3205
  url = new URL(input.url);
3076
3206
  method = init?.method || input?.method || 'GET';
@@ -3083,15 +3213,15 @@ class SessionCore {
3083
3213
  headers = init.headers ? new Headers(init.headers) : new Headers();
3084
3214
  }
3085
3215
  // create DPoP token, and add tokens to request
3086
- await this._renewTokensIfExpired();
3087
- dpopPayload = dpopPayload ?? {
3088
- htu: `${url.origin}${url.pathname}`,
3089
- htm: method.toUpperCase()
3090
- };
3091
- const dpop = await this._createSignedDPoPToken(dpopPayload);
3092
- // overwrite headers: authorization, dpop
3093
- headers.set("dpop", dpop);
3094
- headers.set("authorization", `DPoP ${this.information.tokenDetails.access_token}`);
3216
+ if (this.sessionInformation.tokenDetails) {
3217
+ dpopPayload = dpopPayload ?? {
3218
+ htu: `${url.origin}${url.pathname}`,
3219
+ htm: method.toUpperCase()
3220
+ };
3221
+ const dpop = await this.createSignedDPoPToken(dpopPayload);
3222
+ headers.set("dpop", dpop);
3223
+ headers.set("authorization", `DPoP ${this.sessionInformation.tokenDetails.access_token}`);
3224
+ }
3095
3225
  // check explicitly; to avoid unexpected behaviour
3096
3226
  if (input instanceof Request) { // clone the provided request, and override the headers
3097
3227
  return fetch(new Request(input, { ...init, headers }));
@@ -3099,59 +3229,64 @@ class SessionCore {
3099
3229
  // just override the headers
3100
3230
  return fetch(url, { ...init, headers });
3101
3231
  }
3102
- //
3103
- // Setters
3104
- //
3105
- async setTokenDetails(tokenDetails) {
3106
- this.information.tokenDetails = tokenDetails;
3107
- await this._updateSessionDetailsFromToken(tokenDetails.access_token);
3108
- }
3109
- clearRefreshPromise() {
3110
- this.refreshPromise = undefined;
3111
- this.resolveRefresh = undefined;
3112
- this.rejectRefresh = undefined;
3113
- }
3114
- //
3115
- // Getters
3116
- //
3117
3232
  get isActive() {
3118
3233
  return this.isActive_;
3119
3234
  }
3120
3235
  get webId() {
3121
3236
  return this.webId_;
3122
3237
  }
3123
- getExpiresIn() {
3124
- return this.information.tokenDetails?.expires_in ?? -1;
3125
- }
3126
- isExpired() {
3127
- if (!this.exp_)
3128
- return true;
3129
- return this._isTokenExpired(this.exp_);
3130
- }
3131
- getTokenDetails() {
3132
- return this.information.tokenDetails;
3133
- }
3134
3238
  //
3135
- // Helpers
3239
+ // Helper Methods
3136
3240
  //
3137
3241
  /**
3138
- * Check if the current token is expired (which may happen during device/browser/tab hibernation),
3139
- * and if expired, restore the session.
3242
+ * Set the session to active if there is an access token.
3140
3243
  */
3141
- async _renewTokensIfExpired() {
3142
- if (this.isExpired()) {
3143
- if (!this.refreshPromise) {
3144
- await this.restore(); // Initiate and wait
3145
- }
3146
- else {
3147
- await this.refreshPromise; // Wait for already pending
3244
+ async setSessionDetails() {
3245
+ // check for access token
3246
+ if (!this.sessionInformation.tokenDetails?.access_token) {
3247
+ this.logout();
3248
+ }
3249
+ // generate ath
3250
+ this.currentAth_ = await this.computeAth(this.sessionInformation.tokenDetails.access_token);
3251
+ // check for active session
3252
+ this.webId_ = decodeJwt(this.sessionInformation.tokenDetails.access_token)["webid"];
3253
+ this.isActive_ = this.webId !== undefined;
3254
+ // deactivating session when token expire
3255
+ this.setSessionDeactivateTimeout();
3256
+ // refreshing tokens
3257
+ this.setTokenRefreshTimeout();
3258
+ }
3259
+ setSessionDeactivateTimeout() {
3260
+ const deactivate_buffer_seconds = 5;
3261
+ const timeUntilDeactivate = (this.sessionInformation.tokenDetails.expires_in - deactivate_buffer_seconds) * 1000;
3262
+ if (this.sessionDeactivateTimeout)
3263
+ clearTimeout(this.sessionDeactivateTimeout);
3264
+ this.sessionDeactivateTimeout = setTimeout(() => this.logout(), timeUntilDeactivate);
3265
+ }
3266
+ setTokenRefreshTimeout() {
3267
+ const refresh_buffer_seconds = 95;
3268
+ const timeUntilRefresh = (this.sessionInformation.tokenDetails.expires_in - refresh_buffer_seconds) * 1000;
3269
+ if (this.tokenRefreshTimeout)
3270
+ clearTimeout(this.tokenRefreshTimeout);
3271
+ this.tokenRefreshTimeout = setTimeout(async () => {
3272
+ const newTokens = await renewTokens()
3273
+ .catch((error) => {
3274
+ // anything missing or wrong => could not renew tokens.
3275
+ if (this.onSessionExpirationWarning)
3276
+ this.onSessionExpirationWarning();
3277
+ return undefined;
3278
+ });
3279
+ if (!newTokens) {
3280
+ return;
3148
3281
  }
3149
- }
3282
+ this.sessionInformation.tokenDetails = newTokens;
3283
+ this.setSessionDetails();
3284
+ }, timeUntilRefresh);
3150
3285
  }
3151
3286
  /**
3152
3287
  * RFC 9449 - Hash of the access token
3153
3288
  */
3154
- async _computeAth(accessToken) {
3289
+ async computeAth(accessToken) {
3155
3290
  // Convert the ASCII string of the token to a Uint8Array
3156
3291
  const encoder = new TextEncoder();
3157
3292
  const data = encoder.encode(accessToken); // ASCII by default
@@ -3167,470 +3302,6 @@ class SessionCore {
3167
3302
  .replace(/=+$/, '');
3168
3303
  return base64url;
3169
3304
  }
3170
- /**
3171
- * Creates a signed DPoP (Demonstration of Proof-of-Possession) token.
3172
- *
3173
- * @param payload The payload to include in the DPoP token. By default, it includes `htu` (HTTP target URI) and `htm` (HTTP method).
3174
- * @returns A promise that resolves to the signed DPoP token string.
3175
- * @throws Error if the session has not been initialized - if no token details are available.
3176
- */
3177
- async _createSignedDPoPToken(payload) {
3178
- if (!this.information.tokenDetails || !this.currentAth_) {
3179
- throw new Error("Session not established.");
3180
- }
3181
- payload.ath = this.currentAth_;
3182
- const jwk_public_key = await exportJWK(this.information.tokenDetails.dpop_key_pair.publicKey);
3183
- return new SignJWT(payload)
3184
- .setIssuedAt()
3185
- .setJti(window.crypto.randomUUID())
3186
- .setProtectedHeader({
3187
- alg: "ES256",
3188
- typ: "dpop+jwt",
3189
- jwk: jwk_public_key,
3190
- })
3191
- .sign(this.information.tokenDetails.dpop_key_pair.privateKey);
3192
- }
3193
- async _updateSessionDetailsFromToken(access_token) {
3194
- if (!access_token) {
3195
- await this.logout();
3196
- return;
3197
- }
3198
- try {
3199
- const decodedToken = decodeJwt(access_token);
3200
- const webId = decodedToken.webid;
3201
- if (!webId) {
3202
- throw new Error('Missing webid claim in access token');
3203
- }
3204
- const exp = decodedToken.exp;
3205
- if (!exp) {
3206
- throw new Error('Missing exp claim in access token');
3207
- }
3208
- this.currentAth_ = await this._computeAth(access_token); // must be done before session set to active
3209
- this.webId_ = webId;
3210
- this.exp_ = exp;
3211
- this.isActive_ = true;
3212
- }
3213
- catch (error) {
3214
- await this.logout();
3215
- }
3216
- }
3217
- /**
3218
- * Checks if a JWT expiration timestamp ('exp') has passed.
3219
- */
3220
- _isTokenExpired(exp, bufferSeconds = 0) {
3221
- if (typeof exp !== 'number' || isNaN(exp)) {
3222
- return true;
3223
- }
3224
- const currentTimeSeconds = Math.floor(Date.now() / 1000);
3225
- return exp < (currentTimeSeconds + bufferSeconds);
3226
- }
3227
- }
3228
-
3229
- // extracting this from Session.ts such that the jest tests would compile :)
3230
- const getWorkerUrl = () => new URL('./RefreshWorker.js', import.meta.url);
3231
-
3232
- /**
3233
- * A simple IndexedDB wrapper.
3234
- */
3235
- class SessionIDB {
3236
- dbName;
3237
- storeName;
3238
- dbVersion;
3239
- db = null;
3240
- /**
3241
- * Creates a new instance
3242
- * @param dbName The name of the IndexedDB database
3243
- * @param storeName The name of the object store
3244
- * @param dbVersion The database version
3245
- */
3246
- constructor(dbName = 'soidc', storeName = 'session', dbVersion = 1) {
3247
- this.dbName = dbName;
3248
- this.storeName = storeName;
3249
- this.dbVersion = dbVersion;
3250
- }
3251
- /**
3252
- * Initializes the IndexedDB database
3253
- * @returns Promise that resolves when the database is ready
3254
- */
3255
- async init() {
3256
- return new Promise((resolve, reject) => {
3257
- const request = indexedDB.open(this.dbName, this.dbVersion);
3258
- request.onerror = (event) => {
3259
- reject(new Error(`Database error: ${event.target.error}`));
3260
- };
3261
- request.onsuccess = (event) => {
3262
- this.db = event.target.result;
3263
- resolve(this);
3264
- };
3265
- request.onupgradeneeded = (event) => {
3266
- const db = event.target.result;
3267
- // Check if the object store already exists, if not create it
3268
- if (!db.objectStoreNames.contains(this.storeName)) {
3269
- db.createObjectStore(this.storeName);
3270
- }
3271
- };
3272
- });
3273
- }
3274
- /**
3275
- * Stores any value in the database with the given ID as key
3276
- * @param id The identifier/key for the value
3277
- * @param value The value to store
3278
- */
3279
- async setItem(id, value) {
3280
- if (!this.db) {
3281
- await this.init();
3282
- }
3283
- return new Promise((resolve, reject) => {
3284
- const transaction = this.db.transaction(this.storeName, 'readwrite');
3285
- // Handle transation
3286
- transaction.oncomplete = () => {
3287
- resolve();
3288
- };
3289
- transaction.onerror = (event) => {
3290
- reject(new Error(`Transaction error for setItem(${id},...): ${event.target.error}`));
3291
- };
3292
- transaction.onabort = (event) => {
3293
- reject(new Error(`Transaction aborted for setItem(${id},...): ${event.target.error}`));
3294
- };
3295
- // Perform the request within the transaction
3296
- const store = transaction.objectStore(this.storeName);
3297
- store.put(value, id);
3298
- });
3299
- }
3300
- /**
3301
- * Retrieves a value from the database by ID
3302
- * @param id The identifier/key for the value
3303
- * @returns The stored value or null if not found
3304
- */
3305
- async getItem(id) {
3306
- if (!this.db) {
3307
- await this.init();
3308
- }
3309
- return new Promise((resolve, reject) => {
3310
- const transaction = this.db.transaction(this.storeName, 'readonly');
3311
- // Handle transation
3312
- transaction.onerror = (event) => {
3313
- reject(new Error(`Transaction error for getItem(${id}): ${event.target.error}`));
3314
- };
3315
- transaction.onabort = (event) => {
3316
- reject(new Error(`Transaction aborted for getItem(${id}): ${event.target.error}`));
3317
- };
3318
- // Perform the request within the transaction
3319
- const store = transaction.objectStore(this.storeName);
3320
- const request = store.get(id);
3321
- request.onsuccess = () => {
3322
- resolve(request.result || null);
3323
- };
3324
- });
3325
- }
3326
- /**
3327
- * Removes an item from the database
3328
- * @param id The identifier of the item to remove
3329
- */
3330
- async deleteItem(id) {
3331
- if (!this.db) {
3332
- await this.init();
3333
- }
3334
- return new Promise((resolve, reject) => {
3335
- const transaction = this.db.transaction(this.storeName, 'readwrite');
3336
- // Handle transation
3337
- transaction.oncomplete = () => {
3338
- resolve();
3339
- };
3340
- transaction.onerror = (event) => {
3341
- reject(new Error(`Transaction error for deleteItem(${id}): ${event.target.error}`));
3342
- };
3343
- transaction.onabort = (event) => {
3344
- reject(new Error(`Transaction aborted for deleteItem(${id}): ${event.target.error}`));
3345
- };
3346
- // Perform the request within the transaction
3347
- const store = transaction.objectStore(this.storeName);
3348
- store.delete(id);
3349
- });
3350
- }
3351
- /**
3352
- * Clears all items from the database
3353
- */
3354
- async clear() {
3355
- if (!this.db) {
3356
- await this.init();
3357
- }
3358
- return new Promise((resolve, reject) => {
3359
- const transaction = this.db.transaction(this.storeName, 'readwrite');
3360
- // Handle transation
3361
- transaction.oncomplete = () => {
3362
- resolve();
3363
- };
3364
- transaction.onerror = (event) => {
3365
- reject(new Error(`Transaction error for clear(): ${event.target.error}`));
3366
- };
3367
- transaction.onabort = (event) => {
3368
- reject(new Error(`Transaction aborted for clear(): ${event.target.error}`));
3369
- };
3370
- // Perform the request within the transaction
3371
- const store = transaction.objectStore(this.storeName);
3372
- store.clear();
3373
- });
3374
- }
3375
- /**
3376
- * Closes the database connection
3377
- */
3378
- close() {
3379
- if (this.db) {
3380
- this.db.close();
3381
- this.db = null;
3382
- }
3383
- }
3384
- }
3385
-
3386
- var RefreshMessageTypes;
3387
- (function (RefreshMessageTypes) {
3388
- RefreshMessageTypes["SCHEDULE"] = "SCHEDULE";
3389
- RefreshMessageTypes["REFRESH"] = "REFRESH";
3390
- RefreshMessageTypes["STOP"] = "STOP";
3391
- RefreshMessageTypes["DISCONNECT"] = "DISCONNECT";
3392
- RefreshMessageTypes["TOKEN_DETAILS"] = "TOKEN_DETAILS";
3393
- RefreshMessageTypes["ERROR_ON_REFRESH"] = "ERROR_ON_REFRESH";
3394
- RefreshMessageTypes["EXPIRED"] = "EXPIRED";
3395
- })(RefreshMessageTypes || (RefreshMessageTypes = {}));
3396
- // A Set to store all connected ports (tabs)
3397
- const ports = new Set();
3398
- const broadcast = (message) => {
3399
- for (const p of ports) {
3400
- p.postMessage(message);
3401
- }
3402
- };
3403
- let refresher;
3404
- self.onconnect = (event) => {
3405
- const port = event.ports[0];
3406
- ports.add(port);
3407
- // lazy init
3408
- if (!refresher) {
3409
- refresher = new Refresher(broadcast, new SessionIDB());
3410
- }
3411
- // handle messages
3412
- port.onmessage = (event) => {
3413
- const { type, payload } = event.data;
3414
- switch (type) {
3415
- case RefreshMessageTypes.SCHEDULE:
3416
- refresher.handleSchedule(payload);
3417
- break;
3418
- case RefreshMessageTypes.REFRESH:
3419
- refresher.handleRefresh(port);
3420
- break;
3421
- case RefreshMessageTypes.STOP:
3422
- refresher.handleStop();
3423
- break;
3424
- case RefreshMessageTypes.DISCONNECT:
3425
- ports.delete(port);
3426
- break;
3427
- }
3428
- };
3429
- port.onmessageerror = () => ports.delete(port);
3430
- port.start();
3431
- };
3432
- class Refresher {
3433
- tokenDetails;
3434
- exp;
3435
- refreshTimeout;
3436
- finalLogoutTimeout;
3437
- timersAreRunning = false;
3438
- broadcast;
3439
- database;
3440
- refreshPromise;
3441
- constructor(broadcast, database) {
3442
- this.broadcast = broadcast;
3443
- this.database = database;
3444
- }
3445
- async handleSchedule(tokenDetails) {
3446
- this.tokenDetails = tokenDetails;
3447
- this.exp = decodeJwt(this.tokenDetails.access_token).exp;
3448
- this.broadcast({
3449
- type: RefreshMessageTypes.TOKEN_DETAILS,
3450
- payload: { tokenDetails: this.tokenDetails }
3451
- });
3452
- console.log(`[RefreshWorker] Scheduling timers, expiry in ${this.tokenDetails.expires_in}s`);
3453
- this.scheduleTimers(this.tokenDetails.expires_in);
3454
- this.timersAreRunning = true;
3455
- }
3456
- async handleRefresh(requestingPort) {
3457
- if (this.tokenDetails && this.exp && !this.isTokenExpired(this.exp)) {
3458
- console.log(`[RefreshWorker] Providing current tokens`);
3459
- requestingPort.postMessage({
3460
- type: RefreshMessageTypes.TOKEN_DETAILS,
3461
- payload: { tokenDetails: this.tokenDetails }
3462
- });
3463
- }
3464
- else {
3465
- console.log(`[RefreshWorker] Refreshing tokens`);
3466
- this.performRefresh();
3467
- }
3468
- }
3469
- handleStop() {
3470
- if (!this.tokenDetails) {
3471
- console.log('[RefreshWorker] Received STOP, being idle');
3472
- return;
3473
- }
3474
- this.broadcast({ type: RefreshMessageTypes.EXPIRED });
3475
- this.tokenDetails = undefined;
3476
- this.exp = undefined;
3477
- this.refreshPromise = undefined;
3478
- console.log('[RefreshWorker] Received STOP, clearing timers');
3479
- this.clearAllTimers();
3480
- }
3481
- async performRefresh() {
3482
- if (this.refreshPromise) {
3483
- console.log('[RefreshWorker] Refresh already in progress, waiting...');
3484
- return this.refreshPromise;
3485
- }
3486
- this.refreshPromise = this.doRefresh();
3487
- return this.refreshPromise;
3488
- }
3489
- async doRefresh() {
3490
- try {
3491
- this.tokenDetails = await renewTokens(this.database);
3492
- this.exp = decodeJwt(this.tokenDetails.access_token).exp;
3493
- this.broadcast({
3494
- type: RefreshMessageTypes.TOKEN_DETAILS,
3495
- payload: { tokenDetails: this.tokenDetails }
3496
- });
3497
- console.log(`[RefreshWorker] Token refreshed`);
3498
- console.log(`[RefreshWorker] Scheduling timers, expiry in ${this.tokenDetails.expires_in}s`);
3499
- this.scheduleTimers(this.tokenDetails.expires_in);
3500
- }
3501
- catch (error) {
3502
- this.broadcast({
3503
- type: RefreshMessageTypes.ERROR_ON_REFRESH,
3504
- error: error.message
3505
- });
3506
- console.log(`[RefreshWorker]`, error.message);
3507
- }
3508
- finally {
3509
- this.refreshPromise = undefined;
3510
- }
3511
- }
3512
- clearAllTimers() {
3513
- if (this.refreshTimeout)
3514
- clearTimeout(this.refreshTimeout);
3515
- if (this.finalLogoutTimeout)
3516
- clearTimeout(this.finalLogoutTimeout);
3517
- this.timersAreRunning = false;
3518
- }
3519
- scheduleTimers(expiresIn) {
3520
- this.clearAllTimers();
3521
- this.timersAreRunning = true;
3522
- const expiresInMs = expiresIn * 1000;
3523
- const REFRESH_THRESHOLD_RATIO = 0.8;
3524
- const MINIMUM_REFRESH_BUFFER_MS = 30 * 1000;
3525
- const timeUntilRefresh = REFRESH_THRESHOLD_RATIO * expiresInMs;
3526
- if (timeUntilRefresh > MINIMUM_REFRESH_BUFFER_MS) {
3527
- this.refreshTimeout = setTimeout(() => this.performRefresh(), timeUntilRefresh);
3528
- }
3529
- const LOGOUT_WARNING_BUFFER_MS = 5 * 1000;
3530
- const timeUntilLogout = expiresInMs - LOGOUT_WARNING_BUFFER_MS;
3531
- this.finalLogoutTimeout = setTimeout(() => {
3532
- this.tokenDetails = undefined;
3533
- this.broadcast({ type: RefreshMessageTypes.EXPIRED });
3534
- }, timeUntilLogout);
3535
- }
3536
- isTokenExpired(exp, bufferSeconds = 0) {
3537
- if (typeof exp !== 'number' || isNaN(exp)) {
3538
- return true;
3539
- }
3540
- const currentTimeSeconds = Math.floor(Date.now() / 1000);
3541
- return exp < (currentTimeSeconds + bufferSeconds);
3542
- }
3543
- setTokenDetails(tokenDetails) {
3544
- this.tokenDetails = tokenDetails;
3545
- }
3546
- // For testing
3547
- getTimersAreRunning() { return this.timersAreRunning; }
3548
- getTokenDetails() { return this.tokenDetails; }
3549
- }
3550
-
3551
- /**
3552
- * This Session provides background token refreshing using a Web Worker.
3553
- */
3554
- class WebWorkerSession extends SessionCore {
3555
- worker;
3556
- onSessionExpirationWarning;
3557
- onSessionExpiration;
3558
- constructor(clientDetails, sessionOptions) {
3559
- const database = new SessionIDB();
3560
- const options = { ...sessionOptions, database };
3561
- super(clientDetails, options);
3562
- this.onSessionExpirationWarning = sessionOptions?.onSessionExpirationWarning;
3563
- this.onSessionExpiration = sessionOptions?.onSessionExpiration;
3564
- // Allow consumer to provide worker URL, or use default
3565
- const workerUrl = sessionOptions?.workerUrl ?? getWorkerUrl();
3566
- this.worker = new SharedWorker(workerUrl, { type: 'module' });
3567
- this.worker.port.onmessage = (event) => {
3568
- this.handleWorkerMessage(event.data).catch(console.error);
3569
- };
3570
- window.addEventListener('beforeunload', () => {
3571
- this.worker.port.postMessage({ type: RefreshMessageTypes.DISCONNECT });
3572
- });
3573
- }
3574
- async handleWorkerMessage(data) {
3575
- const { type, payload, error } = data;
3576
- switch (type) {
3577
- case RefreshMessageTypes.TOKEN_DETAILS:
3578
- const wasActive = this.isActive;
3579
- await this.setTokenDetails(payload.tokenDetails);
3580
- if (wasActive !== this.isActive)
3581
- this.onSessionStateChange?.();
3582
- if (this.refreshPromise && this.resolveRefresh) {
3583
- this.resolveRefresh();
3584
- this.clearRefreshPromise();
3585
- }
3586
- break;
3587
- case RefreshMessageTypes.ERROR_ON_REFRESH:
3588
- if (this.isActive)
3589
- this.onSessionExpirationWarning?.();
3590
- if (this.refreshPromise && this.rejectRefresh) {
3591
- if (this.isActive) {
3592
- this.rejectRefresh(new Error(error || 'Token refresh failed'));
3593
- }
3594
- else {
3595
- this.rejectRefresh(new Error("No session to restore"));
3596
- }
3597
- this.clearRefreshPromise();
3598
- }
3599
- break;
3600
- case RefreshMessageTypes.EXPIRED:
3601
- if (this.isActive) {
3602
- this.onSessionExpiration?.();
3603
- await this.logout();
3604
- }
3605
- if (this.refreshPromise && this.rejectRefresh) {
3606
- this.rejectRefresh(new Error(error || 'Token refresh failed'));
3607
- this.clearRefreshPromise();
3608
- }
3609
- break;
3610
- }
3611
- }
3612
- ;
3613
- async handleRedirectFromLogin() {
3614
- await super.handleRedirectFromLogin();
3615
- if (this.isActive) { // If login was successful, tell the worker to schedule refreshing
3616
- this.worker.port.postMessage({ type: RefreshMessageTypes.SCHEDULE, payload: this.getTokenDetails() });
3617
- }
3618
- }
3619
- async restore() {
3620
- if (this.refreshPromise) {
3621
- return this.refreshPromise;
3622
- }
3623
- this.refreshPromise = new Promise((resolve, reject) => {
3624
- this.resolveRefresh = resolve;
3625
- this.rejectRefresh = reject;
3626
- });
3627
- this.worker.port.postMessage({ type: RefreshMessageTypes.REFRESH });
3628
- return this.refreshPromise;
3629
- }
3630
- async logout() {
3631
- this.worker.port.postMessage({ type: RefreshMessageTypes.STOP });
3632
- await super.logout();
3633
- }
3634
3305
  }
3635
3306
 
3636
3307
  class BrowserSession {
@@ -3643,7 +3314,7 @@ class BrowserSession {
3643
3314
  isLoggedIn: false,
3644
3315
  webId: undefined,
3645
3316
  });
3646
- this.session = new WebWorkerSession();
3317
+ this.session = new Session();
3647
3318
  this._authenticatedFetch = this.session.authFetch.bind(this.session);
3648
3319
  }
3649
3320
  async handleIncomingRedirect(restorePreviousSession = false) {