Skip to content

Support full-object encodedJson input on Send create and edit - #1308

Merged
adudek-bw merged 2 commits into
mainfrom
tools/pm-39240/rust-cli-send-encoded-json
Jul 31, 2026
Merged

Support full-object encodedJson input on Send create and edit#1308
adudek-bw merged 2 commits into
mainfrom
tools/pm-39240/rust-cli-send-encoded-json

Conversation

@adudek-bw

Copy link
Copy Markdown
Contributor

🎟️ Tracking

PM-39240 (https://bitwarden.atlassian.net/browse/PM-39240)

📔 Objective

Implements full-object encodedJson input support for bw send create and bw send edit, completing the work deferred from PR #1176 (PM-34719).

@adudek-bw
adudek-bw requested review from a team as code owners July 29, 2026 13:33
@adudek-bw
adudek-bw requested a review from dereknance July 29, 2026 13:33
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: REQUEST CHANGES

Reviewed the replacement of the encoded_json "not yet implemented" gates on bw send create/bw send edit with real full-object JSON input, covering crates/bw/src/tools/send.rs and crates/bw/tests/send.rs. The merge precedence (flag > JSON > existing), the AuthEdit::Preserve fallback that avoids silently stripping a password/email gate, the type-immutability check on edit, and the file-Send rejection all look correct and are well covered by the new unit tests. Two concerns are worth resolving before merge: the strictness of deserializing directly into SendView, and the unconditional stdin read on both subcommands.

Code Review Details
  • ⚠️ : Parsing into SendView (deny_unknown_fields, no field defaults) rejects bw send template output, breaking the template → create round-trip the template code claims to support; the base64 fallback also masks the real parse error
    • crates/bw/src/tools/send.rs:566
  • ⚠️ : Unconditional stdin read makes flag-only create/edit block or consume caller stdin when stdin is a non-TTY pipe (ssh, docker -i, while read loops)
    • crates/bw/src/tools/send.rs:544

Also noted, no action required: build_create_request_from_view silently ignores --deleteInDays (and --file/--text) on the JSON path because SendCreateArgs::delete_in_days carries default_value_t = 7 and cannot be distinguished from an explicit value, which is slightly at odds with the function's doc comment stating that an explicitly provided CLI flag overrides the corresponding JSON field.

Comment on lines +566 to +575
fn parse_encoded_send_view(raw: &str) -> color_eyre::eyre::Result<SendView> {
if let Ok(decoded) = STANDARD.decode(raw.trim())
&& let Ok(text) = std::str::from_utf8(&decoded)
&& let Ok(view) = serde_json::from_str::<SendView>(text)
{
return Ok(view);
}

serde_json::from_str::<SendView>(raw).wrap_err("Error parsing the encoded request data.")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ IMPORTANT: Parsing into SendView directly rejects the output of bw send template, breaking the documented template → create workflow.

Details and fix

SendView (crates/bitwarden-send/src/send.rs:326-365) is #[serde(rename_all = "camelCase", deny_unknown_fields)] with no #[serde(default)], so a successful parse requires every non-Option field: name, hasPassword, type, accessCount, disabled, hideEmail, revisionDate, deletionDate, emails, authType.

SendTextTemplate (this file, ~line 505) emits only name, notes, type, text, deletionDate. So:

bw send template send.text | jq '.name="x" | .text.text="y" | .deletionDate="2030-01-01T00:00:00Z"' | bw send create

fails with Error parsing the encoded request data (missing field hasPassword). The render_template doc comment in this same file explicitly states the template shapes exist "so round-trips via bw send create are unambiguous" — that round-trip does not work. deny_unknown_fields additionally rejects legacy bw send get output, which carries extra fields such as object and accessUrl.

Consider parsing into a CLI-local input DTO with #[serde(default)] on the server-owned read-only fields (hasPassword, accessCount, revisionDate, authType, emails, disabled, hideEmail) and tolerating unknown keys, then converting to SendView. At minimum, extend SendTextTemplate/SendFileTemplate so their output parses, and add a test covering template output → parse_encoded_send_view.

Secondary: when the input is valid base64 of JSON but the JSON fails to deserialize, the if let chain falls through and re-parses the base64 string as JSON, so the user sees expected value at line 1 column 1 instead of the real cause (missing field 'hasPassword'). Splitting the base64-decode step from the JSON-parse step and surfacing the decoded-JSON error when the input decoded cleanly would make these failures diagnosable.

Comment thread crates/bw/src/tools/send.rs Outdated
Comment on lines +544 to +557
fn read_encoded_json_input(positional: Option<String>) -> color_eyre::eyre::Result<Option<String>> {
if let Some(raw) = positional {
return Ok(Some(raw));
}
if std::io::stdin().is_terminal() {
return Ok(None);
}
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
if buf.trim().is_empty() {
return Ok(None);
}
Ok(Some(buf))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ IMPORTANT: Flag-only send create/send edit now block on stdin whenever stdin is a non-TTY pipe that stays open.

Details and fix

read_encoded_json_input is called unconditionally for create and edit (lines 296-309), before any check of whether the caller actually supplied JSON-style input. When no positional is given and stdin is not a terminal, read_to_string blocks until EOF.

Previously neither subcommand touched stdin, so this is a behavior change for existing flag-only usage:

ssh host 'bw send edit --itemid <id> --deleteInDays 3'   # stdin is an open sshd pipe -> hangs
docker run -i image bw send create --name x --text y     # hangs until stdin closes
while read line; do bw send create --name x --text "$line"; done < list.txt  # swallows the rest of list.txt

A related case: if stdin happens to carry non-JSON bytes, a fully specified flag-only invocation now fails with Error parsing the encoded request data even though the flags were sufficient.

Suggested fix: only consult stdin when the command has no other input source — for create, when both --text and --file are absent; for edit, when no other override flag was supplied. That keeps the template | jq | bw send create piping story while leaving flag-only invocations independent of stdin.

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.

❓ Could this impact users that rely on using the CLI via scripts?

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.

Yes! Should be fixed now :)

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.

Changes look good to me! I'll approve, but I think it may be worth adding a unit test to cover the stdin gate that's being used now, I'm happy to re-approve if you introduce this.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

🔍 SDK Breaking Change Detection

SDK Version: tools/pm-39240/rust-cli-send-encoded-json (1ae696a)

⚠️ If breaking changes are detected, a corresponding pull request addressing them must be ready for merge in the affected client repository.

Client Status Details
typescript ✅ No breaking changes detected Compilation passed with new SDK version - View Details
android ✅ No breaking changes detected Compilation passed with new SDK version - View Details

Breaking change detection uses the build of the SDK from this branch, including any incompatibities pre-existing on or merged into this branch. Check the workflow logs to confirm.
Results update as workflows complete.

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.76395% with 71 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.90%. Comparing base (a91e406) to head (c89f32d).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
crates/bw/src/tools/send.rs 84.76% 71 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1308      +/-   ##
==========================================
+ Coverage   85.81%   85.90%   +0.09%     
==========================================
  Files         490      493       +3     
  Lines       70590    71347     +757     
==========================================
+ Hits        60574    61290     +716     
- Misses      10016    10057      +41     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcamirault mcamirault left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

One suggestion for improving an error message and I'd echo the AI comments, particularly the second one, but overall looks good

Comment thread crates/bw/src/tools/send.rs Outdated

let resolved_name = name.unwrap_or(view.name);
if resolved_name.is_empty() {
return Err(eyre!("--name is required for text Sends."));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⛏️ Name should be required for all Send types (though interestingly when I looked at the server it doesn't actually enforce this, something we might want to fix)

Comment thread crates/bw/src/tools/send.rs Outdated
// stdout while the requested file path goes uncreated would be a worse UX than
// an explicit "not implemented" error.
Some(SendCommands::Get(args)) if args.output_path.is_some() => Err(eyre!(
"`--output` on `bw send get` is not yet implemented (tracked under PM-39238)."

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.

Suggested change
"`--output` on `bw send get` is not yet implemented (tracked under PM-39238)."
"`--output` on `bw send get` is not yet implemented"

Comment thread crates/bw/src/tools/send.rs Outdated
Comment on lines +544 to +557
fn read_encoded_json_input(positional: Option<String>) -> color_eyre::eyre::Result<Option<String>> {
if let Some(raw) = positional {
return Ok(Some(raw));
}
if std::io::stdin().is_terminal() {
return Ok(None);
}
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
if buf.trim().is_empty() {
return Ok(None);
}
Ok(Some(buf))
}

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.

❓ Could this impact users that rely on using the CLI via scripts?

Comment thread crates/bw/src/tools/send.rs Outdated
Comment on lines +706 to +708
"Creating file Sends from JSON is not supported: the CLI needs a local file path \
to read and encrypt the contents. Use `--file <path>` instead (file-send \
creation is tracked under PM-39238)."

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.

Suggested change
"Creating file Sends from JSON is not supported: the CLI needs a local file path \
to read and encrypt the contents. Use `--file <path>` instead (file-send \
creation is tracked under PM-39238)."
"Creating file Sends from JSON is not supported: the CLI needs a local file path \
to read and encrypt the contents. Use `--file <path>` instead."

Fixes two IMPORTANT findings from review:
- Parse full-object JSON into a CLI-local SendJsonInput DTO instead of
  SendView directly. SendView's deny_unknown_fields/no-defaults broke the
  documented `bw send template` -> create round trip and rejected `bw send
  get` output (which carries extra fields like `object`/`accessUrl`).
- Gate the stdin read behind a stdin_eligible check so flag-only
  create/edit invocations never touch stdin, fixing a hang/data-loss risk
  for scripted usage (ssh pipes, `docker run -i`, `while read` loops).

Also fixes the base64-decode-succeeds-but-JSON-fails case, which
previously masked the real deserialize error by silently re-parsing the
base64 string as JSON, and applies the wording nits from mcamirault and
harr1424 (generalize the --name-required message, drop stale PM-39238
references from user-facing error text).
@adudek-bw
adudek-bw requested a review from harr1424 July 30, 2026 12:59
@adudek-bw
adudek-bw requested a review from mcamirault July 30, 2026 17:10
@adudek-bw
adudek-bw merged commit b2c68b4 into main Jul 31, 2026
83 checks passed
@adudek-bw
adudek-bw deleted the tools/pm-39240/rust-cli-send-encoded-json branch July 31, 2026 16:53
bw-ghapp Bot added a commit to bitwarden/sdk-swift that referenced this pull request Jul 31, 2026
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.

4 participants