Skip to content

feat(provider): add Doppler secrets manager provider#376

Merged
jdx merged 9 commits intojdx:mainfrom
natefaerber:feat/doppler-provider
Apr 4, 2026
Merged

feat(provider): add Doppler secrets manager provider#376
jdx merged 9 commits intojdx:mainfrom
natefaerber:feat/doppler-provider

Conversation

@natefaerber
Copy link
Copy Markdown
Contributor

Summary

  • Adds a new Doppler secrets manager provider (type = "doppler")
  • Supports project, config, and token fields (all optional, falls back to CLI defaults / env vars)
  • Uses doppler secrets get <name> --plain for single secrets and --json for batch fetching
  • Includes ProviderType::Doppler enum variant and fnox provider add doppler support
  • Full documentation page and updates to overview, sidebar, index, and CLI reference

Configuration

[providers]
app-prod = { type = "doppler", project = "my-app", config = "prd" }
app-dev  = { type = "doppler", project = "my-app", config = "dev" }

[secrets]
PROD_DB_URL = { provider = "app-prod", value = "DATABASE_URL" }
DEV_DB_URL  = { provider = "app-dev",  value = "DATABASE_URL" }

Test plan

  • All 141 lib tests pass (including provider_add_types_match_provider_definitions)
  • cargo clippy -- -D warnings passes clean
  • Manually tested with live Doppler projects (multiple providers, different projects/configs)
  • CI passes

🤖 Generated with Claude Code

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/providers/doppler.rs

Ok(stdout.trim().to_string())
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
}
Ok(stdout)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/providers/doppler.rs Outdated
Comment on lines +119 to +126
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 1, 2026

Greptile Summary

Adds a Doppler secrets manager provider that delegates to the doppler CLI for secret retrieval, supporting optional project, config, and token fields, batch fetching via --json, and secure token delivery through environment variables rather than CLI flags. Previous review concerns (token in process args, empty-string false-negatives, overly broad "not found" error pattern) are all resolved.

Confidence Score: 5/5

Safe 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

Filename Overview
src/providers/doppler.rs Core Doppler provider — well-structured with CLI error classification, batch fetch, and env-var token injection; one broad error pattern ('does not exist') may misclassify project/config errors as secret-not-found
src/providers/mod.rs Adds doppler module declaration and import for code-gen; correct
src/commands/provider/add.rs Adds Doppler template config in provider add wizard with sensible placeholder defaults
src/commands/provider/mod.rs Adds ProviderType::Doppler enum variant; passes existing provider-type coverage test
providers/doppler.toml Provider metadata/code-gen definition; fields and auth_command are correct
docs/providers/doppler.md Thorough provider documentation covering auth options, multi-environment patterns, and CI/CD usage
docs/providers/overview.md Doppler correctly added to Cloud Secret Storage table with accurate description

Sequence Diagram

sequenceDiagram
    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(())
Loading

Greploops — Automatically fix all review issues by running /greploops in Claude Code. It iterates: fix, push, re-review, repeat until 5/5 confidence.
Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal.

Reviews (5): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment thread src/providers/doppler.rs
Comment on lines +311 to +316
const SECRET_NOT_FOUND_PATTERNS: &[&str] = &[
"could not find secret",
"secret not found",
"does not exist",
"not found",
];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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",
];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/providers/doppler.rs Outdated
Comment on lines +117 to +124
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(),
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/providers/doppler.rs Outdated
Comment on lines +48 to +50
if let Some(ref token) = self.get_token() {
args.push(format!("--token={}", token));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

natefaerber and others added 6 commits April 2, 2026 15:58
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).
@jdx jdx merged commit 7dda3a6 into jdx:main Apr 4, 2026
14 checks passed
jdx added a commit that referenced this pull request Apr 4, 2026
#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>
jdx pushed a commit that referenced this pull request Apr 4, 2026
### 🚀 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants