My agentic slop goes here. Not intended for anyone else!
README.md

Keyeio - Secure API Key Storage for Eio Applications#

Keyeio provides secure storage and retrieval of API keys and credentials for Eio-based applications. It uses the XDG Base Directory Specification via the xdge library for consistent, platform-appropriate storage locations.

Features#

  • 🔐 Secure Storage: Keys stored in XDG-compliant directories with strict file permissions (0o600)
  • 📁 Multiple Profiles: Support multiple configurations per service (default, production, staging, etc.)
  • 🎯 Type-Safe API: Abstract types and comprehensive accessors following OCaml best practices
  • 🖥️ Cmdliner Integration: Easy command-line argument handling matching xdge patterns
  • 🔮 Future-Proof: Designed to support Secret Service API (GNOME Keyring, KWallet) integration

Installation#

opam install keyeio

Quick Start#

Basic Usage#

open Cmdliner

let main (xdg, _) profile =
  (* Get credentials from profile *)
  let api_key = Keyeio.Profile.get_required profile ~key:"api_key" in
  let base_url = Keyeio.Profile.get profile ~key:"base_url"
    |> Option.value ~default:"https://api.example.com" in

  (* Use credentials with your API client *)
  Printf.printf "Connecting to %s\n" base_url;
  (* ... *)

let () =
  Eio_main.run @@ fun env ->

  (* Create Cmdliner terms *)
  let xdg_term = Xdge.Cmd.term "myapp" env#fs () in
  let key_term = Keyeio.Cmd.term
    ~app_name:"myapp"
    ~fs:env#fs
    ~service:"immiche"  (* Service name *)
    () in

  let cmd = Cmd.v (Cmd.info "myapp")
    Term.(const main $ xdg_term $ key_term) in
  exit (Cmd.eval cmd)

Storage Format#

Keys are stored in ~/.config/<appname>/keys/<service>.toml:

[default]
api_key = "abc123..."
base_url = "https://api.example.com"

[production]
api_key = "xyz789..."
base_url = "https://api.prod.example.com"

[staging]
api_key = "def456..."
base_url = "https://api.staging.example.com"

Examples#

The library includes comprehensive examples in example/keyeio_example.ml:

# Build the example
dune build keyeio/example/keyeio_example.exe

# List all configured services
./keyeio_example.exe list

# Show profiles for a service
./keyeio_example.exe profiles immiche

# Use default profile
./keyeio_example.exe basic

# Use a specific profile
./keyeio_example.exe basic --profile production

# Simulate API client
./keyeio_example.exe client --profile staging

Example Output#

$ ./keyeio_example.exe profiles immiche
=== List Profiles Example ===
Service: immiche
Available profiles:
  - default (keys: api_key, base_url)
  - production (keys: api_key, base_url, extra_field)
  - staging (keys: api_key, base_url)

Service details:
Service immiche:
  default:
    Profile immiche.default:
      api_key: test_api***
      base_url: https://immich.example.com
  production:
    Profile immiche.production:
      api_key: prod_api***
      base_url: https://immich.prod.example.com
      extra_field: some_value

API Overview#

Creating a Keyeio Context#

val create : Xdge.t -> t

Creates a keyeio context from an XDG context. Keys are stored in keys/ subdirectory of the config directory.

Loading Services#

val load_service : t -> service:string -> (Service.t, [> `Msg of string]) result

Load all profiles for a service from ~/.config/<appname>/keys/<service>.toml.

val list_services : t -> (string list, [> `Msg of string]) result

List all available services (TOML files in the keys directory).

Working with Profiles#

module Profile : sig
  val service : t -> string
  val name : t -> string
  val get : t -> key:string -> string option
  val get_required : t -> key:string -> string  (* raises Key_not_found *)
  val keys : t -> string list
  val pp : Format.formatter -> t -> unit
end

Working with Services#

module Service : sig
  val name : t -> string
  val profile_names : t -> string list
  val get_profile : t -> string -> Profile.t option
  val default_profile : t -> Profile.t option
  val pp : Format.formatter -> t -> unit
end

Cmdliner Integration#

module Cmd : sig
  val term :
    app_name:string ->
    fs:Eio.Fs.dir_ty Eio.Path.t ->
    service:string ->
    ?profile:string ->       (* Default: "default" *)
    ?key_file:bool ->        (* Add --key-file flag, default: true *)
    unit ->
    Profile.t Cmdliner.Term.t

  val env_docs : app_name:string -> service:string -> unit -> string
end

The term function generates:

  • --profile NAME: Select which profile to use
  • --key-file PATH: Override with direct file path (optional)

Security#

Current Security Model#

  • Keys stored as TOML files in ~/.config/<appname>/keys/
  • Files created with permissions 0o600 (owner read/write only)
  • Sensitive values masked in pretty-printing output
  • Follows standard Unix file permission security model

Future Enhancements#

The library is designed to support future integration with system keychains:

  • Linux: freedesktop.org Secret Service API (GNOME Keyring, KWallet, KeePassXC)
  • macOS: Keychain Services API
  • Windows: Credential Manager API

The abstract backend type allows adding these integrations without breaking the API.

Integration Examples#

With Immiche (Immich API Client)#

let main (xdg, _) profile =
  Eio_main.run @@ fun env ->

  let api_key = Keyeio.Profile.get_required profile ~key:"api_key" in
  let base_url = Keyeio.Profile.get_required profile ~key:"base_url" in

  let client = Immiche.create ~sw ~env ~base_url ~api_key () in
  (* Use client... *)

let () =
  Eio_main.run @@ fun env ->
  let xdg_term = Xdge.Cmd.term "myapp" env#fs () in
  let key_term = Keyeio.Cmd.term ~app_name:"myapp" ~fs:env#fs ~service:"immiche" () in
  (* ... *)

With Karakeepe (Hoarder API Client)#

let key_term = Keyeio.Cmd.term
  ~app_name:"myapp"
  ~fs:env#fs
  ~service:"karakeepe"
  () in

let api_key = Keyeio.Profile.get_required profile ~key:"api_key" in
let base_url = Keyeio.Profile.get_required profile ~key:"base_url" in

let bookmarks = Karakeepe.fetch_all_bookmarks ~sw ~env ~api_key base_url in
(* Process bookmarks... *)

Environment Variables#

The storage location respects XDG Base Directory Specification:

  • XDG_CONFIG_HOME: Base directory for config files (default: ~/.config)
  • <APPNAME>_CONFIG_DIR: Application-specific override (highest priority)

Keys are stored in: $XDG_CONFIG_HOME/<appname>/keys/<service>.toml

Documentation#

Full API documentation is available:

dune build @doc
open _build/default/_doc/_html/keyeio/index.html

License#

ISC License

See Also#

  • xdge - XDG Base Directory Specification for Eio
  • immiche - Immich API client using keyeio
  • karakeepe - Hoarder API client using keyeio