bwx 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,28 +54,114 @@ bwx doctor
54
54
 
55
55
  | Command | Description |
56
56
  |---------|-------------|
57
- | `bwx status` | Show vault state |
57
+ | `bwx status` | Show vault state and whether reads work right now |
58
58
  | `bwx unlock` | Unlock vault & cache session |
59
59
  | `bwx lock` | Lock vault & clear session |
60
60
  | `bwx sync` | Sync vault from server |
61
61
  | `bwx doctor` | Check setup (bw CLI, email, password, config) |
62
62
 
63
+ `status` reports a `session` alongside the vault state, because `bw` answers for the
64
+ vault and not for bwx: a `locked` vault with a `valid` session reads fine and needs no
65
+ unlock. `valid` means reads work now, `stale` means the cached session was rejected, and
66
+ `none` means the next read unlocks.
67
+
63
68
  ### Read
64
69
 
65
70
  | Command | Description |
66
71
  |---------|-------------|
67
72
  | `bwx get <field> <item>` | Get field (`password`, `username`, `totp`, `notes`, `uri`, `item`) |
68
- | `bwx field <item> <name>` | Get custom field by name |
73
+ | `bwx get totp <item>` | TOTP code (`--fresh [n]`, `--seed`) |
74
+ | `bwx field <name> <item>` | Get custom field by name (same argument order as `get`) |
69
75
  | `bwx search <query>` | Search items (`--type`, `--folder`, `--limit`, `--full-items`) |
70
- | `bwx list [type]` | List items (`--folder`, `--limit`, `--full-items`) |
76
+ | `bwx list [type]` | List items (`--type`, `--folder`, `--limit`, `--full-items`) |
77
+ | `bwx folders` | List folder names and IDs |
78
+ | `bwx trash` | List items in the trash |
71
79
  | `bwx run --env NAME=<field>:<item> -- <cmd>` | Run a command with secrets in its environment |
72
80
  | `bwx attach list <item>` | List attachments for an item |
73
81
  | `bwx attach get <item> <attachment>` | Download attachment by ID or filename |
74
82
 
