UNPKG

tidecloak-js

Version:

TideCloak client side JS SDK

2,202 lines 80.6 kB
/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Modifications Copyright (C) 2025 Tide Foundation Ltd
 * Tide Protocol - Infrastructure for a TRUE Zero-Trust paradigm
 *
 * This modified version is subject to the terms of the Tide Community Open 
 * Code License 2.0 as published by Tide Foundation Limited. You may modify
 * and redistribute it in accordance with and subject to the terms of that License.
 *
 * This program is free software and is subject to the terms of the 
 * Tide Community Open Code License as published by the Tide Foundation Limited. 
 * You may modify it and redistribute it in accordance with and subject to the 
 * terms of that License. This program is distributed WITHOUT WARRANTY of any 
 * kind, including without any implied warranty of MERCHANTABILITY or FITNESS 
 * FOR A PARTICULAR PURPOSE. 
 * See the Tide Community Open Code License for more details. 
 * You should have received a copy of the Tide Community Open Code License along
 * with this program. If not, see https://tide.org/licenses_tcoc2-0-0-en
 */

// MODIFIED: Added dependency to external Tide helper libraries.

import { RequestEnclave, ApprovalEnclave } from "heimdall-tide"
import { StringFromUint8Array, StringToUint8Array, CreateTideMemory } from "../modules/tide-js/Cryptide/Serialization.js";
import { AuthorizedEncryptionFlow } from "../modules/tide-js/Flow/EncryptionFlows/AuthorizedEncryptionFlow.js";
import dVVKSigningFlow_DEPRECATED from "../modules/tide-js/Flow/SigningFlows/dVVKSigningFlow_DEPRECATED.js";
import CardanoTxBodySignRequest from "../modules/tide-js/Models/Transactions/CardanoTxBodySignRequest.js"
import RuleSettingsSignRequest from "../modules/tide-js/Models/Rules/RuleSettingSignRequest.js"
import AuthorizationBuilder from "../modules/tide-js/Models/AuthorizationBuilder.js"
import { GenSessKey, GetPublic } from "../modules/tide-js/Cryptide/Math.js";
import NetworkClient from "../modules/tide-js/Clients/NetworkClient.js";
import { ModelRegistry } from "../modules/tide-js/Models/ModelRegistry.js";
import processThresholdRules from "../modules/tide-js/RulesEngine/thresholdRules.js";
import dVVKDecryptionFlow from "../modules/tide-js/Flow/DecryptionFlows/dVVKDecryptionFlow.js";
import dVVKSigningFlow from "../modules/tide-js/Flow/SigningFlows/dVVKSigningFlow.js";


