feat: edit + password gen + import + ssh key + loadable extension + backup#12
Merged
Conversation
…on + backup
Six features in one batch — each lifts a real workflow gap that came up
in the dogfooding discussion. Total +35 tests (108 web + 25 core, was 73 + 25).
## 1. Edit existing credentials
Hover any row → click the new ✎ icon next to the delete button. The
AddCredentialForm dual-purposes into Edit mode: pre-fills every field,
hides the storage-location toggle (you can't move items between stores
from the form), and dispatches `PATCH /api/vault/items/:id` for server
items or an IndexedDB `put()` for local-only items. The server already
had the PATCH endpoint; this PR adds `api.updateItem` + a parallel
`updateItem` in the unified storage facade.
Adjacent fix: bulk-delete toast captured `selected.size` *after*
clearing the set ("0 credential(s) deleted"). Captured the count first.
## 2. Password generator
New `packages/web/src/passwords/` — pure CSPRNG-backed module, no
third-party deps. Build-up strategy reserves one slot per requested
class up front (lowercase / uppercase / digits / symbols), fills the
rest from the full alphabet, then Fisher-Yates shuffles. Guarantees
class coverage without overwriting earlier injections — first cut had
a flaky bug where forcing 3 missing classes could overwrite the only
representative of an existing class.
UI: inline `PasswordGenerator` popover under the Password field with a
length slider (6–64), per-class toggles, a "No 0/O/1/l" ambiguous-
chars filter, live entropy meter (Shannon: length × log₂(alphabet)),
and a strength label (`weak` / `fair` / `strong` / `excellent`).
User's last options persist to localStorage.
## 3. Import from Bitwarden CSV
New `packages/web/src/import/` with a hand-rolled RFC-4180 CSV parser
(quoted fields, escaped quotes, CRLF, embedded newlines) and a
Bitwarden-specific column mapper. v1 supports the most common export
format only; KeePass / 1Password follow-up.
UI: `ImportDialog` modal with file-picker → preview (first 50
candidates + skipped rows with reasons) → confirm. The actual save
loop runs in the parent so encryption + storage routing live in one
place. A "store all imported items on this device only" toggle scopes
the whole batch to IndexedDB.
## 4. SSH private-key field
`VaultLoginPlaintext` gains optional `privateKey` (PEM-encoded). When
set:
- AddCredentialForm grows a textarea (only for SSH-protocol entries)
with a placeholder showing the OpenSSH BEGIN/END markers
- ConnectDialog's previously-disabled "Use SSH private key" option
becomes active: downloads the .pem with `<sanitised-name>.pem` and
copies a matching `ssh -i ~/.ssh/passman/<name>.pem user@host -p N`
command to the clipboard
- New `sshkey.ts` helpers: `looksLikePem` (loose validator that accepts
OPENSSH / RSA / EC / generic PRIVATE KEY blocks) + `downloadSshKey` +
`buildSshKeyCommand`
The key is encrypted with the same vault key as the password — the
server never sees its contents.
## 5. Loadable Chrome extension
`packages/extension/vite.config.ts` is new. Multi-entry build (background,
content, popup) with `manualChunks: undefined` so each MV3 context
bundles independently — the platform can't share Rollup chunks across
the three contexts.
A `copyExtensionAssets` plugin copies `manifest.json` to `dist/` and
rewrites `popup/index.html` so its module script src points at the
bundled `popup.js` (Vite strips the `.ts` extension).
Output is the flat `dist/` layout `chrome://extensions → Load unpacked`
expects:
background.js + .map
content.js + .map
popup.js + .map
popup.html
manifest.json
CI's "skipping extension build until vite config lands" comment is
removed and the build is back in the pipeline.
## 6. Backup / export
`packages/web/src/backup/` exports the whole vault — both server +
local-only items — as a single JSON file:
passman-<vault>-<YYYYMMDD>.json
The file contains already-encrypted ciphertext blobs (the same
envelopes the server stores). Unreadable without the master password;
the vault key never leaves the browser. v1 is export-only — Restore
flow tracked for a follow-up that consumes the same `{ format,
version, vault, items }` envelope.
UI: "Export backup" link in the user-card footer at the bottom of the
sidebar.
## Tests
108 web tests (was 73) — +35 across:
- `passwords.test.ts` (8): length, alphabet membership, class-coverage,
ambiguous filter, error cases, distinctness, entropy formula, strength
- `import.test.ts` (12): CSV edge cases, Bitwarden column mapping,
case-insensitive headers, skip reasons
- `sshkey.test.ts` (8): PEM marker validation across formats, command
builder filename sanitisation, port/user handling, missing-host/key
- `backup.test.ts` (4): envelope shape, location preservation, empty
vault, never-leaks-plaintext invariant
- `local-storage.test.ts` (+2): updateLocalItem round-trip + not-found
## Bundle impact
Web CSS: 4.84 → 5.54 KB gzipped (+0.70 KB)
Web JS: 77.84 → 82.04 KB gzipped (+4.20 KB)
Extension (new): popup 0.78 + content 1.70 + background 17.78 KB gzipped
## Docs
- USER_GUIDE.md grows §5b–5f covering the four credential-form features
+ §6 rewritten to reflect the loadable extension build steps
- The static preview mockups still mirror the old DOM (no Edit icon,
no Import button, no SSH textarea) — refresh tracked as a polish
follow-up. Live React app is the source of truth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Six features in one batch. Each lifts a real workflow gap that came up in the dogfooding discussion. Total 108 web tests + 25 core tests (was 73 + 25).
PATCHfor server /put()for local)privateKeyPEM field on credentials (encrypted with the vault key); ConnectDialog's previously-disabled "Use SSH key" option becomes active — downloads.pem+ copies matchingssh -icommandvite.config.tswith multi-entry (background, content, popup) + manifest-copy plugin; CI re-enablesnpm run build --workspace=@passman/extensionpassman-<vault>-<YYYYMMDD>.jsonwith every encrypted blob (server + local). Unreadable without the master passwordArchitecture
Three new modules under
packages/web/src/:passwords/—generator.tswith CSPRNG + Fisher-Yates shuffle. Reserves one slot per requested class up front, fills the rest, then shuffles — guarantees class coverage without overwriting earlier injections (a flaky bug caught during testing).import/—csv.ts(RFC-4180 parser handling quoted fields, escaped quotes, CRLF, embedded newlines) +bitwarden.ts(case-insensitive column lookup so v2024 capitalisation differences both parse).backup/—export.tsbuilds the v1 envelope ({ format, version, exportedAt, vault, items[] }) and triggers a Blob download.Plus:
connect/sshkey.ts—looksLikePemvalidator (accepts OPENSSH / RSA / EC / generic PKCS#8 PRIVATE KEY blocks),downloadSshKey,buildSshKeyCommand.storage/—updateItemadded to the unified facade +updateLocalItemto the IndexedDB layer.Verification
npm run typecheck --workspaces— clean across core / web / extensionnpm test --workspaces— 108 web + 25 core passing (5 stability runs at 108/108)npm run build --workspace=@passman/web— clean (CSS 5.54 / JS 82.04 KB gzipped)npm run build --workspace=@passman/extension— produces a loadable MV3 bundle (background.js + content.js + popup.js + popup.html + manifest.json underdist/)Adjacent fix
Bulk-delete toast captured
selected.sizeaftersetSelected(new Set()), which would render "0 credential(s) deleted". Captured the count first.Out of scope (deliberate)
parseXxxCsv → ImportResultshape..pemdownload +ssh -icommand path is the practical compromise.Bundle impact
Web CSS: 4.84 → 5.54 KB gzipped (+0.70 KB)
Web JS: 77.84 → 82.04 KB gzipped (+4.20 KB)
Extension (new): popup 0.78 + content 1.70 + background 17.78 KB gzipped
Test plan
ssh -icommandLoad unpackedpackages/extension/dist/ in Chrome, verify the toolbar icon appears🤖 Generated with Claude Code