Skip to content

feat: edit + password gen + import + ssh key + loadable extension + backup#12

Merged
valehdba merged 1 commit into
mainfrom
feat/v1-polish-batch
May 10, 2026
Merged

feat: edit + password gen + import + ssh key + loadable extension + backup#12
valehdba merged 1 commit into
mainfrom
feat/v1-polish-batch

Conversation

@valehdba
Copy link
Copy Markdown
Owner

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

# Feature What it adds
1 Edit existing credentials ✎ icon next to the per-row delete; AddCredentialForm dual-purposes into Edit mode (pre-fills, hides storage toggle, dispatches PATCH for server / put() for local)
2 Password generator Inline popover under the Password field — length slider, per-class toggles, ambiguous-chars filter, live entropy meter, strength label. CSPRNG, no third-party deps
3 Import from Bitwarden CSV Hand-rolled RFC-4180 parser + Bitwarden column mapper; modal with file-picker → preview → confirm; per-batch local-only toggle
4 SSH private-key field Optional privateKey PEM field on credentials (encrypted with the vault key); ConnectDialog's previously-disabled "Use SSH key" option becomes active — downloads .pem + copies matching ssh -i command
5 Loadable Chrome extension New vite.config.ts with multi-entry (background, content, popup) + manifest-copy plugin; CI re-enables npm run build --workspace=@passman/extension
6 Backup / export "Export backup" in the sidebar user card downloads passman-<vault>-<YYYYMMDD>.json with every encrypted blob (server + local). Unreadable without the master password

Architecture

Three new modules under packages/web/src/:

  • passwords/generator.ts with 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.ts builds the v1 envelope ({ format, version, exportedAt, vault, items[] }) and triggers a Blob download.

Plus:

  • connect/sshkey.tslooksLikePem validator (accepts OPENSSH / RSA / EC / generic PKCS#8 PRIVATE KEY blocks), downloadSshKey, buildSshKeyCommand.
  • storage/updateItem added to the unified facade + updateLocalItem to the IndexedDB layer.

Verification

  • npm run typecheck --workspaces — clean across core / web / extension
  • npm test --workspaces108 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 under dist/)
  • Live-rendered the login page via Vite + Claude Preview to confirm the new build still boots into the branded auth view

Adjacent fix

Bulk-delete toast captured selected.size after setSelected(new Set()), which would render "0 credential(s) deleted". Captured the count first.

Out of scope (deliberate)

  • KeePass / 1Password import formats — Bitwarden first; the other parsers slot into the same parseXxxCsv → ImportResult shape.
  • Restore from backup — v1 is export-only. The file shape is intentionally stable so a future Restore can consume it.
  • SSH agent integration — out of reach from a sandboxed browser; the .pem download + ssh -i command path is the practical compromise.
  • Move credentials between server ↔ local — atomicity-sensitive (write here, delete there); kept as a separate operation.
  • Refresh the static preview mockups — live React app is the source of truth; mockups still mirror the pre-batch DOM. Polish follow-up.

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

  • Build clean across all workspaces
  • Typecheck clean
  • 108 web tests + 25 core tests passing (stable across 5 runs)
  • Extension produces loadable MV3 dist/
  • Manual: add a credential, edit it, verify the form pre-fills correctly
  • Manual: open the password generator, dial in length=24 + symbols, click "Use this password"
  • Manual: export a Bitwarden CSV, import it into Passman, verify rows preview + skipped reasons + import succeeds
  • Manual: paste an OpenSSH PEM into a credential, click Connect → "Connect with SSH key" downloads the .pem + copies ssh -i command
  • Manual: Load unpacked packages/extension/dist/ in Chrome, verify the toolbar icon appears
  • Manual: click "Export backup", verify the JSON file downloads and contains every credential's encrypted_data

🤖 Generated with Claude Code

…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>
@valehdba valehdba merged commit 91283eb into main May 10, 2026
9 checks passed
@valehdba valehdba deleted the feat/v1-polish-batch branch May 10, 2026 10:30
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.

1 participant