// MODIFIED: Refactored `Keycloak` class into `TideCloak`.
function TideCloak (config) {    
    if (!(this instanceof TideCloak)) {
        throw new Error("The 'TideCloak' constructor must be invoked with 'new'.")
    }

    if (typeof config !== 'string' && !isObject(config)) {
        throw new Error("The 'TideCloak' constructor must be provided with a configuration object, or a URL to a JSON configuration file.");
    }

    if (isObject(config)) {
        const requiredProperties = 'oidcProvider' in config
            ? ['clientId']
            : ['url', 'realm', 'clientId', 'homeOrkUrl', 'vendorId', 'clientOriginAuth'];

        for (const property of requiredProperties) {
            if (!config[property]) {
                throw new Error(`The configuration object is missing the required '${property}' property.`);
            }
        }
    }

    var kc = this;
    var adapter;
    var refreshQueue = [];
    var callbackStorage;

    var loginIframe = {
        enable: true,
        callbackList: [],
        interval: 5
    };

    kc.didInitialize = false;

    var useNonce = true;
    var logInfo = createLogger(console.info);
    var logWarn = createLogger(console.warn);

    if (!globalThis.isSecureContext) {
        logWarn(
            "[TIDECLOAK] TideCloak-JS must be used in a 'secure context' to function properly as it relies on browser APIs that are otherwise not available.\n" +
            "Continuing to run your application insecurely will lead to unexpected behavior and breakage.\n\n" +
            "For more information see: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts"
        );
    }

    kc.init = function (initOptions = {}) {
        if (kc.didInitialize) {
            throw new Error("A 'TideCloak' instance can only be initialized once.");
        }

        kc.didInitialize = true;

        kc.authenticated = false;

        callbackStorage = createCallbackStorage();
        var adapters = ['default', 'cordova', 'cordova-native'];

        if (adapters.indexOf(initOptions.adapter) > -1) {
            adapter = loadAdapter(initOptions.adapter);
        } else if (typeof initOptions.adapter === "object") {
            adapter = initOptions.adapter;
        } else {
            if (window.Cordova || window.cordova) {
                adapter = loadAdapter('cordova');
            } else {
                adapter = loadAdapter();
            }
        }

        if (typeof initOptions.useNonce !== 'undefined') {
            useNonce = initOptions.useNonce;
        }

        if (typeof initOptions.checkLoginIframe !== 'undefined') {
            loginIframe.enable = initOptions.checkLoginIframe;
        }

        if (initOptions.checkLoginIframeInterval) {
            loginIframe.interval = initOptions.checkLoginIframeInterval;
        }

        if (initOptions.onLoad === 'login-required') {
            kc.loginRequired = true;
        }

        if (initOptions.responseMode) {
            if (initOptions.responseMode === 'query' || initOptions.responseMode === 'fragment') {
                kc.responseMode = initOptions.responseMode;
            } else {
                throw 'Invalid value for responseMode';
            }
        }

        if (initOptions.flow) {
            switch (initOptions.flow) {
                case 'standard':
                    kc.responseType = 'code';
                    break;
                case 'implicit':
                    kc.responseType = 'id_token token';
                    break;
                case 'hybrid':
                    kc.responseType = 'code id_token token';
                    break;
                default:
                    throw 'Invalid value for flow';
            }
            kc.flow = initOptions.flow;
        }

        if (initOptions.timeSkew != null) {
            kc.timeSkew = initOptions.timeSkew;
        }

        if (initOptions.redirectUri) {
            kc.redirectUri = initOptions.redirectUri;
        }

        if (initOptions.silentCheckSsoRedirectUri) {
            kc.silentCheckSsoRedirectUri = initOptions.silentCheckSsoRedirectUri;
        }

        if (typeof initOptions.silentCheckSsoFallback === 'boolean') {
            kc.silentCheckSsoFallback = initOptions.silentCheckSsoFallback;
        } else {
            kc.silentCheckSsoFallback = true;
        }

        if (typeof initOptions.pkceMethod !== "undefined") {
            if (initOptions.pkceMethod !== "S256" && initOptions.pkceMethod !== false) {
                throw new TypeError(`Invalid value for pkceMethod', expected 'S256' or false but got ${initOptions.pkceMethod}.`);
            }

            kc.pkceMethod = initOptions.pkceMethod;
        } else {
            kc.pkceMethod = "S256";
        }

        if (typeof initOptions.enableLogging === 'boolean') {
            kc.enableLogging = initOptions.enableLogging;
        } else {
            kc.enableLogging = false;
        }

        if (initOptions.logoutMethod === 'POST') {
            kc.logoutMethod = 'POST';
        } else {
            kc.logoutMethod = 'GET';
        }

        if (typeof initOptions.scope === 'string') {
            kc.scope = initOptions.scope;
        }

        if (typeof initOptions.acrValues === 'string') {
            kc.acrValues = initOptions.acrValues;
        }

        if (typeof initOptions.messageReceiveTimeout === 'number' && initOptions.messageReceiveTimeout > 0) {
            kc.messageReceiveTimeout = initOptions.messageReceiveTimeout;
        } else {
            kc.messageReceiveTimeout = 10000;
        }

        if (!kc.responseMode) {
            kc.responseMode = 'fragment';
        }
        if (!kc.responseType) {
            kc.responseType = 'code';
            kc.flow = 'standard';
        }

        var promise = createPromise();

        var initPromise = createPromise();
        initPromise.promise.then(function () {
            kc.onReady && kc.onReady(kc.authenticated);
            promise.setSuccess(kc.authenticated);
        }).catch(function (error) {
            promise.setError(error);
        });

        var configPromise = loadConfig();

        function onLoad() {

            var doLogin = function (prompt) {
                if (!prompt) {
                    options.prompt = 'none';
                }

                if (initOptions.locale) {
                    options.locale = initOptions.locale;
                }
                kc.login(options).then(function () {
                    initPromise.setSuccess();
                }).catch(function (error) {
                    initPromise.setError(error);
                });
            }

            var checkSsoSilently = async function () {
                var ifrm = document.createElement("iframe");
                var src = await kc.createLoginUrl({ prompt: 'none', redirectUri: kc.silentCheckSsoRedirectUri });
                ifrm.setAttribute("src", src);
                ifrm.setAttribute("sandbox", "allow-storage-access-by-user-activation allow-scripts allow-same-origin");
                ifrm.setAttribute("title", "keycloak-silent-check-sso");
                ifrm.style.display = "none";
                document.body.appendChild(ifrm);

                var messageCallback = function (event) {
                    if (event.origin !== window.location.origin || ifrm.contentWindow !== event.source) {
                        return;
                    }

                    var oauth = parseCallback(event.data);
                    processCallback(oauth, initPromise);

                    document.body.removeChild(ifrm);
                    window.removeEventListener("message", messageCallback);
                };

                window.addEventListener("message", messageCallback);
            };

            var options = {};
            switch (initOptions.onLoad) {
                case 'check-sso':
                    if (loginIframe.enable) {
                        setupCheckLoginIframe().then(function () {
                            checkLoginIframe().then(function (unchanged) {
                                if (!unchanged) {
                                    kc.silentCheckSsoRedirectUri ? checkSsoSilently() : doLogin(false);
                                } else {
                                    initPromise.setSuccess();
                                }
                            }).catch(function (error) {
                                initPromise.setError(error);
                            });
                        });
                    } else {
                        kc.silentCheckSsoRedirectUri ? checkSsoSilently() : doLogin(false);
                    }
                    break;
                case 'login-required':
                    doLogin(true);
                    break;
                default:
                    throw 'Invalid value for onLoad';
            }
        }

        function processInit() {
            var callback = parseCallback(window.location.href);

            if (callback) {
                window.history.replaceState(window.history.state, null, callback.newUrl);
            }

            if (callback && callback.valid) {
                return setupCheckLoginIframe().then(function () {
                    processCallback(callback, initPromise);
                }).catch(function (error) {
                    initPromise.setError(error);
                });
            }

            if (initOptions.token && initOptions.refreshToken) {
                setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken);

                if (loginIframe.enable) {
                    setupCheckLoginIframe().then(function () {
                        checkLoginIframe().then(function (unchanged) {
                            if (unchanged) {
                                kc.onAuthSuccess && kc.onAuthSuccess();
                                initPromise.setSuccess();
                                scheduleCheckIframe();
                            } else {
                                initPromise.setSuccess();
                            }
                        }).catch(function (error) {
                            initPromise.setError(error);
                        });
                    });
                } else {
                    kc.updateToken(-1).then(function () {
                        kc.onAuthSuccess && kc.onAuthSuccess();
                        initPromise.setSuccess();
                    }).catch(function (error) {
                        kc.onAuthError && kc.onAuthError();
                        if (initOptions.onLoad) {
                            onLoad();
                        } else {
                            initPromise.setError(error);
                        }
                    });
                }
            } else if (initOptions.onLoad) {
                onLoad();
            } else {
                initPromise.setSuccess();
            }
        }

        configPromise.then(function () {
            check3pCookiesSupported()
                .then(processInit)
                .catch(function (error) {
                    promise.setError(error);
                });
        });
        configPromise.catch(function (error) {
            promise.setError(error);
        });

        return promise.promise;
    }

    kc.login = function (options) {
        return adapter.login(options);
    }

    kc.ensureTokenReady = async function() {
        if(kc.isTokenExpired()){
            await kc.updateToken(-1);
        }
    }

    // MODIFIED: Added role-based encryption functionality.
    kc.encrypt = async function(toEncrypt){
        await kc.ensureTokenReady();
        // Check config
        if (!Array.isArray(toEncrypt)) {
            throw 'Pass array as parameter';
        }

        // Check user authenticated
        if (!kc.tokenParsed) {
            throw 'Not authenticated';
        }

        const dataToSend = toEncrypt.map(e => {
            if (!isObject(e)) throw 'All entries must be an object to encrypt';
            for (const property of ["data", "tags"]) {
                if (!e[property]) {
                    throw new Error(`The configuration object is missing the required '${property}' property.`);
                }
            }
            if (!Array.isArray(e.tags)) throw 'tags must be provided as a string array in object to encrypt';
            if (typeof e.data !== "string") throw 'data must be provded as string in object to encrypt';

            // Check that the user has the roles required to encrypt the datas
            for (const tag of e.tags) {
                if (typeof tag !== "string") throw "tags must be provided as an array of strings";
                var tagAccess = kc.hasRealmRole("_tide_" + tag + ".selfencrypt");
                if (!tagAccess) throw `'User has not been given any access to '${tag}'`;
            }
            return {
                data: StringToUint8Array(e.data),
                tags: e.tags
            }
        });

        kc.initEnclave();

        // Now lets actually encrypt
        // Construct Tide serialized data payloads
        return (await kc.requestEnclave.encrypt(dataToSend)).map(e => bytesToBase64(e));
    }

    function StringToUint8Array(string) {
        const enc = new TextEncoder();
        return enc.encode(string);
    }

    function StringFromUint8Array(bytes){
        const decoder = new TextDecoder('utf-8');
        return decoder.decode(bytes);
    }

    kc.initEnclave = function(){        
        if(!kc.doken) throw '[TIDECLOAK] No doken found';
        if(!kc.tokenParsed) throw '[TIDECLOAK] Token not parsed';
        // Now lets actually encrypt
        if(!kc.requestEnclave){
            kc.requestEnclave = new RequestEnclave({
                homeOrkOrigin: kc.dokenParsed["t.uho"],
                signed_client_origin: config['clientOriginAuth'],
                vendorId: config.vendorId,
                voucherURL: getVoucherUrl()
            }).init({
                doken: kc.doken,
                dokenRefreshCallback: async () => {
                    await kc.ensureTokenReady();
                    if(!kc.doken) throw '[TIDECLOAK] No doken found';
                    return kc.doken;
                },
                requireReloginCallback: async () => {
                    kc.login({
                        idpHint:   'tide',              // the “alias” of the IdP you’ve configured in the realm
                        prompt:    'login',                     // forces them to actually re-enter credentials
                        redirectUri: window.location.href       // send them back to the exact same URL
                    });
                }
            });
        }
    }

    // MODIFIED: Added Tide-based micro-vouchers.
    function getVoucherUrl(){
        if(!kc.tokenParsed) throw 'User authentication required to access voucher service';
        const sid = kc.tokenParsed["sid"];
        return getRealmUrl() + '/tidevouchers/fromUserSession?sessionId=' + sid;
    }

    // MODIFIED: Added role-based decryption functionality.
    kc.decrypt = async function(toDecrypt){
        await kc.ensureTokenReady();
        // Check config
        if (!Array.isArray(toDecrypt)) {
            throw 'Pass array as parameter';
        }

        // Check user authenticated
        if (!kc.tokenParsed) {
            throw 'Not authenticated';
        }

        const dataToSend = toDecrypt.map(e => {
            if (!isObject(e)) throw 'All entries must be an object to decrypt';
            for (const property of ["encrypted", "tags"]) {
                if (!e[property]) {
                    throw new Error(`The configuration object is missing the required '${property}' property.`);
                }
            }
            if (!Array.isArray(e.tags)) throw 'tags must be provided as a string array in object to decrypt';
            if (typeof e.encrypted !== "string") throw 'data must be provded as string in object to decrypt';

            // Check that the user has the roles required to encrypt the datas
            for (const tag of e.tags) {
                if (typeof tag !== "string") throw "tags must be provided as an array of strings";
                var tagAccess = kc.hasRealmRole("_tide_" + tag + ".selfdecrypt");
                if (!tagAccess) throw `'User has not been given any access to '${tag}'`;
            }
            return {
                encrypted: base64ToBytes(e.encrypted),
                tags: e.tags
            }
        });

        kc.initEnclave();

        // Now lets actually decrypt
        // Construct Tide serialized data payloads
        return (await kc.requestEnclave.decrypt(dataToSend)).map(d => StringFromUint8Array(d));
    }

    function generateRandomData(len) {
        if (typeof crypto === "undefined" || typeof crypto.getRandomValues === "undefined") {
            throw new Error("Web Crypto API is not available.");
        }

        return crypto.getRandomValues(new Uint8Array(len));
    }

    function generateCodeVerifier(len) {
        return generateRandomString(len, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');
    }

    function generateRandomString(len, alphabet) {
        var randomData = generateRandomData(len);
        var chars = new Array(len);
        for (var i = 0; i < len; i++) {
            chars[i] = alphabet.charCodeAt(randomData[i] % alphabet.length);
        }
        return String.fromCharCode.apply(null, chars);
    }

    async function generatePkceChallenge(pkceMethod, codeVerifier) {
        if (pkceMethod !== "S256") {
            throw new TypeError(`Invalid value for 'pkceMethod', expected 'S256' but got '${pkceMethod}'.`);
        }

        // hash codeVerifier, then encode as url-safe base64 without padding
        const hashBytes = new Uint8Array(await sha256Digest(codeVerifier));
        const encodedHash = bytesToBase64(hashBytes)
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/\=/g, '');

        return encodedHash;
    }

    function buildClaimsParameter(requestedAcr) {
        var claims = {
            id_token: {
                acr: requestedAcr
            }
        }
        return JSON.stringify(claims);
    }

    kc.createLoginUrl = async function (options) {
        var state = createUUID();
        var nonce = createUUID();

        var redirectUri = adapter.redirectUri(options);

        var callbackState = {
            state: state,
            nonce: nonce,
            redirectUri: encodeURIComponent(redirectUri),
            loginOptions: options
        };

        if (options && options.prompt) {
            callbackState.prompt = options.prompt;
        }

        var baseUrl;
        if (options && options.action == 'register') {
            baseUrl = kc.endpoints.register();
        } else {
            baseUrl = kc.endpoints.authorize();
        }

        var scope = options && options.scope || kc.scope;
        if (!scope) {
            // if scope is not set, default to "openid"
            scope = "openid";
        } else if (scope.indexOf("openid") === -1) {
            // if openid scope is missing, prefix the given scopes with it
            scope = "openid " + scope;
        }

        var url = baseUrl
            + '?client_id=' + encodeURIComponent(kc.clientId)
            + '&redirect_uri=' + encodeURIComponent(redirectUri)
            + '&state=' + encodeURIComponent(state)
            + '&response_mode=' + encodeURIComponent(kc.responseMode)
            + '&response_type=' + encodeURIComponent(kc.responseType)
            + '&scope=' + encodeURIComponent(scope);
        if (useNonce) {
            url = url + '&nonce=' + encodeURIComponent(nonce);
        }

        if (options && options.prompt) {
            url += '&prompt=' + encodeURIComponent(options.prompt);
        }

        if (options && typeof options.maxAge === 'number') {
            url += '&max_age=' + encodeURIComponent(options.maxAge);
        }

        if (options && options.loginHint) {
            url += '&login_hint=' + encodeURIComponent(options.loginHint);
        }

        if (options && options.idpHint) {
            url += '&kc_idp_hint=' + encodeURIComponent(options.idpHint);
        }

        if (options && options.action && options.action != 'register') {
            url += '&kc_action=' + encodeURIComponent(options.action);
        }

        if (options && options.locale) {
            url += '&ui_locales=' + encodeURIComponent(options.locale);
        }

        if (options && options.acr) {
            var claimsParameter = buildClaimsParameter(options.acr);
            url += '&claims=' + encodeURIComponent(claimsParameter);
        }

        if ((options && options.acrValues) || kc.acrValues) {
            url += '&acr_values=' + encodeURIComponent(options.acrValues || kc.acrValues);
        }

        if (kc.pkceMethod) {
            try {
                const codeVerifier = generateCodeVerifier(96);
                const pkceChallenge = await generatePkceChallenge(kc.pkceMethod, codeVerifier);

                callbackState.pkceCodeVerifier = codeVerifier;

                url += '&code_challenge=' + pkceChallenge;
                url += '&code_challenge_method=' + kc.pkceMethod;
            } catch (error) {
                throw new Error("Failed to generate PKCE challenge.", { cause: error });
            }
        }

        callbackStorage.add(callbackState);

        return url;
    }

    kc.logout = function (options) {
        return adapter.logout(options);
    }

    kc.createLogoutUrl = function (options) {

        const logoutMethod = options?.logoutMethod ?? kc.logoutMethod;
        if (logoutMethod === 'POST') {
            return kc.endpoints.logout();
        }

        var url = kc.endpoints.logout()
            + '?client_id=' + encodeURIComponent(kc.clientId)
            + '&post_logout_redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false));

        if (kc.idToken) {
            url += '&id_token_hint=' + encodeURIComponent(kc.idToken);
        }

        return url;
    }

    kc.register = function (options) {
        return adapter.register(options);
    }

    kc.createRegisterUrl = async function (options) {
        if (!options) {
            options = {};
        }
        options.action = 'register';
        return await kc.createLoginUrl(options);
    }

    kc.createAccountUrl = function (options) {
        var realm = getRealmUrl();
        var url = undefined;
        if (typeof realm !== 'undefined') {
            url = realm
                + '/account'
                + '?referrer=' + encodeURIComponent(kc.clientId)
                + '&referrer_uri=' + encodeURIComponent(adapter.redirectUri(options));
        }
        return url;
    }

    kc.accountManagement = function () {
        return adapter.accountManagement();
    }

    kc.hasRealmRole = function (role) {
        var access = kc.realmAccess;
        return !!access && access.roles.indexOf(role) >= 0;
    }

    kc.hasResourceRole = function (role, resource) {
        if (!kc.resourceAccess) {
            return false;
        }

        var access = kc.resourceAccess[resource || kc.clientId];
        return !!access && access.roles.indexOf(role) >= 0;
    }

    kc.loadUserProfile = function () {
        var url = getRealmUrl() + '/account';
        var req = new XMLHttpRequest();
        req.open('GET', url, true);
        req.setRequestHeader('Accept', 'application/json');
        req.setRequestHeader('Authorization', 'bearer ' + kc.token);

        var promise = createPromise();

        req.onreadystatechange = function () {
            if (req.readyState == 4) {
                if (req.status == 200) {
                    kc.profile = JSON.parse(req.responseText);
                    promise.setSuccess(kc.profile);
                } else {
                    promise.setError();
                }
            }
        }

        req.send();

        return promise.promise;
    }

    kc.loadUserInfo = function () {
        var url = kc.endpoints.userinfo();
        var req = new XMLHttpRequest();
        req.open('GET', url, true);
        req.setRequestHeader('Accept', 'application/json');
        req.setRequestHeader('Authorization', 'bearer ' + kc.token);

        var promise = createPromise();

        req.onreadystatechange = function () {
            if (req.readyState == 4) {
                if (req.status == 200) {
                    kc.userInfo = JSON.parse(req.responseText);
                    promise.setSuccess(kc.userInfo);
                } else {
                    promise.setError();
                }
            }
        }

        req.send();

        return promise.promise;
    }

    kc.isTokenExpired = function (minValidity) {
        if (!kc.tokenParsed || (!kc.refreshToken && kc.flow != 'implicit')) {
            throw 'Not authenticated';
        }

        if (kc.timeSkew == null) {
            logInfo('[TIDECLOAK] Unable to determine if token is expired as timeskew is not set');
            return true;
        }

        var expiresIn = kc.tokenParsed['exp'] - Math.ceil(new Date().getTime() / 1000) + kc.timeSkew;
        if (minValidity) {
            if (isNaN(minValidity)) {
                throw 'Invalid minValidity';
            }
            expiresIn -= minValidity;
        }
        return expiresIn < 0;
    }

    kc.updateToken = function (minValidity) {
        var promise = createPromise();

        if (!kc.refreshToken) {
            promise.setError();
            return promise.promise;
        }

        minValidity = minValidity || 5;

        var exec = function () {
            var refreshToken = false;
            if (minValidity == -1) {
                refreshToken = true;
                logInfo('[TIDECLOAK] Refreshing token: forced refresh');
            } else if (!kc.tokenParsed || kc.isTokenExpired(minValidity)) {
                refreshToken = true;
                logInfo('[TIDECLOAK] Refreshing token: token expired');
            }

            if (!refreshToken) {
                promise.setSuccess(false);
            } else {
                var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken;
                var url = kc.endpoints.token();

                refreshQueue.push(promise);

                if (refreshQueue.length == 1) {
                    var req = new XMLHttpRequest();
                    req.open('POST', url, true);
                    req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
                    req.withCredentials = true;

                    params += '&client_id=' + encodeURIComponent(kc.clientId);

                    var timeLocal = new Date().getTime();

                    req.onreadystatechange = function () {
                        if (req.readyState == 4) {
                            if (req.status == 200) {
                                logInfo('[TIDECLOAK] Token refreshed');

                                timeLocal = (timeLocal + new Date().getTime()) / 2;

                                var tokenResponse = JSON.parse(req.responseText);

                                setToken(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], timeLocal, tokenResponse['doken']);

                                kc.onAuthRefreshSuccess && kc.onAuthRefreshSuccess();
                                for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) {
                                    p.setSuccess(true);
                                }
                            } else {
                                logWarn('[TIDECLOAK] Failed to refresh token');

                                if (req.status == 400) {
                                    kc.clearToken();
                                }

                                if (req.status == 500) {
                                    // Check to see if error message tells us to reauthenticate the user
                                    console.log("CHECKING REAUTH");
                                }

                                kc.onAuthRefreshError && kc.onAuthRefreshError();
                                for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) {
                                    p.setError("Failed to refresh token: An unexpected HTTP error occurred while attempting to refresh the token.");
                                }
                            }
                        }
                    };

                    req.send(params);
                }
            }
        }

        if (loginIframe.enable) {
            var iframePromise = checkLoginIframe();
            iframePromise.then(function () {
                exec();
            }).catch(function (error) {
                promise.setError(error);
            });
        } else {
            exec();
        }

        return promise.promise;
    }

    kc.clearToken = function () {
        if (kc.token) {
            setToken(null, null, null);
            kc.onAuthLogout && kc.onAuthLogout();
            if (kc.loginRequired) {
                kc.login();
            }
        }
    }

    // Add the checkThresholdRule function to the Heimdall instance.
    // This function calls the generic threshold rule processor from the thresholdRules module.
    kc.checkThresholdRule = function (key, idSubstring, outputKey, ruleSettings, draftJson) {
        // Process the threshold rules using the provided parameters and return the result.
        return processThresholdRules(key, idSubstring, outputKey, ruleSettings, draftJson);
    };
    kc.createCardanoTxDraft = function (txBody) {
        const txBodyBytes = base64ToBytes(txBody);
        return bytesToBase64(CreateTideMemory(txBodyBytes, txBodyBytes.length + 4));
    }

    kc.sign = async function (signModel, authFlow, draft, authorizers, ruleSetting, expiry){
        await kc.ensureTokenReady();
        const signModelId = signModel.split(":")
        if (signModelId.length !== 2 || !signModelId[0] || !signModelId[1]) {
            throw "SignModel is not in the correct format. Expected format: 'ModelName:Version' (e.g. 'UserContext:1').";
        }

        const authFlowId = authFlow.split(":")
        if (authFlowId.length !== 2 || !authFlowId[0] || !authFlowId[1]) {
            throw "AuthFlow is not in the correct format. Expected format: 'ModelName:Version' (e.g. 'VRK:1').";
        }
        
        const sessKey = GenSessKey();
        const gSessKey = GetPublic(sessKey);
        const vvkInfo = await new NetworkClient(config.homeOrkUrl).GetKeyInfo(config.vendorId);;
  
        // Check user authenticated
        if (!kc.tokenParsed) {
            throw 'Not authenticated';
        }

        // Check config
        if (!Array.isArray(authorizers)) {
            throw 'Pass authorizers in an array!';
        }

        const signRequest = new BaseTideRequest(signModelId[0], signModel[1], authFlow, draft);
        if(expiry) signRequest.setCustomExpiry(expiry);
        new AuthorizationBuilder(signRequest, authorizers, ruleSetting).addAuthorization();

        const signingFlow = new dVVKSigningFlow_DEPRECATED(
            config.vendorId,
            vvkInfo.UserPublic,
            vvkInfo.OrkInfo,
            sessKey,
            gSessKey,
            getVoucherUrl(),
        );

        const result = (await signingFlow.start(signRequest));
        return result;
    }

    kc.signCardanoTx = async function (txBody, authorizers, ruleSettings, expiry) {
        await kc.ensureTokenReady();
        const sessKey = GenSessKey();
        const gSessKey = GetPublic(sessKey);
    
        const vvkInfo = await new NetworkClient(config.homeOrkUrl).GetKeyInfo(config.vendorId);;
  
        // Check user authenticated
        if (!kc.tokenParsed) {
            throw 'Not authenticated';
        }

        // Check config
        if (!Array.isArray(authorizers)) {
            throw 'Pass authorizers in an array!';
        }

        const cardanoSignRequest = new CardanoTxBodySignRequest("BlindSig:1");
        cardanoSignRequest.setTxBody(txBody);
        cardanoSignRequest.serializeDraft();

        new AuthorizationBuilder(cardanoSignRequest, authorizers, ruleSettings).addAuthorization();
        cardanoSignRequest.setCustomExpiry(expiry);

        const txSigningFlow = new dVVKSigningFlow_DEPRECATED(
            config.vendorId,
            vvkInfo.UserPublic,
            vvkInfo.OrkInfo,
            sessKey,
            gSessKey,
            getVoucherUrl(),
        );

        const result = (await txSigningFlow.start(cardanoSignRequest));
        return bytesToBase64(result[0])
    }

    kc.createRuleSettingsDraft = function (ruleSettings, previousRuleSetting, previousRuleSettingCert) {
        const ruleReqDraft = new RuleSettingsSignRequest("Admin:1")
        ruleReqDraft.setNewRuleSetting(StringToUint8Array(ruleSettings));
        if(previousRuleSetting !== undefined && previousRuleSettingCert !== undefined) {
            ruleReqDraft.setPreviousRuleSetting(StringToUint8Array(previousRuleSetting))
            ruleReqDraft.setPreviousRuleSettingCert(base64ToBytes(previousRuleSettingCert))
        }

        return bytesToBase64(ruleReqDraft.getDraft());
    }


    function getRealmUrl() {
        if (typeof kc.authServerUrl !== 'undefined') {
            if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') {
                return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm);
            } else {
                return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm);
            }
        } else {
            return undefined;
        }
    }

    function getOrigin() {
        if (!window.location.origin) {
            return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
        } else {
            return window.location.origin;
        }
    }

    function processCallback(oauth, promise) {
        var code = oauth.code;
        var error = oauth.error;
        var prompt = oauth.prompt;

        var timeLocal = new Date().getTime();

        if (oauth['kc_action_status']) {
            kc.onActionUpdate && kc.onActionUpdate(oauth['kc_action_status'], oauth['kc_action']);
        }

        if (error) {
            if (prompt != 'none') {
                if (oauth.error_description && oauth.error_description === "authentication_expired") {
                    kc.login(oauth.loginOptions);
                } else {
                    var errorData = { error: error, error_description: oauth.error_description };
                    kc.onAuthError && kc.onAuthError(errorData);
                    promise && promise.setError(errorData);
                }
            } else {
                promise && promise.setSuccess();
            }
            return;
        } else if ((kc.flow != 'standard') && (oauth.access_token || oauth.id_token)) {
            authSuccess(oauth.access_token, null, oauth.id_token, true, oauth.doken);
        }

        if ((kc.flow != 'implicit') && code) {
            var params = 'code=' + code + '&grant_type=authorization_code';
            var url = kc.endpoints.token();

            var req = new XMLHttpRequest();
            req.open('POST', url, true);
            req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');

            params += '&client_id=' + encodeURIComponent(kc.clientId);
            params += '&redirect_uri=' + oauth.redirectUri;

            if (oauth.pkceCodeVerifier) {
                params += '&code_verifier=' + oauth.pkceCodeVerifier;
            }

            req.withCredentials = true;

            req.onreadystatechange = function () {
                if (req.readyState == 4) {
                    if (req.status == 200) {

                        var tokenResponse = JSON.parse(req.responseText);
                        authSuccess(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], kc.flow === 'standard', tokenResponse['doken']); // added doken field
                        scheduleCheckIframe();
                    } else {

                        if (req.status == 500) {
                            // Check to see if error message tells us to reauthenticate the user
                            console.log("CHECKING REAUTH");
                        }

                        kc.onAuthError && kc.onAuthError();
                        promise && promise.setError();
                    }
                }
            };

            req.onerror = function() {
                // Try to log the user in again
                kc.login({
                    idpHint:   'tide',             
                    prompt:    'login',                     // forces them to actually re-enter credentials
                    redirectUri: window.location.href       // send them back to the exact same URL
                });
            }

            req.send(params);
        }

        function authSuccess(accessToken, refreshToken, idToken, fulfillPromise, doken=null) {
            timeLocal = (timeLocal + new Date().getTime()) / 2;

            setToken(accessToken, refreshToken, idToken, timeLocal, doken);

            if (useNonce && (kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce)) {
                logInfo('[TIDECLOAK] Invalid nonce, clearing token');
                kc.clearToken();
                promise && promise.setError();
            } else {
                if (fulfillPromise) {
                    kc.onAuthSuccess && kc.onAuthSuccess();
                    promise && promise.setSuccess();
                }
            }
        }

    }

    function loadConfig() {
        var promise = createPromise();
        var configUrl;

        if (typeof config === 'string') {
            configUrl = config;
        }

        function setupOidcEndoints(oidcConfiguration) {
            if (!oidcConfiguration) {
                kc.endpoints = {
                    authorize: function () {
                        return getRealmUrl() + '/protocol/openid-connect/auth';
                    },
                    token: function () {
                        return getRealmUrl() + '/protocol/openid-connect/token';
                    },
                    logout: function () {
                        return getRealmUrl() + '/protocol/openid-connect/logout';
                    },
                    checkSessionIframe: function () {
                        return getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html';
                    },
                    thirdPartyCookiesIframe: function () {
                        return getRealmUrl() + '/protocol/openid-connect/3p-cookies/step1.html';
                    },
                    register: function () {
                        return getRealmUrl() + '/protocol/openid-connect/registrations';
                    },
                    userinfo: function () {
                        return getRealmUrl() + '/protocol/openid-connect/userinfo';
                    }
                };
            } else {
                kc.endpoints = {
                    authorize: function () {
                        return oidcConfiguration.authorization_endpoint;
                    },
                    token: function () {
                        return oidcConfiguration.token_endpoint;
                    },
                    logout: function () {
                        if (!oidcConfiguration.end_session_endpoint) {
                            throw "Not supported by the OIDC server";
                        }
                        return oidcConfiguration.end_session_endpoint;
                    },
                    checkSessionIframe: function () {
                        if (!oidcConfiguration.check_session_iframe) {
                            throw "Not supported by the OIDC server";
                        }
                        return oidcConfiguration.check_session_iframe;
                    },
                    register: function () {
                        throw 'Redirection to "Register user" page not supported in standard OIDC mode';
                    },
                    userinfo: function () {
                        if (!oidcConfiguration.userinfo_endpoint) {
                            throw "Not supported by the OIDC server";
                        }
                        return oidcConfiguration.userinfo_endpoint;
                    }
                }
            }
        }

        if (configUrl) {
            var req = new XMLHttpRequest();
            req.open('GET', configUrl, true);
            req.setRequestHeader('Accept', 'application/json');

            req.onreadystatechange = function () {
                if (req.readyState == 4) {
                    if (req.status == 200 || fileLoaded(req)) {
                        var config = JSON.parse(req.responseText);

                        kc.authServerUrl = config['auth-server-url'];
                        kc.realm = config['realm'];
                        kc.clientId = config['resource'];
                        setupOidcEndoints(null);
                        promise.setSuccess();
                    } else {
                        promise.setError();
                    }
                }
            };

            req.send();
        } else {
            kc.clientId = config.clientId;

            var oidcProvider = config['oidcProvider'];
            if (!oidcProvider) {
                kc.authServerUrl = config.url;
                kc.realm = config.realm;
                setupOidcEndoints(null);
                promise.setSuccess();
            } else {
                if (typeof oidcProvider === 'string') {
                    var oidcProviderConfigUrl;
                    if (oidcProvider.charAt(oidcProvider.length - 1) == '/') {
                        oidcProviderConfigUrl = oidcProvider + '.well-known/openid-configuration';
                    } else {
                        oidcProviderConfigUrl = oidcProvider + '/.well-known/openid-configuration';
                    }
                    var req = new XMLHttpRequest();
                    req.open('GET', oidcProviderConfigUrl, true);
                    req.setRequestHeader('Accept', 'application/json');

                    req.onreadystatechange = function () {
                        if (req.readyState == 4) {
                            if (req.status == 200 || fileLoaded(req)) {
                                var oidcProviderConfig = JSON.parse(req.responseText);
                                setupOidcEndoints(oidcProviderConfig);
                                promise.setSuccess();
                            } else {
                                promise.setError();
                            }
                        }
                    };

                    req.send();
                } else {
                    setupOidcEndoints(oidcProvider);
                    promise.setSuccess();
                }
            }
        }

        return promise.promise;
    }

    function fileLoaded(xhr) {
        return xhr.status == 0 && xhr.responseText && xhr.responseURL.startsWith('file:');
    }

    function setToken(token, refreshToken, idToken, timeLocal, doken=null) {
        if (kc.tokenTimeoutHandle) {
            clearTimeout(kc.tokenTimeoutHandle);
            kc.tokenTimeoutHandle = null;
        }

        if (refreshToken) {
            kc.refreshToken = refreshToken;
            kc.refreshTokenParsed = decodeToken(refreshToken);
        } else {
            delete kc.refreshToken;
            delete kc.refreshTokenParsed;
        }

        if (idToken) {
            kc.idToken = idToken;
            kc.idTokenParsed = decodeToken(idToken);
        } else {
            delete kc.idToken;
            delete kc.idTokenParsed;
        }

        if (token) {
            kc.token = token;
            kc.tokenParsed = decodeToken(token);
            kc.sessionId = kc.tokenParsed.sid;
            kc.authenticated = true;
            kc.subject = kc.tokenParsed.sub;
            kc.realmAccess = kc.tokenParsed.realm_access;
            kc.resourceAccess = kc.tokenParsed.resource_access;

            if (timeLocal) {
                kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat;
            }

            if (kc.timeSkew != null) {
                logInfo('[TIDECLOAK] Estimated time difference between browser and server is ' + kc.timeSkew + ' seconds');

                if (kc.onTokenExpired) {
                    var expiresIn = (kc.tokenParsed['exp'] - (new Date().getTime() / 1000) + kc.timeSkew) * 1000;
                    logInfo('[TIDECLOAK] Token expires in ' + Math.round(expiresIn / 1000) + ' s');
                    if (expiresIn <= 0) {
                        kc.onTokenExpired();
                    } else {
                        kc.tokenTimeoutHandle = setTimeout(kc.onTokenExpired, expiresIn);
                    }
                }
            }
        } else {
            delete kc.token;
            delete kc.tokenParsed;
            delete kc.subject;
            delete kc.realmAccess;
            delete kc.resourceAccess;

            kc.authenticated = false;
        }

        if(doken){
            kc.doken = doken;
            kc.dokenParsed = decodeToken(doken);

            // update heimdall's doken too
            if(kc.requestEnclave) kc.requestEnclave.updateDoken(kc.doken);
        }else{
            delete kc.doken;
        }
    }

    function createUUID() {
        if (typeof crypto === "undefined" || typeof crypto.randomUUID === "undefined") {
            throw new Error("Web Crypto API is not available.");
        }

        return crypto.randomUUID();
    }

    function parseCallback(url) {
        var oauth = parseCallbackUrl(url);
        if (!oauth) {
            return;
        }

        var oauthState = callbackStorage.get(oauth.state);

        if (oauthState) {
            oauth.valid = true;
            oauth.redirectUri = oauthState.redirectUri;
            oauth.storedNonce = oauthState.nonce;
            oauth.prompt = oauthState.prompt;
            oauth.pkceCodeVerifier = oauthState.pkceCodeVerifier;
            oauth.loginOptions = oauthState.loginOptions;
        }

        return oauth;
    }

    function parseCallbackUrl(url) {
        var supportedParams;
        switch (kc.flow) {
            case 'standard':
                supportedParams = ['code', 'state', 'session_state', 'kc_action_status', 'kc_action', 'iss'];
                break;
            case 'implicit':
                supportedParams = ['access_token', 'token_type', 'id_token', 'state', 'session_state', 'expires_in', 'kc_action_status', 'kc_action', 'iss'];
                break;
            case 'hybrid':
                supportedParams = ['access_token', 'token_type', 'id_token', 'code', 'state', 'session_state', 'expires_in', 'kc_action_status', 'kc_action', 'iss'];
                break;
        }

        supportedParams.push('error');
        supportedParams.push('error_description');
        supportedParams.push('error_uri');

        var queryIndex = url.indexOf('?');
        var fragmentIndex = url.indexOf('#');

        var newUrl;
        var parsed;

        if (kc.responseMode === 'query' && queryIndex !== -1) {
            newUrl = url.substring(0, queryIndex);
            parsed = parseCallbackParams(url.substring(queryIndex + 1, fragmentIndex !== -1 ? fragmentIndex : url.length), supportedParams);
            if (parsed.paramsString !== '') {
                newUrl += '?' + parsed.paramsString;
            }
            if (fragmentIndex !== -1) {
                newUrl += url.substring(fragmentIndex);
            }
        } else if (kc.responseMode === 'fragment' && fragmentIndex !== -1) {
            newUrl = url.substring(0, fragmentIndex);
            parsed = parseCallbackParams(url.substring(fragmentIndex + 1), supportedParams);
            if (parsed.paramsString !== '') {
                newUrl += '#' + parsed.paramsString;
            }
        }

        if (parsed && parsed.oauthParams) {
            if (kc.flow === 'standard' || kc.flow === 'hybrid') {
                if ((parsed.oauthParams.code || parsed.oauthParams.error) && parsed.oauthParams.state) {
                    parsed.oauthParams.newUrl = newUrl;
                    return parsed.oauthParams;
                }
            } else if (kc.flow === 'implicit') {
                if ((parsed.oauthParams.access_token || parsed.oauthParams.error) && parsed.oauthParams.state) {
                    parsed.oauthParams.newUrl = newUrl;
                    return parsed.oauthParams;
                }
            }
        }
    }

    function parseCallbackParams(paramsString, supportedParams) {
        var p = paramsString.split('&');
        var result = {
            paramsString: '',
            oauthParams: {}
        }
        for (var i = 0; i < p.length; i++) {
            var split = p[i].indexOf("=");
            var key = p[i].slice(0, split);
            if (supportedParams.indexOf(key) !== -1) {
                result.oauthParams[key] = p[i].slice(split + 1);
            } else {
                if (result.paramsString !== '') {
                    result.paramsString += '&';
                }
                result.paramsString += p[i];
            }
        }
        return result;
    }

    function createPromise() {
        // Need to create a native Promise which also preserves the
        // interface of the custom promise type previously used by the API
        var p = {
            setSuccess: function (result) {
                p.resolve(result);
            },

            setError: function (result) {
                p.reject(result);
            }
        };
        p.promise = new Promise(function (resolve, reject) {
            p.resolve = resolve;
            p.reject = reject;
        });

        return p;
    }

    // Function to extend existing native Promise with timeout
    function applyTimeoutToPromise(promise, timeout, errorMessage) {
        var timeoutHandle = null;
        var timeoutPromise = new Promise(function (resolve, reject) {
            timeoutHandle = setTimeout(function () {
                reject({ "error": errorMessage || "Promise is not settled within timeout of " + timeout + "ms" });
            }, timeout);
        });

        return Promise.race([promise, timeoutPromise]).finally(function () {
            clearTimeout(timeoutHandle);
        });
    }

    function setupCheckLoginIframe() {
        var promise = createPromise();

        if (!loginIframe.enable) {
            promise.setSuccess();
            return promise.promise;
        }

        if (loginIframe.iframe) {
            promise.setSuccess();
            return promise.promise;
        }

        var iframe = document.createElement('iframe');
        loginIframe.iframe = iframe;

        iframe.onload = function () {
            var authUrl = kc.endpoints.authorize();
            if (authUrl.charAt(0) === '/') {
                loginIframe.iframeOrigin = getOrigin();
            } else {
                loginIframe.iframeOrigin = authUrl.substring(0, authUrl.indexOf('/', 8));
            }
            promise.setSuccess();
        }

        var src = kc.endpoints.checkSessionIframe();
        iframe.setAttribute('src', src);
        iframe.setAttribute('sandbox', 'allow-storage-access-by-user-activation allow-scripts allow-same-origin');
        iframe.setAttribute('title', 'keycloak-session-iframe');
        iframe.style.display = 'none';
        document.body.appendChild(iframe);

        var messageCallback = function (event) {
            if ((event.origin !== loginIframe.iframeOrigin) || (loginIframe.iframe.contentWindow !== event.source)) {
                return;
            }

            if (!(event.data == 'unchanged' || event.data == 'changed' || event.data == 'error')) {
                return;
            }


            if (event.data != 'unchanged') {
                kc.clearToken();
            }

            var callbacks = loginIframe.callbackList.splice(0, loginIframe.callbackList.length);

            for (var i = callbacks.length - 1; i >= 0; --i) {
                var promise = callbacks[i];
                if (event.data == 'error') {
                    promise.setError();
                } else {
                    promise.setSuccess(event.data == 'unchanged');
                }
            }
        };

        window.addEventListener('message', messageCallback, false);

        return promise.promise;
    }

    function scheduleCheckIframe() {
        if (loginIframe.enable) {
            if (kc.token) {
                setTimeout(function () {
                    checkLoginIframe().then(function (unchanged) {
                        if (unchanged) {
                            scheduleCheckIframe();
                        }
                    });
                }, loginIframe.interval * 1000);
            }
        }
    }

    function checkLoginIframe() {
        var promise = createPromise();

        if (loginIframe.iframe && loginIframe.iframeOrigin) {
            var msg = kc.clientId + ' ' + (kc.sessionId ? kc.sessionId : '');
            loginIframe.callbackList.push(promise);
            var origin = loginIframe.iframeOrigin;
            if (loginIframe.callbackList.length == 1) {
                loginIframe.iframe.contentWindow.postMessage(msg, origin);
            }
        } else {
            promise.setSuccess();
        }

        return promise.promise;
    }

    function check3pCookiesSupported() {
        var promise = createPromise();

        if ((loginIframe.enable || kc.silentCheckSsoRedirectUri) && typeof kc.endpoints.thirdPartyCookiesIframe === 'function') {
            var iframe = document.createElement('iframe');
            iframe.setAttribute('src', kc.endpoints.thirdPartyCookiesIframe());
            iframe.setAttribute('sandbox', 'allow-storage-access-by-user-activation allow-scripts allow-same-origin');
            iframe.setAttribute('title', 'keycloak-3p-check-iframe');
            iframe.style.display = 'none';
            document.body.appendChild(iframe);

            var messageCallback = function (event) {
                if (iframe.contentWindow !== event.source) {
                    return;
                }

                if (event.data !== "supported" && event.data !== "unsupported") {
                    return;
                } else if (event.data === "unsupported") {
                    logWarn(
                        "[TIDECLOAK] Your browser is blocking access to 3rd-party cookies, this means:\n\n" +
                        " - It is not possible to retrieve tokens without redirecting to the TideCloak server (a.k.a. no support for silent authentication).\n" +
                        " - It is not possible to automatically detect changes to the session status (such as the user logging out in another tab).\n\n" +
                        "For more information see: https://www.keycloak.org/securing-apps/javascript-adapter#_modern_browsers"
                    );

                    loginIframe.enable = false;
                    if (kc.silentCheckSsoFallback) {
                        kc.silentCheckSsoRedirectUri = false;
                    }
                }

                document.body.removeChild(iframe);
                window.removeEventListener("message", messageCallback);
                promise.setSuccess();
            };

            window.addEventListener('message', messageCallback, false);
        } else {
            promise.setSuccess();
        }

        return applyTimeoutToPromise(promise.promise, kc.messageReceiveTimeout, "Timeout when waiting for 3rd party check iframe message.");
    }

    function loadAdapter(type) {
        if (!type || type == 'default') {
            return {
                login: async function (options) {
                    window.location.assign(await kc.createLoginUrl(options));
                    return createPromise().promise;
                },

                logout: async function (options) {

                    const logoutMethod = options?.logoutMethod ?? kc.logoutMethod;
                    if (logoutMethod === "GET") {
                        window.location.replace(kc.createLogoutUrl(options));
                        return;
                    }

                    // Create form to send POST request.
                    const form = document.createElement("form");
                    
                    form.setAttribute("method", "POST");
                    form.setAttribute("action", kc.createLogoutUrl(options));
                    form.style.display = "none";

                    // Add data to form as hidden input fields.
                    const data = {
                        id_token_hint: kc.idToken,
                        client_id: kc.clientId,
                        post_logout_redirect_uri: adapter.redirectUri(options, false)
                    };

                    for (const [name, value] of Object.entries(data)) {
                        const input = document.createElement("input");
                        
                        input.setAttribute("type", "hidden");
                        input.setAttribute("name", name);
                        input.setAttribute("value", value);
                        
                        form.appendChild(input);
                    }

                    // Append form to page and submit it to perform logout and redirect.
                    document.body.appendChild(form);
                    form.submit();
                },

                register: async function (options) {
                    window.location.assign(await kc.createRegisterUrl(options));
                    return createPromise().promise;
                },

                accountManagement: function () {
                    var accountUrl = kc.createAccountUrl();
                    if (typeof accountUrl !== 'undefined') {
                        window.location.href = accountUrl;
                    } else {
                        throw "Not supported by the OIDC server";
                    }
                    return createPromise().promise;
                },

                redirectUri: function (options, encodeHash) {
                    if (arguments.length == 1) {
                        encodeHash = true;
                    }

                    if (options && options.redirectUri) {
                        return options.redirectUri;
                    } else if (kc.redirectUri) {
                        return kc.redirectUri;
                    } else {
                        return location.href;
                    }
                }
            };
        }

        if (type == 'cordova') {
            loginIframe.enable = false;
            var cordovaOpenWindowWrapper = function (loginUrl, target, options) {
                if (window.cordova && window.cordova.InAppBrowser) {
                    // Use inappbrowser for IOS and Android if available
                    return window.cordova.InAppBrowser.open(loginUrl, target, options);
                } else {
                    return window.open(loginUrl, target, options);
                }
            };

            var shallowCloneCordovaOptions = function (userOptions) {
                if (userOptions && userOptions.cordovaOptions) {
                    return Object.keys(userOptions.cordovaOptions).reduce(function (options, optionName) {
                        options[optionName] = userOptions.cordovaOptions[optionName];
                        return options;
                    }, {});
                } else {
                    return {};
                }
            };

            var formatCordovaOptions = function (cordovaOptions) {
                return Object.keys(cordovaOptions).reduce(function (options, optionName) {
                    options.push(optionName + "=" + cordovaOptions[optionName]);
                    return options;
                }, []).join(",");
            };

            var createCordovaOptions = function (userOptions) {
                var cordovaOptions = shallowCloneCordovaOptions(userOptions);
                cordovaOptions.location = 'no';
                if (userOptions && userOptions.prompt == 'none') {
                    cordovaOptions.hidden = 'yes';
                }
                return formatCordovaOptions(cordovaOptions);
            };

            var getCordovaRedirectUri = function () {
                return kc.redirectUri || 'http://localhost';
            }

            return {
                login: async function (options) {
                    var promise = createPromise();

                    var cordovaOptions = createCordovaOptions(options);
                    var loginUrl = await kc.createLoginUrl(options);
                    var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', cordovaOptions);
                    var completed = false;

                    var closed = false;
                    var closeBrowser = function () {
                        closed = true;
                        ref.close();
                    };

                    ref.addEventListener('loadstart', function (event) {
                        if (event.url.indexOf(getCordovaRedirectUri()) == 0) {
                            var callback = parseCallback(event.url);
                            processCallback(callback, promise);
                            closeBrowser();
                            completed = true;
                        }
                    });

                    ref.addEventListener('loaderror', function (event) {
                        if (!completed) {
                            if (event.url.indexOf(getCordovaRedirectUri()) == 0) {
                                var callback = parseCallback(event.url);
                                processCallback(callback, promise);
                                closeBrowser();
                                completed = true;
                            } else {
                                promise.setError();
                                closeBrowser();
                            }
                        }
                    });

                    ref.addEventListener('exit', function (event) {
                        if (!closed) {
                            promise.setError({
                                reason: "closed_by_user"
                            });
                        }
                    });

                    return promise.promise;
                },

                logout: function (options) {
                    var promise = createPromise();

                    var logoutUrl = kc.createLogoutUrl(options);
                    var ref = cordovaOpenWindowWrapper(logoutUrl, '_blank', 'location=no,hidden=yes,clearcache=yes');

                    var error;

                    ref.addEventListener('loadstart', function (event) {
                        if (event.url.indexOf(getCordovaRedirectUri()) == 0) {
                            ref.close();
                        }
                    });

                    ref.addEventListener('loaderror', function (event) {
                        if (event.url.indexOf(getCordovaRedirectUri()) == 0) {
                            ref.close();
                        } else {
                            error = true;
                            ref.close();
                        }
                    });

                    ref.addEventListener('exit', function (event) {
                        if (error) {
                            promise.setError();
                        } else {
                            kc.clearToken();
                            promise.setSuccess();
                        }
                    });

                    return promise.promise;
                },

                register: async function (options) {
                    var promise = createPromise();
                    var registerUrl = await kc.createRegisterUrl();
                    var cordovaOptions = createCordovaOptions(options);
                    var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', cordovaOptions);
                    ref.addEventListener('loadstart', function (event) {
                        if (event.url.indexOf(getCordovaRedirectUri()) == 0) {
                            ref.close();
                            var oauth = parseCallback(event.url);
                            processCallback(oauth, promise);
                        }
                    });
                    return promise.promise;
                },

                accountManagement: function () {
                    var accountUrl = kc.createAccountUrl();
                    if (typeof accountUrl !== 'undefined') {
                        var ref = cordovaOpenWindowWrapper(accountUrl, '_blank', 'location=no');
                        ref.addEventListener('loadstart', function (event) {
                            if (event.url.indexOf(getCordovaRedirectUri()) == 0) {
                                ref.close();
                            }
                        });
                    } else {
                        throw "Not supported by the OIDC server";
                    }
                },

                redirectUri: function (options) {
                    return getCordovaRedirectUri();
                }
            }
        }

        if (type == 'cordova-native') {
            loginIframe.enable = false;

            return {
                login: async function (options) {
                    var promise = createPromise();
                    var loginUrl = await kc.createLoginUrl(options);

                    universalLinks.subscribe('keycloak', function (event) {
                        universalLinks.unsubscribe('keycloak');
                        window.cordova.plugins.browsertab.close();
                        var oauth = parseCallback(event.url);
                        processCallback(oauth, promise);
                    });

                    window.cordova.plugins.browsertab.openUrl(loginUrl);
                    return promise.promise;
                },

                logout: function (options) {
                    var promise = createPromise();
                    var logoutUrl = kc.createLogoutUrl(options);

                    universalLinks.subscribe('keycloak', function (event) {
                        universalLinks.unsubscribe('keycloak');
                        window.cordova.plugins.browsertab.close();
                        kc.clearToken();
                        promise.setSuccess();
                    });

                    window.cordova.plugins.browsertab.openUrl(logoutUrl);
                    return promise.promise;
                },

                register: async function (options) {
                    var promise = createPromise();
                    var registerUrl = await kc.createRegisterUrl(options);
                    universalLinks.subscribe('keycloak', function (event) {
                        universalLinks.unsubscribe('keycloak');
                        window.cordova.plugins.browsertab.close();
                        var oauth = parseCallback(event.url);
                        processCallback(oauth, promise);
                    });
                    window.cordova.plugins.browsertab.openUrl(registerUrl);
                    return promise.promise;

                },

                accountManagement: function () {
                    var accountUrl = kc.createAccountUrl();
                    if (typeof accountUrl !== 'undefined') {
                        window.cordova.plugins.browsertab.openUrl(accountUrl);
                    } else {
                        throw "Not supported by the OIDC server";
                    }
                },

                redirectUri: function (options) {
                    if (options && options.redirectUri) {
                        return options.redirectUri;
                    } else if (kc.redirectUri) {
                        return kc.redirectUri;
                    } else {
                        return "http://localhost";
                    }
                }
            }
        }

        throw 'invalid adapter type: ' + type;
    }

    const STORAGE_KEY_PREFIX = 'kc-callback-';

    var LocalStorage = function () {
        if (!(this instanceof LocalStorage)) {
            return new LocalStorage();
        }

        localStorage.setItem('kc-test', 'test');
        localStorage.removeItem('kc-test');

        var cs = this;

        /**
         * Clears all values from local storage that are no longer valid.
         */
        function clearInvalidValues() {
            const currentTime = Date.now();

            for (const [key, value] of getStoredEntries()) {
                // Attempt to parse the expiry time from the value.
                const expiry = parseExpiry(value);

                // Discard the value if it is malformed or expired.
                if (expiry === null || expiry < currentTime) {
                    localStorage.removeItem(key);
                }
            }
        }

        /**
         * Clears all known values from local storage.
         */
        function clearAllValues() {
            for (const [key] of getStoredEntries()) {
                localStorage.removeItem(key);
            }
        }

        /**
         * Gets all entries stored in local storage that are known to be managed by this class.
         * @returns {Array<[string, unknown]>} An array of key-value pairs.
         */
        function getStoredEntries() {
            return Object.entries(localStorage).filter(([key]) => key.startsWith(STORAGE_KEY_PREFIX));
        }

        /**
         * Parses the expiry time from a value stored in local storage.
         * @param {unknown} value
         * @returns {number | null} The expiry time in milliseconds, or `null` if the value is malformed.
         */
        function parseExpiry(value) {
            let parsedValue;

            // Attempt to parse the value as JSON.
            try {
                parsedValue = JSON.parse(value);
            } catch (error) {
                return null;
            }

            // Attempt to extract the 'expires' property.
            if (isObject(parsedValue) && 'expires' in parsedValue && typeof parsedValue.expires === 'number') {
                return parsedValue.expires;
            }

            return null;
        }

        cs.get = function (state) {
            if (!state) {
                return;
            }

            var key = STORAGE_KEY_PREFIX + state;
            var value = localStorage.getItem(key);
            if (value) {
                localStorage.removeItem(key);
                value = JSON.parse(value);
            }

            clearInvalidValues();
            return value;
        };

        cs.add = function (state) {
            clearInvalidValues();

            const key = STORAGE_KEY_PREFIX + state.state;
            const value = JSON.stringify({
                ...state,
                // Set the expiry time to 1 hour from now.
                expires: Date.now() + (60 * 60 * 1000)
            });

            try {
                localStorage.setItem(key, value);
            } catch (error) {
                // If the storage is full, clear all known values and try again.
                clearAllValues();
                localStorage.setItem(key, value);
            }
        };
    };

    var CookieStorage = function () {
        if (!(this instanceof CookieStorage)) {
            return new CookieStorage();
        }

        var cs = this;

        cs.get = function (state) {
            if (!state) {
                return;
            }

            var value = getCookie(STORAGE_KEY_PREFIX + state);
            setCookie(STORAGE_KEY_PREFIX + state, '', cookieExpiration(-100));
            if (value) {
                return JSON.parse(value);
            }
        };

        cs.add = function (state) {
            setCookie(STORAGE_KEY_PREFIX + state.state, JSON.stringify(state), cookieExpiration(60));
        };

        cs.removeItem = function (key) {
            setCookie(key, '', cookieExpiration(-100));
        };

        var cookieExpiration = function (minutes) {
            var exp = new Date();
            exp.setTime(exp.getTime() + (minutes * 60 * 1000));
            return exp;
        };

        var getCookie = function (key) {
            var name = key + '=';
            var ca = document.cookie.split(';');
            for (var i = 0; i < ca.length; i++) {
                var c = ca[i];
                while (c.charAt(0) == ' ') {
                    c = c.substring(1);
                }
                if (c.indexOf(name) == 0) {
                    return c.substring(name.length, c.length);
                }
            }
            return '';
        };

        var setCookie = function (key, value, expirationDate) {
            var cookie = key + '=' + value + '; '
                + 'expires=' + expirationDate.toUTCString() + '; ';
            document.cookie = cookie;
        }
    };

    function createCallbackStorage() {
        try {
            return new LocalStorage();
        } catch (err) {
        }

        return new CookieStorage();
    }

    function createLogger(fn) {
        return function () {
            if (kc.enableLogging) {
                fn.apply(console, Array.prototype.slice.call(arguments));
            }
        };
    }
}

export default TideCloak;

export { RequestEnclave, ApprovalEnclave } from "heimdall-tide";

/**
 * @param {ArrayBuffer} bytes
 * @see https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
 */
function bytesToBase64(bytes) {
    const binString = String.fromCodePoint(...bytes);
    return btoa(binString);
}

/**
 * @param {string} base64
 * @returns {Uint8Array}
 * @see https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
 */
function base64ToBytes(base64) {
  // Decode to “binary” JS string where each char’s code point 0–255 is one byte
  const binString = atob(base64);
  const len = binString.length;
  const bytes = new Uint8Array(len);
  for (let i = 0; i < len; i++) {
    // codePointAt is safe here because each char was originally from 0–255
    bytes[i] = binString.codePointAt(i);
  }
  return bytes;
}

/**
 * @param {string} message
 * @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#basic_example
 */
async function sha256Digest(message) {
    const encoder = new TextEncoder();
    const data = encoder.encode(message);

    if (typeof crypto === "undefined" || typeof crypto.subtle === "undefined") {
        throw new Error("Web Crypto API is not available.");
    }

    return await crypto.subtle.digest("SHA-256", data);
}

/**
 * @param {string} token
 */
function decodeToken(token) {
    const [header, payload] = token.split(".");

    if (typeof payload !== "string") {
        throw new Error("Unable to decode token, payload not found.");
    }

    let decoded;

    try {
        decoded = base64UrlDecode(payload);
    } catch (error) {
        throw new Error("Unable to decode token, payload is not a valid Base64URL value.", { cause: error });
    }

    try {
        return JSON.parse(decoded);
    } catch (error) {
        throw new Error("Unable to decode token, payload is not a valid JSON value.", { cause: error });
    }
}

/**
 * @param {string} input
 */
function base64UrlDecode(input) {
    let output = input
        .replaceAll("-", "+")
        .replaceAll("_", "/");

    switch (output.length % 4) {
        case 0:
            break;
        case 2:
            output += "==";
            break;
        case 3:
            output += "=";
            break;
        default:
            throw new Error("Input is not of the correct length.");
    }

    try {
        return b64DecodeUnicode(output);
    } catch (error) {
        return atob(output);
    }
}

/**
 * @param {string} input
 */
function b64DecodeUnicode(input) {
    return decodeURIComponent(atob(input).replace(/(.)/g, (m, p) => {
        let code = p.charCodeAt(0).toString(16).toUpperCase();

        if (code.length < 2) {
            code = "0" + code;
        }

        return "%" + code;
    }));
}

/**
 * Check if the input is an object that can be operated on.
 * @param {unknown} input
 */
function isObject(input) {
    return typeof input === 'object' && input !== null;
}

export function getHumanReadableObject(modelId, data, expiry) {
    return ModelRegistry.getHumanReadableModelBuilder(modelId, data, expiry).getHumanReadableObject();
}
export { bytesToBase64, base64ToBytes } from "../modules/tide-js/Cryptide/Serialization.js";