feat: add proton-pass plugin (#4)
* feat: add proton-pass plugin * feat: make plugin setQuery prefix-relative See: https://github.com/noctalia-dev/noctalia/commit/1bc8308cb29cc29439c1837fcd553424087dddc1
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Proton Pass
|
||||
|
||||
A Noctalia Launcher plugin that integrates with the Proton Pass CLI.
|
||||
|
||||
## Features
|
||||
|
||||
- Browse your Proton Pass vaults
|
||||
- Browse items within each vault
|
||||
- Copy passwords to the clipboard
|
||||
- Display TOTP codes as a notification (when available)
|
||||
|
||||
## Requirements
|
||||
|
||||
- [proton-pass-cli](https://protonpass.github.io/pass-cli/) installed and available in your `PATH`
|
||||
- An authenticated Proton Pass CLI session (run `pass-cli login`)
|
||||
@@ -0,0 +1,211 @@
|
||||
--!nonstrict
|
||||
|
||||
-- Type returned by `pass-cli vault list --output=json`
|
||||
type Vault = { name: string, vault_id: string, share_id: string }
|
||||
type Vaults = { [number]: Vault }
|
||||
|
||||
-- Type returned by `pass-cli item list <vault_name> --output=json`
|
||||
type Item = { id: string, share_id: string, vault_id: string, state: string, title: string, item_type: string }
|
||||
type Items = { [number]: Item }
|
||||
|
||||
-- Caches to avoid repeated CLI calls while the launcher session is active
|
||||
local cachedVaults: Vaults = {}
|
||||
local cachedItems: { [string]: Items } = {}
|
||||
|
||||
-- Utility functions -----------------------------------------------------------
|
||||
|
||||
-- Search cached items by item id across all cached vaults
|
||||
local function searchCachedItems(item_id: string): Item?
|
||||
for _, vault in cachedItems do
|
||||
for _, item in vault do
|
||||
if item.id == item_id then
|
||||
return item
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Map + filter: transform each element, include only non-nil results
|
||||
local function filterMap<T, U>(sequence: { T }, fn: (T) -> U?)
|
||||
local out = {}
|
||||
for _, v in sequence do
|
||||
local res = fn(v)
|
||||
if res ~= nil then
|
||||
table.insert(out, res)
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function glyphTable(item_type: string)
|
||||
local lookupTable: { [string]: string } = {
|
||||
alias = "at",
|
||||
credit_card = "credit-card",
|
||||
custom = "note",
|
||||
identity = "user-circle",
|
||||
login = "lock",
|
||||
ssh_key = "key",
|
||||
wifi = "router",
|
||||
}
|
||||
|
||||
return lookupTable[item_type] or "question-mark"
|
||||
end
|
||||
|
||||
-- Proton Pass CLI functions ---------------------------------------------------
|
||||
|
||||
local function getVaultsAsync(callback)
|
||||
if #cachedVaults ~= 0 then
|
||||
callback(cachedVaults)
|
||||
return
|
||||
end
|
||||
|
||||
noctalia.runAsync(`pass-cli vault list --output=json`, function(result)
|
||||
if result.exitCode ~= 0 then
|
||||
noctalia.log("proton-pass: failed to list vaults")
|
||||
noctalia.notifyError("Proton Pass", noctalia.tr("failed-list-vaults"))
|
||||
return callback({})
|
||||
end
|
||||
|
||||
local decoded = noctalia.json.decode(result.stdout)
|
||||
local vaults: Vaults = decoded["vaults"] or {}
|
||||
cachedVaults = vaults
|
||||
return callback(vaults)
|
||||
end)
|
||||
end
|
||||
|
||||
local function getItemsAsync(vaultName: string, callback)
|
||||
if cachedItems[vaultName] ~= nil then
|
||||
callback(cachedItems[vaultName])
|
||||
return
|
||||
end
|
||||
|
||||
noctalia.runAsync(`pass-cli item list "{vaultName}" --output=json`, function(result)
|
||||
if result.exitCode ~= 0 then
|
||||
-- noctalia.log(`proton-pass: failed to list items in vault: {vaultName}`)
|
||||
return callback({})
|
||||
end
|
||||
|
||||
local decoded = noctalia.json.decode(result.stdout)
|
||||
local items: Items = decoded["items"] or {}
|
||||
cachedItems[vaultName] = items
|
||||
return callback(items)
|
||||
end)
|
||||
end
|
||||
|
||||
local function getPasswordAsync(item: Item, callback)
|
||||
noctalia.runAsync(`pass-cli item view "pass://{item.share_id}/{item.id}/password"`, function(result)
|
||||
if result.exitCode ~= 0 then
|
||||
noctalia.log(`proton-pass: no password found for item: {item.title}`)
|
||||
return
|
||||
end
|
||||
|
||||
local password: string = result.stdout
|
||||
callback(password)
|
||||
end)
|
||||
end
|
||||
|
||||
local function getItemTotpAsync(item: Item, callback)
|
||||
noctalia.runAsync(`pass-cli item totp "pass://{item.share_id}/{item.id}" --output=json`, function(result)
|
||||
if result.exitCode ~= 0 then
|
||||
-- It's not an error if a TOTP is absent; just log for debugging
|
||||
-- noctalia.log(`proton-pass: no TOTP code for item: {item.title}`)
|
||||
return
|
||||
end
|
||||
|
||||
local decoded = noctalia.json.decode(result.stdout)
|
||||
local totp: string = decoded["totp"]
|
||||
callback(totp)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Row constructors ------------------------------------------------------------
|
||||
|
||||
local function vaultRows(pattern: string, callback)
|
||||
getVaultsAsync(function(vaults)
|
||||
callback(filterMap(vaults, function(vault)
|
||||
local score = noctalia.fuzzyScore(pattern, vault.name)
|
||||
if score == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
return {
|
||||
id = noctalia.json.encode({ type = "vault", id = vault.name }),
|
||||
title = vault.name,
|
||||
glyph = "folder",
|
||||
score = score,
|
||||
query = `{vault.name} `,
|
||||
}
|
||||
end))
|
||||
end)
|
||||
end
|
||||
|
||||
local function itemRows(vault: string, pattern: string, callback)
|
||||
getItemsAsync(vault, function(items)
|
||||
callback(filterMap(items, function(item)
|
||||
-- Exclude Trashed items before scoring
|
||||
if item.state ~= "Active" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local score = noctalia.fuzzyScore(pattern, item.title)
|
||||
if score == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
return {
|
||||
id = noctalia.json.encode({ type = "item", id = item.id }),
|
||||
title = item.title,
|
||||
glyph = glyphTable(item.item_type),
|
||||
score = score,
|
||||
}
|
||||
end))
|
||||
end)
|
||||
end
|
||||
|
||||
-- Entry points ----------------------------------------------------------------
|
||||
|
||||
function onQuery(query: string)
|
||||
local vault, rest = query:match("^(%S+)%s+(.*)$")
|
||||
|
||||
-- Reset cache when query is empty
|
||||
if query == "" then
|
||||
cachedVaults = {}
|
||||
cachedItems = {}
|
||||
end
|
||||
|
||||
if not vault then
|
||||
launcher.setResults(query, { { id = "loading", title = noctalia.tr("loading-vaults"), glyph = "loader" } })
|
||||
vaultRows(query, function(rows)
|
||||
launcher.setResults(query, rows)
|
||||
end)
|
||||
else
|
||||
launcher.setResults(query, { { id = "loading", title = noctalia.tr("loading-items"), glyph = "loader" } })
|
||||
itemRows(vault, rest, function(rows)
|
||||
launcher.setResults(query, rows)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function onActivate(_id: string)
|
||||
local id: { type: string, id: string } = noctalia.json.decode(_id)
|
||||
|
||||
if id.type == "item" then
|
||||
local item = searchCachedItems(id.id)
|
||||
|
||||
if item == nil then
|
||||
noctalia.notifyError("Proton Pass", noctalia.tr("item-not-found"))
|
||||
return
|
||||
end
|
||||
|
||||
getPasswordAsync(item, function(password)
|
||||
noctalia.copyToClipboard(password, "text/plain")
|
||||
noctalia.notify(`{item.title}`, noctalia.tr("copied-password"))
|
||||
|
||||
-- Try to fetch TOTP and notify
|
||||
getItemTotpAsync(item, function(totp)
|
||||
noctalia.notify(`{item.title} TOTP`, totp)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
id = "lucasoe/proton-pass"
|
||||
name = "Proton Pass"
|
||||
version = "0.1.0"
|
||||
min_noctalia = "5.0.0"
|
||||
author = "LucasOe"
|
||||
license = "MIT"
|
||||
icon = "lock"
|
||||
description = "Access Proton Pass from the launcher"
|
||||
tags = ["launcher"]
|
||||
dependencies = ["proton-pass-cli"]
|
||||
|
||||
[[launcher_provider]]
|
||||
id = "proton-pass"
|
||||
entry = "launcher.luau"
|
||||
prefix = "pass"
|
||||
glyph = "lock"
|
||||
include_in_global_search = false
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"copied-password": "Copied password to clipboard",
|
||||
"failed-list-vaults": "Failed to list vaults. Are you logged into pass-cli?",
|
||||
"item-not-found": "Item not found",
|
||||
"loading-items": "Loading Items…",
|
||||
"loading-vaults": "Loading Vaults…"
|
||||
}
|
||||
Reference in New Issue
Block a user