Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/Vault.qml

Raw
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import "vault.js" as VaultData

Item {
    id: root

    required property Clipboard clipboardService
    property string status: "needs-setup"
    property string message: ""
    property var profile: ({
            "name": "personal",
            "serverUrl": "",
            "account": "",
            "unlockTimeoutSeconds": 900,
            "clipboardTimeoutSeconds": 30
        })
    property var cache: ({
            "available": false,
            "ageSeconds": -1,
            "stale": false
        })
    property var items: []
    property var detail: ({})
    property var challenge: ({})
    property int secondFactorProvider: -1
    property string secondFactorDraft: ""
    readonly property bool verificationPending: status === "second-factor" && message === "verifying"
    property string query: ""
    property string selectedId: ""
    property string browserOrigin: ""
    property string profileKey: ""
    property real resultScrollPosition: 0
    property real detailScrollPosition: 0
    property var expandedSections: ({})
    property var revealedFields: ({})
    property var faviconPaths: ({})
    property string feedback: ""
    property string repromptError: ""
    property bool copyPending: false
    property double currentCopyId: 0
    property int lockGeneration: 0
    property bool panelOpen: false
    property bool lockPending: false
    property bool restoreSyncPending: false
    readonly property var visibleItems: VaultData.filterItems(items, query, browserOrigin)
    readonly property var groupedItems: VaultData.groups(visibleItems)
    readonly property bool unlocked: status === "unlocked" || status === "syncing" || status === "sync-failure" || status === "stale-cache"
    readonly property string executable: Quickshell.shellPath("vault-native/run.sh")
    readonly property string configPath: Quickshell.statePath("vault.json")
    property bool configLoaded: false
    property bool setupEditing: false
    property bool setupSaving: false
    property string setupUrlDraft: ""
    property string setupAccountDraft: ""
    property string setupError: ""
    property var pendingProfile: null
    property bool persistProfile: false
    property double configurationId: 0
    property bool restartPending: false
    property bool connectionTesting: false
    property string connectionResult: ""
    readonly property bool connectionBusy: connectionTesting || connectionProcess.running

    function send(request) {
        if (!vaultProcess.running) {
            boundaryFailed("native vault boundary unavailable");
            return;
        }
        vaultProcess.write(JSON.stringify(request) + "\n");
    }

    function boundaryFailed(reason) {
        status = "unavailable-vault";
        message = reason;
        lockPending = false;
        pendingProfile = null;
        restartPending = false;
        if (setupSaving) {
            setupEditing = true;
            setupError = reason;
        }
        setupSaving = false;
        persistProfile = false;
        clearRetainedState();
    }

    function beginSetup() {
        setupUrlDraft = String(profile.serverUrl || "");
        setupAccountDraft = String(profile.account || "");
        setupError = "";
        setupEditing = true;
        lock();
    }

    function cancelSetup() {
        stopConnectionTest();
        setupEditing = false;
        setupError = "";
        setupUrlDraft = String(profile.serverUrl || "");
        setupAccountDraft = String(profile.account || "");
    }

    function configureProfile(value, persist) {
        pendingProfile = value;
        persistProfile = persist;
        setupSaving = true;
        setupError = "";
        configurationId++;
        lockPending = true;
        status = "locked";
        message = "";
        browserOrigin = "";
        clearRetainedState();
        // EOF locks and reaps the old boundary before starting the current executable.
        // Quickshell otherwise retains the old process across QML reloads and builds.
        restartPending = vaultProcess.running;
        if (restartPending)
            vaultProcess.stdinEnabled = false;
        else {
            vaultProcess.stdinEnabled = true;
            vaultProcess.running = true;
        }
    }

    function boundaryStarted() {
        if (setupSaving && pendingProfile !== null && !restartPending)
            send({
                "op": "configure-lock",
                "id": configurationId
            });
    }

    function boundaryExited() {
        clipboardService.resumeAfterVaultCopy();
        if (restartPending) {
            restartPending = false;
            vaultProcess.stdinEnabled = true;
            vaultProcess.running = true;
        } else
            boundaryFailed("native vault boundary stopped");
    }

    function setupTimedOut() {
        boundaryFailed("vault setup timed out; retry saving");
        vaultProcess.stdinEnabled = false;
    }

    function loadConfig() {
        if (configLoaded)
            return;
        configLoaded = true;
        let value;
        try {
            value = JSON.parse(cfgFile.text());
            if (!value || typeof value !== "object" || Array.isArray(value))
                throw new Error("invalid profile");
        } catch (error) {
            setupError = "vault settings could not be read";
            setupEditing = true;
            return;
        }
        configureProfile(value, false);
    }

    function draftProfile() {
        return {
            "name": String(profile.name || "personal"),
            "serverUrl": setupUrlDraft.trim(),
            "account": setupAccountDraft.trim(),
            "unlockTimeoutSeconds": Number(profile.unlockTimeoutSeconds || 900),
            "clipboardTimeoutSeconds": Number(profile.clipboardTimeoutSeconds || 30)
        };
    }

    function saveSetup() {
        if (setupSaving || lockPending || connectionBusy)
            return;
        configureProfile(draftProfile(), true);
    }

    function testConnection() {
        if (setupSaving || lockPending || connectionBusy)
            return;
        setupError = "";
        connectionResult = "";
        connectionTesting = true;
        connectionProcess.stdinEnabled = true;
        connectionProcess.running = true;
    }

    function connectionStarted() {
        if (connectionTesting)
            connectionProcess.write(JSON.stringify({
                "op": "test-connection",
                "profile": draftProfile()
            }) + "\n");
    }

    function connectionExited() {
        if (connectionTesting && !connectionResult)
            connectionResult = "connection test stopped";
        connectionTesting = false;
    }

    function stopConnectionTest() {
        connectionTesting = false;
        connectionResult = "";
        connectionProcess.stdinEnabled = false;
    }

    function connectionTimedOut() {
        connectionResult = "connection test timed out";
        connectionTesting = false;
        connectionProcess.stdinEnabled = false;
    }

    function handleConnectionLine(line) {
        if (!connectionTesting)
            return;
        let event;
        try {
            event = JSON.parse(line);
        } catch (error) {
            connectionResult = "invalid connection test response";
            connectionProcess.stdinEnabled = false;
            return;
        }
        if (event.event === "connection-result")
            connectionResult = String(event.message || "connection test failed");
        else if (event.event === "protocol-error")
            connectionResult = "native vault rejected connection test";
        else if (event.event === "state" && event.status === "unavailable-vault")
            connectionResult = String(event.message || "connection test unavailable");
        else
            return;
        connectionProcess.stdinEnabled = false;
    }

    function unlock(password) {
        const value = String(password || "");
        if (value.length === 0 || lockPending || setupSaving || setupEditing || !profile.serverUrl || !profile.account)
            return;
        status = "loading";
        send({
            "op": "unlock",
            "password": value
        });
    }

    function secondFactorHint() {
        if (secondFactorProvider === 1)
            return "email code selected; check your inbox";
        const provider = (challenge.providers || []).find(value => Number(value.id) === secondFactorProvider);
        return provider ? String(provider.label) + " selected; enter its verification code" : "choose a verification method";
    }

    function selectSecondFactor(provider) {
        const value = Number(provider);
        if (verificationPending || !(challenge.providers || []).some(candidate => Number(candidate.id) === value && !!candidate.codeSupported))
            return;
        if (secondFactorProvider !== value)
            secondFactorDraft = "";
        secondFactorProvider = value;
        message = "";
    }

    function submitSecondFactor(token) {
        const value = String(token || "");
        if (status !== "second-factor" || value.length === 0 || secondFactorProvider < 0 || verificationPending || lockPending)
            return;
        secondFactorDraft = "";
        message = "verifying";
        send({
            "op": "second-factor",
            "token": value,
            "provider": secondFactorProvider
        });
    }

    function cancelUnlock() {
        lock();
    }

    function reprompt(password) {
        const value = String(password || "");
        if (selectedId.length === 0 || value.length === 0)
            return;
        repromptError = "";
        send({
            "op": "reprompt",
            "id": selectedId,
            "password": value
        });
    }

    function prepareOpen(appId) {
        send({
            "op": "context",
            "appId": String(appId || "") || null
        });
        if (unlocked)
            sync();
    }

    function lock(request) {
        stopConnectionTest();
        if (!request && setupSaving) {
            restartPending = false;
            pendingProfile = null;
            persistProfile = false;
            setupSaving = false;
            setupEditing = true;
            setupError = "configuration interrupted by lock";
        }
        lockPending = true;
        status = "locked";
        message = "";
        browserOrigin = "";
        clearRetainedState();
        send(request || {
            "op": "lock"
        });
    }

    function sync() {
        send({
            "op": "sync"
        });
    }

    function select(id) {
        const value = String(id || "");
        if (value.length === 0 || value === selectedId)
            return;
        selectedId = value;
        detail = ({});
        repromptError = "";
        send({
            "op": "detail",
            "id": selectedId
        });
    }

    function moveSelection(delta) {
        const next = VaultData.nextSelection(visibleItems, selectedId, Number(delta));
        if (next.length > 0)
            select(next);
    }

    function refreshDetail() {
        if (unlocked && selectedId.length > 0)
            send({
                "op": "detail",
                "id": selectedId
            });
    }

    function copy(target, slot) {
        if (selectedId.length === 0)
            return;
        send({
            "op": "copy",
            "id": selectedId,
            "target": String(target),
            "slot": slot === undefined ? null : Number(slot)
        });
    }

    function open(field) {
        if (selectedId.length === 0)
            return;
        send({
            "op": "open",
            "id": selectedId,
            "field": String(field)
        });
    }

    function setFavorite(favorite) {
        if (selectedId.length === 0)
            return;
        send({
            "op": "favorite",
            "id": selectedId,
            "favorite": !!favorite
        });
    }

    function sectionExpanded(section) {
        const key = selectedId + "/" + String(section);
        return expandedSections[key] === undefined ? true : !!expandedSections[key];
    }

    function toggleSection(section) {
        const next = Object.assign({}, expandedSections);
        const key = selectedId + "/" + String(section);
        next[key] = !sectionExpanded(section);
        expandedSections = next;
    }

    function isRevealed(field) {
        return !!revealedFields[selectedId + "/" + String(field)];
    }

    function toggleReveal(field) {
        const next = Object.assign({}, revealedFields);
        const key = selectedId + "/" + String(field);
        next[key] = !next[key];
        revealedFields = next;
    }

    function clearRetainedState() {
        lockGeneration++;
        repromptError = "";
        feedback = "";
        copyPending = false;
        restoreSyncPending = false;
        items = [];
        detail = ({});
        challenge = ({});
        secondFactorProvider = -1;
        secondFactorDraft = "";
        query = "";
        selectedId = "";
        resultScrollPosition = 0;
        detailScrollPosition = 0;
        expandedSections = ({});
        revealedFields = ({});
        faviconPaths = ({});
    }

    function applyState(event) {
        status = String(event.status || "unavailable-vault");
        message = String(event.message || "");
        profile = event.profile || profile;
        profileKey = String(event.profileKey || "");
        cache = event.cache || cache;
        challenge = event.challenge || ({});
        if (status !== "second-factor")
            secondFactorDraft = "";
        const providers = challenge.providers || [];
        if (providers.length > 0 && !providers.some(provider => Number(provider.id) === secondFactorProvider && !!provider.codeSupported)) {
            const supported = providers.find(provider => !!provider.codeSupported);
            secondFactorProvider = supported ? Number(supported.id) : -1;
        }
        browserOrigin = String(event.browserOrigin || "");
        items = event.items || [];
        for (let index = 0; index < items.length; index++) {
            const path = faviconPaths[String(items[index].id || "")];
            if (path)
                items[index].favicon = path;
        }
        if (!unlocked) {
            if (status === "needs-setup" || status === "locked" || status === "authentication-failure" || status === "unavailable-vault")
                clearRetainedState();
            return;
        }
        if (visibleItems.length > 0) {
            const retained = visibleItems.some(item => {
                return item.id === selectedId;
            });
            if (retained)
                refreshDetail();
            else
                select(visibleItems[0].id);
        } else {
            selectedId = "";
            detail = ({});
        }
    }

    function handleLine(line) {
        let event;
        try {
            event = JSON.parse(String(line || ""));
        } catch (error) {
            boundaryFailed("invalid native vault response");
            return;
        }
        if (restartPending && event.event !== "copy-ended")
            return;
        if (setupSaving && (event.event === "protocol-error" || event.event === "configure-failed")) {
            boundaryFailed(String(event.message || "native vault rejected settings; retry saving"));
            return;
        }
        if (lockPending) {
            if (event.event === "state" && (event.status === "needs-setup" || event.status === "locked" || event.status === "unavailable-vault"))
                lockPending = false;
            else if (event.event !== "copy-ended" && event.event !== "configure-ready")
                return;
        }
        switch (String(event.event || "")) {
        case "state":
            applyState(event);
            if (event.syncNeeded && !restoreSyncPending) {
                restoreSyncPending = true;
                sync();
            } else if (!event.syncNeeded) {
                restoreSyncPending = false;
            }
            if (status === "unavailable-vault" && setupSaving)
                boundaryFailed(message || "native vault boundary unavailable");
            if (setupSaving && pendingProfile === null && (status === "locked" || status === "needs-setup")) {
                setupSaving = false;
                persistProfile = false;
                setupEditing = true;
                setupError = "configuration interrupted by lock";
            }
            break;
        case "configure-ready":
            if (pendingProfile !== null && event.id === configurationId) {
                lockPending = false;
                const value = pendingProfile;
                pendingProfile = null;
                send({
                    "op": "configure",
                    "profile": value
                });
            }
            break;
        case "configured":
            if (!setupSaving)
                break;
            profile = event.profile;
            setupUrlDraft = String(profile.serverUrl || "");
            setupAccountDraft = String(profile.account || "");
            setupEditing = false;
            setupSaving = false;
            if (persistProfile)
                cfgFile.setText(JSON.stringify(profile, null, 2));
            persistProfile = false;
            break;
        case "detail":
            if (String(event.id || "") === selectedId) {
                // TOTP ticks must not recreate field delegates and discard keyboard focus or presses.
                if (detail.id === event.id && JSON.stringify(detail.fields) === JSON.stringify(event.fields))
                    event.fields = detail.fields;
                detail = event;
            }
            break;
        case "favicon":
            {
                if (!unlocked || !profileKey || event.profileKey !== profileKey)
                    break;
                const id = String(event.id || "");
                const path = String(event.path || "");
                faviconPaths[id] = path;
                const updated = items.slice();
                const index = updated.findIndex(item => String(item.id || "") === id);
                if (index >= 0) {
                    updated[index] = Object.assign({}, updated[index], {
                        "favicon": path
                    });
                    items = updated;
                }
                break;
            }
        case "reprompt-succeeded":
            repromptError = "";
            feedback = "item access granted";
            feedbackTimer.restart();
            break;
        case "reprompt-failed":
            repromptError = "master password incorrect";
            break;
        case "copy-ready":
            copyPending = true;
            currentCopyId = Number(event.id || 0);
            clipboardService.pauseForVaultCopy(Number(profile.clipboardTimeoutSeconds || 30));
            break;
        case "copy-ended":
            if (Number(event.id || 0) === currentCopyId) {
                copyPending = false;
                currentCopyId = 0;
                clipboardService.resumeAfterVaultCopy();
            }
            break;
        case "copied":
            feedback = String(event.field || "field") + " copied";
            feedbackTimer.restart();
            break;
        case "copy-failed":
            feedback = "copy failed";
            feedbackTimer.restart();
            break;
        case "opened":
            feedback = "url opened";
            feedbackTimer.restart();
            break;
        case "action-failed":
            feedback = String(event.action || "action") + " failed";
            feedbackTimer.restart();
            break;
        }
    }

    function panelClosed() {
        stopConnectionTest();
    }

    function stateJson() {
        return JSON.stringify({
            "status": status,
            "profile": profile,
            "cache": cache,
            "itemCount": items.length,
            "selectedId": selectedId,
            "query": query,
            "feedback": feedback
        });
    }

    onSetupUrlDraftChanged: connectionResult = ""
    onSetupAccountDraftChanged: connectionResult = ""
    onPanelOpenChanged: if (!panelOpen)
        panelClosed()

    Component.onCompleted: send({
        "op": "hello"
    })
    Component.onDestruction: {
        connectionProcess.stdinEnabled = false;
        clipboardService.resumeAfterVaultCopy();
        if (vaultProcess.running)
            vaultProcess.write("{\"op\":\"lock\"}\n");
    }

    Connections {
        target: root.clipboardService

        function onVaultCapturePaused() {
            if (!root.copyPending)
                return;
            root.copyPending = false;
            root.send({
                "op": "commit-copy"
            });
        }
    }

    Timer {
        interval: 1000
        repeat: true
        running: root.unlocked && !!root.detail.totp
        onTriggered: root.refreshDetail()
    }

    Timer {
        interval: 5000
        running: root.setupSaving
        onTriggered: root.setupTimedOut()
    }

    Timer {
        id: feedbackTimer
        interval: 1800
        onTriggered: root.feedback = ""
    }

    FileView {
        id: cfgFile

        path: root.configPath
        onLoaded: root.loadConfig()
        onLoadFailed: error => {
            root.configLoaded = true;
            root.setupEditing = true;
            if (error !== FileViewError.FileNotFound)
                root.setupError = "vault settings could not be read";
        }
        onSaveFailed: {
            root.setupEditing = true;
            root.setupError = "vault settings could not be saved";
        }
    }

    Connections {
        target: vaultProcess

        function onExited(exitCode) {
            root.boundaryExited();
        }
    }

    Timer {
        interval: 5000
        running: root.connectionTesting
        onTriggered: root.connectionTimedOut()
    }

    Connections {
        target: connectionProcess

        function onExited(exitCode) {
            root.connectionExited();
        }
    }

    Process {
        id: connectionProcess

        command: [root.executable]
        workingDirectory: Quickshell.shellDir
        stdinEnabled: true
        onStarted: root.connectionStarted()

        stdout: SplitParser {
            onRead: data => root.handleConnectionLine(data)
        }
    }

    Process {
        id: vaultProcess

        command: [root.executable]
        workingDirectory: Quickshell.shellDir
        stdinEnabled: true
        running: true
        onStarted: root.boundaryStarted()

        stdout: SplitParser {
            onRead: data => root.handleLine(data)
        }

        stderr: StdioCollector {
            waitForEnd: true
            onStreamFinished: if (this.text.length > 0)
                root.message = "native vault boundary failed"
        }
    }
}