OutreachBitwardenFix #20694
Draft PR #20867 open 43/43 importer tests pass 4 clients fixed CLA signed · in Community PR board

Bitwarden

bitwarden/clients · issue #20694 · PR #20867 · branch fix/20694-1pux-import-preserves-archived

The 1Password 1pux importer was silently dropping every item exported with state: "archived", contradicting the help-center docs that promise everything from a 1pux export is imported. Users migrating their vault ended up with their archive empty. The fix removes the early-return, stamps cipher.archivedDate = new Date() so isArchived reports true, and lands in libs/importer/ shared by browser, desktop, cli, and web - one change, four clients fixed.

43
jest tests in libs/importer/onepassword, all green
+34 / −7
LOC, source + test combined
4
clients impacted (browser, desktop, cli, web)
2
tests added: correct contract + regression guard
01 · The problem

Every 1Password archive entry was silently dropped on import

The 1Password 1pux export format flags archived items with state: "archived". The Bitwarden importer in libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts had an early return at lines 43-46 that skipped those entries entirely. Users migrating their vault ended up with an empty archive, contradicting Bitwarden's own help-center docs that promise "everything from the 1Password export is imported".

The bug, in three lines

// libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts vault.items.forEach((item: Item) => { if (item.state === "archived") { return; // silently drops every archived item } // ... rest of the importer never runs for archived entries });

Worse, the existing unit test should not import items with state 'archived' explicitly enforced the buggy behaviour, so any naive fix to the source without correcting the test would have left the test suite broken.

02 · The fix

Remove the early return, stamp archivedDate, fix the test that enforced the bug

Two source files touched, plus a regression guard. The fix lands in libs/importer/ which is shared by all four Bitwarden clients, one change, four clients fixed (browser, desktop, cli, web, exactly the labels on the issue).

The corrected importer flow

vault.items.forEach((item: Item) => { const cipher = this.initLoginCipher(); // ... all the existing import logic runs unchanged ... this.cleanupCipher(cipher); // Preserve 1Password's archive state on import (fixes #20694). The 1pux // schema only tells us an item is archived, not WHEN it was archived, // so we stamp the import time - matches the behaviour of archiving a // cipher manually in Bitwarden after the import. if (item.state === "archived") { cipher.archivedDate = new Date(); } this.result.ciphers.push(cipher); });

The CipherView.archivedDate field is what powers Bitwarden's own isArchived getter (see cipher.view.ts:178-179), so this is the canonical, type-safe way to mark a cipher as archived programmatically, no schema migration, no new flag, no new state machine.

Why new Date() and not the original archive time?

Because 1pux does not encode it. The Item type in onepassword-1pux-importer-types.ts:65 exposes only state: "active" | "archived", no timestamp. The import-time stamp is the highest fidelity we can give without inventing data. If 1pux ever exports archivedAt, the substitution is a one-line change.

03 · The tests

Correct the contract, add a regression guard, run the full suite

The existing test was renamed and inverted to assert the correct behaviour; a new test was added to lock in that active items stay unarchived after the fix. 43 of 43 tests pass across the four onepassword importer suites.

The renamed test

// before it("should not import items with state 'archived'", ...) /* enforced the bug */ // after - fixes #20694 it("should import items with state 'archived' as archived ciphers", async () => { const archivedLoginData = JSON.parse(JSON.stringify(LoginData)); // deep clone archivedLoginData["accounts"][0]["vaults"][0]["items"][0]["state"] = "archived"; const result = await importer.parse(JSON.stringify(archivedLoginData)); expect(result.ciphers.length).toBe(1); expect(result.ciphers[0].archivedDate).toBeInstanceOf(Date); expect(result.ciphers[0].isArchived).toBe(true); });

The JSON.parse(JSON.stringify(...)) deep clone fixes a latent issue in the previous test which mutated the shared LoginData fixture across cases.

The new regression guard

it("should leave active items unarchived (regression guard)", async () => { const result = await importer.parse(LoginDataJson); expect(result.ciphers[0].archivedDate).toBeUndefined(); expect(result.ciphers[0].isArchived).toBe(false); });

Locks in that the new if (item.state === "archived") branch does not accidentally archive-stamp every imported cipher.

Local verification. npx jest libs/importer/src/importers/onepassword against the actual Bitwarden clients monorepo: 43 tests pass in 3.043s, no regressions, no skipped tests. npx tsc --noEmit and npx eslint both clean. Husky pre-commit hooks all green.

04 · The outreach

Where this stands in the conversation funnel

Draft PR #20867 is open against main, identity-linked to the GitHub account, CLA signed automatically by the noreply-email preflight learned the hard way on the previous Joplin attempt.

Done

Branch + commit + tests + tsc + eslint

Branch fix/20694-1pux-import-preserves-archived, commit 7a33830, 43/43 jest tests pass, tsc clean, eslint clean, husky pre-commit hooks all green.

Done

Fork + push + open draft PR with @-mention

Fork 999purple999/clients, draft PR #20867 open, @djsmith85 (community contributor who flagged the issue as good first issue with the file pointer) tagged in the PR body for natural notification.

Done

CLA signed + Community PR board triage

license/cla status: SUCCESS, auto-signed via the GitHub noreply identity. bitwarden-bot added the PR to the internal Community PR board as PM-38123 (Jira ticket reference) and prepended the prefix to the PR title.

Done

Cold mail to @MGibson1 (Matt Gibson, Bitwarden employee)

Verified email pattern firstInitial + lastName@bitwarden.com via six sample employee commits (bcunningham, btreston, dnance, gsmith, jengstrom, mgibson). Mail anchored to the PR URL with K-Perception (AES-256-GCM client-side + Y.js CRDT) and HALCYON (Node 24 WebRTC mesh) as the explicit stack-overlap signals for any cryptography or vault-side adjacent role.

Next

Maintainer workflow approval + code-owner review

The CI gate Check user permission failed by design, community PRs need a Bitwarden maintainer to manually approve the workflow run before Build Browser / Build CLI / Build Desktop / Build Web / Chromatic / Scan run for real on the code. This is a security-standard pattern for external forks and resolves on maintainer triage (typically 1-7 days).

Goal

Merge + paid follow-up conversation

Realistic odds: ~50-60% merge probability given the surgical scope, the clean Community PR board entry, the corrected test that enforces the right contract, and the existing CLA signature. Paid-conversation odds independent of merge: ~10-15% given Bitwarden's known external-contractor hiring at the vault/crypto layers and the K-Perception stack match documented in the cold mail.

05 · How to verify

Reproduce every claim on this page in four commands

# 1. Pull the branch from the fork git clone https://github.com/999purple999/clients --branch fix/20694-1pux-import-preserves-archived # 2. Install deps (Node 22, npm) cd clients && npm install # 3. Run the importer test suite npx jest libs/importer/src/importers/onepassword # expect: 4 suites passed, 43 tests passed # 4. Run tsc + eslint on the changed files npx tsc --noEmit --project libs/importer/tsconfig.json npx eslint libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts \ libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts # expect: zero errors from either tool

If anything fails on your machine, the PR cannot land. Run them first.