7.8 KiB
Requirements: Generic API key rotation CLI command
Feature Idea: #1881 Research Date: 2026-05-19 Verdict: VIABLE
Research Summary
The request is for upstream provider credential rotation via CLI. Key findings from codebase analysis:
-
The existing
keys rotatecommand is NOT the target —bin/cli/commands/keys.mjs::runKeysRotateCommandcalls/api/v1/registered-keys/:id/rotate, which rotates OmniRoute-issued client API keys. The author wants to rotate upstream provider API keys stored inprovider_connections. -
The
providers.mjscommand already has the right shape — it registersproviders available,providers list,providers test,providers test-all,providers validate, andproviders metrics. Two new subcommands fit cleanly:providers rotateandproviders status. -
Backend endpoints already exist —
PATCH /api/providers/:idcan update connection fields includingapiKey. Theprovider_store.mjshelperupsertApiKeyProviderConnection()writes encrypted keys. The PATCH path just needs to be confirmed to acceptapiKeyfield updates. -
Expiration tracking is in-place —
src/domain/providerExpiration.ts+GET /api/providers/expirationreturns{ list[], summary }withstatus: active|expiring_soon|expired|unknownper connection.providers statuscan consume this directly. -
Cooldown reset hook —
open-sse/services/auth.ts::clearAccountError()must be called (or triggered via an API call that clears it) after successful rotation so the connection exits cooldown immediately. -
OAuth providers —
token-healthendpoint tracks OAuth connection health. For OAuth providers, rotation means re-running the OAuth flow. The CLI already hasomniroute oauthcommands. A--oauthflag onproviders rotateshould delegate to the existing oauth flow. -
--from-envflag — no precedent in CLI today, but it is a safe, common CLI pattern. The key is read fromprocess.env[VAR]and never logged/echoed.
Reference Implementations
| # | Repository | Stars | Last Updated | Approach | Relevance |
|---|---|---|---|---|---|
| 1 | hashicorp/vault | 32k+ | 2026 | Dynamic secrets, lease renewal via CLI (vault lease renew) |
High — gold standard for credential lifecycle |
| 2 | aws/aws-cli | 15k+ | 2026 | aws iam update-access-key + aws configure set for rotation |
High — env var sourcing pattern |
| 3 | cli/cli (gh) | 38k+ | 2026 | gh auth refresh for OAuth token rotation |
High — OAuth re-auth CLI pattern |
| 4 | dopplerhq/cli | 400+ | 2025 | doppler secrets set KEY=VALUE with env-var sourcing |
Medium — secrets manager CLI UX |
Key Patterns Found
- Confirmation gate:
--yesflag to skip interactive confirmation (already used inkeys remove,keys revoke) - Env-var sourcing:
--from-env VARreadsprocess.env[VAR]; guard against empty string; never log the value - Dry-run / status-only:
--dry-runflag inspects expiry without writing; surfaceproviderExpiration.statusper connection - Grace-period / zero-downtime: brief overlap period where old key is still accepted; out of scope for CLI since provider-side, but
--grace-periodflag in existingkeys rotatesets a precedent - Post-rotate validation: after writing the new key, immediately call the test endpoint (
providers test <id>) to verify the new key works; report pass/fail
Proposed Solution Architecture
Approach
Add two new subcommands to the existing providers command in bin/cli/commands/providers.mjs:
omniroute providers rotate <connectionId> [--new-key <key>] [--from-env <VAR>] [--oauth] [--yes] [--skip-test]omniroute providers status [--provider <name>] [--output table|json]
Both commands follow the dual-path pattern already established in keys.mjs: try server API first (if server is up), fall back to direct SQLite write if offline (for rotate only — status requires the server for expiration data).
The rotate command flow:
- Resolve connection by ID or provider name (partial match allowed, error if ambiguous)
- Source new key:
--new-key>--from-env VAR> interactive prompt (unless--yes) - Confirm unless
--yes PATCH /api/providers/:idwith{ apiKey: newKey }— or directupsertApiKeyProviderConnectionif offline- POST to clear account error / cooldown (call
/api/providers/:id/clear-erroror the equivalent) - Unless
--skip-test: runproviders test <id>and report pass/fail - Report result
New Files
| File | Purpose |
|---|---|
| No new files required | All changes fit in existing modules |
Modified Files
| File | Changes |
|---|---|
bin/cli/commands/providers.mjs |
Add providers rotate and providers status subcommands + action implementations |
bin/cli/provider-store.mjs |
Add updateProviderApiKey(db, connectionId, newEncryptedKey) helper if not already present; reuse upsertApiKeyProviderConnection |
bin/cli/locales/en.json (or equivalent) |
New i18n keys for new subcommand output strings |
Database Changes
None. provider_connections.apiKey already stores encrypted API keys; the PATCH route and existing store helpers handle encryption.
API Changes
- Verify
PATCH /api/providers/:idacceptsapiKeyfield — if not, a minimal addition to the handler is needed - Optional:
POST /api/providers/:id/clear-error(may already exist; verify viasrc/app/api/providers/[id]/route.ts)
UI Changes
None required.
Implementation Effort
- Estimated complexity: Low-Medium
- Estimated files changed: ~3 (providers.mjs, provider-store.mjs, locales)
- Dependencies needed: None
- Breaking changes: No — additive only
- i18n impact: ~12 new translation keys (rotate prompt, confirm, success, error, status table headers, dry-run output)
- Test coverage needed: Unit tests for
runProvidersRotateCommandandrunProvidersStatusCommand; mockapiFetch+openOmniRouteDb; assert--from-envreads fromprocess.env; assert--yesskips confirmation; assert cooldown-clear call is made
Open Questions
- Does
PATCH /api/providers/:idcurrently acceptapiKeyas a writable field? If not, it needs a targeted addition (with the usual Zod validation + encryption). - Is there a
POST /api/providers/:id/clear-errorroute, or does cooldown clear happen implicitly on next successful request? If the latter, no explicit clear-call is needed from the CLI. - Should
providers statusalso surfacetestStatusandrateLimitedUntil(fromprovider_connections) in addition to expiration data? Recommended yes — gives operators a single-command health overview. - For
--oauth, shouldproviders rotatetrigger the full browser-based OAuth flow inline, or should it just print the auth URL and instruct the user to runomniroute oauth <provider>? The latter is safer and avoids duplicating OAuth flow logic. --from-envwith an unset variable: should it error loudly (recommended) or fall through to interactive prompt?
External References
src/domain/providerExpiration.ts— in-memory expiration store, consumed via/api/providers/expirationsrc/app/api/providers/expiration/route.ts— GET endpoint returning expiration list + summarysrc/app/api/token-health/route.ts— OAuth token health aggregateopen-sse/services/auth.ts—clearAccountError()andmarkAccountUnavailable()bin/cli/commands/providers.mjs— existing provider subcommand structurebin/cli/commands/keys.mjs— precedent for--yes,--from-env(not yet),rotateUXbin/cli/CONVENTIONS.md— normative CLI conventions (must follow)docs/architecture/RESILIENCE_GUIDE.md— connection cooldown section