diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml
index 7b92de0f22a..71a2c62ec1a 100644
--- a/.github/workflows/build-web.yml
+++ b/.github/workflows/build-web.yml
@@ -63,6 +63,11 @@ jobs:
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
steps:
+ - name: Log inputs to job summary
+ uses: bitwarden/ios/.github/actions/log-inputs@main
+ with:
+ inputs: "${{ toJson(inputs) }}"
+
- name: Check out repo
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
@@ -181,6 +186,19 @@ jobs:
ref: ${{ steps.set-server-ref.outputs.server_ref }}
persist-credentials: false
+ - name: Download SDK Artifacts
+ if: ${{ inputs.sdk_branch != '' }}
+ uses: bitwarden/gh-actions/download-artifacts@main
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ workflow: build-wasm-internal.yml
+ workflow_conclusion: success
+ branch: ${{ inputs.sdk_branch }}
+ artifacts: sdk-internal
+ repo: bitwarden/sdk-internal
+ path: sdk-internal
+ if_no_artifact_found: fail
+
- name: Check Branch to Publish
env:
PUBLISH_BRANCHES: "main,rc,hotfix-rc-web"
diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile
index 6d27e12537a..27036e16240 100644
--- a/apps/web/Dockerfile
+++ b/apps/web/Dockerfile
@@ -15,6 +15,12 @@ RUN if [ "${LICENSE_TYPE}" != "commercial" ] ; then \
rm -rf node_modules/@bitwarden/commercial-sdk-internal ; \
fi
+# Override SDK if custom artifacts are present
+RUN if [ -d "sdk-internal" ]; then \
+ echo "Overriding SDK with custom artifacts from sdk-internal" ; \
+ npm link ./sdk-internal ; \
+ fi
+
WORKDIR /source/apps/web
ARG NPM_COMMAND=dist:bit:selfhost
RUN npm run ${NPM_COMMAND}
diff --git a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html
index d96d083ffe0..5f047316a29 100644
--- a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html
+++ b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html
@@ -54,7 +54,7 @@
{{ "owner" | i18n }}
}
-
+ |
{{ "weakness" | i18n }}
|
diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts
index 90452ba573a..ef861b7cab3 100644
--- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts
+++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts
@@ -593,7 +593,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
this.toastService.showToast({
variant: "success",
- message: this.i18nService.t("itemsWereSentToArchive"),
+ message: this.i18nService.t("itemWasSentToArchive"),
});
} catch {
this.toastService.showToast({
diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts
index f795f9533eb..6400c0ca9a8 100644
--- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts
+++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts
@@ -157,7 +157,7 @@ export class VaultCipherRowComponent implements OnInit
// If item is archived always show unarchive button, even if user is not premium
protected get showUnArchiveButton() {
- if (!this.archiveEnabled()) {
+ if (!this.archiveEnabled() || this.viewingOrgVault) {
return false;
}
diff --git a/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts
index e66779f0372..8f1a281050f 100644
--- a/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts
+++ b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts
@@ -91,4 +91,33 @@ describe("BitwardenCsvImporter", () => {
expect(result.collections[0].name).toBe("collection1/collection2");
expect(result.collections[1].name).toBe("collection1");
});
+
+ it("should parse archived items correctly", async () => {
+ const archivedDate = "2025-01-15T10:30:00.000Z";
+ const data =
+ `name,type,archivedDate,login_uri,login_username,login_password` +
+ `\nArchived Login,login,${archivedDate},https://example.com,user,pass`;
+
+ importer.organizationId = null;
+ const result = await importer.parse(data);
+
+ expect(result.success).toBe(true);
+ expect(result.ciphers.length).toBe(1);
+
+ const cipher = result.ciphers[0];
+ expect(cipher.name).toBe("Archived Login");
+ expect(cipher.archivedDate).toBeDefined();
+ expect(cipher.archivedDate.toISOString()).toBe(archivedDate);
+ });
+
+ it("should handle missing archivedDate gracefully", async () => {
+ const data = `name,type,login_uri` + `\nTest Login,login,https://example.com`;
+
+ importer.organizationId = null;
+ const result = await importer.parse(data);
+
+ expect(result.success).toBe(true);
+ expect(result.ciphers.length).toBe(1);
+ expect(result.ciphers[0].archivedDate).toBeUndefined();
+ });
});
diff --git a/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts
index b900e9e8d7a..cca1b80e3bd 100644
--- a/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts
+++ b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts
@@ -51,6 +51,15 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer {
cipher.reprompt = CipherRepromptType.None;
}
+ if (!this.isNullOrWhitespace(value.archivedDate)) {
+ try {
+ cipher.archivedDate = new Date(value.archivedDate);
+ } catch (e) {
+ // eslint-disable-next-line
+ console.error("Unable to parse archivedDate value", e);
+ }
+ }
+
if (!this.isNullOrWhitespace(value.fields)) {
const fields = this.splitNewLine(value.fields);
for (let i = 0; i < fields.length; i++) {
diff --git a/libs/importer/src/importers/buttercup-csv-importer.spec.ts b/libs/importer/src/importers/buttercup-csv-importer.spec.ts
new file mode 100644
index 00000000000..51c9d4cb2d8
--- /dev/null
+++ b/libs/importer/src/importers/buttercup-csv-importer.spec.ts
@@ -0,0 +1,87 @@
+import { ButtercupCsvImporter } from "./buttercup-csv-importer";
+import {
+ buttercupCsvTestData,
+ buttercupCsvWithCustomFieldsTestData,
+ buttercupCsvWithNoteTestData,
+ buttercupCsvWithSubfoldersTestData,
+ buttercupCsvWithUrlFieldTestData,
+} from "./spec-data/buttercup-csv/testdata.csv";
+
+describe("Buttercup CSV Importer", () => {
+ let importer: ButtercupCsvImporter;
+
+ beforeEach(() => {
+ importer = new ButtercupCsvImporter();
+ });
+
+ describe("given basic login data", () => {
+ it("should parse login data when provided valid CSV", async () => {
+ const result = await importer.parse(buttercupCsvTestData);
+ expect(result.success).toBe(true);
+ expect(result.ciphers.length).toBe(2);
+
+ const cipher = result.ciphers[0];
+ expect(cipher.name).toEqual("Test Entry");
+ expect(cipher.login.username).toEqual("testuser");
+ expect(cipher.login.password).toEqual("testpass123");
+ expect(cipher.login.uris.length).toEqual(1);
+ expect(cipher.login.uris[0].uri).toEqual("https://example.com");
+ });
+
+ it("should assign entries to folders based on group_name", async () => {
+ const result = await importer.parse(buttercupCsvTestData);
+ expect(result.success).toBe(true);
+ expect(result.folders.length).toBe(1);
+ expect(result.folders[0].name).toEqual("General");
+ expect(result.folderRelationships.length).toBe(2);
+ });
+ });
+
+ describe("given URL field variations", () => {
+ it("should handle lowercase url field", async () => {
+ const result = await importer.parse(buttercupCsvWithUrlFieldTestData);
+ expect(result.success).toBe(true);
+
+ const cipher = result.ciphers[0];
+ expect(cipher.login.uris.length).toEqual(1);
+ expect(cipher.login.uris[0].uri).toEqual("https://lowercase-url.com");
+ });
+ });
+
+ describe("given note field", () => {
+ it("should map note field to notes", async () => {
+ const result = await importer.parse(buttercupCsvWithNoteTestData);
+ expect(result.success).toBe(true);
+
+ const cipher = result.ciphers[0];
+ expect(cipher.notes).toEqual("This is a note");
+ });
+ });
+
+ describe("given custom fields", () => {
+ it("should import custom fields and exclude official props", async () => {
+ const result = await importer.parse(buttercupCsvWithCustomFieldsTestData);
+ expect(result.success).toBe(true);
+
+ const cipher = result.ciphers[0];
+ expect(cipher.fields.length).toBe(2);
+ expect(cipher.fields[0].name).toEqual("custom_field");
+ expect(cipher.fields[0].value).toEqual("custom value");
+ expect(cipher.fields[1].name).toEqual("another_field");
+ expect(cipher.fields[1].value).toEqual("another value");
+ });
+ });
+
+ describe("given subfolders", () => {
+ it("should create nested folder structure", async () => {
+ const result = await importer.parse(buttercupCsvWithSubfoldersTestData);
+ expect(result.success).toBe(true);
+
+ const folderNames = result.folders.map((f) => f.name);
+ expect(folderNames).toContain("Work/Projects");
+ expect(folderNames).toContain("Work");
+ expect(folderNames).toContain("Personal/Finance");
+ expect(folderNames).toContain("Personal");
+ });
+ });
+});
diff --git a/libs/importer/src/importers/buttercup-csv-importer.ts b/libs/importer/src/importers/buttercup-csv-importer.ts
index ac3a4cd2512..07fe53bc625 100644
--- a/libs/importer/src/importers/buttercup-csv-importer.ts
+++ b/libs/importer/src/importers/buttercup-csv-importer.ts
@@ -3,7 +3,18 @@ import { ImportResult } from "../models/import-result";
import { BaseImporter } from "./base-importer";
import { Importer } from "./importer";
-const OfficialProps = ["!group_id", "!group_name", "title", "username", "password", "URL", "id"];
+const OfficialProps = [
+ "!group_id",
+ "!group_name",
+ "!type",
+ "title",
+ "username",
+ "password",
+ "URL",
+ "url",
+ "note",
+ "id",
+];
export class ButtercupCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise {
@@ -21,16 +32,24 @@ export class ButtercupCsvImporter extends BaseImporter implements Importer {
cipher.name = this.getValueOrDefault(value.title, "--");
cipher.login.username = this.getValueOrDefault(value.username);
cipher.login.password = this.getValueOrDefault(value.password);
- cipher.login.uris = this.makeUriArray(value.URL);
- let processingCustomFields = false;
+ // Handle URL field (case-insensitive)
+ const urlValue = value.URL || value.url || value.Url;
+ cipher.login.uris = this.makeUriArray(urlValue);
+
+ // Handle note field (case-insensitive)
+ const noteValue = value.note || value.Note || value.notes || value.Notes;
+ if (noteValue) {
+ cipher.notes = noteValue;
+ }
+
+ // Process custom fields, excluding official props (case-insensitive)
for (const prop in value) {
// eslint-disable-next-line
if (value.hasOwnProperty(prop)) {
- if (!processingCustomFields && OfficialProps.indexOf(prop) === -1) {
- processingCustomFields = true;
- }
- if (processingCustomFields) {
+ const lowerProp = prop.toLowerCase();
+ const isOfficialProp = OfficialProps.some((p) => p.toLowerCase() === lowerProp);
+ if (!isOfficialProp && value[prop]) {
this.processKvp(cipher, prop, value[prop]);
}
}
diff --git a/libs/importer/src/importers/spec-data/buttercup-csv/testdata.csv.ts b/libs/importer/src/importers/spec-data/buttercup-csv/testdata.csv.ts
new file mode 100644
index 00000000000..5e2f7a8d38c
--- /dev/null
+++ b/libs/importer/src/importers/spec-data/buttercup-csv/testdata.csv.ts
@@ -0,0 +1,16 @@
+export const buttercupCsvTestData = `!group_id,!group_name,title,username,password,URL,id
+1,General,Test Entry,testuser,testpass123,https://example.com,entry1
+1,General,Another Entry,anotheruser,anotherpass,https://another.com,entry2`;
+
+export const buttercupCsvWithUrlFieldTestData = `!group_id,!group_name,title,username,password,url,id
+1,General,Entry With Lowercase URL,user1,pass1,https://lowercase-url.com,entry1`;
+
+export const buttercupCsvWithNoteTestData = `!group_id,!group_name,title,username,password,URL,note,id
+1,General,Entry With Note,user1,pass1,https://example.com,This is a note,entry1`;
+
+export const buttercupCsvWithCustomFieldsTestData = `!group_id,!group_name,title,username,password,URL,custom_field,another_field,id
+1,General,Entry With Custom Fields,user1,pass1,https://example.com,custom value,another value,entry1`;
+
+export const buttercupCsvWithSubfoldersTestData = `!group_id,!group_name,title,username,password,URL,id
+1,Work/Projects,Project Entry,projectuser,projectpass,https://project.com,entry1
+2,Personal/Finance,Finance Entry,financeuser,financepass,https://finance.com,entry2`;
diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts
index 620f465789c..7adf7b4138f 100644
--- a/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts
+++ b/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts
@@ -59,6 +59,7 @@ export class BaseVaultExportService {
cipher.notes = c.notes;
cipher.fields = null;
cipher.reprompt = c.reprompt;
+ cipher.archivedDate = c.archivedDate ? c.archivedDate.toISOString() : null;
// Login props
cipher.login_uri = null;
cipher.login_username = null;
diff --git a/libs/tools/export/vault-export/vault-export-core/src/types/bitwarden-csv-export-type.ts b/libs/tools/export/vault-export/vault-export-core/src/types/bitwarden-csv-export-type.ts
index 30c6bb89bc1..efe15a844fc 100644
--- a/libs/tools/export/vault-export/vault-export-core/src/types/bitwarden-csv-export-type.ts
+++ b/libs/tools/export/vault-export/vault-export-core/src/types/bitwarden-csv-export-type.ts
@@ -12,6 +12,7 @@ export type BitwardenCsvExportType = {
login_password: string;
login_totp: string;
favorite: number | null;
+ archivedDate: string | null;
};
export type BitwardenCsvIndividualExportType = BitwardenCsvExportType & {