75
- `search` and `list` print the same reduced projection — id, type, name, username — in
76
- human, `--plain`, and `--json` form, so asking for machine-readable output never widens
77
- what is exposed. `--full-items` is the explicit opt-in to complete item objects, which
78
- carry passwords, notes, and custom fields.
83
+ `search` and `list` print the same reduced projection — id, type, name, username,
84
+ hasTotp — in human, `--plain`, and `--json` form, so asking for machine-readable output
85
+ never widens what is exposed. `--full-items` is the explicit opt-in to complete item
86
+ objects, which carry passwords, notes, and custom fields.
87
+
88
+ `hasTotp` reports that an item has a TOTP secret without carrying it, so finding your 2FA
89
+ items does not mean dumping the vault:
90
+
91
+ ```bash
92
+ bwx list --json --limit 0 | jq -r '.data[] | select(.hasTotp) | .name'
93
+ ```
94
+
95
+ A lookup that finds nothing suggests what you probably meant, and one that matches
96
+ several items lists them, so neither ends in a guessing round:
97
+
98
+ ```
99
+ $ bwx get password "GitHub PAT vforsh old"
100
+ error: No item matching "GitHub PAT vforsh old". Did you mean:
101
+
102
+ 934c1f57-… note GitHub PAT (vforsh)
103
+ 3683fb9a-… note GitHub PAT (yaforsh) [codex-monitor-pr]
104
+ ```
105
+
106
+ ### TOTP
107
+
108
+ Codes are computed locally from the stored secret. A TOTP therefore costs no extra `bw`
109
+ spawn and batches with the other fields of the same item, and bwx can report how long the
110
+ code has left.
111
+
112
+ ```bash
113
+ bwx get totp "AWS" # code on stdout, "Valid for 17s" on stderr
114
+ bwx get totp "AWS" --json # { "data": "123456", "meta": { "secondsRemaining": 17, "period": 30 } }
115
+ bwx get totp "AWS" --fresh # wait for the next window if under 5s remain
116
+ bwx get totp "AWS" --fresh 15 # ...or under 15s
117
+ bwx get totp "AWS" --seed # the stored secret, for moving it to another authenticator
118
+ ```
119
+
120
+ The remainder matters more than it looks: a vault read plus the time to actually use the
121
+ code often leaves single-digit seconds, and a code that expires in transit fails the login
122
+ without saying why. `--fresh` sleeps at most one period and hands back a full window.
123
+
124
+ Bare base32 and `otpauth://` URIs are both understood, including non-default `period`,
125
+ `digits`, and `algorithm` (SHA1/SHA256/SHA512). Secrets bwx will not compute exactly —
126
+ `steam://`, unknown algorithms, anything that fails to decode — fall back to `bw get totp`
127
+ rather than being guessed at, since a wrong code is indistinguishable from a right one
128
+ until the login fails.
129
+
130
+ Storing a secret:
131
+
132
+ ```bash
133
+ BWX_SEED="JBSWY3DPEHPK3PXP" bwx create --type login --name "AWS" --username me --totp-env BWX_SEED
134
+ bwx edit "AWS" --totp "otpauth://totp/AWS:me?secret=JBSWY3DPEHPK3PXP&issuer=AWS"
135
+ bwx edit "AWS" --rm-totp
136
+ ```
137
+
138
+ Spaces, hyphens, and `=` padding are stripped and case is normalized; an `otpauth://` URI
139
+ is stored verbatim so its parameters survive. Malformed secrets are rejected before the
140
+ write, because a bad seed is otherwise discovered at the login it was supposed to unlock.
141
+
142
+ ### Listings are capped
143
+
144
+ Listings stop at 50 items by default — an uncapped `bwx list` returns the whole vault,
145
+ and with `--full-items` that is every secret in one call. `--limit 0` opts out. Truncation
146
+ is never silent: it warns on stderr (surviving `--json`) and adds `meta` to the JSON
147
+ envelope.
148
+
149
+ ```bash
150
+ bwx list --limit 0 # everything
151
+ bwx list --json # { "data": [...], "meta": { "total": 491, "shown": 50, "truncated": true } }
152
+ ```
153
+
154
+ ### Folders
155
+
156
+ `--folder` takes a name or an ID on `list`, `search`, `create`, and `edit`; `none` means
157
+ the unfiled bucket. Names are resolved against the vault, so `bwx folders` is only needed
158
+ to see what exists.
159
+
160
+ ```bash
161
+ bwx folders
162
+ bwx list --folder "API Keys"
163
+ bwx edit "Acme" --folder none # unfile
164
+ ```
79
165
 
80
166
  ### Secrets into a child process
81
167
 
@@ -111,9 +197,37 @@ The check is cached in `~/.config/bwx/state.json`, so a fresh vault costs no ext
111
197
 
112
198
  | Command | Description |
113
199
  |---------|-------------|
114
- | `bwx create` | Create item (`--type`, `--name`, `--username`, `--password-stdin`, `--field-file k=path`) |
115
- | `bwx edit <item>` | Edit item (`--name`, `--password-env`, `--add-field-file k=path`, `--rm-field name`) |
200
+ | `bwx create` | Create item (`--type`, `--name`, `--username`, `--password-stdin`, `--totp-env`, `--field-file k=path`) |
201
+ | `bwx edit <item>` | Edit item (`--name`, `--password-env`, `--totp`, `--rm-totp`, `--add-field-file k=path`, `--rm-field name`) |
116
202
  | `bwx delete <item>` | Delete item (`--permanent`, `--force`) |
