Skip to content

Conversation

@harr1424
Copy link
Contributor

@harr1424 harr1424 commented Dec 24, 2025

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-24618
https://bitwarden.atlassian.net/browse/PM-23109
bitwarden/server#5895

📔 Objective

PM-24618: reveal the functionality related to email based OTP in the CLI's send help output bw send --help
PM-23109: update models to add an AuthType enum and ensure the process of creating and receiving a send supports storing emails associated with the send to be used for email based OTP authentication and the AuthType enum value

💻 Command Output

john@Johns-MacBook-Pro clients % bw send --help
Usage: bw send [options] [command] <data>

Work with Bitwarden sends. A Send can be quickly created using this command or subcommands can be used to fine-tune the Send

Arguments:
  data                            The data to Send. Specify as a filepath with the --file option

Options:
  -f, --file                      Specifies that <data> is a filepath
  -d, --deleteInDays <days>       The number of days in the future to set deletion date, defaults to 7 (default: "7")
  --password <password>           optional password to access this Send. Can also be specified in JSON.
  --email <email>                 optional emails to access this Send. Can also be specified in JSON.
  -a, --maxAccessCount <amount>   The amount of max possible accesses.
  --hidden                        Hide <data> in web by default. Valid only if --file is not set.
  -n, --name <name>               The name of the Send. Defaults to a guid for text Sends and the filename for files.
  --notes <notes>                 Notes to add to the Send.
  --fullObject                    Specifies that the full Send object should be returned rather than just the access url.
  -h, --help                      display help for command

Commands:
  list                            List all the Sends owned by you
  template <object>               Get json templates for send objects
  get [options] <id>              Get Sends owned by you.
  receive [options] <url>         Access a Bitwarden Send from a url
  create [options] [encodedJson]  create a Send
  edit [options] [encodedJson]    edit a Send
  remove-password <id>            removes the saved password from a Send.
  delete <id>                     delete a Send
john@Johns-MacBook-Pro clients % bw send list
[{"object":"send","id":"b30db19a-b7dd-4868-b240-b3c000241108","accessId":"mrENs923aEiyQLPAACQRCA","accessUrl":"https://vault.bitwarden.com/#/send/mrENs923aEiyQLPAACQRCA/SGPAqktzOnHM_H9eSba67g","name":"096b5c3f-a629-4a9f-b6bb-4c913c0df35c","notes":null,"key":"SGPAqktzOnHM/H9eSba67g==","type":0,"maxAccessCount":null,"accessCount":0,"revisionDate":"2025-12-27T02:11:17.647Z","deletionDate":"2026-01-03T02:11:01.097Z","expirationDate":null,"passwordSet":false,"emails":["[email protected]"],"disabled":false,"hideEmail":false,"authType":"Email","text":{"text":"hello world","hidden":false}},

{"object":"send","id":"fc9990eb-ad93-4f2b-a427-b3c00025a65f","accessId":"65CZ_JOtK0-kJ7PAACWmXw","accessUrl":"https://vault.bitwarden.com/#/send/65CZ_JOtK0-kJ7PAACWmXw/MXzuEPT0zX3vc7RRDdvZfw","name":"e91ec017-1915-4b1f-b80e-7d8e57657fbc","notes":null,"key":"MXzuEPT0zX3vc7RRDdvZfw==","type":0,"maxAccessCount":null,"accessCount":0,"revisionDate":"2025-12-27T02:17:04.634Z","deletionDate":"2026-01-03T02:17:04.322Z","expirationDate":null,"passwordSet":false,"emails":["[email protected]"],"disabled":false,"hideEmail":false,"authType":"Email","text":{"text":"hello world","hidden":false}}]
john@Johns-MacBook-Pro clients % bw send get fc9990eb-ad93-4f2b-a427-b3c00025a65f
{"object":"send","id":"fc9990eb-ad93-4f2b-a427-b3c00025a65f","accessId":"65CZ_JOtK0-kJ7PAACWmXw","accessUrl":"https://vault.bitwarden.com/#/send/65CZ_JOtK0-kJ7PAACWmXw/MXzuEPT0zX3vc7RRDdvZfw","name":"e91ec017-1915-4b1f-b80e-7d8e57657fbc","notes":null,"key":"MXzuEPT0zX3vc7RRDdvZfw==","type":0,"maxAccessCount":null,"accessCount":0,"revisionDate":"2025-12-27T02:17:04.634Z","deletionDate":"2026-01-03T02:17:04.322Z","expirationDate":null,"passwordSet":false,"emails":["[email protected]"],"disabled":false,"hideEmail":false,"authType":"Email","text":{"text":"hello world","hidden":false}}

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@github-actions
Copy link
Contributor

