feat(provider): add Doppler secrets manager provider#376
Conversation
There was a problem hiding this comment.
Code Review
This pull request adds support for Doppler as a secrets provider in fnox. The implementation includes the necessary provider configuration, CLI integration, and documentation. I have identified two issues: first, the code incorrectly trims the output of the Doppler CLI, which could corrupt secret values that contain leading or trailing whitespace. Second, the implementation incorrectly treats an empty secret value as a 'not found' error, whereas Doppler supports empty string secrets.
|
|
||
| Ok(stdout.trim().to_string()) | ||
| } | ||
| } |
There was a problem hiding this comment.
Trimming the stdout of the Doppler command can lead to data corruption if a secret value intentionally contains leading or trailing whitespace (e.g., a private key with a trailing newline or a secret that is just a space). Since the Doppler CLI's --plain flag is designed to output the raw secret value without extra formatting, it is safer to return the output as-is.
| } | |
| Ok(stdout) |
There was a problem hiding this comment.
Disagree — every other CLI-based provider in this codebase (1Password, Bitwarden, Bitwarden SM, Infisical, password-store, Proton Pass, Vault) trims stdout for the same reason: CLI tools append a trailing newline. Removing the trim here would be inconsistent with the rest of the codebase and could cause subtle issues downstream (e.g., env vars with trailing newlines).
This comment was generated by Claude Code.
| provider: PROVIDER_NAME.to_string(), | ||
| secret: value.to_string(), | ||
| hint: "Check that the secret exists in your Doppler project/config".to_string(), | ||
| url: PROVIDER_URL.to_string(), | ||
| }); | ||
| } | ||
|
|
||
| Ok(result) |
There was a problem hiding this comment.
This check incorrectly treats an empty secret value as a "not found" error. Doppler allows secrets to have empty string values. If the execute_doppler_command call succeeded (i.e., returned Ok), it means the secret exists in Doppler. If the result is empty, it should be returned as an empty string rather than an error.
Ok(result)There was a problem hiding this comment.
Good catch — fixed. Removed the empty-string guard entirely. If the secret doesn't exist, the Doppler CLI will exit non-zero and we'll hit the error classification path. An empty string is a valid secret value.
This comment was generated by Claude Code.
Greptile SummaryAdds a Doppler secrets manager provider that delegates to the Confidence Score: 5/5Safe to merge — all prior P0/P1 findings are resolved and the one remaining finding is P2 All three previously flagged issues (token exposure in process args, empty-string check, generic 'not found' pattern) have been fixed per the review thread replies. The only remaining finding is a P2 concern about the 'does not exist' error pattern potentially misclassifying Doppler project/config errors as secret-not-found in edge cases; it does not affect correctness for the common path and does not block merge. src/providers/doppler.rs — the 'does not exist' entry in SECRET_NOT_FOUND_PATTERNS warrants a minor follow-up to avoid misleading error messages for project/config misconfigurations Important Files Changed
Sequence DiagramsequenceDiagram
participant fnox
participant DopplerProvider
participant DopplerCLI as doppler CLI
note over fnox,DopplerCLI: Single secret fetch
fnox->>DopplerProvider: get_secret("DATABASE_URL")
DopplerProvider->>DopplerCLI: doppler secrets get DATABASE_URL --plain<br/>[env: DOPPLER_TOKEN=...]
DopplerCLI-->>DopplerProvider: plaintext value
DopplerProvider-->>fnox: Ok("value")
note over fnox,DopplerCLI: Batch secret fetch (2+ secrets)
fnox->>DopplerProvider: get_secrets_batch([...])
DopplerProvider->>DopplerCLI: doppler secrets get NAME1 NAME2 --json<br/>[env: DOPPLER_TOKEN=...]
DopplerCLI-->>DopplerProvider: JSON {"NAME1":{"computed":"v1"},...}
DopplerProvider-->>fnox: HashMap<key, Ok(value)>
note over fnox,DopplerCLI: Connection test
fnox->>DopplerProvider: test_connection()
DopplerProvider->>DopplerCLI: doppler secrets --only-names
DopplerCLI-->>DopplerProvider: secret names list
DopplerProvider-->>fnox: Ok(())
Greploops — Automatically fix all review issues by running Reviews (5): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile |
| const SECRET_NOT_FOUND_PATTERNS: &[&str] = &[ | ||
| "could not find secret", | ||
| "secret not found", | ||
| "does not exist", | ||
| "not found", | ||
| ]; |
There was a problem hiding this comment.
Overly broad "not found" pattern can misclassify errors
"not found" is a substring of many generic error messages (e.g. "Host not found", "command not found"). In classify_cli_error, this pattern is checked after RESOURCE_NOT_FOUND_PATTERNS, but any network-level error containing "not found" will still be promoted to ProviderSecretNotFound when secret_ref is Some. Consider narrowing the list to Doppler-specific phrases such as "secret not found" and "could not find secret", and removing the catch-all "not found" entry.
| const SECRET_NOT_FOUND_PATTERNS: &[&str] = &[ | |
| "could not find secret", | |
| "secret not found", | |
| "does not exist", | |
| "not found", | |
| ]; | |
| const SECRET_NOT_FOUND_PATTERNS: &[&str] = &[ | |
| "could not find secret", | |
| "secret not found", | |
| "does not exist", | |
| ]; |
There was a problem hiding this comment.
Good catch — removed the generic `"not found"` entry from `SECRET_NOT_FOUND_PATTERNS`. The remaining patterns (`"could not find secret"`, `"secret not found"`, `"does not exist"`) are specific enough to avoid false positives from network or system errors.
This comment was generated by Claude Code.
| if result.is_empty() { | ||
| return Err(FnoxError::ProviderSecretNotFound { | ||
| provider: PROVIDER_NAME.to_string(), | ||
| secret: value.to_string(), | ||
| hint: "Check that the secret exists in your Doppler project/config".to_string(), | ||
| url: PROVIDER_URL.to_string(), | ||
| }); | ||
| } |
There was a problem hiding this comment.
Empty-string check silently misreports legitimately empty secrets
doppler secrets get <NAME> --plain returns an empty string when the secret value itself is empty (Doppler permits blank values). The current guard turns that into a ProviderSecretNotFound error, which is incorrect — the secret exists, it is just empty. Unless fnox has a policy that empty secrets are invalid, this check should be removed and the empty string returned as-is.
There was a problem hiding this comment.
Agreed — fixed. Removed the empty-string check. Doppler CLI exits non-zero when a secret doesn't exist, so the error classification handles the "not found" case. Empty string values are now returned as-is.
This comment was generated by Claude Code.
| if let Some(ref token) = self.get_token() { | ||
| args.push(format!("--token={}", token)); | ||
| } |
There was a problem hiding this comment.
Service token exposed in process argument list
Appending the authentication token as a CLI flag (the --token branch in build_common_args) makes the live token value visible to any user who can run ps aux or read /proc/<pid>/cmdline on the host. The Doppler CLI supports receiving the token via a dedicated environment variable instead, which keeps it out of the process argument list entirely. The token should be set on the Command object via cmd.env(...) inside execute_doppler_command, and the --token flag should be removed from build_common_args.
There was a problem hiding this comment.
Good catch — fixed. The token is now passed via `cmd.env("DOPPLER_TOKEN", token)` instead of as a `--token` CLI argument, keeping it out of the process argument list.
This comment was generated by Claude Code.
Pass auth token via DOPPLER_TOKEN env var instead of --token CLI arg to avoid exposing it in process argument list. Remove empty-string guard that incorrectly treated empty secret values as not-found errors. Narrow SECRET_NOT_FOUND_PATTERNS to avoid false positives from generic "not found" substrings in unrelated error messages.
The health check loop silently continued when LocalStack wasn't ready, causing downstream aws commands to fail with connection errors. Now fails with docker logs on timeout, and verifies services respond before creating test resources.
LocalStack latest (2026.3.1.dev) now requires LOCALSTACK_AUTH_TOKEN to run, causing all Ubuntu bats jobs to fail. Pin to 4.14.0 (last v4 release before the licensing change).
This reverts commit 44a7811.
This reverts commit b8d8af1.
# Conflicts: # docs/public/schema.json
#380) ## Summary - Move `clone_provider_error` and `map_batch_error` from per-provider free functions to methods on `FnoxError` - Eliminates boilerplate that would otherwise be duplicated by every CLI-based provider (currently in infisical, and duplicated in the incoming Doppler PR #376) - Adds unit tests for the new methods on `FnoxError` ## Test plan - [x] All 12 infisical provider tests pass - [x] All 6 error module tests pass (including 4 new ones) - [x] Full `cargo test` passes (131/132, pre-existing keychain skip) 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk refactor that centralizes provider error cloning/mapping logic; behavior should remain equivalent but could subtly affect provider error formatting if the fallback parameters are misused. > > **Overview** > Moves provider-specific error helper functions into `FnoxError` as `clone_provider_error` and `map_batch_error`, so providers can reuse the same structured error cloning/mapping behavior. > > Updates the Infisical batch secret fetch path to call `e.map_batch_error(...)` and removes the now-duplicated local helper functions, adding/adjusting unit tests to cover the new `FnoxError` methods and updated Infisical behavior. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 9fe2343. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
### 🚀 Features - **(provider)** add Doppler secrets manager provider by [@natefaerber](https://github.com/natefaerber) in [#376](#376) ### 🐛 Bug Fixes - **(ci)** pin LocalStack to v4 (last free community version) by [@jdx](https://github.com/jdx) in [#379](#379) - **(sync)** skip post-processing when resolving secrets for sync by [@rpendleton](https://github.com/rpendleton) in [#371](#371) ### 🚜 Refactor - **(providers)** extract shared error helpers to FnoxError methods by [@jdx](https://github.com/jdx) in [#380](#380) ### 📦️ Dependency Updates - lock file maintenance by [@renovate[bot]](https://github.com/renovate[bot]) in [#369](#369) - update taiki-e/upload-rust-binary-action digest to 0e34102 by [@renovate[bot]](https://github.com/renovate[bot]) in [#372](#372) - update dependency vue to v3.5.32 by [@renovate[bot]](https://github.com/renovate[bot]) in [#373](#373) ### New Contributors - @natefaerber made their first contribution in [#376](#376) - @rpendleton made their first contribution in [#371](#371)
Summary
type = "doppler")project,config, andtokenfields (all optional, falls back to CLI defaults / env vars)doppler secrets get <name> --plainfor single secrets and--jsonfor batch fetchingProviderType::Dopplerenum variant andfnox provider add dopplersupportConfiguration
Test plan
provider_add_types_match_provider_definitions)cargo clippy -- -D warningspasses clean🤖 Generated with Claude Code