203
+ | `bwx restore <item>` | Restore an item from the trash |
204
+ | `bwx generate` | Generate a password or passphrase (local, no vault access) |
205
+
206
+ Login-only flags (`--username`, any `--password-*` or `--totp-*`, `--uri`) require a login
207
+ item. A secure note has nowhere to put them, so passing them to one is an error rather
208
+ than an exit 0 over a credential that was silently dropped.
209
+
210
+ `bwx delete` requires `--force` whenever it is not talking to a terminal. The confirmation
211
+ prompt cannot render for a script or an agent, so without it `--force` would be decorative
212
+ and every non-interactive delete unguarded. Deletes go to the trash unless `--permanent`,
213
+ and `bwx trash` / `bwx restore` are the way back.
214
+
215
+ ### Generated passwords
216
+
217
+ `--password-generate` on `create` and `edit` sends a new secret straight to the vault. It
218
+ is never printed, never echoed back in `--json`, and never reaches a terminal, a
219
+ transcript, or a log — which is what makes it safe to use from an agent.
220
+
221
+ ```bash
222
+ bwx create --type login --name "Deploy" --username svc --password-generate
223
+ bwx edit "Deploy" --password-generate --generate-length 32
224
+ bwx generate --length 32 # print one instead
225
+ bwx generate --passphrase --words 5
226
+ ```
227
+
228
+ `bwx generate` runs locally and needs no unlocked vault. Flags: `--length <n>`,
229
+ `--no-uppercase`, `--no-lowercase`, `--no-numbers`, `-s/--special`, `--ambiguous`,
230
+ `--passphrase`, `--words <n>`, `--separator <char>`, `--capitalize`, `--include-number`.
117
231
 
118
232
  ### Config
119
233
 
@@ -147,6 +261,7 @@ bwx create --name "Acme" --field-file '!API Key=./token.txt' --field-env account
147
261
  Supported source flags:
148
262
 
149
263
  - Passwords: `--password-stdin`, `--password-file <path>`, `--password-env <name>`
264
+ - TOTP secrets: `--totp-stdin`, `--totp-file <path>`, `--totp-env <name>`
150
265
  - Notes: `--notes-file <path>`
151
266
  - Create fields: `--field-file k=path`, `--field-env k=ENV`
152
267
  - Edit fields: `--add-field-file k=path`, `--add-field-env k=ENV`
@@ -156,7 +271,7 @@ Supported source flags:
156
271
 
157
272
  | Flag | Effect |
158
273
  |------|--------|
159
- | `--json` | JSON output: `{ "data": ... }` / `{ "error": ... }` |
274
+ | `--json` | JSON output: `{ "data": ... }` / `{ "error": ... }`, plus `meta` where a response describes itself |
160
275
  | `--plain` | Tab-separated, one item per line |
161
276
  | `-q, --quiet` | Suppress logs and warnings |
162
277
  | `-v, --verbose` | Include stack traces |
@@ -191,6 +306,17 @@ Field syntax for `--field`, `--field-file`, `--field-env`, and edit variants:
191
306
  | 8 | Timeout |
192
307
  | 9 | User cancelled |
193
308
 
309
+ ## Upgrading to 0.5.0
310
+
311
+ Breaking changes, all of them cases where the old behavior failed quietly:
312
+
313
+ - `bwx field` takes `<name> <item>`, matching `bwx get`. It was `<item> <name>`.
314
+ - `bwx delete` needs `--force` when not interactive; it used to delete unprompted.
315
+ - `bwx list` and `bwx search` stop at 50 items unless given `--limit`. Pass `--limit 0`
316
+ for the old behavior.
317
+ - `--limit` rejects non-numeric values instead of ignoring them (and returning everything).
318
+ - Login-only flags on a secure note are an error instead of a silent drop.
319
+
194
320
  ## Stack
195
321
 
196
322
  Bun, TypeScript, Commander, Zod, picocolors.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bwx",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "keywords": [
6
6
  "bitwarden",
@@ -11,13 +11,14 @@ Extended Bitwarden CLI. Wraps `bw` with auto-unlock, session caching, and struct
11
11
 