github-actions bot commented Dec 24, 2025

Logo
Checkmarx One – Scan Summary & Detailsa3d95fe7-c53c-466c-9c07-72fc44cd9eaa

Great job! No new security vulnerabilities introduced in this pull request

@codecov
Copy link

codecov bot commented Dec 24, 2025

Codecov Report

❌ Patch coverage is 85.29412% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.41%. Comparing base (8a6f9bf) to head (07eb16d).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
apps/cli/src/tools/send/commands/edit.command.ts 91.66% 0 Missing and 1 partial ⚠️
apps/cli/src/tools/send/send.program.ts 0.00% 1 Missing ⚠️
...on/src/tools/send/models/response/send.response.ts 0.00% 1 Missing ⚠️
...ibs/common/src/tools/send/models/view/send.view.ts 66.66% 0 Missing and 1 partial ⚠️
...ibs/common/src/tools/send/services/send.service.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #18106      +/-   ##
==========================================
+ Coverage   42.24%   42.41%   +0.17%     
==========================================
  Files        3598     3599       +1     
  Lines      104480   104540      +60     
  Branches    15765    15786      +21     
==========================================
+ Hits        44133    44344     +211     
+ Misses      58467    58289     -178     
- Partials     1880     1907      +27     

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

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@harr1424 harr1424 changed the title WIP explore existing Send attributes and logic [PM-24618] Integrate CLI sends with server branch tools/pm-21918/send-authentication-commands Dec 27, 2025
@harr1424 harr1424 marked this pull request as ready for review December 27, 2025 02:37
@harr1424 harr1424 requested a review from a team as a code owner December 27, 2025 02:37
@harr1424 harr1424 added the ai-review Request a Claude code review label Dec 27, 2025
mcamirault
mcamirault previously approved these changes Dec 29, 2025
Copy link
Contributor

@mcamirault mcamirault left a comment

Choose a reason for hiding this comment

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

LGTM

@harr1424 harr1424 added the needs-qa Marks a PR as requiring QA approval label Dec 29, 2025
@harr1424 harr1424 changed the title [PM-24618] Integrate CLI sends with server branch tools/pm-21918/send-authentication-commands [PM-24618] Integrate CLI sends with server branch Dec 29, 2025
@claude
Copy link
Contributor

claude bot commented Dec 29, 2025

Claude finished @harr1424's task in 4m 48s —— View job


Overall Assessment: APPROVE

This PR successfully integrates email-based OTP authentication for CLI sends, adding the AuthType enum following ADR-0025 patterns, mutual exclusivity validation between emails and password, and comprehensive test coverage (765 lines of new tests). The implementation correctly handles authType inference and data flow through all model layers.

Code Review Details
  • QUESTION: Should AuthType enum be defined in SDK instead?
    • libs/common/src/tools/send/models/domain/send.ts:24
    • Currently defined in clients repo, but shared types are typically in SDK for cross-repository consistency

Additional Observations

Positive aspects:

  • AuthType enum correctly follows ADR-0025 no-enums pattern (matches SendType implementation)
  • Mutual exclusivity validation is thorough and handles edge cases (empty strings, whitespace, empty arrays)
  • Test coverage is excellent with 365 lines for create command and 400 lines for edit command
  • Email parsing via parseEmail utility is well-implemented with comprehensive validation
  • Data flow through SendResponse → SendData → Send → SendView is consistent

