app.keychain - Secure Credential Storage API
Securely store sensitive data (API keys, tokens, passwords, etc.) using macOS Keychain.
Permission: Requires
keychaindeclared in the manifest (L1 standard) Isolation: Each plugin’s credentials are isolated from each other; plugins cannot access other plugins’ credentials.
Methods
app.keychain.set(key, value)
Store a credential. Updates if the key already exists.
Parameters:
key(string) - Credential namevalue(string) - Credential value
Returns: boolean, error
local ok, err = app.keychain.set("api_key", "sk-xxxxxxxxxxxx")
if not ok then
app.log.error("Storage failed: " .. err)
end
app.keychain.get(key)
Retrieve a credential.
Parameters:
key(string) - Credential name
Returns: string|nil, error - Credential value or nil (returns nil without error when not found)
local token, err = app.keychain.get("api_key")
if token then
app.log.info("Token length: " .. #token)
elseif err then
app.log.error("Read failed: " .. err)
else
app.log.info("Credential does not exist")
end
app.keychain.delete(key)
Delete a credential. Deleting a non-existent key also returns success.
Parameters:
key(string) - Credential name
Returns: boolean, error
local ok, err = app.keychain.delete("api_key")
app.keychain.has(key)
Check if a credential exists.
Parameters:
key(string) - Credential name
Returns: boolean
if app.keychain.has("api_key") then
local key = app.keychain.get("api_key")
-- Use key...
end
Examples
API Key Management
function MyPlugin:handleSetup(context)
-- Check if already configured
if app.keychain.has("github_token") then
local result = app.dialog.alert({
title = "Already Configured",
message = "GitHub Token already exists. Update it?",
buttons = {"Update", "Cancel"}
})
if result ~= 1 then return end
end
-- Collect credentials via form
local result = app.dialog.form({
title = "Configure GitHub Token",
fields = {
{type = "text", id = "token", label = "Personal Access Token", default = ""}
}
})
if result and result.token ~= "" then
local ok, err = app.keychain.set("github_token", result.token)
if ok then
app.notification.show("Configuration Successful", "GitHub Token has been securely stored")
else
app.dialog.alert({title = "Error", message = "Storage failed: " .. err})
end
end
end
function MyPlugin:handleFetch(context)
local token = app.keychain.get("github_token")
if not token then
app.dialog.alert({title = "Not Configured", message = "Please run 'Configure Token' first"})
return
end
local resp = app.http.get("https://api.github.com/user", {
["Authorization"] = "Bearer " .. token,
["Accept"] = "application/vnd.github.v3+json"
})
if resp and resp.status == 200 then
local user = app.json.parse(resp.body)
app.notification.show("GitHub", "Logged in as: " .. user.login)
end
end
Multi-Credential Management
function MyPlugin:handleManageCredentials(context)
local keys = {"api_key", "secret", "webhook_url"}
local items = {}
for _, key in ipairs(keys) do
local exists = app.keychain.has(key)
table.insert(items, key .. ": " .. (exists and "Configured" or "Not configured"))
end
local selected = app.dialog.choose(items, {
title = "Credential Management",
message = "Select a credential to manage"
})
if selected then
local key = keys[1] -- Determine key from selected item
for i, item in ipairs(items) do
if item == selected then key = keys[i] end
end
local result = app.dialog.alert({
title = key,
message = app.keychain.has(key) and "Currently configured" or "Currently not configured",
buttons = {"Set", "Delete", "Cancel"}
})
if result == 1 then
local input = app.dialog.form({
title = "Set " .. key,
fields = {{type = "text", id = "value", label = "Value"}}
})
if input then
app.keychain.set(key, input.value)
end
elseif result == 2 then
app.keychain.delete(key)
app.notification.show("Deleted", key)
end
end
end
Notes
- Plugin Isolation: Each plugin’s credentials are isolated; different plugins cannot read each other’s credentials
- Storage Security: Uses macOS Keychain (
kSecClassGenericPassword) with encrypted storage - String Only:
valuemust be a string type; serialize complex data to JSON first if needed - Persistence: Credentials are stored in the system Keychain and are not automatically removed when a plugin is uninstalled
- Access Level: Uses
kSecAttrAccessibleAfterFirstUnlock, accessible after the device is first unlocked