12
12
  ```bash
13
13
  bwx get password "GitHub" # retrieve a password (auto-unlocks)
14
- bwx get totp "AWS (user@example.com)" # TOTP code
14
+ bwx get totp "AWS (user@example.com)" # TOTP code (+ seconds left, on stderr)
15
+ bwx get totp "AWS" --fresh # wait out a nearly-expired code first
15
16
  bwx get username "GitHub" # username
16
17
  bwx get notes "Deploy key" # secure note content
17
18
  bwx get uri "GitHub" # first URI
18
19
  bwx get item "GitHub" # full item JSON
19
20
  bwx get "API Key" "Acme" # custom field by name
20
- bwx field "Acme" "API Key" # custom field alias
21
+ bwx field "API Key" "Acme" # custom-field-only lookup, same arg order
21
22
 
22
23
  bwx search github # fuzzy search, human table
23
24
  bwx search github --json # id/type/name/username only — no secrets
@@ -26,6 +27,9 @@ bwx search github --type login --limit 5
26
27
  # hand a secret to a child process instead of printing it
27
28
  bwx run --env KEENETIC_PASS=password:'http://my.keenetic.net' -- expect ./recovery.exp
28
29
 
30
+ # generate straight into the vault — the value never enters your context
31
+ bwx create --type login --name "Deploy" --username svc --password-generate
32
+
29
33
  bwx attach list "Apple Developer" # list item attachments
30
34
  bwx attach get "Apple Developer" AuthKey.p8
31
35
  bwx attach get "Apple Developer" AuthKey.p8 --output ./secrets/
@@ -43,17 +47,56 @@ echo "secret" | bwx create --name "Piped Note"
43
47
  Built-in fields: `password | username | totp | notes | uri | item`.
44
48
  Anything else is looked up as a custom field. Lists available fields on miss.
45
49
 
46
- ### `bwx field <item> <name>`
50
+ A miss suggests near matches; an ambiguous name lists candidates with IDs. Read the
51
+ suggestions instead of retrying variations — the answer is usually in them.
52
+
53
+ ### `bwx get totp <item>`
54
+
55
+ Codes are computed locally from the stored secret, so a TOTP costs no extra `bw` spawn and
56
+ batches with other fields from the same item.
57
+
58
+ ```bash
59
+ bwx get totp "AWS" # code on stdout, "Valid for 17s" on stderr
60
+ bwx get totp "AWS" --json # { "data": "123456", "meta": { "secondsRemaining", "period" } }
61
+ bwx get totp "AWS" --fresh # wait for the next window if under 5s remain
62
+ bwx get totp "AWS" --fresh 15 # ...or under 15s
63
+ bwx get totp "AWS" --seed # the stored secret, for migrating to another app
64
+ ```
65
+
66
+ **Check `secondsRemaining` before using a code.** A `bw` read plus the time to hand the code
67
+ over routinely leaves under 5 seconds, and a code that expires in transit fails the login
68
+ with no useful error. `--fresh` sleeps at most one period and returns a full-window code.
47
69
 
48
- Alias for retrieving custom fields with item-first argument order.
70
+ Accepts bare base32 and `otpauth://` URIs, honoring their `period`, `digits`, and
71
+ `algorithm` (SHA1/SHA256/SHA512). `steam://` secrets and anything else bwx cannot compute
72
+ exactly are delegated to `bw get totp` instead of guessed at.
73
+
74
+ ### `bwx field <name> <item>`
75
+
76
+ Custom fields only, so an item whose custom field is named `password` is still reachable.
77
+ Takes its arguments in the same order as `get` (this changed in 0.5.0).
49
78
 
50
79
  ### `bwx search <query>` / `bwx list [type]`
51
80
 
52
- Flags: `--type login|note|card|identity` (`search`), `--folder <id>`, `--limit <n>`, `--full-items`
81
+ Flags: `--type login|note|card|identity`, `--folder <name|id>`, `--limit <n>`, `--full-items`
53
82
 
54
83
  Human, `--plain`, and `--json` output all carry the same reduced projection — `id`, `type`,
55
- `name`, `username` — so listings never leak passwords, notes, or custom fields. `--full-items`
56
- opts into complete item objects and **does** include secrets; avoid it in logged sessions.
84
+ `name`, `username`, `hasTotp` — so listings never leak passwords, notes, or custom fields.
85
+ `--full-items` opts into complete item objects and **does** include secrets; avoid it in
86
+ logged sessions.
87
+
88
+ `hasTotp` says whether an item has a TOTP secret, never what it is. Use it to find 2FA items
89
+ (`bwx list --json --limit 0 | jq '.data[]|select(.hasTotp)'`) instead of reaching for
90
+ `--full-items`, which would dump the whole vault in plaintext to answer a boolean.
91
+
92
+ **Listings stop at 50 items** unless you pass `--limit` (`--limit 0` for all). Truncation is
93
+ reported on stderr and in `meta` on the JSON envelope — check it before concluding a listing
94
+ was complete. `--limit` rejects non-numeric values rather than ignoring them.
95
+
96
+ ### `bwx folders`
97
+
98
+ Lists folder names and IDs. `--folder` accepts either on `list`, `search`, `create`, and
99
+ `edit`; `none` selects unfiled items.
57
100
 
58
101
  ### `bwx run --env NAME=<field>:<item> -- <command>`
59
102
 
@@ -70,6 +113,8 @@ bwx run --env TOKEN='API Key:Acme' --env USER=username:Acme -- ./deploy.sh
70
113
  - Put `--` before the command when it takes its own flags.
71
114
  - Child stdio is inherited; bwx exits with the child's status (`128 + signal` if killed).
72
115
  - `-v` logs injected variable names only, never values.
116
+ - Several variables from one item cost a single vault read, so group them here rather
117
+ than issuing separate `bwx get` calls.
73
118
 
74
119
  ### `bwx attach list <item>`
75
120
 
@@ -81,7 +126,10 @@ Downloads an attachment by ID or filename. Defaults to saving as the attachment
81
126
 
82
127
  ### `bwx create [options]`
83
128
 
84
- Flags: `--type login|note`, `--name`, `--notes`, `--notes-file`, `--username`, `--password`, `--password-stdin`, `--password-file`, `--password-env`, `--uri` (repeatable), `--field k=v` (repeatable), `--field-file k=path`, `--field-env k=ENV`, `--folder`, `--favorite`, `--from-json`
129
+ Flags: `--type login|note`, `--name`, `--notes`, `--notes-file`, `--username`, `--password`, `--password-stdin`, `--password-file`, `--password-env`, `--password-generate`, `--generate-length <n>`, `--totp`, `--totp-stdin`, `--totp-file`, `--totp-env`, `--uri` (repeatable), `--field k=v` (repeatable), `--field-file k=path`, `--field-env k=ENV`, `--folder <name|id>`, `--favorite`, `--from-json`
130
+
131
+ `--username`, any `--password-*` or `--totp-*`, and `--uri` require `--type login`; a note
132
+ cannot hold them and passing them to one is now an error rather than a silent drop.
85
133
 
86
134
  Field syntax: `k=v` (text), `!k=v` (hidden), `bool:k=v` (boolean). The same key syntax works for `--field-file` and `--field-env`, where the value is a path or environment variable name. Stdin is read as `--notes` when piped; prefer `--password-stdin` for login passwords.
87
135
 
@@ -93,26 +141,68 @@ bwx edit "API Key" --add-field region=us-east-1
93
141
  printf '%s' "$TOKEN" | bwx edit "GitHub" --password-stdin --uri https://github.com
94
142
  bwx edit "Note" --rm-field old-key