Future improvements (not blocking):

  • Consider adding authType and emails to template method in send.response.ts for better feature discoverability via bw send template
  • SendView.fromJSON could be enhanced to restore authType and emails properties (currently only toJSON includes them)

Code coverage gaps (per Codecov):

  • send.view.ts: 40% patch coverage (2 lines + 1 partial) - primarily fromJSON/toJSON methods
  • send.program.ts: 0% (1 line) - CLI setup code
  • edit.command.ts: 92.85% (1 partial)
  • send.response.ts (common): 0% (1 line)

🤖 Generated with Claude Code

req.maxAccessCount = maxAccessCount;
req.emails = emails;

if (emails != null && emails.length > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ IMPORTANT: Unsafe array length check on potentially undefined value

Details and fix

Line 82 extracts emails with ?? undefined, but line 93 checks emails.length > 0 without first verifying emails is not undefined. This will throw TypeError: Cannot read property 'length' of undefined if emails is undefined.

Fix:

if (emails != null && emails.length > 0) {
  req.authType = AuthType.Email;
} else if (password != null && password.trim().length > 0) {
  req.authType = AuthType.Password;
} else {
  req.authType = AuthType.None;
}

Why: JavaScript/TypeScript will throw when accessing .length on undefined. The nullish coalescing on line 82 explicitly returns undefined, making this a runtime error waiting to happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Loose equality operator != will handle undefined as intended here, which is likely why the suggested "fix" is identical to the existing code.

this.deletionDate = o.deletionDate;
this.expirationDate = o.expirationDate;
this.passwordSet = o.password != null;
this.emails = o.emails ?? [];
Copy link
Contributor

Choose a reason for hiding this comment

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

CRITICAL: Missing authType assignment in constructor causes data loss

Details and fix

The constructor (lines 98-132) creates a SendResponse from a SendView but doesn't copy the authType field. This is added at line 124, but the corresponding field in the toView static method (line 36-58) is also missing authType assignment.

Current state:

  • Line 124: this.authType = o.authType; ✓ (constructor copies it)
  • Line 55: view.emails = send.emails ?? [];
  • Line 58: Missing view.authType = send.authType;

Fix needed in toView method around line 57:

view.emails = send.emails ?? [];
view.disabled = send.disabled;
view.hideEmail = send.hideEmail;
view.authType = send.authType;  // Add this line
return view;

Impact: The toView method is used in create.command.ts line 147. Without copying authType, sends lose their authentication type during the create flow.


req.key = null;
req.maxAccessCount = maxAccessCount;
req.emails = emails;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ IMPORTANT: authType calculated but never assigned to encrypted send before API call

Details and fix

Lines 93-99 calculate req.authType based on whether emails or password is provided, but this value is never transferred to encSend before the API save at line 154.

Looking at line 152: encSend.emails = emails && emails.join(","); - the emails are assigned, but authType is not.

Fix needed around line 152:

encSend.deletionDate = sendView.deletionDate;
encSend.expirationDate = sendView.expirationDate;
encSend.emails = emails && emails.join(",");
encSend.authType = req.authType;  // Add this

Impact: The server receives sends without the authType field, potentially breaking email-based OTP authentication or causing it to default incorrectly.

import { SendFile } from "./send-file";
import { SendText } from "./send-text";

export const AuthType = Object.freeze({
Copy link
Contributor

Choose a reason for hiding this comment

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

QUESTION: Should AuthType come from SDK instead of being defined here?

Context

@itsadrago previously asked whether this enum should be sourced from the sdk-internal repo's api-api crate rather than being defined in the clients repository.

Current pattern: This follows the same pattern as SendType (also defined in clients repo using Object.freeze), which aligns with ADR-0025 (no TypeScript enums).

Consideration: If the server defines this enum and it's available via SDK, importing it would ensure the values stay synchronized. However, I don't see evidence of this enum existing in the current SDK.

Question for team: Is this enum part of the API contract that should come from SDK, or is it correct to define it client-side like SendType?

@harr1424 harr1424 marked this pull request as draft December 29, 2025 22:55
@harr1424 harr1424 marked this pull request as ready for review December 30, 2025 15:55
Comment on lines +66 to +71
const hasEmails = req.emails != null && req.emails.length > 0;
const hasPassword = req.password != null && req.password !== "";

if (hasEmails && hasPassword) {
return Response.badRequest("--password and --emails are mutually exclusive.");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🎨 SUGGESTED: Apply this same validation pattern to create.command for consistency

Details

This validation correctly checks for meaningful presence (length > 0 for emails, non-empty string for password) rather than just checking if values are defined.

However, create.command.ts line 85 uses a different pattern:

if (emails !== undefined && password !== undefined) {
  return Response.badRequest("--password and --emails are mutually exclusive.");
}

This creates inconsistent behavior:

  • edit: Allows {"emails": [], "password": "foo"} (correct per tests)
  • create: Rejects {"emails": [], "password": "foo"} (incorrect per create.command.spec.ts line 287-306)

Recommendation: Update create.command line 85-86 to use the same validation pattern you've implemented here:

const hasEmails = emails != null && emails.length > 0;
const hasPassword = password != null && password.trim().length > 0;

if (hasEmails && hasPassword) {
  return Response.badRequest("--password and --emails are mutually exclusive.");
}

This ensures both commands treat empty arrays and whitespace-only passwords consistently as "no auth specified".

None: 2,
} as const);

export type AuthType = (typeof AuthType)[keyof typeof AuthType];
Copy link
Contributor

Choose a reason for hiding this comment

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

QUESTION: Should AuthType enum be defined in SDK instead?

Context

The AuthType enum is defined here in the clients repository, but this feature requires coordination with the server (per PR description linking to bitwarden/server#5895).

Typically, shared enums/types used by both client and server are defined in the SDK to ensure consistency.

Questions:

  1. Is there a corresponding AuthType in the server codebase?
  2. Should this be moved to the SDK for cross-repository consistency?
  3. Are the enum values (0=Email, 1=Password, 2=None) aligned with server expectations?

This doesn't block the PR but should be clarified for future maintenance.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@harr1424 harr1424 removed the ai-review Request a Claude code review label Dec 30, 2025
Copy link
Contributor

@mcamirault mcamirault left a comment

Choose a reason for hiding this comment

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

Overall looks good, though I believe the Create command tests aren't checking what they should

Comment on lines 96 to 98
if (emails != null && emails.length > 0) {
req.authType = AuthType.Email;
} else if (password != null && password.trim().length > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⛏️ Use the variables from 85-86 so this block is the same as in edit.command.ts

encSend.deletionDate = sendView.deletionDate;
encSend.expirationDate = sendView.expirationDate;
encSend.emails = emails && emails.join(",");
encSend.authType = req.authType;
Copy link
Contributor

Choose a reason for hiding this comment

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

❓ The sendService.encrypt method already gets the other three fields from the SendView model when it creates the Send model. Is there a reason we're setting fields again here? Can't we move line 156 into the service then delete 152-156?

encSend.deletionDate = sendView.deletionDate;
encSend.expirationDate = sendView.expirationDate;
encSend.emails = req.emails && req.emails.join(",");
encSend.authType = req.authType;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as on the create command

}),
null,
undefined,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ This doesn't yet verify that authType was set correctly. There should be another check here of what was passed to the API the same way the Edit command tests do (lines 96-98 of edit.command.spec.ts). Same for all the tests in this file

? s.emails
.split(",")
.map((e) => e.trim())
.filter((e) => e)
Copy link
Contributor

Choose a reason for hiding this comment

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

This filter call doesn't seem necessary

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The server shouldn't send back any emails consisting of empty strings or strings consisting only of whitespace, but in this situation the filter can be used to discard such "falsy" values.

None of the other fields in this class are validated, so I agree it may make sense to remove it if adhering to existing conventions in this file is more important that programming defensively.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-qa Marks a PR as requiring QA approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants