Skip to content

Architecture

keyledger/
├── main.go CLI entry point (Cobra commands)
├── provider/
│ ├── provider.go Core interfaces and optional interfaces
│ ├── types.go Key, KeyInventory, Actor, KeyWorkspace, KeyPattern
│ ├── workspace.go WorkSpaceDisplay / WorkspaceName helpers
│ └── registry.go Global provider registry (Register / All)
├── providers/
│ ├── openai/ OpenAI implementation
│ ├── anthropic/ Anthropic implementation
│ ├── aws/ AWS implementation
│ ├── google/ Google Cloud implementation
│ ├── mistral/ Mistral implementation (session-based auth)
│ ├── groq/ Stub
│ ├── cohere/ Stub
│ └── ... Other stubs
├── internal/
│ ├── ci/ Non-interactive runner functions (watch…)
│ ├── credentials/
│ │ ├── providers.go Credential field names and env var mappings per provider
│ │ └── search.go Auto-discovery of local credentials (SSO profiles, etc.)
│ ├── inventory/
│ │ ├── collector.go Parallel provider collection
│ │ ├── health.go Risk scoring (ScoreKey, Summarize)
│ │ └── workspaces.go WorkspaceTree — per-provider scope summaries
│ ├── kratos/
│ │ └── login.go Ory Kratos browser-flow login (2-step, cookie jar)
│ ├── snapshot/ Snapshot serialisation / diff
│ ├── store/
│ │ ├── store.go SQLite store (provider config, settings, encrypted creds, snapshots)
│ │ ├── snapshots.go Snapshot list / delete operations
│ │ ├── config.go Config struct and loader
│ │ └── defaults.go Default thresholds and paths
│ ├── tui/ Bubble Tea TUI (app, views, messages)
│ └── unsealapi/ Headless HTTPS unseal API for Docker / CI
└── docs/ Astro Starlight documentation site
main.go
└── loadConfigAndStore()
├── store.Load() read settings from SQLite
└── store.Open() open SQLite, register as encrypted credential store
TUI / CI runner
└── inventory.Collect()
├── for each enabled provider (parallel, up to cfg.Parallel goroutines)
│ ├── keyring.LoadCredentials(name) read from encrypted SQLite store
│ ├── provider.ListKeys(ctx, cfg) call provider admin API
│ └── inventory.ScoreKey(key, thresholds) compute risk score
└── return Result{ Inventories, Errors }

Providers register themselves via provider.Register(p) called from init(). Fully-implemented providers are activated by a blank import in main.go:

import (
_ "github.com/riptideslabs/keyledger/providers/openai"
_ "github.com/riptideslabs/keyledger/providers/anthropic"
// …
)

The registry is a simple slice protected by a mutex. provider.All() returns all registered providers in registration order.

Credentials are never stored in the config file. The flow is:

  1. User enters a credential in the TUI or via the unseal API
  2. store.EncSet(provider, field, value) is called
  3. The value is encrypted with AES-256-GCM and written to the enc_credentials SQLite table

At collection time, store.EncGet(provider, field) reads all fields for a provider from the encrypted store and returns them as map[string]string. The store must be unlocked (password entered) before credentials can be read. This map is passed to the provider as ProviderConfig.Credentials.

inventory.ScoreKey(k *provider.Key, t store.Thresholds) runs after every ListKeys call and sets Key.RiskScore and Key.RiskReasons in-place. No network calls — purely time-based arithmetic on the key’s AgeDays and IdleDays fields.

The TUI uses Bubble Tea with a single root model in internal/tui/app.go. Sub-models handle individual screens and communicate via typed messages defined in messages.go:

Sub-modelFilePurpose
keysViewModelkeys_view.goKeys table with filter, sort, WorkSpaces tab
scopesViewModelworkspaces_view.goCollapsible workspace/project tree
healthViewModelhealth_view.goRisk-grouped key list
diffViewModeldiff_view.goSnapshot vs live diff
snapshotsModelsnapshots_view.goSnapshot list, delete, export
providersModelproviders_screen.goProvider config and credential management
statusBarModelstatus_bar.goContext-sensitive footer with key bindings

Async operations (provider refresh, Kratos login, snapshot save/delete) return tea.Cmd functions that run in goroutines and send result messages back to the update loop.

switchView(v ViewID) is the single place that changes the active view; it also calls updateStatusBar() immediately so the footer always reflects the current context.