feat: add CLI to export decrypted provider credentials (omniroute auth export) (#6683)#7724
Merged
Merged
Conversation
Contributor
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a local-only
omniroute auth exportCLI command that dumps decrypted provider credentials (apiKey,accessToken,refreshToken,idToken) for one connection (--id <id>) or all connections, asjson(default) orenv(--format).This is a pure CLI/direct-SQLite command — no
src/app/api/route is added, matching thereset-encrypted-columns/keys revealprecedent. It reuses the existing CLI-side crypto mirror (bin/cli/encryption.mjs::decryptCredential) and row-read helpers (bin/cli/provider-store.mjs::listProviderConnections/findProviderConnection) — no new crypto, no new DB module.Security controls (the feature's entire purpose is to print plaintext secrets — these are the mitigations, not decoration)
--forcegate before any DB access — without--force, the command prints a confirmation message and returns0without opening the database or decrypting anything.keys reveal'srevealWarning.0600—--out <file>useswriteFileSync(..., { mode: 0o600 }), plus achmodSyncpass if the file pre-existed with looser permissions.<field>DecryptFailed: true), never the caught error text — one stale-key/corrupt row doesn't abort the whole export.Error/console message — only connection ids and field names appear in error paths.src/app/api/route — CLI-only, direct SQLite read.Requires
STORAGE_ENCRYPTION_KEYto be set (checked explicitly, exits1with a clear error otherwise — no ciphertext/plaintext ever appears in that error).Files changed
bin/cli/commands/auth-export.mjs— new command implementationbin/cli/commands/registry.mjs— registersauth exportbin/cli/locales/en.json/bin/cli/locales/pt-BR.json— newauthExport.*i18n namespacedocs/reference/CLI-TOOLS.md— new "Credential Export (⚠ handle with care)" doc sectiontests/unit/cli-auth-export-command.test.ts— new test file (8 cases)changelog.d/features/6683-cli-auth-export.mdHow it was validated (TDD, Hard Rule #18)
Wrote
tests/unit/cli-auth-export-command.test.tsfirst. Proved red→green by temporarily movingauth-export.mjsaside (notgit stash— a plain file move) and re-running the suite: all 8 tests failed withERR_MODULE_NOT_FOUND(red), then passed again after restoring the file (green).Test coverage: force-gate dry-run (no DB touch, no secret in output), missing-
STORAGE_ENCRYPTION_KEYfailure path (no leak),--idfilter + JSON decrypt round-trip, malformed-ciphertext tolerance (<field>DecryptFailedflag, sibling fields unaffected),--format envline shape,--outfile mode0600(including a pre-existing-looser-permissions case), not-found--id(no secret in the error), and a dedicated security regression test capturing all stdout/stderr/error text across the dry-run, missing-key, and malformed-ciphertext paths and asserting no plaintext secret ever appears outside its own legitimate export payload.Gates run (green)
node scripts/check/check-test-discovery.mjsnpm run typecheck:core— cleannpm run typecheck:noimplicit:core— pre-existing errors only, all in files this PR does not touch (open-sse/services/combo.ts,open-sse/utils/usageTracking.ts,src/shared/services/cliRuntime.ts— confirmed viagit diff --stat origin/release/v3.8.49...HEADtouching none of them)npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files>— 0 errors (bin/cli/**is outside ESLint's configured scope entirely, confirmed pre-existing)npm run check:complexity-ratchets— 2059/890, unchanged from baselinenode scripts/check/check-file-size.mjs— no✗on touched filesnpm run check:cycles— no new cyclesnode scripts/check/check-cli-i18n.mjs— allt()keys resolve,pt-BR.jsonhas all top-level sectionsnpm run check:docs-sync— pass (ran via the pre-commit hook)node --import tsx/esm --test tests/unit/cli-auth-export-command.test.ts— 8/8 greennpm run test:coverageintentionally not run locally (shared box, heaviest gate — CI runs it authoritatively); this PR is additive-only (one new file + two-line registry hook + i18n namespace + doc + new isolated test file), so no existing coverage should regress.Deferred / decisions
t("common.forceOpt")/t("common.notFound"), but those keys don't exist incommon.*. Added dedicatedauthExport.forceOpt/authExport.notFoundkeys instead of touching the sharedcommonnamespace, to keep the change surface minimal.--forcemessage — the dry-run message is now fully generic and never touches the DB, which is a stricter interpretation of "no DB access before--force" than the plan's illustrative pseudocode.en/pt-BR, matching thekeys.*precedent and the CLI i18n gate's actual requirement (top-level section parity inpt-BR.json, not full-locale translation).Closes #6683