95
143
  ```
96
- Flags: `--name`, `--notes`, `--notes-file`, `--username`, `--password`, `--password-stdin`, `--password-file`, `--password-env`, `--uri` (replaces all), `--add-field k=v`, `--add-field-file k=path`, `--add-field-env k=ENV`, `--rm-field name`, `--folder`, `--favorite/--no-favorite`, `--from-json`
144
+ Flags: `--name`, `--notes`, `--notes-file`, `--username`, `--password`, `--password-stdin`, `--password-file`, `--password-env`, `--password-generate`, `--generate-length <n>`, `--totp`, `--totp-stdin`, `--totp-file`, `--totp-env`, `--rm-totp`, `--uri` (replaces all), `--add-field k=v`, `--add-field-file k=path`, `--add-field-env k=ENV`, `--rm-field name`, `--folder <name|id>` (`none` unfiles), `--favorite/--no-favorite`, `--from-json`
145
+
146
+ ### Storing a TOTP secret
147
+
148
+ ```bash
149
+ BWX_SEED="JBSWY3DPEHPK3PXP" bwx create --type login --name "AWS" --username me --totp-env BWX_SEED
150
+ bwx edit "AWS" --totp "otpauth://totp/AWS:me?secret=JBSWY3DPEHPK3PXP&issuer=AWS"
151
+ bwx edit "AWS" --rm-totp
152
+ ```
153
+
154
+ Takes a bare base32 secret (spaces, hyphens, and `=` padding are stripped; case is
155
+ normalized) or a full `otpauth://` URI, which is stored verbatim so its `period`/`digits`/
156
+ `algorithm` survive. Malformed secrets are rejected **before** the write — a bad seed
157
+ otherwise sits in the vault until it fails the login it was meant to unlock, which is the
158
+ one moment it cannot be fixed.
159
+
160
+ Prefer `--totp-env` or `--totp-stdin` over `--totp`: a seed on the command line lands in
161
+ shell history and `ps` output.
97
162
 
98
163
  ### `bwx delete <item>`
99
164
 
100
165
  ```bash
101
- bwx delete "Test Note" # prompts in TTY
102
- bwx delete "Test Note" --force # skip prompt
103
- bwx delete "Test Note" --permanent # no trash
166
+ bwx delete "Test Note" --force # --force is REQUIRED when not a TTY
167
+ bwx delete "Test Note" --permanent --force # skip the trash
168
+ bwx trash # what is recoverable
169
+ bwx restore "Test Note" # undo a non-permanent delete
170
+ ```
171
+
172
+ The prompt only exists for a human at a terminal, so an agent must pass `--force`
173
+ deliberately. Deletes are recoverable through `bwx restore` unless `--permanent`.
174
+
175
+ ### `bwx generate`
176
+
177
+ ```bash
178
+ bwx generate --length 32
179
+ bwx generate --passphrase --words 5 --separator -
104
180
  ```
105
181
 
182
+ Local, no vault access, no unlock. Flags: `--length <n>`, `--no-uppercase`,
183
+ `--no-lowercase`, `--no-numbers`, `-s/--special`, `--ambiguous`, `--passphrase`,
184
+ `--words <n>`, `--separator <char>`, `--capitalize`, `--include-number`.
185
+
186
+ Prefer `--password-generate` on `create`/`edit` over generating and passing the value:
187
+ it goes to the vault without ever entering your context, and `--json` returns
188
+ `***GENERATED***` in place of the password.
189
+
106
190
  ### Other
107
191
 
108
192
  | Command | Description |
109
193
  |---------|-------------|
110
- | `bwx status` | Vault state (no auto-unlock) |
194
+ | `bwx status` | Vault state + session usability (no auto-unlock) |
111
195
  | `bwx sync` | Force vault sync |
112
196
  | `bwx unlock` / `bwx lock` | Manual session control |
197
+ | `bwx folders` | Folder names and IDs |
198
+ | `bwx trash` / `bwx restore <item>` | Inspect and undo deletes |
113
199
  | `bwx config list` / `bwx cfg ls` | Show config path + redacted content |
114
200
  | `bwx config master-password` | Set/update master password in Keychain |
115
201
 
