// ==UserScript==
// @name         AWS Link Accountifier
// @namespace    https://github.com/david-04/aws-link-accountifier
// @version      1.0
// @author       David Hofmann
// @description  Bind AWS console links to an account and - when opening such links - trigger an account change if required
// @homepage     https://github.com/david-04/aws-link-accountifier
// @updateURL    https://raw.githubusercontent.com/david-04/aws-link-accountifier/main/dist/aws-link-accountifier.js
// @downloadURL  https://raw.githubusercontent.com/david-04/aws-link-accountifier/main/dist/aws-link-accountifier.js
// @match        *://*.aws.amazon.com/*
// @match        *://*/*aws-accountified-redirect.htm*
// @run-at       document-start
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// ==/UserScript==
var AwsLinkAccountifier;
(function (AwsLinkAccountifier) {
    //------------------------------------------------------------------------------------------------------------------
    // Highlight the account/roles that should be used for the login
    //------------------------------------------------------------------------------------------------------------------
    function injectAccountSelectionHint(redirectState) {
        const accountId = redirectState.requiredAccount.id;
        const accountName = getDescriptiveAccountName(redirectState);
        const banner = document.createElement("DIV");
        banner.innerHTML = `Please sign in to account <b>${AwsLinkAccountifier.sanitize(accountName)}</b>`;
        const style = banner.style;
        style.backgroundColor = "yellow";
        style.fontSize = "1.4em";
        style.borderBottom = "1px solid black";
        style.padding = "0.75em";
        style.marginBottom = "1rem";
        style.width = "calc(100vw + 50px)";
        style.maxWidth = "calc(100vw + 50px)";
        style.marginLeft = "-50px";
        style.paddingLeft = "calc(50px + 0.75em)";
        document.body.insertBefore(banner, document.body.firstChild);
        highlightAccount(accountId, "yellow");
    }
    AwsLinkAccountifier.injectAccountSelectionHint = injectAccountSelectionHint;
    //------------------------------------------------------------------------------------------------------------------
    // Highlight the given account's login options
    //------------------------------------------------------------------------------------------------------------------
    function highlightAccount(accountId, color) {
        document.querySelectorAll("* * .saml-account-name").forEach(node => {
            var _a, _b;
            const element = node;
            const innerText = element.innerText;
            if (innerText.endsWith(` ${accountId}`) || innerText.endsWith(` (${accountId})`)) {
                const style = (_b = (_a = element.parentElement) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.style;
                if (style) {
                    if (color) {
                        style.backgroundColor = color;
                    }
                    else {
                        style.removeProperty("background-color");
                    }
                    style.paddingTop = "1.5em";
                    style.paddingBottom = "0.5em";
                }
            }
        });
    }
    //------------------------------------------------------------------------------------------------------------------
    // Get a descriptive name for the account
    //------------------------------------------------------------------------------------------------------------------
    function getDescriptiveAccountName(redirectState) {
        const account = redirectState.requiredAccount;
        if (account.alias) {
            return `${account.alias} (${account.id})`;
        }
        else if (account.exampleRole) {
            return `${account.id} (e.g. ${account.exampleRole})`;
        }
        else {
            return account.id;
        }
    }
})(AwsLinkAccountifier || (AwsLinkAccountifier = {}));
var AwsLinkAccountifier;
(function (AwsLinkAccountifier) {
    AwsLinkAccountifier.AWS_USER_INFO_COOKIE_NAME = "aws-userInfo";
    //------------------------------------------------------------------------------------------------------------------
    // Details about an AWS session
    //------------------------------------------------------------------------------------------------------------------
    class AwsSession {
        //--------------------------------------------------------------------------------------------------------------
        // Initialization
        //--------------------------------------------------------------------------------------------------------------
        constructor(accountId, accountAlias, role) {
            this.accountId = accountId;
            this.accountAlias = accountAlias;
            this.role = role;
        }
        //--------------------------------------------------------------------------------------------------------------
        // Create a URL hint for this session's account
        //--------------------------------------------------------------------------------------------------------------
        toUrlHint() {
            const hint = { account: { id: this.accountId } };
            if (this.accountAlias && this.accountAlias !== this.accountId) {
                hint.account.alias = this.accountAlias;
            }
            if (this.role) {
                hint.account.exampleRole = this.role;
            }
            return hint;
        }
        //--------------------------------------------------------------------------------------------------------------
        // Verify if this session's account matches the redirect requirements
        //--------------------------------------------------------------------------------------------------------------
        matchesAccount(redirectState) {
            return redirectState.requiredAccount.id === this.accountId;
        }
    }
    AwsLinkAccountifier.AwsSession = AwsSession;
    //------------------------------------------------------------------------------------------------------------------
    // Retrieve information about the current session
    //------------------------------------------------------------------------------------------------------------------
    function getCurrentAwsSession() {
        const stringified = AwsLinkAccountifier.getCookie(AwsLinkAccountifier.AWS_USER_INFO_COOKIE_NAME);
        if (stringified) {
            const userInfo = JSON.parse(stringified);
            const accountId = extractPropertyFragment(userInfo, "arn", /^arn:aws:sts:[^:]*:/, /:.*/);
            if (accountId) {
                return new AwsSession(accountId, extractPropertyFragment(userInfo, "alias", /^/, /$/), extractPropertyFragment(userInfo, "arn", /^arn:aws:sts:[^:]*:[^:]*:assumed-role\//, /\/.*/));
            }
        }
        return undefined;
    }
    AwsLinkAccountifier.getCurrentAwsSession = getCurrentAwsSession;
    //------------------------------------------------------------------------------------------------------------------
    // Extract a piece of information from an object property
    //------------------------------------------------------------------------------------------------------------------
    function extractPropertyFragment(object, propertyName, matchAndRemove, remove) {
        const value = AwsLinkAccountifier.getStringProperty(object, propertyName);
        if (value && value.match(matchAndRemove)) {
            return value.replace(matchAndRemove, "").replace(remove, "");
        }
        return undefined;
    }
})(AwsLinkAccountifier || (AwsLinkAccountifier = {}));
var AwsLinkAccountifier;
(function (AwsLinkAccountifier) {
    let getAwsSessionCount = 0;
    const isAwsConsole = window.location.host.toLowerCase().endsWith("aws.amazon.com");
    const isAwsSignIn = window.location.host.toLowerCase().endsWith("signin.aws.amazon.com");
    const isRedirectPage = 0 <= window.location.pathname.indexOf("aws-accountified-redirect.htm");
    //------------------------------------------------------------------------------------------------------------------
    // Extract the URL hint and start or schedule the redirect processing
    //------------------------------------------------------------------------------------------------------------------
    function main() {
        if (isRedirectPage) {
            processRedirectPage();
        }
        if (isAwsSignIn) {
            const state = AwsLinkAccountifier.getRedirectState();
            if (state && state.shouldAutoLogout) {
                AwsLinkAccountifier.setRedirectState(Object.assign(Object.assign({}, state), { shouldAutoLogout: false }));
                if (AwsLinkAccountifier.getSettings().accountSwitchUrl.toLowerCase().indexOf("signin.aws.amazon.com") < 0) {
                    // login is done via external SSO - redirect away from AWS' default login page
                    AwsLinkAccountifier.initiateAccountSwitch();
                    return;
                }
            }
        }
        if (isAwsConsole) {
            AwsLinkAccountifier.extractUrlHint();
            AwsLinkAccountifier.onDOMContentLoaded(processNotificationsAndRedirects);
        }
        AwsLinkAccountifier.initializeMenu({
            copyLink: isAwsConsole && !isAwsSignIn,
            switchRole: isAwsConsole && !isAwsSignIn,
            setAccountSwitchUrl: isAwsConsole || isRedirectPage,
            useThisPageForRedirects: isRedirectPage
        });
    }
    AwsLinkAccountifier.main = main;
    //------------------------------------------------------------------------------------------------------------------
    // Redirect or inject messages to log out and in again
    //------------------------------------------------------------------------------------------------------------------
    function processNotificationsAndRedirects() {
        document.removeEventListener("DOMContentLoaded", processNotificationsAndRedirects);
        const redirectState = AwsLinkAccountifier.getRedirectState();
        if (redirectState) {
            if (isAwsSignIn) {
                AwsLinkAccountifier.injectAccountSelectionHint(redirectState);
            }
            else if (isAwsConsole) {
                const awsSession = AwsLinkAccountifier.getCurrentAwsSession();
                if (awsSession || 10 * 10 < ++getAwsSessionCount) {
                    redirectOrDecorateConsolePage(redirectState, awsSession);
                }
                else {
                    setTimeout(processNotificationsAndRedirects, 100);
                }
            }
        }
    }
    //------------------------------------------------------------------------------------------------------------------
    // Augment the console page
    //------------------------------------------------------------------------------------------------------------------
    function redirectOrDecorateConsolePage(redirectState, awsSession) {
        if (!awsSession) {
            AwsLinkAccountifier.deleteRedirectState();
            console.error(`Failed to retrieve AWS user info - cookie ${AwsLinkAccountifier.AWS_USER_INFO_COOKIE_NAME} not set or format has changed?`);
        }
        else if (awsSession.matchesAccount(redirectState)) {
            AwsLinkAccountifier.deleteRedirectState();
            if (window.location.href !== redirectState.targetUrl) {
                window.location.href = redirectState.targetUrl;
            }
        }
        else if (redirectState.shouldAutoLogout) {
            AwsLinkAccountifier.setRedirectState(Object.assign(Object.assign({}, redirectState), { shouldAutoLogout: false }));
            AwsLinkAccountifier.initiateAccountSwitch();
        }
    }
    //------------------------------------------------------------------------------------------------------------------
    // Intercept redirect service page loads
    //------------------------------------------------------------------------------------------------------------------
    function processRedirectPage() {
        var _a;
        try {
            const hash = decodeURIComponent(((_a = window.location.hash) !== null && _a !== void 0 ? _a : "").replace(/^#/, "").trim());
            if (hash) {
                const parameters = JSON.parse(hash);
                if (parameters
                    && "object" === typeof parameters
                    && "string" === typeof parameters.url
                    && parameters.url.match(/^http/)
                    && parameters.account
                    && "object" === typeof parameters.account) {
                    AwsLinkAccountifier.storeHint(parameters.url, { account: parameters.account });
                    window.location.href = parameters.url;
                }
                else {
                    console.error("The hash does not contain a valid 'url'");
                }
            }
        }
        catch (exception) {
            console.error(exception);
        }
    }
})(AwsLinkAccountifier || (AwsLinkAccountifier = {}));
var AwsLinkAccountifier;
(function (AwsLinkAccountifier) {
    //------------------------------------------------------------------------------------------------------------------
    // Initialize the context menu
    //------------------------------------------------------------------------------------------------------------------
    function initializeMenu(options) {
        if (options.copyLink) {
            GM_registerMenuCommand("Copy link (redirect)", () => copyLinkToClipboard(AwsLinkAccountifier.createRedirectLink), "c");
            GM_registerMenuCommand("Copy link (direct)", () => copyLinkToClipboard(AwsLinkAccountifier.createDirectLink), "d");
        }
        if (options.switchRole) {
            GM_registerMenuCommand("Switch role", switchRole, "s");
        }
        if (options.setAccountSwitchUrl) {
            GM_registerMenuCommand("Set account-switch URL", setAccountSwitchUrl, "u");
        }
        if (options.useThisPageForRedirects) {
            AwsLinkAccountifier.onDOMContentLoaded(setRedirectUrl);
        }
    }
    AwsLinkAccountifier.initializeMenu = initializeMenu;
    //------------------------------------------------------------------------------------------------------------------
    // Copy link to clipboard
    //------------------------------------------------------------------------------------------------------------------
    function copyLinkToClipboard(generateLink) {
        var _a;
        const urlHint = (_a = AwsLinkAccountifier.getCurrentAwsSession()) === null || _a === void 0 ? void 0 : _a.toUrlHint();
        const url = urlHint ? generateLink(window.location.href, urlHint) : undefined;
        if (url) {
            GM_setClipboard(url);
        }
        else {
            GM_notification({
                title: "Error",
                text: "Failed to retrieve AWS account details",
                silent: true
            });
        }
    }
    //------------------------------------------------------------------------------------------------------------------
    // Trigger an account switch
    //------------------------------------------------------------------------------------------------------------------
    function switchRole() {
        try {
            const account = AwsLinkAccountifier.getCurrentAwsSession();
            if (account === null || account === void 0 ? void 0 : account.role) {
                AwsLinkAccountifier.setRedirectState({
                    targetUrl: window.location.href,
                    requiredAccount: {
                        id: account.accountId,
                        alias: account.accountAlias,
                        exampleRole: account.role
                    },
                    shouldAutoLogout: false,
                    expiresAt: new Date().getTime() + 10 * 60 * 1000
                });
                AwsLinkAccountifier.initiateAccountSwitch();
            }
        }
        catch (exception) {
            console.error(exception);
        }
    }
    //------------------------------------------------------------------------------------------------------------------
    // Set account switch URL
    //------------------------------------------------------------------------------------------------------------------
    function setAccountSwitchUrl() {
        const accountSwitchUrl = prompt(`
            Enter the URL to trigger an account change.
            It can include these placeholders:
            - \${ACCOUNT_ID}
            - \${ACCOUNT_ALIAS}
            - \${ROLE_NAME}
            `.trim().replace(/[ \t]*\r?\n[ \t]*/g, "\n"), AwsLinkAccountifier.getSettings().accountSwitchUrl);
        if (accountSwitchUrl) {
            AwsLinkAccountifier.updateSettings({ accountSwitchUrl });
        }
    }
    //------------------------------------------------------------------------------------------------------------------
    // Use this page for redirects
    //------------------------------------------------------------------------------------------------------------------
    function setRedirectUrl() {
        const redirectVersion = document.body.dataset.awsAccountifiedRedirectVersion;
        if (redirectVersion && "string" === typeof redirectVersion) {
            const callback = () => AwsLinkAccountifier.updateSettings({ redirectUrl: window.location.href.replace(/#.*/, "") });
            GM_registerMenuCommand("Use this page for redirects", callback, "s");
        }
    }
})(AwsLinkAccountifier || (AwsLinkAccountifier = {}));
var AwsLinkAccountifier;
(function (AwsLinkAccountifier) {
    function getPresetAccountSwitchUrl() {
        return "https://signin.aws.amazon.com/switchrole?account=${ACCOUNT_ID}&roleName=${ROLE_NAME}";
    }
    AwsLinkAccountifier.getPresetAccountSwitchUrl = getPresetAccountSwitchUrl;
    function getPresetRedirectUrl() {
        return "https://david-04.github.io/aws-link-accountifier/aws-accountified-redirect.html";
    }
    AwsLinkAccountifier.getPresetRedirectUrl = getPresetRedirectUrl;
})(AwsLinkAccountifier || (AwsLinkAccountifier = {}));
var AwsLinkAccountifier;
(function (AwsLinkAccountifier) {
    const REDIRECT_STATE_KEY = "redirectState";
    //------------------------------------------------------------------------------------------------------------------
    // Set the redirect state
    //------------------------------------------------------------------------------------------------------------------
    function setRedirectState(state) {
        GM_setValue(REDIRECT_STATE_KEY, state);
    }
    AwsLinkAccountifier.setRedirectState = setRedirectState;
    //------------------------------------------------------------------------------------------------------------------
    // Delete the redirect state
    //------------------------------------------------------------------------------------------------------------------
    function deleteRedirectState() {
        GM_deleteValue(REDIRECT_STATE_KEY);
    }
    AwsLinkAccountifier.deleteRedirectState = deleteRedirectState;
    //------------------------------------------------------------------------------------------------------------------
    // Retrieve the current redirect state
    //------------------------------------------------------------------------------------------------------------------
    function getRedirectState() {
        const state = GM_getValue(REDIRECT_STATE_KEY, undefined);
        if (state) {
            if (new Date().getTime() <= state.expiresAt) {
                return state;
            }
            deleteRedirectState();
        }
        return undefined;
    }
    AwsLinkAccountifier.getRedirectState = getRedirectState;
})(AwsLinkAccountifier || (AwsLinkAccountifier = {}));
var AwsLinkAccountifier;
(function (AwsLinkAccountifier) {
    const SETTINGS_KEY = "settings";
    //------------------------------------------------------------------------------------------------------------------
    // Default settings
    //------------------------------------------------------------------------------------------------------------------
    const DEFAULT_SETTINGS = {
        accountSwitchUrl: AwsLinkAccountifier.getPresetAccountSwitchUrl(),
        redirectUrl: AwsLinkAccountifier.getPresetRedirectUrl()
    };
    //------------------------------------------------------------------------------------------------------------------
    // Retrieve settings
    //------------------------------------------------------------------------------------------------------------------
    function getSettings() {
        var _a;
        return migrateSettings(Object.assign(Object.assign({}, DEFAULT_SETTINGS), ((_a = GM_getValue("settings", DEFAULT_SETTINGS)) !== null && _a !== void 0 ? _a : {})));
    }
    AwsLinkAccountifier.getSettings = getSettings;
    //------------------------------------------------------------------------------------------------------------------
    // Update the settings
    //------------------------------------------------------------------------------------------------------------------
    function updateSettings(settings) {
        GM_setValue(SETTINGS_KEY, Object.assign(Object.assign({}, getSettings()), settings));
    }
    AwsLinkAccountifier.updateSettings = updateSettings;
    //------------------------------------------------------------------------------------------------------------------
    // Migrate old settings
    //------------------------------------------------------------------------------------------------------------------
    function migrateSettings(settings) {
        var _a;
        const redirectUrl = (_a = settings.redirectUrl) !== null && _a !== void 0 ? _a : settings.redirectService;
        delete settings.redirectService;
        if ("string" === typeof redirectUrl) {
            return Object.assign(Object.assign({}, settings), { redirectUrl });
        }
        else {
            return settings;
        }
    }
})(AwsLinkAccountifier || (AwsLinkAccountifier = {}));
var AwsLinkAccountifier;
(function (AwsLinkAccountifier) {
    AwsLinkAccountifier.URL_HINT_PREFIX = "#aws-link-accountifier=";
    //------------------------------------------------------------------------------------------------------------------
    // Extract the hint from the URL, store the re-direct state, and go to the non-hinted original URL
    //------------------------------------------------------------------------------------------------------------------
    function extractUrlHint() {
        try {
            const { url, hint } = splitUrlAndHint(window.location.href);
            if (undefined !== hint) {
                AwsLinkAccountifier.deleteRedirectState();
                if (hint) {
                    storeHint(url, parseHint(hint));
                }
                window.location.replace(url);
            }
        }
        catch (exception) {
            console.error(exception);
        }
    }
    AwsLinkAccountifier.extractUrlHint = extractUrlHint;
    //------------------------------------------------------------------------------------------------------------------
    // Split the URL into the original URL and the appended hint
    //------------------------------------------------------------------------------------------------------------------
    function splitUrlAndHint(url) {
        const index = url.indexOf(AwsLinkAccountifier.URL_HINT_PREFIX);
        if (0 < index) {
            return {
                url: url.substring(0, index),
                hint: url.substring(index + AwsLinkAccountifier.URL_HINT_PREFIX.length)
            };
        }
        else {
            return { url };
        }
    }
    //------------------------------------------------------------------------------------------------------------------
    // Store the hint to trigger redirects later on
    //------------------------------------------------------------------------------------------------------------------
    function storeHint(targetUrl, hint) {
        try {
            AwsLinkAccountifier.setRedirectState({
                targetUrl,
                requiredAccount: {
                    id: hint.account.id,
                    alias: hint.account.alias,
                    exampleRole: hint.account.exampleRole
                },
                shouldAutoLogout: true,
                expiresAt: new Date().getTime() + 10 * 60 * 1000
            });
        }
        catch (exception) {
            console.error(exception);
        }
    }
    AwsLinkAccountifier.storeHint = storeHint;
    //------------------------------------------------------------------------------------------------------------------
    // Extract the details from the hint
    //------------------------------------------------------------------------------------------------------------------
    function parseHint(hint) {
        var _a;
        try {
            const json = JSON.parse(decodeURIComponent(hint));
            if (!json || "object" !== typeof json || "string" !== typeof ((_a = json === null || json === void 0 ? void 0 : json.account) === null || _a === void 0 ? void 0 : _a.id)) {
                throw new Error(`account/id is missing`);
            }
            return json;
        }
        catch (exception) {
            throw new Error(`Invalid URL hint: ${hint} - (${exception})`);
        }
    }
    //------------------------------------------------------------------------------------------------------------------
    // Generate a direct link with an embedded hint
    //------------------------------------------------------------------------------------------------------------------
    function createDirectLink(url, hint) {
        return splitUrlAndHint(url).url + AwsLinkAccountifier.URL_HINT_PREFIX + encodeURIComponent(JSON.stringify(hint));
    }
    AwsLinkAccountifier.createDirectLink = createDirectLink;
    //------------------------------------------------------------------------------------------------------------------
    // Generate a redirecting link
    //------------------------------------------------------------------------------------------------------------------
    function createRedirectLink(url, hint) {
        return `${AwsLinkAccountifier.getSettings().redirectUrl}#${encodeURIComponent(JSON.stringify(Object.assign(Object.assign({}, hint), { url })))}`;
    }
    AwsLinkAccountifier.createRedirectLink = createRedirectLink;
    //------------------------------------------------------------------------------------------------------------------
    // Trigger an account switch based on the current redirect state
    //------------------------------------------------------------------------------------------------------------------
    function initiateAccountSwitch() {
        var _a, _b, _c;
        const account = (_a = AwsLinkAccountifier.getRedirectState()) === null || _a === void 0 ? void 0 : _a.requiredAccount;
        if (account) {
            const url = AwsLinkAccountifier.getSettings().accountSwitchUrl
                .replace(/\$\{ACCOUNT_ID\}/g, encodeURIComponent(account.id))
                .replace(/\$\{ACCOUNT_ALIAS\}/g, encodeURIComponent((_b = account.alias) !== null && _b !== void 0 ? _b : account.id))
                .replace(/\$\{ROLE_NAME\}/g, encodeURIComponent((_c = account.exampleRole) !== null && _c !== void 0 ? _c : ""));
            if (url !== window.location.href) {
                window.location.href = url;
            }
        }
    }
    AwsLinkAccountifier.initiateAccountSwitch = initiateAccountSwitch;
})(AwsLinkAccountifier || (AwsLinkAccountifier = {}));
var AwsLinkAccountifier;
(function (AwsLinkAccountifier) {
    //------------------------------------------------------------------------------------------------------------------
    // Get a string property from an object
    //------------------------------------------------------------------------------------------------------------------
    function getStringProperty(object, key) {
        if (object && "object" === typeof object && "string" === typeof (object[key])) {
            return object[key];
        }
        else {
            return undefined;
        }
    }
    AwsLinkAccountifier.getStringProperty = getStringProperty;
    //------------------------------------------------------------------------------------------------------------------
    // Get a cookie
    //------------------------------------------------------------------------------------------------------------------
    function getCookie(cName) {
        const name = cName + "=";
        const cDecoded = decodeURIComponent(document.cookie);
        const array = cDecoded.split('; ');
        let result;
        array.forEach(value => {
            if (value.indexOf(name) === 0)
                result = value.substring(name.length);
        });
        return result;
    }
    AwsLinkAccountifier.getCookie = getCookie;
    //------------------------------------------------------------------------------------------------------------------
    // Set a cookie
    //------------------------------------------------------------------------------------------------------------------
    function setCookie(name, value, domain, path, ttlMs) {
        const expires = new Date(new Date().getTime() + ttlMs);
        document.cookie = `${name}=${encodeURIComponent(value)};expires=${expires};domain=${domain};path=${path}`;
    }
    AwsLinkAccountifier.setCookie = setCookie;
    //------------------------------------------------------------------------------------------------------------------
    // Sanitize HTML content
    //------------------------------------------------------------------------------------------------------------------
    function sanitize(text) {
        return text.replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;");
    }
    AwsLinkAccountifier.sanitize = sanitize;
    //------------------------------------------------------------------------------------------------------------------
    // Execute the given callback when the page has been loaded
    //------------------------------------------------------------------------------------------------------------------
    function onDOMContentLoaded(callback) {
        if (document.readyState === "complete" || document.readyState === "interactive") {
            callback();
        }
        else {
            document.addEventListener("DOMContentLoaded", callback);
        }
    }
    AwsLinkAccountifier.onDOMContentLoaded = onDOMContentLoaded;
})(AwsLinkAccountifier || (AwsLinkAccountifier = {}));
AwsLinkAccountifier.main();