202
+ `bwx status` reports `session: valid | stale | none` separately from the vault state.
203
+ **A `locked` vault with a `valid` session reads fine — do not run `bwx unlock` on it.**
204
+ Only `stale`/`none` mean the next read pays for an unlock.
205
+
116
206
  ## Global flags
117
207
 
118
208
  | Flag | Effect |
@@ -137,9 +227,12 @@ even under `--json`; `-q` silences them.
137
227
 
138
228
  - Never use `--full-items` (or `get item` for discovery) in logged sessions.
139
229
  - Prefer `bwx run --env ...` over `export VAR="$(bwx get ...)"`; secrets never touch stdout or argv.
230
+ - Prefer `--password-generate` over generating a secret yourself and passing it in.
140
231
  - Request the narrowest field, resolving an exact item ID first.
141
232
  - Use `-q` for command substitution so unlock logs cannot contaminate merged output.
142
- - On writes, prefer `--password-stdin`, `--password-env`, `--field-file`, `--field-env`.
233
+ - On writes, prefer `--password-stdin`, `--password-env`, `--totp-stdin`, `--totp-env`, `--field-file`, `--field-env`.
234
+ - Check `secondsRemaining` on a TOTP, or pass `--fresh`, before handing the code anywhere.
235
+ - Use `hasTotp` on a listing to find 2FA items; never `--full-items` for that.
143
236
  - Never echo, log, checksum, or commit retrieved values; `unset` temporary variables.
144
237
 
145
238
  ## Auth
@@ -40,6 +40,7 @@ export interface NewItemOptions {
40
40
  notes?: string | null;
41
41
  username?: string | null;
42
42
  password?: string | null;
43
+ totp?: string | null;
43
44
  uris?: string[];
44
45
  fields?: ParsedField[];
45
46
  folderId?: string | null;
@@ -65,7 +66,7 @@ export function buildNewItem(options: NewItemOptions): Record<string, unknown> {
65
66
  item.login = {
66
67
  username: options.username ?? null,
67
68
  password: options.password ?? null,
68
- totp: null,
69
+ totp: options.totp ?? null,
69
70
  uris: options.uris?.length
70
71
  ? options.uris.map((uri) => ({ match: null, uri }))
71
72
  : null,
@@ -95,10 +96,13 @@ export interface PatchOptions {
95
96
  notes?: string;
96
97
  username?: string;
97
98
  password?: string;
99
+ /** `null` clears the stored secret; `undefined` leaves it alone. */
100
+ totp?: string | null;
98
101
  uris?: string[];
99
102
  addFields?: ParsedField[];
100
103
  rmFields?: string[];
101
- folderId?: string;
104
+ /** `null` unfiles the item; `undefined` leaves its folder alone. */
105
+ folderId?: string | null;
102
106
  favorite?: boolean;
103
107
  }
104
108
 
@@ -114,10 +118,18 @@ export function patchItem(
114
118
  if (patch.favorite !== undefined) result.favorite = patch.favorite;
115
119
 
116
120
  // Login fields
117
- if (patch.username !== undefined || patch.password !== undefined || patch.uris !== undefined) {
118
- const login = (result.login as Record<string, unknown>) ?? {};
121
+ if (
122
+ patch.username !== undefined ||
123
+ patch.password !== undefined ||
124
+ patch.totp !== undefined ||
125
+ patch.uris !== undefined
126
+ ) {
127
+ // Copied, not aliased: `{ ...item }` above is shallow, so assigning into
128
+ // the original `login` would edit the caller's item as a side effect.
129
+ const login = { ...((result.login as Record<string, unknown>) ?? {}) };
119
130
  if (patch.username !== undefined) login.username = patch.username;
120
131
  if (patch.password !== undefined) login.password = patch.password;
132
+ if (patch.totp !== undefined) login.totp = patch.totp;
121
133
  if (patch.uris !== undefined) {
122
134
  login.uris = patch.uris.map((uri) => ({ match: null, uri }));
123
135
  }