From 9ed69ef4b81954b41511b22b5083f6286807a8c6 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:48:00 -0700 Subject: [PATCH 01/38] [PM-24304][PM-24305] - [Defect] Some fields are not disabled when editing an item from My Vault (#15982) * disable all remaining form fields for editing personally owned My Items * fix failing tests * ensure collection field is also properly disabled * clean up logic * fix failing test * fix test * refactor variable to avoid using `is` prefix * directly reference parent form for status rather than subscribe to it * refactor subscription for form status changes * use observable as an Output * disable attachment button on desktop vault when the form * disable custom field components when custom fields already exist and parent form is disabled * disable attachments button in the browser when the edit form is disabled * grab icon button instance for disabled state --------- Co-authored-by: Nick Krantz --- .../open-attachments.component.html | 7 ++- .../open-attachments.component.spec.ts | 23 +++++++ .../open-attachments.component.ts | 9 +++ .../vault/app/vault/vault-v2.component.html | 8 ++- .../src/vault/app/vault/vault-v2.component.ts | 7 +++ .../vault-item-dialog.component.html | 8 ++- .../vault-item-dialog.component.ts | 6 ++ .../src/cipher-form/cipher-form-container.ts | 6 +- .../additional-options-section.component.html | 2 +- ...ditional-options-section.component.spec.ts | 3 +- .../additional-options-section.component.ts | 5 ++ .../autofill-options.component.spec.ts | 3 +- .../autofill-options.component.ts | 17 +++++- .../components/cipher-form.component.ts | 7 ++- .../custom-fields.component.html | 5 +- .../custom-fields.component.spec.ts | 60 ++++++++++++++++++- .../custom-fields/custom-fields.component.ts | 7 +++ .../item-details-section.component.html | 1 + .../item-details-section.component.ts | 49 +++++++++------ .../login-details-section.component.spec.ts | 2 + .../login-details-section.component.ts | 17 +++++- .../sshkey-section.component.ts | 8 ++- 22 files changed, 224 insertions(+), 36 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html index 6345c3ea4e1..2650345e94b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html @@ -1,5 +1,10 @@ - @@ -104,6 +105,7 @@ [label]="'reorderToggleButton' | i18n: field.value.name" (keydown)="handleKeyDown($event, field.value.name, i)" data-testid="reorder-toggle-button" + [disabled]="parentFormDisabled" *ngIf="canEdit(field.value.type)" > @@ -113,7 +115,8 @@ bitLink linkType="primary" (click)="openAddEditCustomFieldDialog()" - *ngIf="!isPartialEdit" + data-testid="add-field-button" + *ngIf="!isPartialEdit && !parentFormDisabled" > {{ "addField" | i18n }} diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts index ced8763f895..1d1bcfa1ee0 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts @@ -4,6 +4,7 @@ import { DebugElement } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -16,7 +17,12 @@ import { } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; -import { DialogRef, BitPasswordInputToggleDirective, DialogService } from "@bitwarden/components"; +import { + DialogRef, + BitPasswordInputToggleDirective, + DialogService, + BitIconButtonComponent, +} from "@bitwarden/components"; import { CipherFormConfig } from "../../abstractions/cipher-form-config.service"; import { CipherFormContainer } from "../../cipher-form-container"; @@ -39,6 +45,7 @@ describe("CustomFieldsComponent", () => { let announce: jest.Mock; let patchCipher: jest.Mock; let config: CipherFormConfig; + const formStatusChange$ = new BehaviorSubject<"disabled" | "enabled">("enabled"); beforeEach(async () => { open = jest.fn(); @@ -65,6 +72,7 @@ describe("CustomFieldsComponent", () => { registerChildForm: jest.fn(), config, getInitialCipherView: jest.fn(() => originalCipherView), + formStatusChange$, }, }, { @@ -552,4 +560,54 @@ describe("CustomFieldsComponent", () => { expect(editButtons).toHaveLength(3); }); }); + + describe("parent form disabled", () => { + beforeEach(() => { + originalCipherView!.fields = mockFieldViews; + formStatusChange$.next("disabled"); + component.ngOnInit(); + + fixture.detectChanges(); + }); + + afterEach(() => { + formStatusChange$.next("enabled"); + fixture.detectChanges(); + }); + + it("disables edit and reorder buttons", () => { + const reorderButtonQuery = By.directive(BitIconButtonComponent); + const editButtonQuery = By.directive(BitIconButtonComponent); + + let reorderButton = fixture.debugElement.query(reorderButtonQuery); + let editButton = fixture.debugElement.query(editButtonQuery); + + expect(reorderButton.componentInstance.disabled()).toBe(true); + expect(editButton.componentInstance.disabled()).toBe(true); + + formStatusChange$.next("enabled"); + fixture.detectChanges(); + + reorderButton = fixture.debugElement.query(reorderButtonQuery); + editButton = fixture.debugElement.query(editButtonQuery); + + expect(reorderButton.componentInstance.disabled()).toBe(false); + expect(editButton.componentInstance.disabled()).toBe(false); + }); + + it("hides add field button", () => { + const query = By.css('button[data-testid="add-field-button"]'); + + let addFieldButton = fixture.debugElement.query(query); + + expect(addFieldButton).toBeNull(); + + formStatusChange$.next("enabled"); + fixture.detectChanges(); + + addFieldButton = fixture.debugElement.query(query); + + expect(addFieldButton).not.toBeNull(); + }); + }); }); diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts index c8edba6c9fd..e3612e75a1b 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts @@ -113,6 +113,9 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit { /** Emits when a new custom field should be focused */ private focusOnNewInput$ = new Subject(); + /** Tracks the disabled status of the edit cipher form */ + protected parentFormDisabled: boolean = false; + disallowHiddenField?: boolean; destroyed$: DestroyRef; @@ -133,6 +136,10 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit { // getRawValue ensures disabled fields are included this.updateCipher(this.fields.getRawValue()); }); + + this.cipherFormContainer.formStatusChange$.pipe(takeUntilDestroyed()).subscribe((status) => { + this.parentFormDisabled = status === "disabled"; + }); } /** Fields form array, referenced via a getter to avoid type-casting in multiple places */ diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html index fc208bd9b92..9bf6dc32758 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html @@ -11,6 +11,7 @@ [attr.aria-checked]="itemDetailsForm.value.favorite" [label]="'favorite' | i18n" (click)="toggleFavorite()" + [disabled]="favoriteButtonDisabled" > diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts index 4fd999ae601..bc5e7c43d12 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts @@ -82,6 +82,8 @@ export class ItemDetailsSectionComponent implements OnInit { protected userId: UserId; + protected favoriteButtonDisabled = false; + @Input({ required: true }) config: CipherFormConfig; @@ -241,15 +243,19 @@ export class ItemDetailsSectionComponent implements OnInit { /** * When the cipher does not belong to an organization but the user's organization * requires all ciphers to be owned by an organization, disable the entire form - * until the user selects an organization. + * until the user selects an organization. Once the organization is set, enable the form. + * Ensure to properly set the collections control state when the form is enabled. */ private setFormState() { if (this.config.originalCipher && !this.allowPersonalOwnership) { if (this.itemDetailsForm.controls.organizationId.value === null) { this.cipherFormContainer.disableFormFields(); this.itemDetailsForm.controls.organizationId.enable(); + this.favoriteButtonDisabled = true; } else { this.cipherFormContainer.enableFormFields(); + this.favoriteButtonDisabled = false; + this.setCollectionControlState(); } } } @@ -305,7 +311,6 @@ export class ItemDetailsSectionComponent implements OnInit { }); const orgId = this.itemDetailsForm.controls.organizationId.value as OrganizationId; - const organization = this.organizations.find((o) => o.id === orgId); const initializedWithCachedCipher = this.cipherFormContainer.initializedWithCachedCipher(); // Configure form for clone mode. @@ -327,9 +332,7 @@ export class ItemDetailsSectionComponent implements OnInit { await this.updateCollectionOptions(prefillCollections); - if (!organization?.canEditAllCiphers && !prefillCipher.canAssignToCollections) { - this.itemDetailsForm.controls.collectionIds.disable(); - } + this.setCollectionControlState(); if (this.partialEdit) { this.itemDetailsForm.disable(); @@ -344,22 +347,34 @@ export class ItemDetailsSectionComponent implements OnInit { c.readOnly && this.originalCipherView.collectionIds.includes(c.id as CollectionId), ); - - // When Owners/Admins access setting is turned on. - // Disable Collections Options if Owner/Admin does not have Edit/Manage permissions on item - // Disable Collections Options if Custom user does not have Edit/Manage permissions on item - if ( - (organization?.allowAdminAccessToAllCollectionItems && - (!this.originalCipherView.viewPassword || !this.originalCipherView.edit)) || - (organization?.type === OrganizationUserType.Custom && - !this.originalCipherView.viewPassword) - ) { - this.itemDetailsForm.controls.collectionIds.disable(); - } } } } + private setCollectionControlState() { + const initialCipherView = this.cipherFormContainer.getInitialCipherView(); + const orgId = this.itemDetailsForm.controls.organizationId.value as OrganizationId; + const organization = this.organizations.find((o) => o.id === orgId); + if (!organization || !initialCipherView) { + return; + } + // Disable the collection control if either of the following apply: + // 1. The organization does not allow editing all ciphers and the existing cipher cannot be assigned to + // collections + // 2. When Owners/Admins access setting is turned on. + // AND either: + // a. Disable Collections Options if Owner/Admin does not have Edit/Manage permissions on item + // b. Disable Collections Options if Custom user does not have Edit/Manage permissions on item + if ( + (!organization.canEditAllCiphers && !initialCipherView.canAssignToCollections) || + (organization.allowAdminAccessToAllCollectionItems && + (!initialCipherView.viewPassword || !initialCipherView.edit)) || + (organization.type === OrganizationUserType.Custom && !initialCipherView.viewPassword) + ) { + this.itemDetailsForm.controls.collectionIds.disable(); + } + } + /** * Updates the collection options based on the selected organization. * @param startingSelection - Optional starting selection of collectionIds to be automatically selected. diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts index c5b1fc7897b..b07a50fd383 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts @@ -3,6 +3,7 @@ import { Component } from "@angular/core"; import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -47,6 +48,7 @@ describe("LoginDetailsSectionComponent", () => { getInitialCipherView.mockClear(); cipherFormContainer = mock({ getInitialCipherView, + formStatusChange$: new BehaviorSubject<"enabled" | "disabled">("enabled"), website: "example.com", }); diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts index e74d9915cdb..061a8c4abf4 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DatePipe, NgIf } from "@angular/common"; -import { Component, inject, OnInit, Optional } from "@angular/core"; +import { Component, DestroyRef, inject, OnInit, Optional } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { map } from "rxjs"; @@ -81,6 +81,8 @@ export class LoginDetailsSectionComponent implements OnInit { */ private existingFido2Credentials?: Fido2CredentialView[]; + private destroyRef = inject(DestroyRef); + get hasPasskey(): boolean { return this.existingFido2Credentials != null && this.existingFido2Credentials.length > 0; } @@ -148,6 +150,19 @@ export class LoginDetailsSectionComponent implements OnInit { if (this.cipherFormContainer.config.mode === "partial-edit") { this.loginDetailsForm.disable(); } + + // If the form is enabled, ensure to disable password or TOTP + // for hidden password users + this.cipherFormContainer.formStatusChange$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((status) => { + if (status === "enabled") { + if (!this.viewHiddenFields) { + this.loginDetailsForm.controls.password.disable(); + this.loginDetailsForm.controls.totp.disable(); + } + } + }); } private initFromExistingCipher(existingLogin: LoginView) { diff --git a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts index f83f93267c9..f92c4420d03 100644 --- a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts +++ b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts @@ -98,9 +98,13 @@ export class SshKeySectionComponent implements OnInit { // Disable the form if the cipher form container is enabled // to prevent user interaction - this.cipherFormContainer.formEnabled$ + this.cipherFormContainer.formStatusChange$ .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => this.sshKeyForm.disable()); + .subscribe((status) => { + if (status === "enabled") { + this.sshKeyForm.disable(); + } + }); } /** Set form initial form values from the current cipher */ From c7b2e3f9c2b6d55f6ce6b5b78b938c7305359bdb Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:09:09 +0000 Subject: [PATCH 02/38] Fix typo (#16152) --- apps/browser/store/locales/en/copy.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/store/locales/en/copy.resx b/apps/browser/store/locales/en/copy.resx index 82e4eb1d88e..377b13e7423 100644 --- a/apps/browser/store/locales/en/copy.resx +++ b/apps/browser/store/locales/en/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. From d4e2e73d135cd73093feb8d7578f78a0ca86fb27 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 10:09:06 +0000 Subject: [PATCH 03/38] Autosync the updated translations (#16160) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/sv/messages.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index ff1bf0a2a1c..d050d1a18e2 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -7029,10 +7029,10 @@ "message": "Okänt objekt, du kan behöva begära tillstånd för att få tillgång till detta objekt." }, "unknownSecret": { - "message": "Unknown secret, you may need to request permission to access this secret." + "message": "Okänd hemlighet, du kan behöver begära behörighet för att komma åt denna hemlighet." }, "unknownProject": { - "message": "Unknown project, you may need to request permission to access this project." + "message": "Okänt projekt, du kan behöver begära behörighet för att komma åt detta projekt." }, "cannotSponsorSelf": { "message": "Du kan inte lösa in för det aktiva kontot. Ange en annan e-postadress." @@ -10823,7 +10823,7 @@ "message": "Dessa händelser är endast exempel och återspeglar inte verkliga händelser inom din Bitwarden-organisation." }, "viewEvents": { - "message": "View Events" + "message": "Visa händelser" }, "cannotCreateCollection": { "message": "Gratisorganisationer kan ha upp till 2 samlingar. Uppgradera till en betald plan för att lägga till fler samlingar." @@ -11125,13 +11125,13 @@ "message": "Check your Tax ID to verify the format is correct and there are no typos." }, "pendingVerification": { - "message": "Pending verification" + "message": "Väntande verifiering" }, "checkInputFormat": { "message": "Check input format for typos." }, "exampleTaxIdFormat": { - "message": "Example $CODE$ format: $EXAMPLE$", + "message": "Exempel $CODE$ format: $EXAMPLE$", "placeholders": { "code": { "content": "$1", From 00dc1702317568276517e7f1afeaef4f6f07974a Mon Sep 17 00:00:00 2001 From: aj-bw <81774843+aj-bw@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:47:19 -0400 Subject: [PATCH 04/38] add shadow package, restore entrypoint functionality (#16124) --- apps/web/Dockerfile | 1 + apps/web/entrypoint.sh | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 7ac2223ab10..6017d60df5f 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -67,6 +67,7 @@ EXPOSE 5000 RUN apk add --no-cache curl \ icu-libs \ + shadow \ && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu # Copy app from the build stage diff --git a/apps/web/entrypoint.sh b/apps/web/entrypoint.sh index 72fd2b43b1d..96bb7773534 100644 --- a/apps/web/entrypoint.sh +++ b/apps/web/entrypoint.sh @@ -22,10 +22,11 @@ fi if [ "$(id -u)" = "0" ]; then # Create user and group - addgroup -g "$LGID" -S "$GROUPNAME" 2>/dev/null || true - adduser -u "$LUID" -G "$GROUPNAME" -S -D -H "$USERNAME" 2>/dev/null || true - mkdir -p /home/$USERNAME - chown $USERNAME:$GROUPNAME /home/$USERNAME + groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 || + groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1 + useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 || + usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 + mkhomedir_helper $USERNAME # The rest... From ebe133f579b298c78d1ea148b9a4cc9ac763377c Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:50:35 +0000 Subject: [PATCH 05/38] Autosync the updated translations (#16159) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/store/locales/ar/copy.resx | 56 +++++++++++----------- apps/browser/store/locales/az/copy.resx | 5 +- apps/browser/store/locales/be/copy.resx | 2 +- apps/browser/store/locales/bn/copy.resx | 2 +- apps/browser/store/locales/bs/copy.resx | 2 +- apps/browser/store/locales/ca/copy.resx | 2 +- apps/browser/store/locales/cs/copy.resx | 2 +- apps/browser/store/locales/cy/copy.resx | 2 +- apps/browser/store/locales/da/copy.resx | 2 +- apps/browser/store/locales/de/copy.resx | 2 +- apps/browser/store/locales/el/copy.resx | 2 +- apps/browser/store/locales/en_GB/copy.resx | 2 +- apps/browser/store/locales/en_IN/copy.resx | 2 +- apps/browser/store/locales/es/copy.resx | 2 +- apps/browser/store/locales/et/copy.resx | 2 +- apps/browser/store/locales/eu/copy.resx | 2 +- apps/browser/store/locales/fil/copy.resx | 2 +- apps/browser/store/locales/gl/copy.resx | 2 +- apps/browser/store/locales/hi/copy.resx | 2 +- apps/browser/store/locales/hr/copy.resx | 2 +- apps/browser/store/locales/hu/copy.resx | 2 +- apps/browser/store/locales/id/copy.resx | 52 ++++++++++---------- apps/browser/store/locales/it/copy.resx | 2 +- apps/browser/store/locales/ja/copy.resx | 53 ++++++++++---------- apps/browser/store/locales/ka/copy.resx | 2 +- apps/browser/store/locales/km/copy.resx | 2 +- apps/browser/store/locales/kn/copy.resx | 2 +- apps/browser/store/locales/ko/copy.resx | 56 +++++++++++----------- apps/browser/store/locales/lt/copy.resx | 2 +- apps/browser/store/locales/ml/copy.resx | 2 +- apps/browser/store/locales/mr/copy.resx | 2 +- apps/browser/store/locales/my/copy.resx | 2 +- apps/browser/store/locales/nb/copy.resx | 2 +- apps/browser/store/locales/ne/copy.resx | 2 +- apps/browser/store/locales/nl/copy.resx | 2 +- apps/browser/store/locales/nn/copy.resx | 2 +- apps/browser/store/locales/or/copy.resx | 2 +- apps/browser/store/locales/pt_BR/copy.resx | 2 +- apps/browser/store/locales/pt_PT/copy.resx | 2 +- apps/browser/store/locales/ro/copy.resx | 2 +- apps/browser/store/locales/si/copy.resx | 2 +- apps/browser/store/locales/sl/copy.resx | 2 +- apps/browser/store/locales/sr/copy.resx | 2 +- apps/browser/store/locales/sv/copy.resx | 2 +- apps/browser/store/locales/te/copy.resx | 2 +- apps/browser/store/locales/th/copy.resx | 2 +- apps/browser/store/locales/tr/copy.resx | 5 +- apps/browser/store/locales/vi/copy.resx | 2 +- apps/browser/store/locales/zh_CN/copy.resx | 53 ++++++++++---------- apps/browser/store/locales/zh_TW/copy.resx | 54 ++++++++++----------- 50 files changed, 209 insertions(+), 209 deletions(-) diff --git a/apps/browser/store/locales/ar/copy.resx b/apps/browser/store/locales/ar/copy.resx index a83bafbf1ae..78246175bef 100644 --- a/apps/browser/store/locales/ar/copy.resx +++ b/apps/browser/store/locales/ar/copy.resx @@ -124,50 +124,48 @@ في المنزل، في العمل، أو في أثناء التنقل، يقوم بتواردن بتأمين جميع كلمات المرور والمعلومات الحساسة بسهولة. - مُعترف به كأفضل مدير كلمات مرور من قِبل PCMag وWIRED وThe Verge وCNET وG2 وغيرها! + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -أمّن حياتك الرقمية +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -أمّن حياتك الرقمية واحمِ بياناتك من الاختراقات بإنشاء كلمات مرور فريدة وقوية وحفظها لكل حساب. احفظ كل شيء في مخزن كلمات مرور مشفّر من البداية إلى النهاية، لا يمكن لأحد الوصول إليه سواك. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -الوصول إلى بياناتك، من أي مكان، وفي أي وقت، وعلى أي جهاز +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -يمكنك بسهولة إدارة كلمات مرور غير محدودة وتخزينها وتأمينها ومشاركتها عبر عدد غير محدود من الأجهزة دون قيود. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -يجب أن يمتلك الجميع الأدوات اللازمة للبقاء آمنًا على الإنترنت +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -استخدم بيتواردن مجانًا دون إعلانات أو بيع بيانات. تؤمن بيتواردن بحق الجميع في البقاء آمنًا على الإنترنت. توفر الباقات المميزة إمكانية الوصول إلى ميزات متقدمة. -عزز قدرات فرقك مع بتواردن -تأتي باقاتنا للفرق والمؤسسات مزودة بميزات احترافية للأعمال. من الأمثلة على ذلك تكامل SSO، والاستضافة الذاتية، وتكامل الدليل، وتوفير SCIM، والسياسات العالمية، والوصول إلى واجهة برمجة التطبيقات، وسجلات الأحداث، والمزيد. +More reasons to choose Bitwarden: -استخدم بيتواردن لتأمين فريق عملك ومشاركة المعلومات الحساسة مع زملائك. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. -أسباب إضافية لاختيار بتواردن: +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -تشفير عالمي المستوى -كلمات المرور محمية بتشفير متقدم من البداية إلى النهاية (AES-256 بت، وهاشتاج مُملح، وPBKDF2 SHA-256) لضمان أمان بياناتك وخصوصيتها. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -عمليات تدقيق خارجية -تُجري بيتواردن بانتظام عمليات تدقيق أمنية شاملة من جهات خارجية بالتعاون مع شركات أمنية مرموقة. تشمل هذه العمليات السنوية تقييمات لشفرة المصدر واختبارات اختراق عبر عناوين IP وخوادم وتطبيقات الويب الخاصة بـبتواردن. +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. -مصادقة ثنائية متقدمة -أمّن تسجيل دخولك باستخدام مُصادق خارجي، أو رموز مُرسلة عبر البريد الإلكتروني، أو بيانات اعتماد FIDO2 WebAuthn مثل مفتاح أمان الأجهزة أو كلمة المرور. +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. -إرسال بتواردن -انقل البيانات مباشرةً إلى الآخرين مع الحفاظ على أمان مشفّر من البداية إلى النهاية والحد من التعرض. +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. -مولد مدمج -أنشئ كلمات مرور طويلة ومعقدة ومميزة وأسماء مستخدمين فريدة لكل موقع تزوره. تكامل مع مزودي أسماء البريد الإلكتروني المستعارة لمزيد من الخصوصية. +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. -ترجمات عالمية -تتوفر ترجمات بتواردن لأكثر من 60 لغة، مترجمة من قِبل المجتمع العالمي عبر Crowdin. - -تطبيقات متعددة المنصات -أمّن بياناتك الحساسة وشاركها داخل مخزن بتواردن من أي متصفح أو جهاز محمول أو نظام تشغيل سطح مكتب، وغير ذلك الكثير. - -يؤمن بتواردن أكثر من مجرد كلمات مرور -تُمكّن حلول إدارة بيانات الاعتماد المشفرة من البداية إلى النهاية من بتواردن المؤسسات من تأمين كل شيء، بما في ذلك أسرار المطورين وتجارب مفاتيح المرور. تفضل بزيارة Bitwarden.com لمعرفة المزيد عن المدير السري لبتواردن وBitwarden Passwordless.dev! +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! diff --git a/apps/browser/store/locales/az/copy.resx b/apps/browser/store/locales/az/copy.resx index 65c416c00d3..6a1a0c9d471 100644 --- a/apps/browser/store/locales/az/copy.resx +++ b/apps/browser/store/locales/az/copy.resx @@ -142,7 +142,7 @@ Komanda və Müəssisələr üçün planlar, professional biznes özəllikləri Bitwarden-i seçmək üçün daha çox səbəb: Dünya səviyyəli şifrələmə -Parollar, qabaqcıl ucdan uca şifrələmə (AES-256 bit, salted hashtag və PBKDF2 SHA-256) ilə qorunur, beləliklə veriləriniz güvəndə və məxfi qalır. +Parollar, qabaqcıl ucdan uca şifrələmə (AES-256 bit, salted hashing və PBKDF2 SHA-256) ilə qorunur, beləliklə veriləriniz güvəndə və məxfi qalır. 3-cü tərəf auditləri Bitwarden, müntəzəm olaraq tanınmış təhlükəsizlik firmaları ilə hərtərəfli üçüncü tərəf təhlükəsizlik auditləri keçirir. Bu illik auditlərə Bitwarden IP-ləri, serverləri və veb tətbiqləri arasında mənbə kodu qiymətləndirmələri və nüfuz testi daxildir. @@ -163,7 +163,8 @@ Bitwarden tərcümələri 60-dan çox dildə mövcuddur, Crowdin vasitəsilə ql İstənilən brauzerdən, mobil cihazdan və ya masaüstü əməliyyat sistemindən və daha çoxundan Bitwarden Seyfinizdəki həssas veriləri güvəndə saxlayın və paylaşın. Bitwarden parollardan daha çox qoruyur -Bitwarden-nin ucdan-uca şifrələnmiş kimlik məlumatlarını idarəetmə həlləri, təşkilatlara hər şeyi, o cümlədən tərtibatçı sirrlərini və keçid açarı təcrübələrini qorumaq gücü verir. Bitwarden Sirr Meneceri və Bitwarden Passwordless.dev haqqında daha ətraflı öyrənmək üçün Bitwarden.com saytını ziyarət edin! +Bitwarden-nin ucdan-uca şifrələnmiş kimlik məlumatlarını idarəetmə həlləri, təşkilatlara hər şeyi, o cümlədən tərtibatçı sirrlərini və keçid açarı təcrübələrini qorumaq gücü verir. Bitwarden Sirr Meneceri və Bitwarden Passwordless.dev haqqında daha ətraflı öyrənmək üçün Bitwarden.com saytını ziyarət edin! + Bitwarden evdə və ya işdə olarkən bütün parol, keçid açarı və həssas məlumatlarınızı asanlıqla qoruyur. diff --git a/apps/browser/store/locales/be/copy.resx b/apps/browser/store/locales/be/copy.resx index 8ac2e86232f..a30db1fd85e 100644 --- a/apps/browser/store/locales/be/copy.resx +++ b/apps/browser/store/locales/be/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/bn/copy.resx b/apps/browser/store/locales/bn/copy.resx index 1bcfb190016..3341a6f10d7 100644 --- a/apps/browser/store/locales/bn/copy.resx +++ b/apps/browser/store/locales/bn/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/bs/copy.resx b/apps/browser/store/locales/bs/copy.resx index 82e4eb1d88e..377b13e7423 100644 --- a/apps/browser/store/locales/bs/copy.resx +++ b/apps/browser/store/locales/bs/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/ca/copy.resx b/apps/browser/store/locales/ca/copy.resx index 5a06f818e3c..b20eed1e87d 100644 --- a/apps/browser/store/locales/ca/copy.resx +++ b/apps/browser/store/locales/ca/copy.resx @@ -144,7 +144,7 @@ Utilitzeu Bitwarden per protegir la vostra força de treball i compartir informa Més raons per a triar Bitwarden: Xifratge de classe mundial -Les contrasenyes estan protegides amb un xifratge avançat d'extrem a extrem (AES-256 bits, hashtag salat i PBKDF2 SHA-256) perquè les vostres dades es mantinguen segures i privades. +Les contrasenyes estan protegides amb un xifratge avançat d'extrem a extrem (AES-256 bits, hashing salat i PBKDF2 SHA-256) perquè les vostres dades es mantinguen segures i privades. Auditories de tercers Bitwarden realitza regularment auditories de seguretat exhaustives de tercers amb empreses de seguretat notables. Aquestes auditories anuals inclouen avaluacions del codi font i proves de penetració a les IP, servidors i aplicacions web de Bitwarden. diff --git a/apps/browser/store/locales/cs/copy.resx b/apps/browser/store/locales/cs/copy.resx index c3a58379dc5..f23315b0d16 100644 --- a/apps/browser/store/locales/cs/copy.resx +++ b/apps/browser/store/locales/cs/copy.resx @@ -144,7 +144,7 @@ Použijte Bitwarden k zabezpečení svých zaměstnanců a sdílení citlivých Další důvody, proč si vybrat Bitwarden: Šifrování na světové úrovni -Hesla jsou chráněna pokročilým end-to-end šifrováním (AES-256 bitů, solený hashtag a PBKDF2 SHA-256), takže Vaše data zůstanou v bezpečí a soukromí. +Hesla jsou chráněna pokročilým end-to-end šifrováním (AES-256 bitů, solený hashing a PBKDF2 SHA-256), takže Vaše data zůstanou v bezpečí a soukromí. Audity třetích stran Společnost Bitwarden pravidelně provádí komplexní bezpečnostní audity třetích stran s významnými bezpečnostními firmami. Tyto každoroční audity zahrnují posouzení zdrojového kódu a penetrační testy napříč IP adresami, servery a webovými aplikacemi společnosti Bitwarden. diff --git a/apps/browser/store/locales/cy/copy.resx b/apps/browser/store/locales/cy/copy.resx index 0cb03a8a291..e6e48ea12a5 100644 --- a/apps/browser/store/locales/cy/copy.resx +++ b/apps/browser/store/locales/cy/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/da/copy.resx b/apps/browser/store/locales/da/copy.resx index 928e54d8edd..38c96b485d2 100644 --- a/apps/browser/store/locales/da/copy.resx +++ b/apps/browser/store/locales/da/copy.resx @@ -144,7 +144,7 @@ Brug Bitwarden til at sikre arbejdsstyrken og dele sensitive oplysninger med kol Flere grunde til at vælge Bitwarden: Kryptering i verdensklasse -Adgangskoder er beskyttet med avanceret ende-til-ende-kryptering (AES-256 bit, saltet hashtag og PBKDF2 SHA-256), så dataene forbliver sikre og private. +Adgangskoder er beskyttet med avanceret ende-til-ende-kryptering (AES-256 bit, saltet hashing og PBKDF2 SHA-256), så dataene forbliver sikre og private. Tredjepartsrevisioner Bitwarden udfører regelmæssigt omfattende tredjeparts sikkerhedsrevisioner med velrenommerede sikkerhedsfirmaer. Disse årlige revisioner inkluderer kildekodevurderinger og penetrationstest på tværs af Bitwarden IP'er, servere og webapplikationer. diff --git a/apps/browser/store/locales/de/copy.resx b/apps/browser/store/locales/de/copy.resx index eb3ab2afd48..014a5b09bc9 100644 --- a/apps/browser/store/locales/de/copy.resx +++ b/apps/browser/store/locales/de/copy.resx @@ -144,7 +144,7 @@ Nutze Bitwarden, um deine Mitarbeiter abzusichern und sensible Informationen mit Weitere Gründe, Bitwarden zu wählen: Weltklasse-Verschlüsselung -Passwörter werden mit fortschrittlicher Ende-zu-Ende-Verschlüsselung (AES-256 bit, salted hashtag und PBKDF2 SHA-256) geschützt, damit deine Daten sicher und geheim bleiben. +Passwörter werden mit fortschrittlicher Ende-zu-Ende-Verschlüsselung (AES-256 bit, salted hashing und PBKDF2 SHA-256) geschützt, damit deine Daten sicher und geheim bleiben. 3rd-Party-Prüfungen Bitwarden führt regelmäßig umfassende Sicherheitsprüfungen durch Dritte von namhaften Sicherheitsfirmen durch. Diese jährlichen Prüfungen umfassen Quellcode-Bewertungen und Penetration-Tests für Bitwarden-IPs, Server und Webanwendungen. diff --git a/apps/browser/store/locales/el/copy.resx b/apps/browser/store/locales/el/copy.resx index 5c1d99de2a2..e464d6d6ee6 100644 --- a/apps/browser/store/locales/el/copy.resx +++ b/apps/browser/store/locales/el/copy.resx @@ -143,7 +143,7 @@ Περισσότεροι λόγοι για να επιλέξετε το Bitwarden: Κρυπτογράφηση Παγκόσμιας Κλάσης -Οι κωδικοί πρόσβασης προστατεύονται με προηγμένη κρυπτογράφηση από άκρο σε άκρο (AES-256 bit, salted hashtag και PBKDF2 SHA-256), ώστε τα δεδομένα σας να παραμένουν ασφαλή και ιδιωτικά. +Οι κωδικοί πρόσβασης προστατεύονται με προηγμένη κρυπτογράφηση από άκρο σε άκρο (AES-256 bit, salted hashing και PBKDF2 SHA-256), ώστε τα δεδομένα σας να παραμένουν ασφαλή και ιδιωτικά. Έλεγχοι από Τρίτους Το Bitwarden διεξάγει τακτικά ολοκληρωμένους ελέγχους ασφαλείας από τρίτους με αξιόλογες εταιρείες ασφαλείας. Αυτοί οι ετήσιοι έλεγχοι περιλαμβάνουν αξιολογήσεις πηγαίου κώδικα και δοκιμές διείσδυσης σε όλες τις IP του Bitwarden, τους διακομιστές και τις εφαρμογές ιστού. diff --git a/apps/browser/store/locales/en_GB/copy.resx b/apps/browser/store/locales/en_GB/copy.resx index 7c408ad8897..b84e733eb6d 100644 --- a/apps/browser/store/locales/en_GB/copy.resx +++ b/apps/browser/store/locales/en_GB/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-Party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/en_IN/copy.resx b/apps/browser/store/locales/en_IN/copy.resx index 31e5d2326f6..935236d7f29 100644 --- a/apps/browser/store/locales/en_IN/copy.resx +++ b/apps/browser/store/locales/en_IN/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/es/copy.resx b/apps/browser/store/locales/es/copy.resx index a17440a0d6b..34a9850168a 100644 --- a/apps/browser/store/locales/es/copy.resx +++ b/apps/browser/store/locales/es/copy.resx @@ -143,7 +143,7 @@ Utiliza Bitwarden para proteger a tu personal y compartir información confidenc Más razones para elegir Bitwarden: Encriptación de clase mundial -Las contraseñas están protegidas con cifrado avanzado de extremo a extremo (AES-256 bits, hashtag salado y PBKDF2 SHA-256) para que tus datos permanezcan seguros y privados. +Las contraseñas están protegidas con cifrado avanzado de extremo a extremo (AES-256 bits, hashing salado y PBKDF2 SHA-256) para que tus datos permanezcan seguros y privados. Auditorías de terceros Bitwarden realiza regularmente auditorías integrales de seguridad de terceros con empresas de seguridad notables. Estas auditorías anuales incluyen evaluaciones de código fuente y pruebas de penetración en las direcciones IP, los servidores y las aplicaciones web de Bitwarden. diff --git a/apps/browser/store/locales/et/copy.resx b/apps/browser/store/locales/et/copy.resx index d2a6be860ce..cfdeb28aadb 100644 --- a/apps/browser/store/locales/et/copy.resx +++ b/apps/browser/store/locales/et/copy.resx @@ -144,7 +144,7 @@ Kasuta Bitwardenit oma töötajate turvamiseks ja tundliku informatsiooni jagami Veel Põhjusi Miks Valida Bitwarden: Maailmatasemel Krüpteering -Paroolid on kaitstud tipptehnoloogilise täieliku krüpteerimisega (AES-256 bit, salted hashtag ja PBKDF2 SHA-256), nii et sinu andmed püsivad privaatsete ja turvalistena. +Paroolid on kaitstud tipptehnoloogilise täieliku krüpteerimisega (AES-256 bit, salted hashing ja PBKDF2 SHA-256), nii et sinu andmed püsivad privaatsete ja turvalistena. Põhjalikud turvatestid kolmandate firmade poolt Bitwarden korraldab regulaarselt põhjalike turvateste tuntud kolmandate firmade poolt. Need igaaastased kontrolltestid sisaldavad kogu koodibaasi ülevaatust ja test-küberrünnakuid Bitwardeni IP-de, serverite ja veebirakenduste vastu. diff --git a/apps/browser/store/locales/eu/copy.resx b/apps/browser/store/locales/eu/copy.resx index e4271e8ae37..b5225827636 100644 --- a/apps/browser/store/locales/eu/copy.resx +++ b/apps/browser/store/locales/eu/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/fil/copy.resx b/apps/browser/store/locales/fil/copy.resx index 0f68a90bfaa..56ddcefbf99 100644 --- a/apps/browser/store/locales/fil/copy.resx +++ b/apps/browser/store/locales/fil/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/gl/copy.resx b/apps/browser/store/locales/gl/copy.resx index feff0fc99e2..49055ff6511 100644 --- a/apps/browser/store/locales/gl/copy.resx +++ b/apps/browser/store/locales/gl/copy.resx @@ -144,7 +144,7 @@ Usa Bitwarden para protexer ao teu persoal e compartir información confidencial Máis razóns para escoller Bitwarden: Cifrado de clase mundial -Os contrasinais están protexidos con cifrado avanzado de extremo a extremo (AES-256 bits, hashtag salgado e PBKDF2 SHA-256) para que os teus datos permanezan seguros e privados. +Os contrasinais están protexidos con cifrado avanzado de extremo a extremo (AES-256 bits, hashing salgado e PBKDF2 SHA-256) para que os teus datos permanezan seguros e privados. Auditorías de terceiros Bitwarden realiza regularmente auditorías de seguridade completas de terceiros con importantes empresas de seguridade. Estas auditorías anuais inclúen avaliacións do código fonte e probas de penetración nas IP, servidores e aplicacións web de Bitwarden. diff --git a/apps/browser/store/locales/hi/copy.resx b/apps/browser/store/locales/hi/copy.resx index 1ea7314d529..e145772e0c1 100644 --- a/apps/browser/store/locales/hi/copy.resx +++ b/apps/browser/store/locales/hi/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/hr/copy.resx b/apps/browser/store/locales/hr/copy.resx index c71315011e6..6a7905ad73e 100644 --- a/apps/browser/store/locales/hr/copy.resx +++ b/apps/browser/store/locales/hr/copy.resx @@ -144,7 +144,7 @@ Koristi Bitwarden da osiguraš svoje zaposlenike i podijeliš osjetljive podatke Više razloga za odabir Bitwardena: Enkripcija svjetske razine -Lozinke su zaštićene naprednom end-to-end enkripcijom (AES-256 bit, salted hashtag i PBKDF2 SHA-256) tako da tvoji podaci ostaju sigurni i privatni. +Lozinke su zaštićene naprednom end-to-end enkripcijom (AES-256 bit, salted hashing i PBKDF2 SHA-256) tako da tvoji podaci ostaju sigurni i privatni. Revizije treće strane Bitwarden je redovito podložan opsežnim sigurnosnim revizijama treće strane s poznatim sigurnosnim tvrtkama. Ove godišnje revizije uključuju procjene izvornog koda i testiranje penetracije preko Bitwarden IP adresa, poslužitelja i web aplikacija. diff --git a/apps/browser/store/locales/hu/copy.resx b/apps/browser/store/locales/hu/copy.resx index 3e6b8e42d46..814ebabaada 100644 --- a/apps/browser/store/locales/hu/copy.resx +++ b/apps/browser/store/locales/hu/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/id/copy.resx b/apps/browser/store/locales/id/copy.resx index 2cd61460ccd..4e382f94861 100644 --- a/apps/browser/store/locales/id/copy.resx +++ b/apps/browser/store/locales/id/copy.resx @@ -124,48 +124,48 @@ Di rumah, di kantor, atau di perjalanan, Bitwarden mengamankan semua kata sandi, kunci sandi, dan informasi sensitif Anda dengan mudah. - Dikenal sebagai pengelola sandi terbaik oleh PCMag, WIRED, The Verge, CNET, G2, dan lainnya! + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -AMANKAN KEHIDUPAN DIGITAL ANDA -Amankan kehidupan digital Anda dan dapatkan perlindungan dari peretasan data dengan membuat dan menyimpan kata sandi yang unik dan kuat untuk setiap akun. Rawat semuanya dalam brankas kata sandi terenkripsi dari ujung-ke-ujung yang hanya bisa diakses oleh Anda. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -AKSES DATA ANDA, DI MANA SAJA, KAPAN SAJA, DI PERANGKAT APA PUN -Kelola, simpan, amankan, dan bagikan tanpa batas dengan mudah kata sandi antar perangkat tak terbatas dan tanpa batasan. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -SETIAP ORANG HARUS MEMILIKI PERALATAN UNTUK TETAP AMAN KETIKA DARING -Gunakan Bitwarden secara gratis tanpa iklan atau menjual data. Bitwarden percaya setiap orang harus memiliki kemampuan untuk tetap aman ketika daring. Paket premium menawarkan akses ke fitur-fitur yang lebih lanjut. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -BERDAYAKAN TIM ANDA DENGAN BITWARDEN -Paket untuk Teams dan Enterprise memiliki kemampuan bisnis profesional, termasuk pemaduan SSO, hosting mandiri, pemaduan direktori dan pembekalan SCIM, kebijakan global, akses API, log kejadian, dan banyak lagi. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Gunakan Bitwarden untuk mengamankan kerja Anda dan membagikan informasi sensitif kepada rekan kerja. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Alasan lebih lanjut untuk memilih Bitwarden: +More reasons to choose Bitwarden: -Enkripsi Kelas Dunia -Semua kata sandi dilindungi dengan enkripsi ujung-ke-ujung yang lebih lanjut (AES-256 bit, tanda pagar bergaram, dan PBKDF2 SHA-256) sehingga data Anda tetap aman dan privat. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. -Audit Pihak Ketiga -Bitwarden secara rutin melakukan audit keamanan yang dilakukan pihak ketiga secara menyeluruh dengan perusahaan keamanan terkemuka. Audit tahunan ini termasuk penilaian sumber kode dan pengujian penembusan antar IP, server, dan aplikasi web Bitwarden. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -2FA Terdepan -Amankan proses masuk Anda dengan pengotentikasi pihak ketiga, kode yang dikirim ke surel, atau pengenal WebAuthn FIDO2 seperti kunci keamanan perangkat keras atau kunci sandi. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. Bitwarden Send -Kirim data ke orang lain secara langsung sembari menjaga keamanan dari ujung-ke-ujung dan membatasi singkapan. +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. -Pembuat Sandi Bawaan -Buat kata sandi yang panjang, rumit, dan berbeda serta nama pengguna unik untuk setiap situs yang Anda kunjungi. Campurkan dengan nama lain surel untuk privasi lebih lanjut. +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. -Terjemahan Global -Terjemahan Bitwarden hadir dalam lebih dari 60 bahasa, diterjemahkan oleh komunitas global melalui Crowdin. +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. -Aplikasi Lintas Platform -Amankan dan bagikan data sensitif dalam Brankas Bitwarden Anda dari peramban apa pun, ponsel, atau sistem operasi desktop, dan lebih banyak lagi. +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. -Bitwarden mengamankan lebih dari sekedar kata sandi -Solusi pengelolaan identitas terenkripsi ujung-ke-ujung dari Bitwarden memberdayakan organisasi untuk mengamankan segalanya, termasuk rahasia pengembang dan kunci sandi. Kunjungi bitwarden.com untuk mempelajari lebih lanjut tentang Pengelola Rahasia Bitwarden dan passwordless.dev Bitwarden! +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! diff --git a/apps/browser/store/locales/it/copy.resx b/apps/browser/store/locales/it/copy.resx index 96801c9d025..e76e78bc4ab 100644 --- a/apps/browser/store/locales/it/copy.resx +++ b/apps/browser/store/locales/it/copy.resx @@ -144,7 +144,7 @@ Utilizza Bitwarden per proteggere i tuoi dipendenti e condividere informazioni s Altri motivi per scegliere Bitwarden: Crittografia di livello mondiale -Le password sono protette con crittografia end-to-end avanzata (AES-256 bit, salted hashtag, e PBKDF2 SHA-256) in modo che i tuoi dati rimangano sicuri e privati. +Le password sono protette con crittografia end-to-end avanzata (AES-256 bit, salted hashing, e PBKDF2 SHA-256) in modo che i tuoi dati rimangano sicuri e privati. Controlli di terze parti Bitwarden conduce regolarmente controlli di sicurezza completi di terze parti con importanti società di sicurezza. Questi controlli annuali includono valutazioni del codice sorgente e test di penetrazione su IP, server, e applicazioni web di Bitwarden. diff --git a/apps/browser/store/locales/ja/copy.resx b/apps/browser/store/locales/ja/copy.resx index 3ecba765d01..c728a6307fd 100644 --- a/apps/browser/store/locales/ja/copy.resx +++ b/apps/browser/store/locales/ja/copy.resx @@ -124,47 +124,48 @@ 自宅、職場、または外出先でも、Bitwarden はすべてのパスワード、パスキー、機密情報を簡単に保護します。 - PCMag、WIRED、The Verge、CNET、G2 などから最高のパスワードマネージャーとして認められています! + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -デジタルライフを守る -データ漏洩を防ぐために、各アカウントに対してユニークで強力なパスワードを生成し保存することで、デジタルライフを守りましょう。エンドツーエンドで暗号化されたパスワード保管庫にすべてを保存し、あなただけがアクセスできます。 +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -どこでも、いつでも、どのデバイスでもデータにアクセス -デバイスやパスワード数の制限は一切なく、簡単に管理、保存、保護、共有できます。 +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -誰もがオンラインで安全を保つためのツールを持つべき -Bitwarden は広告やデータ販売なしに無料で利用できます。Bitwarden は、誰もがオンラインで安全を保つ能力を持つべきだと信じています。プレミアムプランでは高度な機能にアクセスできます。 +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -チームを Bitwarden で強化 -チームおよびエンタープライズ向けのプランには、SSO 統合、セルフホスティング、ディレクトリ統合と SCIM プロビジョニング、グローバルポリシー、API アクセス、イベントログなどのプロフェッショナルなビジネス機能が含まれます。 +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwardenを使用して、従業員を保護し、同僚と機密情報を共有しましょう。 +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Bitwardenを選ぶ理由 -世界クラスの暗号化 -パスワードは、先進的なエンドツーエンド暗号化(AES-256ビット、ソルト付きハッシュタグ、PBKDF2 SHA-256)で保護され、データは安全かつプライベートに保たれます。 +More reasons to choose Bitwarden: -第三者監査 -Bitwarden は、著名なセキュリティ企業による包括的な第三者セキュリティ監査を定期的に実施しています。これらの年次監査には、ソースコードの評価や Bitwarden の IP、サーバー、ウェブアプリケーション全体にわたるペネトレーションテストが含まれます。 +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. -高度な2要素認証 -サードパーティーの認証アプリ、メールコード、またはハードウェアセキュリティキーやパスキーなどの FIDO2 WebAuthn 資格情報でログインを保護します。 +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. + +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. Bitwarden Send -エンドツーエンドで暗号化されたセキュリティを維持しながら、他者にデータを直接送信し、データ漏洩を制限します。 +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. -内蔵ジェネレーター -訪問するすべてのサイトに対して、長く複雑でユニークなパスワードとユーザー名を作成できます。追加のプライバシーのためにメールエイリアスプロバイダーと統合することもできます。 +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. -グローバル翻訳 -Bitwarden の翻訳は、Crowdin を通じてグローバルコミュニティによって翻訳され、60以上の言語に対応しています。 +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. -クロスプラットフォームアプリケーション -任意のブラウザ、モバイルデバイス、デスクトップ OS などから、Bitwarden 保管庫内の機密データを保護し、共有できます。 +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. -Bitwarden はパスワードだけを保護するわけではありません -Bitwarden のエンドツーエンド暗号化された資格情報管理ソリューションは、組織が開発者のシークレットやパスキー体験を含むすべてを保護することを支援します。Bitwarden シークレットマネージャーや Bitwarden Passwordless.dev について詳しくは、Bitwarden.com をご覧ください。 +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! diff --git a/apps/browser/store/locales/ka/copy.resx b/apps/browser/store/locales/ka/copy.resx index 82e4eb1d88e..377b13e7423 100644 --- a/apps/browser/store/locales/ka/copy.resx +++ b/apps/browser/store/locales/ka/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/km/copy.resx b/apps/browser/store/locales/km/copy.resx index 82e4eb1d88e..377b13e7423 100644 --- a/apps/browser/store/locales/km/copy.resx +++ b/apps/browser/store/locales/km/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/kn/copy.resx b/apps/browser/store/locales/kn/copy.resx index f68f2c25dab..09a3576a7a8 100644 --- a/apps/browser/store/locales/kn/copy.resx +++ b/apps/browser/store/locales/kn/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/ko/copy.resx b/apps/browser/store/locales/ko/copy.resx index de6ef1c370d..624ce415994 100644 --- a/apps/browser/store/locales/ko/copy.resx +++ b/apps/browser/store/locales/ko/copy.resx @@ -124,50 +124,48 @@ 집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다. - PCMag, WIRED, The Verge, CNET, G2 등에서 최고의 비밀번호 관리자 선정! + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -디지털 라이프를 안전하게 보호하세요 -모든 계정을 위한 강력하고 고유한 비밀번호를 생성하고 저장하여, 데이터 유출로부터 안전하게 보호하세요. 오직 사용자만 접근할 수 있는 엔드투엔드 방식으로 암호화된 비밀번호 보관함에서 모든 것을 관리하세요. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -어디서든, 언제든, 어떤 기기에서든 접근 가능 -무제한의 비밀번호들을 관리, 저장, 보호, 공유하며 무제한의 기기에서 손쉽게 이용하세요. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -모두가 온라인 안전을 위한 도구를 가져야 합니다. -광고나 데이터 판매 없이 Bitwarden을 무료로 이용하세요. Bitwarden은 모두가 안전한 온라인 환경을 누릴 권리가 있다고 믿습니다. 프리미엄 플랜을 통해 고급 기능도 이용 가능합니다. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden으로 팀을 강화하세요 -팀 및 사업용 플랜은 전문 비즈니스 기능을 제공합니다. 예를 들어 SSO 통합, 자체 호스팅, 디렉토리 통합 및 SCIM 프로비저닝, 글로벌 정책, API 접근, 이벤트 로그 등을 포함합니다. -(SSO: 1회 사용자 인증으로 다수의 앱, 웹에 접근, 인증할 수 있는 통합 로그인 솔루션) +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden을 사용하여 직원을 보호하고 동료들과 민감한 정보를 안전하게 공유하세요. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Bitwarden을 선택해야 하는 이유 +More reasons to choose Bitwarden: -세계적 수준의 암호화 -고급 엔드 투 엔드 암호화(AES-256 비트, 솔팅된 해시 태그, PBKDF2 SHA-256)로 비밀번호를 보호하여 데이터의 보안과 개인 정보를 유지합니다. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. -제3자를 통한 보안 감사 -Bitwarden은 저명한 보안 회사와 함께 정기적인 제3자 보안 감사를 수행합니다. 연례 감사에는 소스 코드 평가와 Bitwarden IP, 서버, 웹 애플리케이션에 대한 침투 테스트가 포함됩니다. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -고급 2단계 인증(2FA) -타사 인증 앱, 이메일 코드 또는 하드웨어 보안 키나 패스키와 같은 FIDO2 WebAuthn 자격 증명을 통해 로그인 보안을 강화하세요. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Bitwarden의 데이터 전송 방식 -엔드투엔드 암호화를 유지하면서 데이터를 직접 다른 사람에게 전송하여 노출을 최소화합니다. +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. -내장된 비밀번호 및 사용자 이름 생성자 -긴, 복잡하고 고유한 비밀번호와 각 사이트에 사용할 고유 사용자 이름을 생성하세요. 이메일명 제공업체와 통합하여 추가적인 개인 정보 보호를 제공합니다. +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. -글로벌 번역 -Bitwarden은 Crowdin 글로벌 커뮤니티를 통해 한국어를 포함한 60개 이상의 언어로 번역되어 있습니다. +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. -크로스 플랫폼 애플리케이션 -모든 브라우저, 모바일 기기, 데스크톱 OS 등 다양한 환경에서 Bitwarden 보관함 내의 중요한 데이터를 안전하게 관리하고 공유하세요. +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. -비밀번호 이상의 보안을 제공합니다 -Bitwarden의 엔드투엔드 암호화된 신용 증명 관리 솔루션은 개발자 비밀과 패스키 경험을 포함한 모든 것을 보호하도록 조직을 지원합니다. -Bitwarden 의 Secrets Manager 및Passwordless.dev에 대해 자세히 알아보려면 Bitwarden.com 을 방문하세요! +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! diff --git a/apps/browser/store/locales/lt/copy.resx b/apps/browser/store/locales/lt/copy.resx index d83c6ca99a5..8a11f6fdf58 100644 --- a/apps/browser/store/locales/lt/copy.resx +++ b/apps/browser/store/locales/lt/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/ml/copy.resx b/apps/browser/store/locales/ml/copy.resx index e22993d5b75..8f308181ef3 100644 --- a/apps/browser/store/locales/ml/copy.resx +++ b/apps/browser/store/locales/ml/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/mr/copy.resx b/apps/browser/store/locales/mr/copy.resx index 82e4eb1d88e..377b13e7423 100644 --- a/apps/browser/store/locales/mr/copy.resx +++ b/apps/browser/store/locales/mr/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/my/copy.resx b/apps/browser/store/locales/my/copy.resx index 82e4eb1d88e..377b13e7423 100644 --- a/apps/browser/store/locales/my/copy.resx +++ b/apps/browser/store/locales/my/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/nb/copy.resx b/apps/browser/store/locales/nb/copy.resx index 29d612906c7..a3d89e7bdc2 100644 --- a/apps/browser/store/locales/nb/copy.resx +++ b/apps/browser/store/locales/nb/copy.resx @@ -144,7 +144,7 @@ Bruk Bitwarden til å sikre arbeidsstyrken din og dele sensitiv informasjon med Flere grunner til å velge Bitwarden: Kryptering i verdensklasse -Passord er beskyttet med avansert ende-til-ende-kryptering (AES-256 bit, saltet hashtag og PBKDF2 SHA-256) slik at dataene dine forblir sikre og private. +Passord er beskyttet med avansert ende-til-ende-kryptering (AES-256 bit, saltet hashing og PBKDF2 SHA-256) slik at dataene dine forblir sikre og private. Tredjepartsrevisjoner Bitwarden gjennomfører regelmessig omfattende tredjeparts sikkerhetsrevisjoner med bemerkelsesverdige sikkerhetsfirmaer. Disse årlige revisjonene inkluderer kildekodevurderinger og penetrasjonstesting på tvers av Bitwarden IP-er, servere og webapplikasjoner. diff --git a/apps/browser/store/locales/ne/copy.resx b/apps/browser/store/locales/ne/copy.resx index 82e4eb1d88e..377b13e7423 100644 --- a/apps/browser/store/locales/ne/copy.resx +++ b/apps/browser/store/locales/ne/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/nl/copy.resx b/apps/browser/store/locales/nl/copy.resx index 17720f54f4c..fb9313e0c63 100644 --- a/apps/browser/store/locales/nl/copy.resx +++ b/apps/browser/store/locales/nl/copy.resx @@ -144,7 +144,7 @@ Gebruik Bitwarden om je medewerkers te beveiligen en gevoelige informatie te del Meer redenen om voor Bitwarden te kiezen: Encryptie van wereldklasse -Wachtwoorden worden beschermd met geavanceerde end-to-end versleuteling (AES-256 bit, salted hashtag en PBKDF2 SHA-256) zodat je gegevens veilig en privé blijven. +Wachtwoorden worden beschermd met geavanceerde end-to-end versleuteling (AES-256 bit, salted hashing en PBKDF2 SHA-256) zodat je gegevens veilig en privé blijven. Audits door derde partijen Bitwarden voert regelmatig uitgebreide beveiligingsaudits uit bij gerenommeerde beveiligingsbedrijven. Deze jaarlijkse audits omvatten broncodebeoordelingen en penetratietests voor Bitwarden IP's, servers en webapplicaties. diff --git a/apps/browser/store/locales/nn/copy.resx b/apps/browser/store/locales/nn/copy.resx index 82e4eb1d88e..377b13e7423 100644 --- a/apps/browser/store/locales/nn/copy.resx +++ b/apps/browser/store/locales/nn/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/or/copy.resx b/apps/browser/store/locales/or/copy.resx index 82e4eb1d88e..377b13e7423 100644 --- a/apps/browser/store/locales/or/copy.resx +++ b/apps/browser/store/locales/or/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/pt_BR/copy.resx b/apps/browser/store/locales/pt_BR/copy.resx index 266f4008523..edf2351c92d 100644 --- a/apps/browser/store/locales/pt_BR/copy.resx +++ b/apps/browser/store/locales/pt_BR/copy.resx @@ -143,7 +143,7 @@ Utilize o Bitwarden para proteger os seus colaboradores e compartilhar informaç Mais razões para escolher Bitwarden: Criptografia mundialmente reconhecida -Suas senhas são protegidas com avançada criptografia ponta a ponta (AES-256, salted hashtag, e PBKDF2 SHA-256) para que os seus dados estejam seguros e privados. +Suas senhas são protegidas com avançada criptografia ponta a ponta (AES-256, salted hashing, e PBKDF2 SHA-256) para que os seus dados estejam seguros e privados. Auditoria de Terceiros A Bitwarden regularmente conduz auditorias de terceiros com notáveis empresas de segurança. Essas auditorias anuais incluem qualificação do código fonte e testes de invasão contra os IPs da Bitwarden, servidores e aplicações web. diff --git a/apps/browser/store/locales/pt_PT/copy.resx b/apps/browser/store/locales/pt_PT/copy.resx index fa62a0f1d5c..ab4410a1550 100644 --- a/apps/browser/store/locales/pt_PT/copy.resx +++ b/apps/browser/store/locales/pt_PT/copy.resx @@ -144,7 +144,7 @@ Utilize o Bitwarden para proteger a sua equipa de trabalho e partilhar informaç Mais motivos para escolher o Bitwarden: Encriptação de classe mundial -As palavras-passe são protegidas com encriptação avançada ponto a ponto (AES-256 bits, salted hashtag e PBKDF2 SHA-256) para que os seus dados permaneçam seguros e privados. +As palavras-passe são protegidas com encriptação avançada ponto a ponto (AES-256 bits, salted hashing e PBKDF2 SHA-256) para que os seus dados permaneçam seguros e privados. Auditorias de terceiros O Bitwarden realiza regularmente auditorias abrangentes de segurança de terceiros com empresas de segurança notáveis. Estas auditorias anuais incluem avaliações de código-fonte e testes de penetração em IPs, servidores e aplicações Web do Bitwarden. diff --git a/apps/browser/store/locales/ro/copy.resx b/apps/browser/store/locales/ro/copy.resx index 7b0070fad22..f81094df81a 100644 --- a/apps/browser/store/locales/ro/copy.resx +++ b/apps/browser/store/locales/ro/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/si/copy.resx b/apps/browser/store/locales/si/copy.resx index 82e4eb1d88e..377b13e7423 100644 --- a/apps/browser/store/locales/si/copy.resx +++ b/apps/browser/store/locales/si/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/sl/copy.resx b/apps/browser/store/locales/sl/copy.resx index 80886de48ab..b2a95ed5689 100644 --- a/apps/browser/store/locales/sl/copy.resx +++ b/apps/browser/store/locales/sl/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/sr/copy.resx b/apps/browser/store/locales/sr/copy.resx index 2e737523856..6902ca57abf 100644 --- a/apps/browser/store/locales/sr/copy.resx +++ b/apps/browser/store/locales/sr/copy.resx @@ -144,7 +144,7 @@ Додатни разлози да изаберете Bitwarden: Енкрипција светске класе -Лозинке су заштићене са напредном потпуном енкрипцијом (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) тако да ваши подаци остају сигурни и приватни. +Лозинке су заштићене са напредном потпуном енкрипцијом (AES-256 bit, salted hashing, and PBKDF2 SHA-256) тако да ваши подаци остају сигурни и приватни. Ревизије треће стране Bitwarden редовно спроводи опсежне безбедносне ревизије трећих страна заједно са препознатим безбедносним фирмама. Ове годишње ревизије укључују процене изворног кода и тестирање пробојности Bitwarden-ових ИП адреса, сервера и веб апликација. diff --git a/apps/browser/store/locales/sv/copy.resx b/apps/browser/store/locales/sv/copy.resx index 8f3564f30c3..410bf890b16 100644 --- a/apps/browser/store/locales/sv/copy.resx +++ b/apps/browser/store/locales/sv/copy.resx @@ -144,7 +144,7 @@ Använd Bitwarden för att säkra din personalstyrka och dela känslig informati Fler anledningar att välja Bitwarden: Kryptering i världsklass -Lösenord skyddas med avancerad end-to-end-kryptering (AES-256 bit, salted hashtag och PBKDF2 SHA-256) så att dina data förblir säkra och privata. +Lösenord skyddas med avancerad end-to-end-kryptering (AES-256 bit, salted hashing och PBKDF2 SHA-256) så att dina data förblir säkra och privata. revisioner från tredje part Bitwarden genomför regelbundet omfattande tredjeparts säkerhetsrevisioner med välkända säkerhetsföretag. Dessa årliga revisioner inkluderar källkodsbedömningar och penetrationstestning över Bitwardens IP-adresser, servrar och webbapplikationer. diff --git a/apps/browser/store/locales/te/copy.resx b/apps/browser/store/locales/te/copy.resx index 82e4eb1d88e..377b13e7423 100644 --- a/apps/browser/store/locales/te/copy.resx +++ b/apps/browser/store/locales/te/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/th/copy.resx b/apps/browser/store/locales/th/copy.resx index f6b0c4b22ab..1473e24d92e 100644 --- a/apps/browser/store/locales/th/copy.resx +++ b/apps/browser/store/locales/th/copy.resx @@ -144,7 +144,7 @@ Use Bitwarden to secure your workforce and share sensitive information with coll More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. 3rd-party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. diff --git a/apps/browser/store/locales/tr/copy.resx b/apps/browser/store/locales/tr/copy.resx index 6afe10ff32a..3585e50e2ac 100644 --- a/apps/browser/store/locales/tr/copy.resx +++ b/apps/browser/store/locales/tr/copy.resx @@ -143,7 +143,7 @@ Bitwarden'ı kullanarak çalışanlarınızı güvence altına alın ve hassas b Bitwarden'ı seçmek için daha fazla neden: Dünya Standartlarında Şifreleme -Şifreler, gelişmiş uçtan uca şifreleme (AES-256 bit, tuzlu hashtag ve PBKDF2 SHA-256) ile korunur, böylece verileriniz güvenli ve gizli kalır. +Şifreler, gelişmiş uçtan uca şifreleme (AES-256 bit, tuzlu hashing ve PBKDF2 SHA-256) ile korunur, böylece verileriniz güvenli ve gizli kalır. Üçüncü Taraf Denetimleri Bitwarden, tanınmış güvenlik firmalarıyla düzenli olarak kapsamlı üçüncü taraf güvenlik denetimleri gerçekleştirir. Bu yıllık denetimler, Bitwarden IP'leri, sunucuları ve web uygulamaları genelinde kaynak kodu değerlendirmeleri ve sızma testlerini içerir. @@ -164,7 +164,8 @@ Bitwarden çevirileri, Crowdin aracılığıyla küresel topluluk tarafından 60 Bitwarden Vault'unuzdaki hassas verileri herhangi bir tarayıcı, mobil cihaz, masaüstü işletim sistemi ve daha fazlasından güvenli bir şekilde paylaşın. Bitwarden, şifrelerden daha fazlasını güvence altına alır -Bitwarden'ın uçtan uca şifrelenmiş kimlik bilgisi yönetimi çözümleri, kuruluşların geliştirici sırları ve anahtar deneyimleri dahil her şeyi güvence altına almasını sağlar. Bitwarden Secrets Manager ve Bitwarden Passwordless.dev hakkında daha fazla bilgi edinmek için Bitwarden.com adresini ziyaret edin! +Bitwarden'ın uçtan uca şifrelenmiş kimlik bilgisi yönetimi çözümleri, kuruluşların geliştirici sırları ve anahtar deneyimleri dahil her şeyi güvence altına almasını sağlar. Bitwarden Secrets Manager ve Bitwarden Passwordless.dev hakkında daha fazla bilgi edinmek için Bitwarden.com adresini ziyaret edin! + İster evde ister işte veya yolda olun; Bitwarden tüm parolalarınızı, geçiş anahtarlarınızı ve hassas bilgilerinizi güvenle saklar. diff --git a/apps/browser/store/locales/vi/copy.resx b/apps/browser/store/locales/vi/copy.resx index dd3e258a7c7..48c8f483050 100644 --- a/apps/browser/store/locales/vi/copy.resx +++ b/apps/browser/store/locales/vi/copy.resx @@ -144,7 +144,7 @@ Sử dụng Bitwarden để bảo vệ nhân viên của bạn và chia sẻ th Thêm những lý do để chọn Bitwarden: Mã hóa hàng đầu thế giới -Mật khẩu được bảo vệ bằng mã hóa end-to-end tiên tiến (AES-256 bit, salted hashtag và PBKDF2 SHA-256) để dữ liệu của bạn luôn an toàn và riêng tư. +Mật khẩu được bảo vệ bằng mã hóa end-to-end tiên tiến (AES-256 bit, salted hashing và PBKDF2 SHA-256) để dữ liệu của bạn luôn an toàn và riêng tư. Kiểm toán bảo mật bởi bên thứ ba Bitwarden thường xuyên thực hiện các cuộc kiểm toán bảo mật toàn diện bởi các công ty bảo mật uy tín. Các cuộc kiểm toán hàng năm này bao gồm đánh giá mã nguồn và thử nghiệm xâm nhập trên các tài sản trí tuệ (IP), máy chủ và ứng dụng web của Bitwarden. diff --git a/apps/browser/store/locales/zh_CN/copy.resx b/apps/browser/store/locales/zh_CN/copy.resx index c66c6fdb411..dd611f4ca68 100644 --- a/apps/browser/store/locales/zh_CN/copy.resx +++ b/apps/browser/store/locales/zh_CN/copy.resx @@ -124,48 +124,49 @@ 无论是在家中、工作中还是在旅途中,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。 - 被 PCMag、WIRED、The Verge、CNET、G2 等评为最佳的密码管理器! + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -保护您的数字生活 -通过为每个账户生成并保存唯一而强大的密码,保护您的数字生活并防止数据泄露。所有内容保存在只有您可以访问的端对端加密的密码库中。 +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -随时随地在任何设备上访问您的数据 -不受任何限制,跨无限数量的设备,轻松地管理、存储、保护和分享不限数量的密码。 +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -每个人都应该拥有的保持在线安全的工具 -使用 Bitwarden 是免费的,没有广告,不会出售数据。Bitwarden 相信每个人都应该拥有保持在线安全的能力。高级计划提供了对高级功能的访问。 +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -使用 Bitwarden 为您的团队提供支持 -团队计划和企业计划具有专业的商业功能。例如 SSO 集成、自托管、目录集成,以及 SCIM 配置、全局策略、API 访问、事件日志等。 +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -使用 Bitwarden 保护您的劳动成果,并与同事共享敏感信息。 +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -选择 Bitwarden 的更多理由: +More reasons to choose Bitwarden: -世界级加密 -密码受到先进的端对端加密(AES-256 位、加盐哈希标签和 PBKDF2 SHA-256)保护,使您的数据保持安全和私密。 +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. -第三方审计 -Bitwarden 定期与知名的安全公司进行全面的第三方安全审计。这些年度审计包括对 Bitwarden IP、服务器和 Web 应用程序的源代码评估和渗透测试。 +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -高级 2FA -使用第三方验证器、电子邮件代码或 FIDO2 WebAuthn 凭据(例如硬件安全密钥或通行密钥)保护您的登录。 +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. Bitwarden Send -直接传输数据给他人,同时保持端对端加密的安全性并防止暴露。 +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. -内置生成器 -为您访问的每个网站创建足够长、足够复杂且唯一的密码和用户名。与电子邮件别名提供商集成,增加隐私保护。 +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. -全球翻译 -Bitwarden 的翻译涵盖 60 多种语言,由全球社区使用 Crowdin 翻译。 +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. -跨平台应用程序 -从任何浏览器、移动设备或桌面操作系统中安全地访问和共享 Bitwarden 密码库中的敏感数据。 +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. -Bitwarden 保护的不仅仅是密码 -Bitwarden 的端对端加密凭据管理解决方案使组织能够保护所有内容,包括开发人员机密和通行密钥体验。访问 Bitwarden.com 进一步了解 Bitwarden Secrets Manager 和 Bitwarden Passwordless.dev! +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + 无论是在家中、工作中还是在旅途中,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。 diff --git a/apps/browser/store/locales/zh_TW/copy.resx b/apps/browser/store/locales/zh_TW/copy.resx index eaac9ee8691..ad3f12ae6f0 100644 --- a/apps/browser/store/locales/zh_TW/copy.resx +++ b/apps/browser/store/locales/zh_TW/copy.resx @@ -124,48 +124,48 @@ 無論在家、在辦公或在途中,Bitwarden 都能輕易的保護你的密碼、登入金鑰和敏感資訊。 - 被 PCMag、WIRED、The Verge、CNET、G2 等認可為最佳的密碼管理器! + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -保護您的數位生活 -透過為每個帳戶產生並保存唯一的強密碼,保護您的數位生活並防止資料外洩。將所有內容保存在只有您可以存取的端對端加密密碼庫中。 +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -隨時隨地在任何裝置上存取您的數據 -不受限制地跨裝置輕鬆管理、儲存、保護和分享無限多的密碼。 +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -每個人都應該擁有保持上網安全的工具 -免費使用 Bitwarden,沒有廣告或銷售資料。Bitwarden 認為每個人都應該有能力確保上網安全。進階版計劃提供對進階功能的存取。 +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -透過 BITWARDEN 強化您的團隊 -團隊和企業計劃具有專業的商業功能。包括 SSO 整合、自架服務、目錄整合和 SCIM 配置、全域政策、API 存取、事件日誌等。 +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -使用 Bitwarden 來保護您的員工並與同事分享敏感資訊。 +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -選擇 Bitwarden 的其他理由: +More reasons to choose Bitwarden: -世界級的加密 -密碼受到進階端對端加密(AES-256 位元、加鹽標籤和 PBKDF2 SHA-256)的保護,因此您的資料保持安全和私密。 +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. -第三方審核 -Bitwarden 定期與知名資安公司進行全面的第三方安全稽核。這些年度稽核包括 Bitwarden IP、伺服器和 Web 應用程式的原始程式碼評估和滲透測試。 +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -進階的雙因子驗證 -使用第三方驗證器、透過電子郵件傳送的代碼或 FIDO2 WebAuthn 憑證(例如硬體安全金鑰或密碼)來保護您的登入。 +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Bitwarden 傳送 -直接將資料傳輸給其他人,同時保持端對端加密安全性並限制暴露。 +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. -內建產生器 -為您造訪的每個網站建立長、複雜且獨特的密碼和唯一的使用者名稱。與電子郵件別名提供者整合以獲得額外的隱私。 +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. -全球翻譯 -Bitwarden 有 60 多種語言翻譯,由全球社群透過 Crowdin 翻譯。 +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. -跨平台應用程式 -透過任何瀏覽器、行動裝置或桌面作業系統等保護和共用 Bitwarden 密碼庫中的敏感資料。 +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. -Bitwarden 保護的不僅是密碼 -Bitwarden 的端對端加密憑證管理解決方案可讓組織保護一切,包括開發人員機密和金鑰體驗。請造訪 Bitwarden.com 以了解更多有關 Bitwarden 機密管理員和 Bitwarden Passwordless.dev 的資訊! +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! From cfa8615c289982ef992940a7f2ad4beeb2761777 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:51:31 +0000 Subject: [PATCH 06/38] Autosync the updated translations (#16158) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/he/messages.json | 182 +++++++++++----------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 7f9cb69ce00..bb5377fd12b 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -244,13 +244,13 @@ "message": "סוכן ה־SSH הוא שירות המיועד למפתחים שמאפשר לך לחתום בקשות SSH היישר מכספת ה־Bitwarden שלך." }, "sshAgentPromptBehavior": { - "message": "Ask for authorization when using SSH agent" + "message": "בקשת אישור בעת שימוש בסוכן SSH" }, "sshAgentPromptBehaviorDesc": { - "message": "Choose how to handle SSH-agent authorization requests." + "message": "בחר איך לטפל בבקשות אישור של סוכן SSH." }, "sshAgentPromptBehaviorHelp": { - "message": "Remember SSH authorizations" + "message": "זכור בקשות אישור של SSH" }, "sshAgentPromptBehaviorAlways": { "message": "תמיד" @@ -259,7 +259,7 @@ "message": "אף פעם" }, "sshAgentPromptBehaviorRememberUntilLock": { - "message": "Remember until vault is locked" + "message": "זכור עד שהכספת נעולה" }, "premiumRequired": { "message": "נדרש חשבון פרימיום" @@ -718,7 +718,7 @@ "message": "גודל הקובץ המירבי הוא 500 מגה." }, "legacyEncryptionUnsupported": { - "message": "Legacy encryption is no longer supported. Please contact support to recover your account." + "message": "הצפנה מהדור הקודם אינה נתמכת עוד. אנא צור קשר עם צוות התמיכה כדי לשחזר את חשבונך." }, "editedFolder": { "message": "תיקייה שנשמרה" @@ -1749,7 +1749,7 @@ "message": "מוגבל חשבון" }, "restrictCardTypeImport": { - "message": "Cannot import card item types" + "message": "לא ניתן לייבא סוגי פריטי כרטיסים" }, "restrictCardTypeImportDesc": { "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." @@ -2534,7 +2534,7 @@ } }, "vaultTimeoutPolicyMaximumError": { - "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "message": "פסק זמן חורג את ההגבלה שהוגדרה על ידי הארגון שלך: $HOURS$ שעות ו־$MINUTES$ דקות לכל היותר", "placeholders": { "hours": { "content": "$1", @@ -2574,10 +2574,10 @@ "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." }, "organizationName": { - "message": "Organization name" + "message": "שם הארגון" }, "keyConnectorDomain": { - "message": "Key Connector domain" + "message": "הדומיין של Key Connector" }, "leaveOrganization": { "message": "עזוב ארגון" @@ -2640,7 +2640,7 @@ } }, "exportingIndividualVaultWithAttachmentsDescription": { - "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "message": "רק פריטי הכספת האישיים, כולל קבצים מצורפים המשוייכים עם $EMAIL$ ייוצאו. פריטי כספת ארגוניים לא ייכללו", "placeholders": { "email": { "content": "$1", @@ -2689,7 +2689,7 @@ "message": "צור דוא\"ל" }, "usernameGenerator": { - "message": "Username generator" + "message": "מחולל שם משתמש" }, "generatePassword": { "message": "צור סיסמה" @@ -2698,16 +2698,16 @@ "message": "צור ביטוי סיסמה" }, "passwordGenerated": { - "message": "Password generated" + "message": "נוצרה סיסמה" }, "passphraseGenerated": { - "message": "Passphrase generated" + "message": "נוצר ביטוי סיסמה" }, "usernameGenerated": { - "message": "Username generated" + "message": "נוצר שם משתמש" }, "emailGenerated": { - "message": "Email generated" + "message": "נוצר דוא\"ל" }, "spinboxBoundariesHint": { "message": "הערך חייב להיות בין $MIN$ ל־$MAX$.", @@ -2766,7 +2766,7 @@ "message": "השתמש בסיסמה זו" }, "useThisPassphrase": { - "message": "Use this passphrase" + "message": "השתמש בביטוי סיסמא זה" }, "useThisUsername": { "message": "השתמש בשם משתמש זה" @@ -3037,7 +3037,7 @@ } }, "loginRequestApprovedForEmailOnDevice": { - "message": "Login request approved for $EMAIL$ on $DEVICE$", + "message": "בקשת הכניסה אושרה עבור $EMAIL$ ב־$DEVICE$", "placeholders": { "email": { "content": "$1", @@ -3050,21 +3050,21 @@ } }, "youDeniedLoginAttemptFromAnotherDevice": { - "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + "message": "דחית ניסיון כניסה ממכשיר אחר. אם זה היית אתה, נסה להיכנס עם המכשיר שוב." }, "webApp": { - "message": "Web app" + "message": "אפליקציית אינטרנט" }, "mobile": { - "message": "Mobile", + "message": "נייד", "description": "Mobile app" }, "extension": { - "message": "Extension", + "message": "הרחבה", "description": "Browser extension/addon" }, "desktop": { - "message": "Desktop", + "message": "שולחן עבודה", "description": "Desktop app" }, "cli": { @@ -3075,10 +3075,10 @@ "description": "Software Development Kit" }, "server": { - "message": "Server" + "message": "שרת" }, "loginRequest": { - "message": "Login request" + "message": "בקשת כניסה" }, "deviceType": { "message": "סוג מכשיר" @@ -3222,7 +3222,7 @@ "message": "בקש אישור מנהל" }, "unableToCompleteLogin": { - "message": "Unable to complete login" + "message": "לא ניתן להשלים את ההתחברות" }, "loginOnTrustedDeviceOrAskAdminToAssignPassword": { "message": "You need to log in on a trusted device or ask your administrator to assign you a password." @@ -3268,16 +3268,16 @@ "message": "מכשיר מהימן" }, "trustOrganization": { - "message": "Trust organization" + "message": "סמוך על הארגון" }, "trust": { - "message": "Trust" + "message": "סמוך" }, "doNotTrust": { - "message": "Do not trust" + "message": "אל תסמוך" }, "organizationNotTrusted": { - "message": "Organization is not trusted" + "message": "הארגון אינו אמין" }, "emergencyAccessTrustWarning": { "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" @@ -3289,7 +3289,7 @@ "message": "This organization has an Enterprise policy that will enroll you in account recovery. Enrollment will allow organization administrators to change your password. Only proceed if you recognize this organization and the fingerprint phrase displayed below matches the organization's fingerprint." }, "trustUser": { - "message": "Trust user" + "message": "סמוך על המשתמש" }, "inputRequired": { "message": "נדרש קלט." @@ -3492,10 +3492,10 @@ "message": "בחר אוסף" }, "importTargetHintCollection": { - "message": "Select this option if you want the imported file contents moved to a collection" + "message": "בחר באפשרות זו אם ברצונך להעביר את תוכן הקובץ המיובא לאוסף" }, "importTargetHintFolder": { - "message": "Select this option if you want the imported file contents moved to a folder" + "message": "בחר באפשרות זו אם ברצונך להעביר את תוכן הקובץ המיובא לתיקייה" }, "importUnassignedItemsError": { "message": "קובץ מכיל פריטים לא מוקצים." @@ -3627,15 +3627,15 @@ "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" }, "uriMatchWarningDialogLink": { - "message": "More about match detection", + "message": "עוד על זיהוי התאמה", "description": "Link to match detection docs on warning dialog for advance match strategy" }, "uriAdvancedOption": { - "message": "Advanced options", + "message": "אפשרויות מתקדמות", "description": "Advanced option placeholder for uri option component" }, "warningCapitalized": { - "message": "Warning", + "message": "אזהרה", "description": "Warning (should maintain locale-relevant capitalization)" }, "success": { @@ -3709,14 +3709,14 @@ "message": "לא נמצאו יציאות פנויות עבור כניסת ה־sso." }, "securePasswordGenerated": { - "message": "Secure password generated! Don't forget to also update your password on the website." + "message": "סיסמה מאובטחת נוצרה! אל תשכח גם לעדכן את הסיסמה שלך באתר האינטרנט." }, "useGeneratorHelpTextPartOne": { - "message": "Use the generator", + "message": "השתמש במחולל", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { - "message": "to create a strong unique password", + "message": "כדי ליצור סיסמה חזקה וייחודית", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "biometricsStatusHelptextUnlockNeeded": { @@ -3744,25 +3744,25 @@ "message": "ביטול נעילה ביומטרי אינו זמין כעת מסיבה לא ידועה." }, "itemDetails": { - "message": "Item details" + "message": "פרטי הפריט" }, "itemName": { - "message": "Item name" + "message": "שם הפריט" }, "loginCredentials": { - "message": "Login credentials" + "message": "אישורי כניסה" }, "additionalOptions": { - "message": "Additional options" + "message": "אפשרויות נוספות" }, "itemHistory": { - "message": "Item history" + "message": "היסטוריית פריט" }, "lastEdited": { - "message": "Last edited" + "message": "נערך לאחרונה" }, "upload": { - "message": "Upload" + "message": "העלה" }, "authorize": { "message": "אשר" @@ -3834,7 +3834,7 @@ "message": "שנה סיסמה בסיכון" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "אינך יכול להסיר אוספים עם הרשאות צפייה בלבד: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3843,114 +3843,114 @@ } }, "move": { - "message": "Move" + "message": "העבר" }, "newFolder": { - "message": "New folder" + "message": "תיקייה חדשה" }, "folderName": { - "message": "Folder Name" + "message": "שם התיקייה" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "צור תיקייה מקוננת על ידי הוספת שם תיקיית האב ואחריו “/”. דוגמה: חברתי/פורומים" }, "sendsTitleNoItems": { - "message": "Send sensitive information safely", + "message": "שלח מידע רגיש באופן בטוח", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsBodyNoItems": { - "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", + "message": "שתף קבצים ונתונים באופן מאובטח עם כל אחד, בכל פלטפורמה. המידע שלך יישאר מוצפן מקצה־לקצה תוך הגבלת חשיפה.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "generatorNudgeTitle": { - "message": "Quickly create passwords" + "message": "צור סיסמאות במהירות" }, "generatorNudgeBodyOne": { - "message": "Easily create strong and unique passwords by clicking on", + "message": "צור סיסמאות חזקות וייחודיות בקלות על ידי לחיצה על", "description": "Two part message", "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyTwo": { - "message": "to help you keep your logins secure.", + "message": "כדי לעזור לך לשמור על אבטחת הכניסות שלך.", "description": "Two part message", "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyAria": { - "message": "Easily create strong and unique passwords by clicking on the Generate password button to help you keep your logins secure.", + "message": "צור סיסמאות חזקות וייחודיות בקלות על ידי לחיצה על כפתור \"צור סיסמא\" כדי לעזור לך לשמור על אבטחת הכניסות שלך.", "description": "Aria label for the body content of the generator nudge" }, "newLoginNudgeTitle": { - "message": "Save time with autofill" + "message": "חסוך זמן עם מילוי אוטומטי" }, "newLoginNudgeBodyOne": { - "message": "Include a", + "message": "כלול", "description": "This is in multiple parts to allow for bold text in the middle of the sentence.", "example": "Include a Website so this login appears as an autofill suggestion." }, "newLoginNudgeBodyBold": { - "message": "Website", + "message": "אתר אינטרנט", "description": "This is in multiple parts to allow for bold text in the middle of the sentence.", "example": "Include a Website so this login appears as an autofill suggestion." }, "newLoginNudgeBodyTwo": { - "message": "so this login appears as an autofill suggestion.", + "message": "כדי שהכניסה הזו תופיע כהצעת מילוי אוטומטי.", "description": "This is in multiple parts to allow for bold text in the middle of the sentence.", "example": "Include a Website so this login appears as an autofill suggestion." }, "newCardNudgeTitle": { - "message": "Seamless online checkout" + "message": "תשלום מקוון חלק" }, "newCardNudgeBody": { - "message": "With cards, easily autofill payment forms securely and accurately." + "message": "עם כרטיסים, ניתן למלא טפסי תשלום באופן אוטומטי בצורה מאובטחת ומדויקת." }, "newIdentityNudgeTitle": { - "message": "Simplify creating accounts" + "message": "יצירת חשבונות בקלות" }, "newIdentityNudgeBody": { - "message": "With identities, quickly autofill long registration or contact forms." + "message": "עם זהויות, ניתן למלא באופן אוטומטי ובמהירות טפסי הרשמה או יצירת קשר ארוכים." }, "newNoteNudgeTitle": { - "message": "Keep your sensitive data safe" + "message": "שמור על הנתונים הרגישים שלך בטוחים" }, "newNoteNudgeBody": { - "message": "With notes, securely store sensitive data like banking or insurance details." + "message": "עם הערות, ניתן לאחסן בצורה מאובטחת נתונים רגישים כמו פרטי בנק או ביטוח." }, "newSshNudgeTitle": { - "message": "Developer-friendly SSH access" + "message": "גישת SSH ידידותית למפתחים" }, "newSshNudgeBodyOne": { - "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication.", + "message": "אחסן את המפתחות שלך והתחבר לסוכן SSH בשביל אימות מהיר ומוצפן.", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "newSshNudgeBodyTwo": { - "message": "Learn more about SSH agent", + "message": "למד עוד על סוכן SSH", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "assignToCollections": { - "message": "Assign to collections" + "message": "הקצה לאוספים" }, "assignToTheseCollections": { - "message": "Assign to these collections" + "message": "הקצה לאוספים אלה" }, "bulkCollectionAssignmentDialogDescriptionSingular": { - "message": "Only organization members with access to these collections will be able to see the item." + "message": "רק חברי ארגון עם גישה לאוספים אלה יוכלו לראות את הפריט." }, "bulkCollectionAssignmentDialogDescriptionPlural": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "רק חברי ארגון עם גישה לאוספים אלה יוכלו לראות את הפריטים." }, "noCollectionsAssigned": { - "message": "No collections have been assigned" + "message": "לא הוקצו אוספים" }, "assign": { - "message": "Assign" + "message": "הקצה" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "רק חברי ארגון עם גישה לאוספים אלה יוכלו לראות את הפריטים." }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "בחרת ב־$TOTAL_COUNT$ פריטים. אינך יכול לעדכן $READONLY_COUNT$ מהפריטים בגלל שאין לך הרשאות עריכה.", "placeholders": { "total_count": { "content": "$1", @@ -3962,10 +3962,10 @@ } }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "בחר אוספים להקצות" }, "personalItemsTransferWarning": { - "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ יועברו לצמיתות לארגון הנבחר. לא תהיה יותר הבעלים של הפריטים האלה.", "placeholders": { "personal_items_count": { "content": "$1", @@ -3974,7 +3974,7 @@ } }, "personalItemsWithOrgTransferWarning": { - "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ יועברו לצמיתות אל $ORG$. לא תהיה יותר הבעלים של הפריטים האלה.", "placeholders": { "personal_items_count": { "content": "$1", @@ -3987,10 +3987,10 @@ } }, "personalItemTransferWarningSingular": { - "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + "message": "פריט 1 יועבר לצמיתות לארגון הנבחר. לא תהיה יותר הבעלים של הפריט הזה." }, "personalItemWithOrgTransferWarningSingular": { - "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "message": "פריט 1 יועבר לצמיתות אל $ORG$. לא תהיה יותר הבעלים של הפריט הזה.", "placeholders": { "org": { "content": "$1", @@ -3999,13 +3999,13 @@ } }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "אוספים הוקצו בהצלחה" }, "nothingSelected": { - "message": "You have not selected anything." + "message": "לא בחרת כלום." }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "פריטים הועברו אל $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4014,7 +4014,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "פריט הועבר אל $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4023,7 +4023,7 @@ } }, "movedItemsToOrg": { - "message": "Selected items moved to $ORGNAME$", + "message": "פריטים נבחרים הועברו אל $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4063,19 +4063,19 @@ } }, "showMore": { - "message": "Show more" + "message": "הצג עוד" }, "showLess": { - "message": "Show less" + "message": "הצג פחות" }, "enableAutotype": { - "message": "Enable Autotype" + "message": "הפעל הקלדה אוטומטית" }, "enableAutotypeDescription": { "message": "Bitwarden לא מאמת את מקומות הקלט, נא לוודא שזה החלון והשדה הנכונים בטרם שימוש בקיצור הדרך." }, "moreBreadcrumbs": { - "message": "More breadcrumbs", + "message": "עוד נתיבי ניווט", "description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed." } } From 684299f0544d765eafac81a5d5103246e81657be Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:18:52 -0400 Subject: [PATCH 07/38] [deps] AC: Update core-js to v3.45.0 (#16048) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jimmy Vo --- apps/cli/package.json | 2 +- package-lock.json | 10 +++++----- package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 4964cd4e403..8c5b367cb61 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -69,7 +69,7 @@ "browser-hrtime": "1.1.8", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.44.0", + "core-js": "3.45.0", "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", diff --git a/package-lock.json b/package-lock.json index 5f4aedffd1f..c414adb416d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "bufferutil": "4.0.9", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.44.0", + "core-js": "3.45.0", "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", @@ -204,7 +204,7 @@ "browser-hrtime": "1.1.8", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.44.0", + "core-js": "3.45.0", "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", @@ -18802,9 +18802,9 @@ } }, "node_modules/core-js": { - "version": "3.44.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.44.0.tgz", - "integrity": "sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==", + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz", + "integrity": "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==", "hasInstallScript": true, "license": "MIT", "funding": { diff --git a/package.json b/package.json index 331292b9076..bf6be11a616 100644 --- a/package.json +++ b/package.json @@ -176,7 +176,7 @@ "bufferutil": "4.0.9", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.44.0", + "core-js": "3.45.0", "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", From 492e2b8d06ac45ca92d1da7139905e74348db643 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 26 Aug 2025 17:27:41 +0200 Subject: [PATCH 08/38] Enable the angular check (#15210) Enables the angular check introduced by #15168. --- apps/browser/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 551225231f7..e62f90354d2 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -392,7 +392,7 @@ if (manifestVersion == 2) { cache: true, }, dependencies: ["main"], - plugins: [...requiredPlugins /*new AngularCheckPlugin()*/], // TODO (PM-22630): Re-enable this plugin when angular is removed from the background script. + plugins: [...requiredPlugins, new AngularCheckPlugin()], }; // Safari's desktop build process requires a background.html and vendor.js file to exist From fb4f87958d532a8e5b848797b63142499c487e86 Mon Sep 17 00:00:00 2001 From: Miles Blackwood Date: Tue, 26 Aug 2025 11:35:23 -0400 Subject: [PATCH 09/38] Uses setTimeout to shim requestIdleCallback (#16143) * Makes inline menu handler async to resolve Safari issue. * Don't use idle callback deadline as timeout delay. * Revert "Makes inline menu handler async to resolve Safari issue." This reverts commit d3f7461cfaae440f33d5be48278c01e5d6f74dde. --------- Co-authored-by: Robyn MacCallum --- apps/browser/src/autofill/utils/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index 614a5b014f2..0e102dcfd99 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -37,9 +37,7 @@ export function requestIdleCallbackPolyfill( return globalThis.requestIdleCallback(() => callback(), options); } - const timeoutDelay = options?.timeout || 1; - - return globalThis.setTimeout(() => callback(), timeoutDelay); + return globalThis.setTimeout(() => callback(), 1); } /** From ad2dfe1e99c151e068c58665d75d43389aa581ea Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:41:15 -0400 Subject: [PATCH 10/38] feat(notifications): [PM-19388] Enable push notifications on locked clients * Add back notifications connection on locked accounts * Updated tests. * Make sure web push connection service is started synchronously * Fixed merge conflicts. --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- .../browser/src/background/main.background.ts | 7 +++-- .../src/services/jslib-services.module.ts | 1 + libs/common/src/enums/feature-flag.enum.ts | 2 ++ .../default-notifications.service.spec.ts | 21 +++++++------- .../default-server-notifications.service.ts | 28 ++++++++++++++----- 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index c2c62625bb6..096bbe76e40 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1125,6 +1125,7 @@ export default class MainBackground { new SignalRConnectionService(this.apiService, this.logService), this.authService, this.webPushConnectionService, + this.configService, ); this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService); @@ -1370,12 +1371,14 @@ export default class MainBackground { this.accountService, this.authService, ); - } - async bootstrap() { + // Synchronous startup if (this.webPushConnectionService instanceof WorkerWebPushConnectionService) { this.webPushConnectionService.start(); } + } + + async bootstrap() { this.containerService.attachToGlobal(self); await this.sdkLoadService.loadAndInit(); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index fa7de5484d9..f62407eeafc 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -943,6 +943,7 @@ const safeProviders: SafeProvider[] = [ SignalRConnectionService, AuthServiceAbstraction, WebPushConnectionService, + ConfigService, ], }), safeProvider({ diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 7ab85139f4c..67f68e12847 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -53,6 +53,7 @@ export enum FeatureFlag { /* Platform */ IpcChannelFramework = "ipc-channel-framework", + PushNotificationsWhenLocked = "pm-19388-push-notifications-when-locked", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -112,6 +113,7 @@ export const DefaultFeatureFlagValue = { /* Platform */ [FeatureFlag.IpcChannelFramework]: FALSE, + [FeatureFlag.PushNotificationsWhenLocked]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/libs/common/src/platform/server-notifications/internal/default-notifications.service.spec.ts b/libs/common/src/platform/server-notifications/internal/default-notifications.service.spec.ts index 567e0fbfc3d..9f42328d573 100644 --- a/libs/common/src/platform/server-notifications/internal/default-notifications.service.spec.ts +++ b/libs/common/src/platform/server-notifications/internal/default-notifications.service.spec.ts @@ -1,5 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, bufferCount, firstValueFrom, ObservedValueOf, Subject } from "rxjs"; +import { BehaviorSubject, bufferCount, firstValueFrom, ObservedValueOf, of, Subject } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports @@ -14,6 +14,7 @@ import { NotificationType } from "../../../enums"; import { NotificationResponse } from "../../../models/response/notification.response"; import { UserId } from "../../../types/guid"; import { AppIdService } from "../../abstractions/app-id.service"; +import { ConfigService } from "../../abstractions/config/config.service"; import { Environment, EnvironmentService } from "../../abstractions/environment.service"; import { LogService } from "../../abstractions/log.service"; import { MessageSender } from "../../messaging"; @@ -38,6 +39,7 @@ describe("NotificationsService", () => { let signalRNotificationConnectionService: MockProxy; let authService: MockProxy; let webPushNotificationConnectionService: MockProxy; + let configService: MockProxy; let activeAccount: BehaviorSubject>; @@ -64,6 +66,9 @@ describe("NotificationsService", () => { signalRNotificationConnectionService = mock(); authService = mock(); webPushNotificationConnectionService = mock(); + configService = mock(); + + configService.getFeatureFlag$.mockReturnValue(of(true)); activeAccount = new BehaviorSubject>(null); accountService.activeAccount$ = activeAccount.asObservable(); @@ -104,6 +109,7 @@ describe("NotificationsService", () => { signalRNotificationConnectionService, authService, webPushNotificationConnectionService, + configService, ); }); @@ -227,10 +233,9 @@ describe("NotificationsService", () => { }); it.each([ - // Temporarily rolling back server notifications being connected while locked - // { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Unlocked }, - // { initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Locked }, - // { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Locked }, + { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Unlocked }, + { initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Locked }, + { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Locked }, { initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Unlocked }, ])( "does not re-connect when the user transitions from $initialStatus to $updatedStatus", @@ -255,11 +260,7 @@ describe("NotificationsService", () => { }, ); - it.each([ - // Temporarily disabling server notifications connecting while in a locked state - // AuthenticationStatus.Locked, - AuthenticationStatus.Unlocked, - ])( + it.each([AuthenticationStatus.Locked, AuthenticationStatus.Unlocked])( "connects when a user transitions from logged out to %s", async (newStatus: AuthenticationStatus) => { emitActiveUser(mockUser1); diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts index 4502d9663a3..d21074f5bbf 100644 --- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts +++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts @@ -14,6 +14,7 @@ import { // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { LogoutReason } from "@bitwarden/auth/common"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AccountService } from "../../../auth/abstractions/account.service"; import { AuthService } from "../../../auth/abstractions/auth.service"; @@ -28,6 +29,7 @@ import { import { UserId } from "../../../types/guid"; import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction"; import { AppIdService } from "../../abstractions/app-id.service"; +import { ConfigService } from "../../abstractions/config/config.service"; import { EnvironmentService } from "../../abstractions/environment.service"; import { LogService } from "../../abstractions/log.service"; import { MessagingService } from "../../abstractions/messaging.service"; @@ -55,6 +57,7 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer private readonly signalRConnectionService: SignalRConnectionService, private readonly authService: AuthService, private readonly webPushConnectionService: WebPushConnectionService, + private readonly configService: ConfigService, ) { this.notifications$ = this.accountService.activeAccount$.pipe( map((account) => account?.id), @@ -132,14 +135,25 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer ); } - // This method name is a lie currently as we also have an access token - // when locked, this is eventually where we want to be but it increases load - // on signalR so we are rolling back until we can move the load of browser to - // web push. private hasAccessToken$(userId: UserId) { - return this.authService.authStatusFor$(userId).pipe( - map((authStatus) => authStatus === AuthenticationStatus.Unlocked), - distinctUntilChanged(), + return this.configService.getFeatureFlag$(FeatureFlag.PushNotificationsWhenLocked).pipe( + switchMap((featureFlagEnabled) => { + if (featureFlagEnabled) { + return this.authService.authStatusFor$(userId).pipe( + map( + (authStatus) => + authStatus === AuthenticationStatus.Locked || + authStatus === AuthenticationStatus.Unlocked, + ), + distinctUntilChanged(), + ); + } else { + return this.authService.authStatusFor$(userId).pipe( + map((authStatus) => authStatus === AuthenticationStatus.Unlocked), + distinctUntilChanged(), + ); + } + }), ); } From 28b5a2bb5e0be182606c6127a5423e252a4d1a21 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 26 Aug 2025 11:42:52 -0400 Subject: [PATCH 11/38] [PM-22717] Expose DefaultUserCollectionEmail to clients (#15643) * enforce restrictions based on collection type, set default collection type * fix ts strict errors * fix default collection enforcement in vault header * enforce default collection restrictions in vault collection row * enforce default collection restrictions in AC vault header * enforce default collection restriction for select all * fix ts strict error * switch to signal, fix feature flag * fix story * clean up * remove feature flag, move check for defaultCollecion to CollecitonView * fix test * remove unused configService * fix test: coerce null to undefined for collection Id * clean up leaky abstraction for default collection * fix ts-strict error * fix parens * add new property to models, update logic, refactor for ts-strict * fix type * rename defaultCollection getter * clean up * clean up * clean up, add comment, fix submit * add comment * add feature flag * check model for name * cleanup readonly logic, remove featureflag logic * wip * refactor CollectionRequest into Create and Update models * fix readonly logic * cleanup * set defaultUserCollectionEmail in decryption from Collection * split save into update/create methods * fix readonly logic * fix collections post and put requests * add defaultUserCollection email to model when submitting collection dialog --- apps/cli/src/commands/edit.command.ts | 4 +- apps/cli/src/vault/create.command.ts | 4 +- .../collections/utils/collection-utils.ts | 10 +- .../collection-dialog.component.ts | 30 +++++- apps/web/src/app/core/core.module.ts | 9 +- .../services/vault-filter.service.ts | 4 +- .../abstractions/collection-admin.service.ts | 6 +- .../models/collection-admin.view.ts | 12 +++ .../models/collection-with-id.request.ts | 15 ++- .../collections/models/collection.data.ts | 2 + .../collections/models/collection.request.ts | 48 +++++++-- .../collections/models/collection.response.ts | 2 + .../collections/models/collection.spec.ts | 6 +- .../common/collections/models/collection.ts | 2 + .../collections/models/collection.view.ts | 28 +++++- .../default-collection-admin.service.ts | 98 ++++++++++++++----- .../services/default-collection.service.ts | 2 +- libs/common/src/abstractions/api.service.ts | 7 +- libs/common/src/services/api.service.ts | 7 +- .../item-details-section.component.spec.ts | 31 +++--- 20 files changed, 248 insertions(+), 79 deletions(-) diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 2e273d041d6..92674aa3dcd 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { firstValueFrom, map, switchMap } from "rxjs"; -import { CollectionRequest } from "@bitwarden/admin-console/common"; +import { UpdateCollectionRequest } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -225,7 +225,7 @@ export class EditCommand { : req.users.map( (u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage), ); - const request = new CollectionRequest({ + const request = new UpdateCollectionRequest({ name: await this.encryptService.encryptString(req.name, orgKey), externalId: req.externalId, users, diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index cbaae4d2544..0892bb42214 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -5,7 +5,7 @@ import * as path from "path"; import { firstValueFrom, map } from "rxjs"; -import { CollectionRequest } from "@bitwarden/admin-console/common"; +import { CreateCollectionRequest } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; @@ -233,7 +233,7 @@ export class CreateCommand { : req.users.map( (u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage), ); - const request = new CollectionRequest({ + const request = new CreateCollectionRequest({ name: await this.encryptService.encryptString(req.name, orgKey), externalId: req.externalId, groups, diff --git a/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.ts b/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.ts index 8a0fab520e3..67cb4c7cdc8 100644 --- a/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.ts +++ b/apps/web/src/app/admin-console/organizations/collections/utils/collection-utils.ts @@ -45,9 +45,15 @@ export function cloneCollection( let cloned; if (collection instanceof CollectionAdminView) { - cloned = Object.assign(new CollectionAdminView({ ...collection }), collection); + cloned = Object.assign( + new CollectionAdminView({ ...collection, name: collection.name }), + collection, + ); } else { - cloned = Object.assign(new CollectionView({ ...collection }), collection); + cloned = Object.assign( + new CollectionView({ ...collection, name: collection.name }), + collection, + ); } return cloned; } diff --git a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts index 59d042cae52..52c418a5211 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts @@ -398,6 +398,13 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { } return; } + if ( + this.editMode && + !this.collection.canEditName(this.organization) && + this.formGroup.controls.name.dirty + ) { + throw new Error("Cannot change readonly field: Name"); + } const parent = this.formGroup.controls.parent?.value; const collectionView = new CollectionAdminView({ @@ -414,9 +421,13 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { collectionView.users = this.formGroup.controls.access.value .filter((v) => v.type === AccessItemType.Member) .map(convertToSelectionView); + collectionView.defaultUserCollectionEmail = this.collection.defaultUserCollectionEmail; const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - const savedCollection = await this.collectionAdminService.save(collectionView, userId); + + const collectionResponse = this.editMode + ? await this.collectionAdminService.update(collectionView, userId) + : await this.collectionAdminService.create(collectionView, userId); this.toastService.showToast({ variant: "success", @@ -426,7 +437,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { ), }); - this.close(CollectionDialogAction.Saved, savedCollection); + this.close(CollectionDialogAction.Saved, collectionResponse); }; protected delete = async () => { @@ -483,14 +494,23 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { private handleFormGroupReadonly(readonly: boolean) { if (readonly) { + this.formGroup.controls.access.disable(); this.formGroup.controls.name.disable(); this.formGroup.controls.parent.disable(); - this.formGroup.controls.access.disable(); - } else { + return; + } + + this.formGroup.controls.access.enable(); + + if (!this.editMode) { this.formGroup.controls.name.enable(); this.formGroup.controls.parent.enable(); - this.formGroup.controls.access.enable(); + return; } + + const canEditName = this.collection.canEditName(this.organization); + this.formGroup.controls.name[canEditName ? "enable" : "disable"](); + this.formGroup.controls.parent[canEditName ? "enable" : "disable"](); } private close(action: CollectionDialogAction, collection?: CollectionResponse | CollectionView) { diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index cccbe26c524..22386144732 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -45,6 +45,7 @@ import { import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService, @@ -317,7 +318,13 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: CollectionAdminService, useClass: DefaultCollectionAdminService, - deps: [ApiService, KeyServiceAbstraction, EncryptService, CollectionService], + deps: [ + ApiService, + KeyServiceAbstraction, + EncryptService, + CollectionService, + OrganizationService, + ], }), safeProvider({ provide: SdkLoadService, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index ec77ff97a11..eeecccc87d6 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -250,7 +250,9 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { } collections.forEach((c) => { - const collectionCopy = cloneCollection(new CollectionView({ ...c })) as CollectionFilter; + const collectionCopy = cloneCollection( + new CollectionView({ ...c, name: c.name }), + ) as CollectionFilter; collectionCopy.icon = "bwi-collection-shared"; const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : []; ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter); diff --git a/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts b/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts index b322825e5ea..9fde1b2090b 100644 --- a/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts +++ b/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts @@ -10,7 +10,11 @@ export abstract class CollectionAdminService { organizationId: string, userId: UserId, ): Observable; - abstract save( + abstract update( + collection: CollectionAdminView, + userId: UserId, + ): Promise; + abstract create( collection: CollectionAdminView, userId: UserId, ): Promise; diff --git a/libs/admin-console/src/common/collections/models/collection-admin.view.ts b/libs/admin-console/src/common/collections/models/collection-admin.view.ts index 1cec9c3d39d..fcd2be17b8d 100644 --- a/libs/admin-console/src/common/collections/models/collection-admin.view.ts +++ b/libs/admin-console/src/common/collections/models/collection-admin.view.ts @@ -101,6 +101,17 @@ export class CollectionAdminView extends CollectionView { return this.id === Unassigned; } + /** + * Returns true if the collection name can be edited. Editing the collection name is restricted for collections + * that were DefaultUserCollections but where the relevant user has been offboarded. + * When this occurs, the offboarded user's email is treated as the collection name, and cannot be edited. + * This is important for security so that the server cannot ask the client to encrypt arbitrary data. + * WARNING! This is an IMPORTANT restriction that MUST be maintained for security purposes. + * Do not edit or remove this unless you understand why. + */ + override canEditName(org: Organization): boolean { + return (this.canEdit(org) && !this.defaultUserCollectionEmail) || super.canEditName(org); + } static async fromCollectionAccessDetails( collection: CollectionAccessDetailsResponse, encryptService: EncryptService, @@ -115,6 +126,7 @@ export class CollectionAdminView extends CollectionView { view.unmanaged = collection.unmanaged; view.type = collection.type; view.externalId = collection.externalId; + view.defaultUserCollectionEmail = collection.defaultUserCollectionEmail; view.groups = collection.groups ? collection.groups.map((g) => new CollectionAccessSelectionView(g)) diff --git a/libs/admin-console/src/common/collections/models/collection-with-id.request.ts b/libs/admin-console/src/common/collections/models/collection-with-id.request.ts index f1fccda0cfa..0f7b83705ef 100644 --- a/libs/admin-console/src/common/collections/models/collection-with-id.request.ts +++ b/libs/admin-console/src/common/collections/models/collection-with-id.request.ts @@ -1,19 +1,18 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Collection } from "./collection"; -import { CollectionRequest } from "./collection.request"; +import { BaseCollectionRequest } from "./collection.request"; -export class CollectionWithIdRequest extends CollectionRequest { +export class CollectionWithIdRequest extends BaseCollectionRequest { id: string; + name: string; - constructor(collection?: Collection) { - if (collection == null) { - return; + constructor(collection: Collection) { + if (collection == null || collection.name == null || collection.name.encryptedString == null) { + throw new Error("CollectionWithIdRequest must contain name."); } super({ - name: collection.name, externalId: collection.externalId, }); + this.name = collection.name.encryptedString; this.id = collection.id; } } diff --git a/libs/admin-console/src/common/collections/models/collection.data.ts b/libs/admin-console/src/common/collections/models/collection.data.ts index dac8f41b2ba..a783a3c9ab1 100644 --- a/libs/admin-console/src/common/collections/models/collection.data.ts +++ b/libs/admin-console/src/common/collections/models/collection.data.ts @@ -9,6 +9,7 @@ export class CollectionData { id: CollectionId; organizationId: OrganizationId; name: string; + defaultUserCollectionEmail: string | undefined; externalId: string | undefined; readOnly: boolean = false; manage: boolean = false; @@ -24,6 +25,7 @@ export class CollectionData { this.manage = response.manage; this.hidePasswords = response.hidePasswords; this.type = response.type; + this.defaultUserCollectionEmail = response.defaultUserCollectionEmail; } static fromJSON(obj: Jsonify): CollectionData | null { diff --git a/libs/admin-console/src/common/collections/models/collection.request.ts b/libs/admin-console/src/common/collections/models/collection.request.ts index 39ed03d81fa..310bb08ea72 100644 --- a/libs/admin-console/src/common/collections/models/collection.request.ts +++ b/libs/admin-console/src/common/collections/models/collection.request.ts @@ -1,23 +1,20 @@ import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -export class CollectionRequest { - name: string; +export abstract class BaseCollectionRequest { externalId: string | undefined; groups: SelectionReadOnlyRequest[] = []; users: SelectionReadOnlyRequest[] = []; - constructor(c: { - name: EncString; + static isUpdate = (request: BaseCollectionRequest): request is UpdateCollectionRequest => { + return request instanceof UpdateCollectionRequest; + }; + + protected constructor(c: { users?: SelectionReadOnlyRequest[]; groups?: SelectionReadOnlyRequest[]; externalId?: string; }) { - if (!c.name || !c.name.encryptedString) { - throw new Error("Name not provided for CollectionRequest."); - } - - this.name = c.name.encryptedString; this.externalId = c.externalId; if (c.groups) { @@ -28,3 +25,36 @@ export class CollectionRequest { } } } + +export class CreateCollectionRequest extends BaseCollectionRequest { + name: string; + + constructor(c: { + name: EncString; + users?: SelectionReadOnlyRequest[]; + groups?: SelectionReadOnlyRequest[]; + externalId?: string; + }) { + super(c); + + if (!c.name || !c.name.encryptedString) { + throw new Error("Name not provided for CollectionRequest."); + } + + this.name = c.name.encryptedString; + } +} + +export class UpdateCollectionRequest extends BaseCollectionRequest { + name: string | null; + + constructor(c: { + name: EncString | null; + users?: SelectionReadOnlyRequest[]; + groups?: SelectionReadOnlyRequest[]; + externalId?: string; + }) { + super(c); + this.name = c.name?.encryptedString ?? null; + } +} diff --git a/libs/admin-console/src/common/collections/models/collection.response.ts b/libs/admin-console/src/common/collections/models/collection.response.ts index ad0eed53ddf..e6722635984 100644 --- a/libs/admin-console/src/common/collections/models/collection.response.ts +++ b/libs/admin-console/src/common/collections/models/collection.response.ts @@ -8,6 +8,7 @@ export class CollectionResponse extends BaseResponse { id: CollectionId; organizationId: OrganizationId; name: string; + defaultUserCollectionEmail: string | undefined; externalId: string | undefined; type: CollectionType = CollectionTypes.SharedCollection; @@ -17,6 +18,7 @@ export class CollectionResponse extends BaseResponse { this.organizationId = this.getResponseProperty("OrganizationId"); this.name = this.getResponseProperty("Name"); this.externalId = this.getResponseProperty("ExternalId"); + this.defaultUserCollectionEmail = this.getResponseProperty("DefaultUserCollectionEmail"); this.type = this.getResponseProperty("Type") ?? CollectionTypes.SharedCollection; } } diff --git a/libs/admin-console/src/common/collections/models/collection.spec.ts b/libs/admin-console/src/common/collections/models/collection.spec.ts index 1cb1d39ed57..16066f88ce1 100644 --- a/libs/admin-console/src/common/collections/models/collection.spec.ts +++ b/libs/admin-console/src/common/collections/models/collection.spec.ts @@ -25,6 +25,7 @@ describe("Collection", () => { manage: true, hidePasswords: true, type: CollectionTypes.DefaultUserCollection, + defaultUserCollectionEmail: "defaultCollectionEmail", }), ); encService = mock(); @@ -61,6 +62,7 @@ describe("Collection", () => { manage: true, hidePasswords: true, type: CollectionTypes.DefaultUserCollection, + defaultUserCollectionEmail: "defaultCollectionEmail", }); }); @@ -75,6 +77,7 @@ describe("Collection", () => { collection.hidePasswords = false; collection.manage = true; collection.type = CollectionTypes.DefaultUserCollection; + collection.defaultUserCollectionEmail = "defaultCollectionEmail"; const key = makeSymmetricCryptoKey(); @@ -84,12 +87,13 @@ describe("Collection", () => { externalId: "extId", hidePasswords: false, id: "id", - name: "encName", + _name: "encName", organizationId: "orgId", readOnly: false, manage: true, assigned: true, type: CollectionTypes.DefaultUserCollection, + defaultUserCollectionEmail: "defaultCollectionEmail", }); }); }); diff --git a/libs/admin-console/src/common/collections/models/collection.ts b/libs/admin-console/src/common/collections/models/collection.ts index 7de4462b0c8..cf5573b8f4f 100644 --- a/libs/admin-console/src/common/collections/models/collection.ts +++ b/libs/admin-console/src/common/collections/models/collection.ts @@ -23,6 +23,7 @@ export class Collection extends Domain { hidePasswords: boolean = false; manage: boolean = false; type: CollectionType = CollectionTypes.SharedCollection; + defaultUserCollectionEmail: string | undefined; constructor(c: { id: CollectionId; name: EncString; organizationId: OrganizationId }) { super(); @@ -46,6 +47,7 @@ export class Collection extends Domain { collection.hidePasswords = obj.hidePasswords; collection.manage = obj.manage; collection.type = obj.type; + collection.defaultUserCollectionEmail = obj.defaultUserCollectionEmail; return collection; } diff --git a/libs/admin-console/src/common/collections/models/collection.view.ts b/libs/admin-console/src/common/collections/models/collection.view.ts index c4470fe13fb..d3e15806c86 100644 --- a/libs/admin-console/src/common/collections/models/collection.view.ts +++ b/libs/admin-console/src/common/collections/models/collection.view.ts @@ -16,7 +16,6 @@ export const NestingDelimiter = "/"; export class CollectionView implements View, ITreeNodeObject { id: CollectionId; organizationId: OrganizationId; - name: string; externalId: string | undefined; // readOnly applies to the items within a collection readOnly: boolean = false; @@ -24,11 +23,22 @@ export class CollectionView implements View, ITreeNodeObject { manage: boolean = false; assigned: boolean = false; type: CollectionType = CollectionTypes.SharedCollection; + defaultUserCollectionEmail: string | undefined; + + private _name: string; constructor(c: { id: CollectionId; organizationId: OrganizationId; name: string }) { this.id = c.id; this.organizationId = c.organizationId; - this.name = c.name; + this._name = c.name; + } + + set name(name: string) { + this._name = name; + } + + get name(): string { + return this.defaultUserCollectionEmail ?? this._name; } canEditItems(org: Organization): boolean { @@ -83,6 +93,18 @@ export class CollectionView implements View, ITreeNodeObject { return false; } + /** + * Returns true if the collection name can be edited. Editing the collection name is restricted for collections + * that were DefaultUserCollections but where the relevant user has been offboarded. + * When this occurs, the offboarded user's email is treated as the collection name, and cannot be edited. + * This is important for security so that the server cannot ask the client to encrypt arbitrary data. + * WARNING! This is an IMPORTANT restriction that MUST be maintained for security purposes. + * Do not edit or remove this unless you understand why. + */ + canEditName(org: Organization): boolean { + return this.canEdit(org) && !this.defaultUserCollectionEmail; + } + get isDefaultCollection() { return this.type == CollectionTypes.DefaultUserCollection; } @@ -111,6 +133,7 @@ export class CollectionView implements View, ITreeNodeObject { view.hidePasswords = collection.hidePasswords; view.manage = collection.manage; view.type = collection.type; + view.defaultUserCollectionEmail = collection.defaultUserCollectionEmail; return view; } @@ -125,6 +148,7 @@ export class CollectionView implements View, ITreeNodeObject { view.externalId = collection.externalId; view.type = collection.type; view.assigned = collection.assigned; + view.defaultUserCollectionEmail = collection.defaultUserCollectionEmail; return view; } diff --git a/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts index 3f7e8e31e0b..ca797a0f9ae 100644 --- a/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts +++ b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts @@ -1,6 +1,10 @@ import { combineLatest, firstValueFrom, from, map, Observable, of, switchMap } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; @@ -10,13 +14,15 @@ import { KeyService } from "@bitwarden/key-management"; import { CollectionAdminService, CollectionService } from "../abstractions"; import { CollectionData, - CollectionRequest, CollectionAccessDetailsResponse, CollectionDetailsResponse, CollectionResponse, BulkCollectionAccessRequest, CollectionAccessSelectionView, CollectionAdminView, + BaseCollectionRequest, + UpdateCollectionRequest, + CreateCollectionRequest, } from "../models"; export class DefaultCollectionAdminService implements CollectionAdminService { @@ -25,6 +31,7 @@ export class DefaultCollectionAdminService implements CollectionAdminService { private keyService: KeyService, private encryptService: EncryptService, private collectionService: CollectionService, + private organizationService: OrganizationService, ) {} collectionAdminViews$(organizationId: string, userId: UserId): Observable { @@ -45,27 +52,40 @@ export class DefaultCollectionAdminService implements CollectionAdminService { ); } - async save(collection: CollectionAdminView, userId: UserId): Promise { - const request = await this.encrypt(collection, userId); - - let response: CollectionDetailsResponse; - if (collection.id == null) { - response = await this.apiService.postCollection(collection.organizationId, request); - collection.id = response.id; - } else { - response = await this.apiService.putCollection( - collection.organizationId, - collection.id, - request, - ); + async update( + collection: CollectionAdminView, + userId: UserId, + ): Promise { + const request = await this.encrypt(collection, userId, true); + if (!BaseCollectionRequest.isUpdate(request)) { + throw new Error("Cannot update collection with CreateCollectionRequest."); } - if (response.assigned) { - await this.collectionService.upsert(new CollectionData(response), userId); - } else { - await this.collectionService.delete([collection.id as CollectionId], userId); + const response = await this.apiService.putCollection( + collection.organizationId, + collection.id, + request, + ); + + await this.updateLocalCollections(response, collection, userId); + + return response; + } + + async create( + collection: CollectionAdminView, + userId: UserId, + ): Promise { + const request = await this.encrypt(collection, userId, false); + if (BaseCollectionRequest.isUpdate(request)) { + throw new Error("Cannot create collection with UpdateCollectionRequest."); } + const response = await this.apiService.postCollection(collection.organizationId, request); + collection.id = response.id; + + await this.updateLocalCollections(response, collection, userId); + return response; } @@ -73,6 +93,16 @@ export class DefaultCollectionAdminService implements CollectionAdminService { await this.apiService.deleteCollection(organizationId, collectionId); } + private async updateLocalCollections( + response: CollectionDetailsResponse, + collection: CollectionAdminView, + userId: UserId, + ) { + response.assigned + ? await this.collectionService.upsert(new CollectionData(response), userId) + : await this.collectionService.delete([collection.id as CollectionId], userId); + } + async bulkAssignAccess( organizationId: string, collectionIds: string[], @@ -118,10 +148,15 @@ export class DefaultCollectionAdminService implements CollectionAdminService { ); }); - return await Promise.all(promises); + const r = await Promise.all(promises); + return r; } - private async encrypt(model: CollectionAdminView, userId: UserId): Promise { + private async encrypt( + model: CollectionAdminView, + userId: UserId, + editMode: boolean, + ): Promise { if (!model.organizationId) { throw new Error("Collection has no organization id."); } @@ -154,14 +189,31 @@ export class DefaultCollectionAdminService implements CollectionAdminService { new SelectionReadOnlyRequest(user.id, user.readOnly, user.hidePasswords, user.manage), ); - const collectionRequest = new CollectionRequest({ + if (editMode) { + const org = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(model.organizationId)), + ); + if (org == null) { + throw new Error("No Organization found."); + } + return new UpdateCollectionRequest({ + name: model.canEditName(org) + ? await this.encryptService.encryptString(model.name, key) + : null, + externalId: model.externalId, + users, + groups, + }); + } + + return new CreateCollectionRequest({ name: await this.encryptService.encryptString(model.name, key), externalId: model.externalId, users, groups, }); - - return collectionRequest; } } diff --git a/libs/admin-console/src/common/collections/services/default-collection.service.ts b/libs/admin-console/src/common/collections/services/default-collection.service.ts index aa25cdfa1e7..39b3b491862 100644 --- a/libs/admin-console/src/common/collections/services/default-collection.service.ts +++ b/libs/admin-console/src/common/collections/services/default-collection.service.ts @@ -216,7 +216,7 @@ export class DefaultCollectionService implements CollectionService { getAllNested(collections: CollectionView[]): TreeNode[] { const nodes: TreeNode[] = []; collections.forEach((c) => { - const collectionCopy = Object.assign(new CollectionView({ ...c }), c); + const collectionCopy = Object.assign(new CollectionView({ ...c, name: c.name }), c); const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : []; ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, undefined, NestingDelimiter); diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index d8b8c1c429f..726b04534ad 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -3,8 +3,9 @@ import { CollectionAccessDetailsResponse, CollectionDetailsResponse, - CollectionRequest, CollectionResponse, + CreateCollectionRequest, + UpdateCollectionRequest, } from "@bitwarden/admin-console/common"; import { OrganizationConnectionType } from "../admin-console/enums"; @@ -270,12 +271,12 @@ export abstract class ApiService { ): Promise>; abstract postCollection( organizationId: string, - request: CollectionRequest, + request: CreateCollectionRequest, ): Promise; abstract putCollection( organizationId: string, id: string, - request: CollectionRequest, + request: UpdateCollectionRequest, ): Promise; abstract deleteCollection(organizationId: string, id: string): Promise; abstract deleteManyCollections(organizationId: string, collectionIds: string[]): Promise; diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index e48b941d4a0..6a670368b1f 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -7,8 +7,9 @@ import { firstValueFrom } from "rxjs"; import { CollectionAccessDetailsResponse, CollectionDetailsResponse, - CollectionRequest, CollectionResponse, + CreateCollectionRequest, + UpdateCollectionRequest, } from "@bitwarden/admin-console/common"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports @@ -727,7 +728,7 @@ export class ApiService implements ApiServiceAbstraction { async postCollection( organizationId: string, - request: CollectionRequest, + request: CreateCollectionRequest, ): Promise { const r = await this.send( "POST", @@ -742,7 +743,7 @@ export class ApiService implements ApiServiceAbstraction { async putCollection( organizationId: string, id: string, - request: CollectionRequest, + request: UpdateCollectionRequest, ): Promise { const r = await this.send( "PUT", diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts index 6aa7175d373..e3d863a0af3 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts @@ -33,23 +33,24 @@ const createMockCollection = ( readOnly = false, canEdit = true, ): CollectionView => { - return { - id: id as CollectionId, + const cv = new CollectionView({ name, organizationId: organizationId as OrganizationId, - externalId: "", - readOnly, - hidePasswords: false, - manage: true, - assigned: true, - type: CollectionTypes.DefaultUserCollection, - isDefaultCollection: true, - canEditItems: jest.fn().mockReturnValue(canEdit), - canEdit: jest.fn(), - canDelete: jest.fn(), - canViewCollectionInfo: jest.fn(), - encrypt: jest.fn(), - }; + id: id as CollectionId, + }); + cv.readOnly = readOnly; + cv.manage = true; + cv.type = CollectionTypes.DefaultUserCollection; + cv.externalId = ""; + cv.hidePasswords = false; + cv.assigned = true; + cv.canEditName = jest.fn().mockReturnValue(true); + cv.canEditItems = jest.fn().mockReturnValue(canEdit); + cv.canEdit = jest.fn(); + cv.canDelete = jest.fn(); + cv.canViewCollectionInfo = jest.fn(); + + return cv; }; describe("ItemDetailsSectionComponent", () => { From 34cd41988a5612d041fe56d4c239577a60b3f7ec Mon Sep 17 00:00:00 2001 From: neuronull <9162534+neuronull@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:44:08 -0600 Subject: [PATCH 12/38] Remove `EnableNewCardCombinedExpiryAutofill` feature flag (#16131) --- .../services/autofill.service.spec.ts | 48 +----- .../src/autofill/services/autofill.service.ts | 157 +----------------- libs/common/src/enums/feature-flag.enum.ts | 2 - 3 files changed, 2 insertions(+), 205 deletions(-) diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 80cce5228d3..f9430387c83 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -14,7 +14,7 @@ import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/au import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; -import { FeatureFlag, FeatureFlagValueType } from "@bitwarden/common/enums/feature-flag.enum"; +import { FeatureFlagValueType } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -2987,12 +2987,6 @@ describe("AutofillService", () => { options.cipher.card.expMonth = "5"; } - const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag( - FeatureFlag.EnableNewCardCombinedExpiryAutofill, - ); - - expect(enableNewCardCombinedExpiryAutofill).toEqual(false); - const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, @@ -3003,23 +2997,6 @@ describe("AutofillService", () => { expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", dateFormat[1]]); }); }); - - it("returns an expiration date format matching `yyyy-mm` if no valid format can be identified", async () => { - const value = await autofillService["generateCardFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag( - FeatureFlag.EnableNewCardCombinedExpiryAutofill, - ); - - expect(enableNewCardCombinedExpiryAutofill).toEqual(false); - - expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", "2024-05"]); - }); }); const extraExpectedDateFormats = [ @@ -3092,12 +3069,6 @@ describe("AutofillService", () => { options.cipher.card.expMonth = "05"; } - const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag( - FeatureFlag.EnableNewCardCombinedExpiryAutofill, - ); - - expect(enableNewCardCombinedExpiryAutofill).toEqual(true); - const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, @@ -3108,23 +3079,6 @@ describe("AutofillService", () => { expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", dateFormat[1]]); }); }); - - it("feature-flagged logic returns an expiration date format matching `mm/yy` if no valid format can be identified", async () => { - const value = await autofillService["generateCardFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag( - FeatureFlag.EnableNewCardCombinedExpiryAutofill, - ); - - expect(enableNewCardCombinedExpiryAutofill).toEqual(true); - - expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", "05/24"]); - }); }); }); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index fd707ef96b3..51c0dd3f247 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -29,7 +29,6 @@ import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategySetting, UriMatchStrategy, @@ -1212,161 +1211,7 @@ export default class AutofillService implements AutofillServiceInterface { AutofillService.hasValue(card.expMonth) && AutofillService.hasValue(card.expYear) ) { - let combinedExpiryFillValue = null; - - const enableNewCardCombinedExpiryAutofill = await this.configService.getFeatureFlag( - FeatureFlag.EnableNewCardCombinedExpiryAutofill, - ); - - if (enableNewCardCombinedExpiryAutofill) { - combinedExpiryFillValue = this.generateCombinedExpiryValue(card, fillFields.exp); - } else { - const fullMonth = ("0" + card.expMonth).slice(-2); - - let fullYear: string = card.expYear; - let partYear: string = null; - if (fullYear.length === 2) { - partYear = fullYear; - fullYear = normalizeExpiryYearFormat(fullYear); - } else if (fullYear.length === 4) { - partYear = fullYear.substr(2, 2); - } - - for (let i = 0; i < CreditCardAutoFillConstants.MonthAbbr.length; i++) { - if ( - // mm/yyyy - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - "/" + - CreditCardAutoFillConstants.YearAbbrLong[i], - ) - ) { - combinedExpiryFillValue = fullMonth + "/" + fullYear; - } else if ( - // mm/yy - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - "/" + - CreditCardAutoFillConstants.YearAbbrShort[i], - ) && - partYear != null - ) { - combinedExpiryFillValue = fullMonth + "/" + partYear; - } else if ( - // yyyy/mm - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrLong[i] + - "/" + - CreditCardAutoFillConstants.MonthAbbr[i], - ) - ) { - combinedExpiryFillValue = fullYear + "/" + fullMonth; - } else if ( - // yy/mm - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrShort[i] + - "/" + - CreditCardAutoFillConstants.MonthAbbr[i], - ) && - partYear != null - ) { - combinedExpiryFillValue = partYear + "/" + fullMonth; - } else if ( - // mm-yyyy - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - "-" + - CreditCardAutoFillConstants.YearAbbrLong[i], - ) - ) { - combinedExpiryFillValue = fullMonth + "-" + fullYear; - } else if ( - // mm-yy - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - "-" + - CreditCardAutoFillConstants.YearAbbrShort[i], - ) && - partYear != null - ) { - combinedExpiryFillValue = fullMonth + "-" + partYear; - } else if ( - // yyyy-mm - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrLong[i] + - "-" + - CreditCardAutoFillConstants.MonthAbbr[i], - ) - ) { - combinedExpiryFillValue = fullYear + "-" + fullMonth; - } else if ( - // yy-mm - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrShort[i] + - "-" + - CreditCardAutoFillConstants.MonthAbbr[i], - ) && - partYear != null - ) { - combinedExpiryFillValue = partYear + "-" + fullMonth; - } else if ( - // yyyymm - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrLong[i] + - CreditCardAutoFillConstants.MonthAbbr[i], - ) - ) { - combinedExpiryFillValue = fullYear + fullMonth; - } else if ( - // yymm - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrShort[i] + - CreditCardAutoFillConstants.MonthAbbr[i], - ) && - partYear != null - ) { - combinedExpiryFillValue = partYear + fullMonth; - } else if ( - // mmyyyy - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - CreditCardAutoFillConstants.YearAbbrLong[i], - ) - ) { - combinedExpiryFillValue = fullMonth + fullYear; - } else if ( - // mmyy - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - CreditCardAutoFillConstants.YearAbbrShort[i], - ) && - partYear != null - ) { - combinedExpiryFillValue = fullMonth + partYear; - } - - if (combinedExpiryFillValue != null) { - break; - } - } - - // If none of the previous cases applied, set as default - if (combinedExpiryFillValue == null) { - combinedExpiryFillValue = fullYear + "-" + fullMonth; - } - } + const combinedExpiryFillValue = this.generateCombinedExpiryValue(card, fillFields.exp); this.makeScriptActionWithValue( fillScript, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 67f68e12847..cd9bbfc54c3 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -17,7 +17,6 @@ export enum FeatureFlag { PM14938_BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals", /* Autofill */ - EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill", NotificationRefresh = "notification-refresh", UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", MacOsNativeCredentialSync = "macos-native-credential-sync", @@ -74,7 +73,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.CreateDefaultLocation]: FALSE, /* Autofill */ - [FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE, [FeatureFlag.NotificationRefresh]: FALSE, [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, [FeatureFlag.MacOsNativeCredentialSync]: FALSE, From c72fdebfd934d823ed19b4313afa13a92ac991c2 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Tue, 26 Aug 2025 22:55:29 +0000 Subject: [PATCH 13/38] Bumped Desktop client to 2025.8.2 --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 42eb7017e03..ff958c0d8e6 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2025.8.1", + "version": "2025.8.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 6daff35e115..4be839e28c2 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2025.8.1", + "version": "2025.8.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2025.8.1", + "version": "2025.8.2", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index ea2e8affda2..77b1f186171 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2025.8.1", + "version": "2025.8.2", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index c414adb416d..842480f56da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -277,7 +277,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2025.8.1", + "version": "2025.8.2", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -403,6 +403,7 @@ "license": "GPL-3.0" }, "libs/state-internal": { + "name": "@bitwarden/state-internal", "version": "0.0.1", "license": "GPL-3.0" }, From 4f09ae52abb8a83bc26c33e584f48b7215d72225 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:10:32 +1000 Subject: [PATCH 14/38] [PM-25203] Resolve circular dependencies through LooseComponentsModule (#16157) * Update modules to not import loose-components Instead they should import their dependencies directly. Only OssModule imports loose-components.module.ts. * Remove unused imports and exports --- .../organizations/collections/vault.module.ts | 2 - .../organizations/members/members.module.ts | 4 +- .../organizations/organization.module.ts | 4 +- .../organizations/policies/policies.module.ts | 5 ++- .../organization-reporting.module.ts | 9 +--- .../settings/organization-settings.module.ts | 9 +++- .../two-factor/two-factor-setup.component.ts | 5 ++- .../organization-billing.module.ts | 4 +- apps/web/src/app/oss.module.ts | 7 ++- apps/web/src/app/shared/index.ts | 1 - .../src/app/shared/loose-components.module.ts | 44 +------------------ .../app/tools/import/org-import.component.ts | 5 ++- .../org-vault-export.component.ts | 5 ++- .../vault/individual-vault/vault.module.ts | 3 +- .../device-approvals.component.ts | 4 +- .../organizations/organizations.module.ts | 4 +- 16 files changed, 38 insertions(+), 77 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.module.ts b/apps/web/src/app/admin-console/organizations/collections/vault.module.ts index 037a27cd781..1a093ff8352 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.module.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.module.ts @@ -1,6 +1,5 @@ import { NgModule } from "@angular/core"; -import { LooseComponentsModule } from "../../../shared/loose-components.module"; import { SharedModule } from "../../../shared/shared.module"; import { OrganizationBadgeModule } from "../../../vault/individual-vault/organization-badge/organization-badge.module"; import { ViewComponent } from "../../../vault/individual-vault/view.component"; @@ -15,7 +14,6 @@ import { VaultComponent } from "./vault.component"; imports: [ VaultRoutingModule, SharedModule, - LooseComponentsModule, GroupBadgeModule, CollectionNameBadgeComponent, OrganizationBadgeModule, diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index efc091cb335..e5bc5f29a3b 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -6,7 +6,7 @@ import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; import { ScrollLayoutDirective } from "@bitwarden/components"; import { OrganizationFreeTrialWarningComponent } from "@bitwarden/web-vault/app/billing/organizations/warnings/components"; -import { LooseComponentsModule } from "../../../shared"; +import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedOrganizationModule } from "../shared"; import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component"; @@ -22,10 +22,10 @@ import { MembersComponent } from "./members.component"; @NgModule({ imports: [ SharedOrganizationModule, - LooseComponentsModule, MembersRoutingModule, UserDialogModule, PasswordCalloutComponent, + HeaderModule, ScrollingModule, PasswordStrengthV2Component, ScrollLayoutDirective, diff --git a/apps/web/src/app/admin-console/organizations/organization.module.ts b/apps/web/src/app/admin-console/organizations/organization.module.ts index d956174149b..2f0077d313e 100644 --- a/apps/web/src/app/admin-console/organizations/organization.module.ts +++ b/apps/web/src/app/admin-console/organizations/organization.module.ts @@ -4,7 +4,7 @@ import { NgModule } from "@angular/core"; import { ScrollLayoutDirective } from "@bitwarden/components"; import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module"; -import { LooseComponentsModule } from "../../shared"; +import { HeaderModule } from "../../layouts/header/header.module"; import { CoreOrganizationModule } from "./core"; import { GroupAddEditComponent } from "./manage/group-add-edit.component"; @@ -19,7 +19,7 @@ import { AccessSelectorModule } from "./shared/components/access-selector"; AccessSelectorModule, CoreOrganizationModule, OrganizationsRoutingModule, - LooseComponentsModule, + HeaderModule, ScrollingModule, ScrollLayoutDirective, OrganizationWarningsModule, diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.module.ts b/apps/web/src/app/admin-console/organizations/policies/policies.module.ts index 3999f36ecad..95b22497eba 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.module.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.module.ts @@ -1,6 +1,7 @@ import { NgModule } from "@angular/core"; -import { LooseComponentsModule, SharedModule } from "../../../shared"; +import { HeaderModule } from "../../../layouts/header/header.module"; +import { SharedModule } from "../../../shared"; import { DisableSendPolicyComponent } from "./disable-send.component"; import { MasterPasswordPolicyComponent } from "./master-password.component"; @@ -17,7 +18,7 @@ import { SingleOrgPolicyComponent } from "./single-org.component"; import { TwoFactorAuthenticationPolicyComponent } from "./two-factor-authentication.component"; @NgModule({ - imports: [SharedModule, LooseComponentsModule], + imports: [SharedModule, HeaderModule], declarations: [ DisableSendPolicyComponent, MasterPasswordPolicyComponent, diff --git a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts index 22c3edcf240..46599d7da46 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts @@ -1,19 +1,14 @@ import { NgModule } from "@angular/core"; import { ReportsSharedModule } from "../../../dirt/reports"; -import { LooseComponentsModule } from "../../../shared"; +import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared/shared.module"; import { OrganizationReportingRoutingModule } from "./organization-reporting-routing.module"; import { ReportsHomeComponent } from "./reports-home.component"; @NgModule({ - imports: [ - SharedModule, - ReportsSharedModule, - OrganizationReportingRoutingModule, - LooseComponentsModule, - ], + imports: [SharedModule, ReportsSharedModule, OrganizationReportingRoutingModule, HeaderModule], declarations: [ReportsHomeComponent], }) export class OrganizationReportingModule {} diff --git a/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts b/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts index bfff3b2aa2e..9b0c3035e98 100644 --- a/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts +++ b/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts @@ -2,8 +2,11 @@ import { NgModule } from "@angular/core"; import { ItemModule } from "@bitwarden/components"; -import { LooseComponentsModule, SharedModule } from "../../../shared"; +import { DangerZoneComponent } from "../../../auth/settings/account/danger-zone.component"; +import { HeaderModule } from "../../../layouts/header/header.module"; +import { SharedModule } from "../../../shared"; import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component"; +import { PremiumBadgeComponent } from "../../../vault/components/premium-badge.component"; import { PoliciesModule } from "../../organizations/policies"; import { AccountComponent } from "./account.component"; @@ -13,10 +16,12 @@ import { TwoFactorSetupComponent } from "./two-factor-setup.component"; @NgModule({ imports: [ SharedModule, - LooseComponentsModule, PoliciesModule, OrganizationSettingsRoutingModule, AccountFingerprintComponent, + DangerZoneComponent, + HeaderModule, + PremiumBadgeComponent, ItemModule, ], declarations: [AccountComponent, TwoFactorSetupComponent], diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts index 7259c3f0fe8..e538e72a77e 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts @@ -33,8 +33,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { DialogRef, DialogService, ItemModule } from "@bitwarden/components"; -import { LooseComponentsModule } from "../../../shared/loose-components.module"; +import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared/shared.module"; +import { PremiumBadgeComponent } from "../../../vault/components/premium-badge.component"; import { TwoFactorRecoveryComponent } from "./two-factor-recovery.component"; import { TwoFactorSetupAuthenticatorComponent } from "./two-factor-setup-authenticator.component"; @@ -47,7 +48,7 @@ import { TwoFactorVerifyComponent } from "./two-factor-verify.component"; @Component({ selector: "app-two-factor-setup", templateUrl: "two-factor-setup.component.html", - imports: [ItemModule, LooseComponentsModule, SharedModule], + imports: [ItemModule, HeaderModule, PremiumBadgeComponent, SharedModule], }) export class TwoFactorSetupComponent implements OnInit, OnDestroy { organizationId: string; diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts index d8f4b7393aa..707a854de02 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -4,7 +4,7 @@ import { NgModule } from "@angular/core"; // eslint-disable-next-line no-restricted-imports import { BannerModule } from "../../../../../../libs/components/src/banner/banner.module"; import { UserVerificationModule } from "../../auth/shared/components/user-verification"; -import { LooseComponentsModule } from "../../shared"; +import { HeaderModule } from "../../layouts/header/header.module"; import { BillingSharedModule } from "../shared"; import { AdjustSubscription } from "./adjust-subscription.component"; @@ -29,7 +29,7 @@ import { SubscriptionStatusComponent } from "./subscription-status.component"; UserVerificationModule, BillingSharedModule, OrganizationPlansComponent, - LooseComponentsModule, + HeaderModule, BannerModule, ], declarations: [ diff --git a/apps/web/src/app/oss.module.ts b/apps/web/src/app/oss.module.ts index d5fe718412a..4e04910246f 100644 --- a/apps/web/src/app/oss.module.ts +++ b/apps/web/src/app/oss.module.ts @@ -3,7 +3,9 @@ import { NgModule } from "@angular/core"; import { AuthModule } from "./auth"; import { LoginModule } from "./auth/login/login.module"; import { TrialInitiationModule } from "./billing/trial-initiation/trial-initiation.module"; -import { LooseComponentsModule, SharedModule } from "./shared"; +import { HeaderModule } from "./layouts/header/header.module"; +import { SharedModule } from "./shared"; +import { LooseComponentsModule } from "./shared/loose-components.module"; import { AccessComponent } from "./tools/send/send-access/access.component"; import { OrganizationBadgeModule } from "./vault/individual-vault/organization-badge/organization-badge.module"; import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-filter.module"; @@ -15,6 +17,7 @@ import "./shared/locales"; imports: [ SharedModule, LooseComponentsModule, + HeaderModule, TrialInitiationModule, VaultFilterModule, OrganizationBadgeModule, @@ -24,7 +27,7 @@ import "./shared/locales"; ], exports: [ SharedModule, - LooseComponentsModule, + HeaderModule, TrialInitiationModule, VaultFilterModule, OrganizationBadgeModule, diff --git a/apps/web/src/app/shared/index.ts b/apps/web/src/app/shared/index.ts index 7defcdedfda..7a1160c4105 100644 --- a/apps/web/src/app/shared/index.ts +++ b/apps/web/src/app/shared/index.ts @@ -1,2 +1 @@ export * from "./shared.module"; -export * from "./loose-components.module"; diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index d7dbdbc4ae5..f7f3aa3bfee 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -1,18 +1,7 @@ import { NgModule } from "@angular/core"; -import { - PasswordCalloutComponent, - UserVerificationFormInputComponent, - VaultTimeoutInputComponent, -} from "@bitwarden/auth/angular"; -import { LayoutComponent, NavigationModule } from "@bitwarden/components"; - -import { OrganizationLayoutComponent } from "../admin-console/organizations/layouts/organization-layout.component"; -import { VerifyRecoverDeleteOrgComponent } from "../admin-console/organizations/manage/verify-recover-delete-org.component"; import { RecoverDeleteComponent } from "../auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component"; -import { DangerZoneComponent } from "../auth/settings/account/danger-zone.component"; -import { UserVerificationModule } from "../auth/shared/components/user-verification"; import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component"; import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.component"; import { FreeBitwardenFamiliesComponent } from "../billing/members/free-bitwarden-families.component"; @@ -30,33 +19,15 @@ import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../dirt/reports/pages/organizations/weak-passwords-report.component"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { HeaderModule } from "../layouts/header/header.module"; -import { PremiumBadgeComponent } from "../vault/components/premium-badge.component"; import { OrganizationBadgeModule } from "../vault/individual-vault/organization-badge/organization-badge.module"; import { PipesModule } from "../vault/individual-vault/pipes/pipes.module"; -import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component"; import { SharedModule } from "./shared.module"; // Please do not add to this list of declarations - we should refactor these into modules when doing so makes sense until there are none left. // If you are building new functionality, please create or extend a feature module instead. @NgModule({ - imports: [ - SharedModule, - UserVerificationModule, - AccountFingerprintComponent, - OrganizationBadgeModule, - PipesModule, - PasswordCalloutComponent, - UserVerificationFormInputComponent, - DangerZoneComponent, - LayoutComponent, - NavigationModule, - HeaderModule, - OrganizationLayoutComponent, - VerifyRecoverDeleteOrgComponent, - VaultTimeoutInputComponent, - PremiumBadgeComponent, - ], + imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule], declarations: [ OrgExposedPasswordsReportComponent, OrgInactiveTwoFactorReportComponent, @@ -73,25 +44,12 @@ import { SharedModule } from "./shared.module"; VerifyRecoverDeleteComponent, ], exports: [ - UserVerificationModule, - PremiumBadgeComponent, - OrganizationLayoutComponent, - OrgExposedPasswordsReportComponent, - OrgInactiveTwoFactorReportComponent, - OrgReusedPasswordsReportComponent, - OrgUnsecuredWebsitesReportComponent, - OrgWeakPasswordsReportComponent, - PremiumBadgeComponent, RecoverDeleteComponent, RecoverTwoFactorComponent, RemovePasswordComponent, SponsoredFamiliesComponent, - FreeBitwardenFamiliesComponent, - SponsoringOrgRowComponent, VerifyEmailTokenComponent, VerifyRecoverDeleteComponent, - HeaderModule, - DangerZoneComponent, ], }) export class LooseComponentsModule {} diff --git a/apps/web/src/app/tools/import/org-import.component.ts b/apps/web/src/app/tools/import/org-import.component.ts index fd833f3a698..8fb5a582b1a 100644 --- a/apps/web/src/app/tools/import/org-import.component.ts +++ b/apps/web/src/app/tools/import/org-import.component.ts @@ -14,13 +14,14 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ImportCollectionServiceAbstraction } from "@bitwarden/importer-core"; import { ImportComponent } from "@bitwarden/importer-ui"; -import { LooseComponentsModule, SharedModule } from "../../shared"; +import { HeaderModule } from "../../layouts/header/header.module"; +import { SharedModule } from "../../shared"; import { ImportCollectionAdminService } from "./import-collection-admin.service"; @Component({ templateUrl: "org-import.component.html", - imports: [SharedModule, ImportComponent, LooseComponentsModule], + imports: [SharedModule, ImportComponent, HeaderModule], providers: [ { provide: ImportCollectionServiceAbstraction, diff --git a/apps/web/src/app/tools/vault-export/org-vault-export.component.ts b/apps/web/src/app/tools/vault-export/org-vault-export.component.ts index 94cc9bf18f7..a1de4814a13 100644 --- a/apps/web/src/app/tools/vault-export/org-vault-export.component.ts +++ b/apps/web/src/app/tools/vault-export/org-vault-export.component.ts @@ -5,11 +5,12 @@ import { ActivatedRoute } from "@angular/router"; import { ExportComponent } from "@bitwarden/vault-export-ui"; -import { LooseComponentsModule, SharedModule } from "../../shared"; +import { HeaderModule } from "../../layouts/header/header.module"; +import { SharedModule } from "../../shared"; @Component({ templateUrl: "org-vault-export.component.html", - imports: [SharedModule, ExportComponent, LooseComponentsModule], + imports: [SharedModule, ExportComponent, HeaderModule], }) export class OrganizationVaultExportComponent implements OnInit { protected routeOrgId: string = null; diff --git a/apps/web/src/app/vault/individual-vault/vault.module.ts b/apps/web/src/app/vault/individual-vault/vault.module.ts index 57d3df30df7..573eceef64a 100644 --- a/apps/web/src/app/vault/individual-vault/vault.module.ts +++ b/apps/web/src/app/vault/individual-vault/vault.module.ts @@ -3,7 +3,7 @@ import { NgModule } from "@angular/core"; import { CollectionNameBadgeComponent } from "../../admin-console/organizations/collections"; import { GroupBadgeModule } from "../../admin-console/organizations/collections/group-badge/group-badge.module"; import { CollectionDialogComponent } from "../../admin-console/organizations/shared/components/collection-dialog"; -import { LooseComponentsModule, SharedModule } from "../../shared"; +import { SharedModule } from "../../shared"; import { BulkDialogsModule } from "./bulk-action-dialogs/bulk-dialogs.module"; import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module"; @@ -20,7 +20,6 @@ import { ViewComponent } from "./view.component"; CollectionNameBadgeComponent, PipesModule, SharedModule, - LooseComponentsModule, BulkDialogsModule, CollectionDialogComponent, VaultComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts index f41b91261f7..bd62d972500 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts @@ -21,7 +21,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { TableDataSource, NoItemsModule, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared"; +import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module"; @Component({ @@ -43,7 +43,7 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module"; ], }), ] satisfies SafeProvider[], - imports: [SharedModule, NoItemsModule, LooseComponentsModule], + imports: [SharedModule, NoItemsModule, HeaderModule], }) export class DeviceApprovalsComponent implements OnInit, OnDestroy { tableDataSource = new TableDataSource(); diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations.module.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations.module.ts index e19d028b007..9fce712e325 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; -import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared"; +import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module"; import { SsoComponent } from "../../auth/sso/sso.component"; @@ -11,7 +11,7 @@ import { ScimComponent } from "./manage/scim.component"; import { OrganizationsRoutingModule } from "./organizations-routing.module"; @NgModule({ - imports: [SharedModule, OrganizationsRoutingModule, LooseComponentsModule], + imports: [SharedModule, OrganizationsRoutingModule, HeaderModule], declarations: [ SsoComponent, ScimComponent, From 7ccb94d0a22dadde6c3d83b361301d0d067c2c1c Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Wed, 27 Aug 2025 09:25:36 -0400 Subject: [PATCH 15/38] fix missing null check (#16180) --- .../components/collection-dialog/collection-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts index 52c418a5211..5b207b5a688 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts @@ -421,7 +421,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { collectionView.users = this.formGroup.controls.access.value .filter((v) => v.type === AccessItemType.Member) .map(convertToSelectionView); - collectionView.defaultUserCollectionEmail = this.collection.defaultUserCollectionEmail; + collectionView.defaultUserCollectionEmail = this.collection?.defaultUserCollectionEmail; const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); From fcc2bc96d1779110fa8b16e374f9ace6dcbaf038 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 27 Aug 2025 09:03:44 -0500 Subject: [PATCH 16/38] [PM-21024] Use Server for Password Change URLs (#14912) * migrate change login password service to use bitwarden server rather than fetch directly - avoids CSP entirely * add `HelpUsersUpdatePasswords` policy to policy type * add `HelpUsersUpdatePasswordsPolicy` components * allow list description override for policy description * add `HelpUsersUpdatePasswordsPolicy` when the feature flag is enabled * apply `HelpUsersUpdatePasswords` to everyone in an org * use policy to guard the well known password API * fix tests * refactor to use `policyAppliesToUser$` * remove policy work for change password - this was removed from scope * update copy for show favicon setting - it now handles both favicons and change password urls * remove favicon setting description - no longer needed * only call change password service when the setting is enabled * add popover for permitting cipher details * import permit popover directly into the settings component * replace `nativeFetch` with `fetch` * use string literal to construct URL rather than `URL` class - The `getIconsUrl` can return with an appended path which the new URL constructor will strip when passed as the base parameter * use string literal to construct URL rather than `URL` class instance (#16045) - The `getIconsUrl` can return with an appended path which the new URL constructor will strip when passed as the base parameter * [PM-24716] UI changes for Change URI work (#16043) * use platform service to launch the URI - this allows desktop to open a separate browser instance rather than use electron * fix spacing on web app * add bitLink for focus/hover states * remove spacing around links --- apps/browser/src/_locales/en/messages.json | 18 +-- .../settings/appearance-v2.component.html | 5 +- .../popup/settings/appearance-v2.component.ts | 2 + .../src/app/accounts/settings.component.html | 7 +- .../src/app/accounts/settings.component.ts | 2 + apps/desktop/src/app/app.module.ts | 1 - apps/desktop/src/locales/en/messages.json | 13 +- .../app/settings/preferences.component.html | 28 ++-- .../src/app/settings/preferences.component.ts | 8 +- apps/web/src/locales/en/messages.json | 13 +- .../response/change-password-uri.response.ts | 10 ++ ...rmit-cipher-details-popover.component.html | 22 +++ ...permit-cipher-details-popover.component.ts | 19 +++ libs/vault/src/index.ts | 1 + ...ault-change-login-password.service.spec.ts | 143 ++++++++---------- .../default-change-login-password.service.ts | 93 +++++------- 16 files changed, 205 insertions(+), 180 deletions(-) create mode 100644 libs/common/src/vault/models/response/change-password-uri.response.ts create mode 100644 libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.html create mode 100644 libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 8390bc59633..00e83004016 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1760,14 +1760,8 @@ "popupU2fCloseMessage": { "message": "This browser cannot process U2F requests in this popup window. Do you want to open this popup in a new window so that you can log in using U2F?" }, - "enableFavicon": { - "message": "Show website icons" - }, - "faviconDesc": { - "message": "Show a recognizable image next to each login." - }, - "faviconDescAlt": { - "message": "Show a recognizable image next to each login. Applies to all logged in accounts." + "showIconsChangePasswordUrls": { + "message": "Show website icons and retrieve change password URLs" }, "enableBadgeCounter": { "message": "Show badge counter" @@ -4376,7 +4370,7 @@ }, "uriMatchDefaultStrategyHint": { "message": "URI match detection is how Bitwarden identifies autofill suggestions.", - "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", @@ -5563,6 +5557,12 @@ "message": "Easily create strong and unique passwords by clicking on the Generate password button to help you keep your logins secure.", "description": "Aria label for the body content of the generator nudge" }, + "aboutThisSetting": { + "message": "About this setting" + }, + "permitCipherDetailsDescription": { + "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." }, diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.html b/apps/browser/src/vault/popup/settings/appearance-v2.component.html index 4f7f2757e0e..c9598c76db0 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.html +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.html @@ -45,7 +45,10 @@ - {{ "enableFavicon" | i18n }} + + {{ "showIconsChangePasswordUrls" | i18n }} + + diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts index d998ef846d2..23a609bd008 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts @@ -23,6 +23,7 @@ import { Option, SelectModule, } from "@bitwarden/components"; +import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault"; import { PopupWidthOption } from "../../../platform/browser/browser-popup-utils"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; @@ -46,6 +47,7 @@ import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-butto ReactiveFormsModule, CheckboxModule, BadgeModule, + PermitCipherDetailsPopoverComponent, ], }) export class AppearanceV2Component implements OnInit { diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index a9fdcd2f088..091864e59ae 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -162,14 +162,15 @@ - {{ "enableFavicon" | i18n }} + {{ "showIconsChangePasswordUrls" | i18n }} +
+ +
- {{ "faviconDesc" | i18n }} diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index fd17873a4b1..a6948c689cd 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -52,6 +52,7 @@ import { TypographyModule, } from "@bitwarden/components"; import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management"; +import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault"; import { SetPinComponent } from "../../auth/components/set-pin.component"; import { SshAgentPromptType } from "../../autofill/models/ssh-agent-setting"; @@ -81,6 +82,7 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man SelectModule, TypographyModule, VaultTimeoutInputComponent, + PermitCipherDetailsPopoverComponent, ], }) export class SettingsComponent implements OnInit, OnDestroy { diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index 014e29555e8..4f53e587994 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -31,7 +31,6 @@ import { SharedModule } from "./shared/shared.module"; @NgModule({ imports: [ BrowserAnimationsModule, - SharedModule, AppRoutingModule, VaultFilterModule, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index eaa5f7f0f1e..c805096189b 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1305,11 +1305,8 @@ "message": "Automatically clear copied values from your clipboard.", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, - "enableFavicon": { - "message": "Show website icons" - }, - "faviconDesc": { - "message": "Show a recognizable image next to each login." + "showIconsChangePasswordUrls": { + "message": "Show website icons and retrieve change password URLs" }, "enableMinToTray": { "message": "Minimize to tray icon" @@ -3928,6 +3925,12 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "aboutThisSetting": { + "message": "About this setting" + }, + "permitCipherDetailsDescription": { + "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + }, "assignToCollections": { "message": "Assign to collections" }, diff --git a/apps/web/src/app/settings/preferences.component.html b/apps/web/src/app/settings/preferences.component.html index 80261ecccb7..050d7395caf 100644 --- a/apps/web/src/app/settings/preferences.component.html +++ b/apps/web/src/app/settings/preferences.component.html @@ -67,23 +67,17 @@ {{ "languageDesc" | i18n }} - - - {{ "enableFavicon" | i18n }} - - - - - {{ "faviconDesc" | i18n }} - +
+ + + + {{ "showIconsChangePasswordUrls" | i18n }} + + +
+ +
+
{{ "theme" | i18n }} diff --git a/apps/web/src/app/settings/preferences.component.ts b/apps/web/src/app/settings/preferences.component.ts index e6cc35903a7..58a072ce76a 100644 --- a/apps/web/src/app/settings/preferences.component.ts +++ b/apps/web/src/app/settings/preferences.component.ts @@ -34,6 +34,7 @@ import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { DialogService } from "@bitwarden/components"; +import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault"; import { HeaderModule } from "../layouts/header/header.module"; import { SharedModule } from "../shared"; @@ -41,7 +42,12 @@ import { SharedModule } from "../shared"; @Component({ selector: "app-preferences", templateUrl: "preferences.component.html", - imports: [SharedModule, HeaderModule, VaultTimeoutInputComponent], + imports: [ + SharedModule, + HeaderModule, + VaultTimeoutInputComponent, + PermitCipherDetailsPopoverComponent, + ], }) export class PreferencesComponent implements OnInit, OnDestroy { // For use in template diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index af80433fce7..e3856e7d645 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -2103,11 +2103,8 @@ "languageDesc": { "message": "Change the language used by the web vault." }, - "enableFavicon": { - "message": "Show website icons" - }, - "faviconDesc": { - "message": "Show a recognizable image next to each login." + "showIconsChangePasswordUrls": { + "message": "Show website icons and retrieve change password URLs" }, "default": { "message": "Default" @@ -10986,6 +10983,12 @@ "message": "Billing address required to add credit.", "description": "Error message shown when trying to add credit to a trialing organization without a billing address." }, + "aboutThisSetting": { + "message": "About this setting" + }, + "permitCipherDetailsDescription": { + "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + }, "billingAddress": { "message": "Billing address" }, diff --git a/libs/common/src/vault/models/response/change-password-uri.response.ts b/libs/common/src/vault/models/response/change-password-uri.response.ts new file mode 100644 index 00000000000..1ff3424a269 --- /dev/null +++ b/libs/common/src/vault/models/response/change-password-uri.response.ts @@ -0,0 +1,10 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +export class ChangePasswordUriResponse extends BaseResponse { + uri: string | null; + + constructor(response: any) { + super(response); + this.uri = this.getResponseProperty("uri"); + } +} diff --git a/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.html b/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.html new file mode 100644 index 00000000000..1833a148616 --- /dev/null +++ b/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.html @@ -0,0 +1,22 @@ + + + +

+ {{ "permitCipherDetailsDescription" | i18n }} +

+ +
diff --git a/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.ts b/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.ts new file mode 100644 index 00000000000..8e80ddf7810 --- /dev/null +++ b/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.ts @@ -0,0 +1,19 @@ +import { Component, inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { LinkModule, PopoverModule } from "@bitwarden/components"; + +@Component({ + selector: "vault-permit-cipher-details-popover", + templateUrl: "./permit-cipher-details-popover.component.html", + imports: [PopoverModule, JslibModule, LinkModule], +}) +export class PermitCipherDetailsPopoverComponent { + private platformUtilService = inject(PlatformUtilsService); + + openLearnMore(e: Event) { + e.preventDefault(); + this.platformUtilService.launchUri("https://bitwarden.com/help/website-icons/"); + } +} diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index f3925ac3379..efaefc77ade 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -20,6 +20,7 @@ export { openPasswordHistoryDialog } from "./components/password-history/passwor export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.component"; export * from "./components/carousel"; export * from "./components/new-cipher-menu/new-cipher-menu.component"; +export * from "./components/permit-cipher-details-popover/permit-cipher-details-popover.component"; export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service"; export { SshImportPromptService } from "./services/ssh-import-prompt.service"; diff --git a/libs/vault/src/services/default-change-login-password.service.spec.ts b/libs/vault/src/services/default-change-login-password.service.spec.ts index c9628797f4d..42242f2e4a8 100644 --- a/libs/vault/src/services/default-change-login-password.service.spec.ts +++ b/libs/vault/src/services/default-change-login-password.service.spec.ts @@ -4,10 +4,14 @@ */ import { mock } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { ClientType } from "@bitwarden/common/enums"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { + Environment, + EnvironmentService, +} from "@bitwarden/common/platform/abstractions/environment.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; @@ -18,37 +22,30 @@ import { DefaultChangeLoginPasswordService } from "./default-change-login-passwo describe("DefaultChangeLoginPasswordService", () => { let service: DefaultChangeLoginPasswordService; - let mockShouldNotExistResponse: Response; - let mockWellKnownResponse: Response; - - const getClientType = jest.fn(() => ClientType.Browser); - const mockApiService = mock(); - const platformUtilsService = mock({ - getClientType, - }); + const mockDomainSettingsService = mock(); + + const showFavicons$ = new BehaviorSubject(true); beforeEach(() => { - mockApiService.nativeFetch.mockClear(); + mockApiService.fetch.mockClear(); + mockApiService.fetch.mockImplementation(() => + Promise.resolve({ ok: true, json: () => Promise.resolve({ uri: null }) } as Response), + ); - // Default responses to success state - mockShouldNotExistResponse = new Response("Not Found", { status: 404 }); - mockWellKnownResponse = new Response("OK", { status: 200 }); + mockDomainSettingsService.showFavicons$ = showFavicons$; - mockApiService.nativeFetch.mockImplementation((request) => { - if ( - request.url.endsWith("resource-that-should-not-exist-whose-status-code-should-not-be-200") - ) { - return Promise.resolve(mockShouldNotExistResponse); - } + const mockEnvironmentService = { + environment$: of({ + getIconsUrl: () => "https://icons.bitwarden.com", + } as Environment), + } as EnvironmentService; - if (request.url.endsWith(".well-known/change-password")) { - return Promise.resolve(mockWellKnownResponse); - } - - throw new Error("Unexpected request"); - }); - service = new DefaultChangeLoginPasswordService(mockApiService, platformUtilsService); + service = new DefaultChangeLoginPasswordService( + mockApiService, + mockEnvironmentService, + mockDomainSettingsService, + ); }); it("should return null for non-login ciphers", async () => { @@ -85,7 +82,7 @@ describe("DefaultChangeLoginPasswordService", () => { expect(url).toBeNull(); }); - it("should check the origin for a reliable status code", async () => { + it("should call the icons url endpoint", async () => { const cipher = { type: CipherType.Login, login: Object.assign(new LoginView(), { @@ -95,35 +92,42 @@ describe("DefaultChangeLoginPasswordService", () => { await service.getChangePasswordUrl(cipher); - expect(mockApiService.nativeFetch).toHaveBeenCalledWith( + expect(mockApiService.fetch).toHaveBeenCalledWith( expect.objectContaining({ - url: "https://example.com/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200", + url: "https://icons.bitwarden.com/change-password-uri?uri=https%3A%2F%2Fexample.com%2F", }), ); }); - it("should attempt to fetch the well-known change password URL", async () => { + it("should return the original URI when unable to verify the response", async () => { + mockApiService.fetch.mockImplementation(() => + Promise.resolve({ ok: true, json: () => Promise.resolve({ uri: null }) } as Response), + ); + const cipher = { type: CipherType.Login, login: Object.assign(new LoginView(), { - uris: [{ uri: "https://example.com" }], + uris: [{ uri: "https://example.com/" }], }), } as CipherView; - await service.getChangePasswordUrl(cipher); + const url = await service.getChangePasswordUrl(cipher); - expect(mockApiService.nativeFetch).toHaveBeenCalledWith( - expect.objectContaining({ - url: "https://example.com/.well-known/change-password", - }), - ); + expect(url).toBe("https://example.com/"); }); - it("should return the well-known change password URL when successful at verifying the response", async () => { + it("should return the well known change url from the response", async () => { + mockApiService.fetch.mockImplementation(() => { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ uri: "https://example.com/.well-known/change-password" }), + } as Response); + }); + const cipher = { type: CipherType.Login, login: Object.assign(new LoginView(), { - uris: [{ uri: "https://example.com" }], + uris: [{ uri: "https://example.com/" }, { uri: "https://working.com/" }], }), } as CipherView; @@ -132,49 +136,20 @@ describe("DefaultChangeLoginPasswordService", () => { expect(url).toBe("https://example.com/.well-known/change-password"); }); - it("should return the original URI when unable to verify the response", async () => { - mockShouldNotExistResponse = new Response("Ok", { status: 200 }); - - const cipher = { - type: CipherType.Login, - login: Object.assign(new LoginView(), { - uris: [{ uri: "https://example.com/" }], - }), - } as CipherView; - - const url = await service.getChangePasswordUrl(cipher); - - expect(url).toBe("https://example.com/"); - }); - - it("should return the original URI when the well-known URL is not found", async () => { - mockWellKnownResponse = new Response("Not Found", { status: 404 }); - - const cipher = { - type: CipherType.Login, - login: Object.assign(new LoginView(), { - uris: [{ uri: "https://example.com/" }], - }), - } as CipherView; - - const url = await service.getChangePasswordUrl(cipher); - - expect(url).toBe("https://example.com/"); - }); - it("should try the next URI if the first one fails", async () => { - mockApiService.nativeFetch.mockImplementation((request) => { - if ( - request.url.endsWith("resource-that-should-not-exist-whose-status-code-should-not-be-200") - ) { - return Promise.resolve(mockShouldNotExistResponse); + mockApiService.fetch.mockImplementation((request) => { + if (request.url.includes("no-wellknown.com")) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ uri: null }), + } as Response); } - if (request.url.endsWith(".well-known/change-password")) { - if (request.url.includes("working.com")) { - return Promise.resolve(mockWellKnownResponse); - } - return Promise.resolve(new Response("Not Found", { status: 404 })); + if (request.url.includes("working.com")) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ uri: "https://working.com/.well-known/change-password" }), + } as Response); } throw new Error("Unexpected request"); @@ -192,19 +167,19 @@ describe("DefaultChangeLoginPasswordService", () => { expect(url).toBe("https://working.com/.well-known/change-password"); }); - it("should return the first URI when the client type is not browser", async () => { - getClientType.mockReturnValue(ClientType.Web); + it("returns the first URI when `showFavicons$` setting is disabled", async () => { + showFavicons$.next(false); const cipher = { type: CipherType.Login, login: Object.assign(new LoginView(), { - uris: [{ uri: "https://example.com/" }, { uri: "https://example-2.com/" }], + uris: [{ uri: "https://example.com/" }, { uri: "https://another.com/" }], }), } as CipherView; const url = await service.getChangePasswordUrl(cipher); - expect(mockApiService.nativeFetch).not.toHaveBeenCalled(); expect(url).toBe("https://example.com/"); + expect(mockApiService.fetch).not.toHaveBeenCalled(); }); }); diff --git a/libs/vault/src/services/default-change-login-password.service.ts b/libs/vault/src/services/default-change-login-password.service.ts index a0b5646c5a9..929f5819c02 100644 --- a/libs/vault/src/services/default-change-login-password.service.ts +++ b/libs/vault/src/services/default-change-login-password.service.ts @@ -1,9 +1,12 @@ import { Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { ChangePasswordUriResponse } from "@bitwarden/common/vault/models/response/change-password-uri.response"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ChangeLoginPasswordService } from "../abstractions/change-login-password.service"; @@ -12,7 +15,8 @@ import { ChangeLoginPasswordService } from "../abstractions/change-login-passwor export class DefaultChangeLoginPasswordService implements ChangeLoginPasswordService { constructor( private apiService: ApiService, - private platformUtilsService: PlatformUtilsService, + private environmentService: EnvironmentService, + private domainSettingsService: DomainSettingsService, ) {} /** @@ -33,24 +37,19 @@ export class DefaultChangeLoginPasswordService implements ChangeLoginPasswordSer return null; } - // CSP policies on the web and desktop restrict the application from making - // cross-origin requests, breaking the below .well-known URL checks. - // For those platforms, this will short circuit and return the first URL. - // PM-21024 will build a solution for the server side to handle this. - if (this.platformUtilsService.getClientType() !== "browser") { + const enableFaviconChangePassword = await firstValueFrom( + this.domainSettingsService.showFavicons$, + ); + + // When the setting is not enabled, return the first URL + if (!enableFaviconChangePassword) { return urls[0].href; } for (const url of urls) { - const [reliable, wellKnownChangeUrl] = await Promise.all([ - this.hasReliableHttpStatusCode(url.origin), - this.getWellKnownChangePasswordUrl(url.origin), - ]); + const wellKnownChangeUrl = await this.fetchWellKnownChangePasswordUri(url.href); - // Some servers return a 200 OK for a resource that should not exist - // Which means we cannot trust the well-known URL is valid, so we skip it - // to avoid potentially sending users to a 404 page - if (reliable && wellKnownChangeUrl != null) { + if (wellKnownChangeUrl) { return wellKnownChangeUrl; } } @@ -60,55 +59,41 @@ export class DefaultChangeLoginPasswordService implements ChangeLoginPasswordSer } /** - * Checks if the server returns a non-200 status code for a resource that should not exist. - * See https://w3c.github.io/webappsec-change-password-url/response-code-reliability.html#semantics - * @param urlOrigin The origin of the URL to check + * Fetches the well-known change-password-uri for the given URL. + * @returns The full URL to the change password page, or null if it could not be found. */ - private async hasReliableHttpStatusCode(urlOrigin: string): Promise { - try { - const url = new URL( - "./.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200", - urlOrigin, - ); + private async fetchWellKnownChangePasswordUri(url: string): Promise { + const getChangePasswordUriRequest = await this.buildChangePasswordUriRequest(url); - const request = new Request(url, { - method: "GET", - mode: "same-origin", - credentials: "omit", - cache: "no-store", - redirect: "follow", - }); + const response = await this.apiService.fetch(getChangePasswordUriRequest); - const response = await this.apiService.nativeFetch(request); - return !response.ok; - } catch { - return false; + if (!response.ok) { + return null; } + + const data = await response.json(); + + const { uri } = new ChangePasswordUriResponse(data); + + return uri; } /** - * Builds a well-known change password URL for the given origin. Attempts to fetch the URL to ensure a valid response - * is returned. Returns null if the request throws or the response is not 200 OK. - * See https://w3c.github.io/webappsec-change-password-url/ - * @param urlOrigin The origin of the URL to check + * Construct the request for the change-password-uri endpoint. */ - private async getWellKnownChangePasswordUrl(urlOrigin: string): Promise { - try { - const url = new URL("./.well-known/change-password", urlOrigin); + private async buildChangePasswordUriRequest(cipherUri: string): Promise { + const searchParams = new URLSearchParams(); + searchParams.set("uri", cipherUri); - const request = new Request(url, { - method: "GET", - mode: "same-origin", - credentials: "omit", - cache: "no-store", - redirect: "follow", - }); + // The change-password-uri endpoint lives within the icons service + // as it uses decrypted cipher data. + const env = await firstValueFrom(this.environmentService.environment$); + const iconsUrl = env.getIconsUrl(); - const response = await this.apiService.nativeFetch(request); + const url = new URL(`${iconsUrl}/change-password-uri?${searchParams.toString()}`); - return response.ok ? url.toString() : null; - } catch { - return null; - } + return new Request(url, { + method: "GET", + }); } } From 4c960906fa2a3d18cd43a7875082ad6ce6665cda Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Wed, 27 Aug 2025 16:32:18 +0200 Subject: [PATCH 17/38] Account Recovery with Key Connector enabled not working prior to removal of Master Password (#15616) --- libs/angular/src/auth/guards/auth.guard.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/libs/angular/src/auth/guards/auth.guard.ts b/libs/angular/src/auth/guards/auth.guard.ts index 8e8e70a6d29..37c464a804d 100644 --- a/libs/angular/src/auth/guards/auth.guard.ts +++ b/libs/angular/src/auth/guards/auth.guard.ts @@ -74,13 +74,6 @@ export const authGuard: CanActivateFn = async ( return router.createUrlTree(["lock"], { queryParams: { promptBiometric: true } }); } - if ( - !routerState.url.includes("remove-password") && - (await firstValueFrom(keyConnectorService.convertAccountRequired$)) - ) { - return router.createUrlTree(["/remove-password"]); - } - // Handle cases where a user needs to set a password when they don't already have one: // - TDE org user has been given "manage account recovery" permission // - TDE offboarding on a trusted device, where we have access to their encryption key wrap with their new password @@ -106,5 +99,14 @@ export const authGuard: CanActivateFn = async ( return router.createUrlTree([route]); } + // Remove password when Key Connector is enabled + if ( + forceSetPasswordReason == ForceSetPasswordReason.None && + !routerState.url.includes("remove-password") && + (await firstValueFrom(keyConnectorService.convertAccountRequired$)) + ) { + return router.createUrlTree(["/remove-password"]); + } + return true; }; From 5f7c0ae999970dbbb05a111d074eb20a7258da15 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Wed, 27 Aug 2025 11:56:42 -0400 Subject: [PATCH 18/38] build: ensure new libraries are added to the root jest.config (#16166) * Add missing libs to jest.config.js Added 15 missing libraries to the jest projects array: - libs/assets - libs/client-type - libs/core-test-utils - libs/dirt/card - libs/guid - libs/logging - libs/messaging-internal - libs/messaging - libs/serialization - libs/state-test-utils - libs/state - libs/storage-core - libs/storage-test-utils - libs/tools/export/vault-export/vault-export-ui - libs/user-core This ensures all existing libraries with jest.config.js files are included in CI test runs. * Update basic-lib generator to add new libs to jest.config.js - Added updateJestConfig function that automatically adds new libraries to jest.config.js - Function finds the appropriate alphabetical position for the new library - Added comprehensive tests for the new functionality - Ensures new libraries are included in CI test runs from creation This prevents the issue where new libraries are created but their tests are not run in CI because they are missing from the jest configuration. * Fix import statements in state-definitions and deserialization-helpers tests - Fixed ClientLocations import in state-definitions.spec.ts to use @bitwarden/storage-core instead of relative import - Simplified deserialization-helpers.spec.ts import to use library root @bitwarden/serialization --- jest.config.js | 25 +++++-- .../src/generators/basic-lib.spec.ts | 41 ++++++++++++ libs/nx-plugin/src/generators/basic-lib.ts | 65 +++++++++++++++++++ .../src/deserialization-helpers.spec.ts | 2 +- libs/state/src/core/state-definitions.spec.ts | 4 +- 5 files changed, 130 insertions(+), 7 deletions(-) diff --git a/jest.config.js b/jest.config.js index b0ffd2382ca..3e4fb665a8c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -26,24 +26,39 @@ module.exports = { "/libs/admin-console/jest.config.js", "/libs/angular/jest.config.js", + "/libs/assets/jest.config.js", "/libs/auth/jest.config.js", "/libs/billing/jest.config.js", + "/libs/client-type/jest.config.js", "/libs/common/jest.config.js", "/libs/components/jest.config.js", + "/libs/core-test-utils/jest.config.js", + "/libs/dirt/card/jest.config.js", "/libs/eslint/jest.config.js", + "/libs/guid/jest.config.js", + "/libs/importer/jest.config.js", + "/libs/key-management/jest.config.js", + "/libs/key-management-ui/jest.config.js", + "/libs/logging/jest.config.js", + "/libs/messaging-internal/jest.config.js", + "/libs/messaging/jest.config.js", + "/libs/node/jest.config.js", + "/libs/platform/jest.config.js", + "/libs/serialization/jest.config.js", + "/libs/state-test-utils/jest.config.js", + "/libs/state/jest.config.js", + "/libs/storage-core/jest.config.js", + "/libs/storage-test-utils/jest.config.js", "/libs/tools/export/vault-export/vault-export-core/jest.config.js", + "/libs/tools/export/vault-export/vault-export-ui/jest.config.js", "/libs/tools/generator/core/jest.config.js", "/libs/tools/generator/components/jest.config.js", "/libs/tools/generator/extensions/history/jest.config.js", "/libs/tools/generator/extensions/legacy/jest.config.js", "/libs/tools/generator/extensions/navigation/jest.config.js", "/libs/tools/send/send-ui/jest.config.js", - "/libs/importer/jest.config.js", - "/libs/platform/jest.config.js", - "/libs/node/jest.config.js", + "/libs/user-core/jest.config.js", "/libs/vault/jest.config.js", - "/libs/key-management/jest.config.js", - "/libs/key-management-ui/jest.config.js", ], // Workaround for a memory leak that crashes tests in CI: diff --git a/libs/nx-plugin/src/generators/basic-lib.spec.ts b/libs/nx-plugin/src/generators/basic-lib.spec.ts index a0357ca1751..9fd7a702375 100644 --- a/libs/nx-plugin/src/generators/basic-lib.spec.ts +++ b/libs/nx-plugin/src/generators/basic-lib.spec.ts @@ -83,3 +83,44 @@ describe("basic-lib generator", () => { expect(tree.exists(`libs/test/src/test.spec.ts`)).toBeTruthy(); }); }); + +it("should update jest.config.js with new library", async () => { + // Create a mock jest.config.js with existing libs + const existingJestConfig = `module.exports = { + projects: [ + "/apps/browser/jest.config.js", + "/libs/admin-console/jest.config.js", + "/libs/auth/jest.config.js", + "/libs/vault/jest.config.js", + ], +};`; + tree.write("jest.config.js", existingJestConfig); + + await basicLibGenerator(tree, options); + + const jestConfigContent = tree.read("jest.config.js"); + expect(jestConfigContent).not.toBeNull(); + const jestConfig = jestConfigContent?.toString(); + + // Should contain the new library in alphabetical order + expect(jestConfig).toContain('"/libs/test/jest.config.js",'); + + // Should be in the right alphabetical position (after auth, before vault) + const authIndex = jestConfig?.indexOf('"/libs/auth/jest.config.js"'); + const testIndex = jestConfig?.indexOf('"/libs/test/jest.config.js"'); + const vaultIndex = jestConfig?.indexOf('"/libs/vault/jest.config.js"'); + + expect(authIndex).toBeDefined(); + expect(testIndex).toBeDefined(); + expect(vaultIndex).toBeDefined(); + expect(authIndex! < testIndex!).toBeTruthy(); + expect(testIndex! < vaultIndex!).toBeTruthy(); +}); + +it("should handle missing jest.config.js file gracefully", async () => { + const consoleSpy = jest.spyOn(console, "warn").mockImplementation(); + // Don't create jest.config.js file + await basicLibGenerator(tree, options); + expect(consoleSpy).toHaveBeenCalledWith("jest.config.js file not found at root"); + consoleSpy.mockRestore(); +}); diff --git a/libs/nx-plugin/src/generators/basic-lib.ts b/libs/nx-plugin/src/generators/basic-lib.ts index 6b214d18921..4f2f542ac89 100644 --- a/libs/nx-plugin/src/generators/basic-lib.ts +++ b/libs/nx-plugin/src/generators/basic-lib.ts @@ -53,6 +53,9 @@ export async function basicLibGenerator( // Update CODEOWNERS with the new lib updateCodeowners(tree, options.directory, options.name, options.team); + // Update jest.config.js with the new lib + updateJestConfig(tree, options.directory, options.name); + // Format all new files with prettier await formatFiles(tree); @@ -124,4 +127,66 @@ function updateCodeowners(tree: Tree, directory: string, name: string, team: str tree.write(codeownersPath, content + newLine); } +/** + * Updates the jest.config.js file to include the new library + * This ensures the library's tests are included in CI runs + * + * @param {Tree} tree - The virtual file system tree + * @param {string} directory - Directory where the library is created + * @param {string} name - The library name + */ +function updateJestConfig(tree: Tree, directory: string, name: string) { + const jestConfigPath = "jest.config.js"; + + if (!tree.exists(jestConfigPath)) { + console.warn("jest.config.js file not found at root"); + return; + } + + const content = tree.read(jestConfigPath)?.toString() || ""; + const libJestPath = `"/${directory}/${name}/jest.config.js",`; + + // Find the libs section and insert the new library in alphabetical order + const lines = content.split("\n"); + let insertIndex = -1; + let foundLibsSection = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Check if we're in the libs section + if (line.includes('"/libs/')) { + foundLibsSection = true; + + // Extract the lib name for comparison + const match = line.match(/"libs([^"]+)/); + if (match) { + const existingLibName = match[1]; + + // If the new lib should come before this existing lib alphabetically + if (name < existingLibName) { + insertIndex = i; + break; + } + } + } + // If we were in libs section but hit a non-libs line, insert at end of libs + else if (foundLibsSection && !line.includes('"/libs/')) { + insertIndex = i; + break; + } + } + + if (insertIndex === -1) { + console.warn(`Could not find appropriate location to insert ${name} in jest.config.js`); + return; + } + + // Insert the new library line + lines.splice(insertIndex, 0, ` ${libJestPath}`); + + // Write back the updated content + tree.write(jestConfigPath, lines.join("\n")); +} + export default basicLibGenerator; diff --git a/libs/serialization/src/deserialization-helpers.spec.ts b/libs/serialization/src/deserialization-helpers.spec.ts index 1918673c8d2..0c4bd2f06eb 100644 --- a/libs/serialization/src/deserialization-helpers.spec.ts +++ b/libs/serialization/src/deserialization-helpers.spec.ts @@ -1,4 +1,4 @@ -import { record } from "@bitwarden/serialization/deserialization-helpers"; +import { record } from "@bitwarden/serialization"; describe("deserialization helpers", () => { describe("record", () => { diff --git a/libs/state/src/core/state-definitions.spec.ts b/libs/state/src/core/state-definitions.spec.ts index d0e6eb3082e..92b3f049a0c 100644 --- a/libs/state/src/core/state-definitions.spec.ts +++ b/libs/state/src/core/state-definitions.spec.ts @@ -1,4 +1,6 @@ -import { ClientLocations, StateDefinition } from "./state-definition"; +import { ClientLocations } from "@bitwarden/storage-core"; + +import { StateDefinition } from "./state-definition"; import * as stateDefinitionsRecord from "./state-definitions"; describe.each(["web", "cli", "desktop", "browser"])( From 38f62a01499b46d4ba9d9ddfeeffce2695645d12 Mon Sep 17 00:00:00 2001 From: Vicki League Date: Wed, 27 Aug 2025 12:40:16 -0400 Subject: [PATCH 19/38] [PM-25222] Fix svg alignment issues caused by new preflight defaults (#16181) --- libs/components/src/tw-theme-preflight.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/components/src/tw-theme-preflight.css b/libs/components/src/tw-theme-preflight.css index 3c38eba3bd3..e5f35885993 100644 --- a/libs/components/src/tw-theme-preflight.css +++ b/libs/components/src/tw-theme-preflight.css @@ -65,4 +65,9 @@ select { appearance: none; } + + /* overriding preflight since the apps were built assuming svgs are inline */ + svg { + display: inline; + } } From 4dd7e0cafa0b71ddc741c7191e7d51557fc2fe25 Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Wed, 27 Aug 2025 09:41:42 -0700 Subject: [PATCH 20/38] Messages: unmark critical (#16146) * Unmark as critical * Revert "Unmark as critical" This reverts commit 3fa79fc4988efefb9e2749ab04a4a60c75930246. * rename unmarkAsCriticalApp -> unmarkAsCritical --- apps/web/src/locales/en/messages.json | 4 ++-- .../app-table-row-scrollable.component.html | 4 ++-- .../access-intelligence/app-table-row-scrollable.component.ts | 2 +- .../access-intelligence/critical-applications.component.html | 2 +- .../access-intelligence/critical-applications.component.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index e3856e7d645..7de7f119e3b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -176,8 +176,8 @@ "totalApplications": { "message": "Total applications" }, - "unmarkAsCriticalApp": { - "message": "Unmark as critical app" + "unmarkAsCritical": { + "message": "Unmark as critical" }, "criticalApplicationSuccessfullyUnmarked": { "message": "Critical application successfully unmarked" diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html index 8e48b5e107d..eb7544faf05 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html @@ -88,8 +88,8 @@ > - diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.ts index a729f21158f..c6923bf5c77 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.ts @@ -19,6 +19,6 @@ export class AppTableRowScrollableComponent { @Input() selectedUrls: Set = new Set(); @Input() isDrawerIsOpenForThisRecord!: (applicationName: string) => boolean; @Input() showAppAtRiskMembers!: (applicationName: string) => void; - @Input() unmarkAsCriticalApp!: (applicationName: string) => void; + @Input() unmarkAsCritical!: (applicationName: string) => void; @Input() checkboxChange!: (applicationName: string, $event: Event) => void; } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html index ffef3f3b0b9..17967ccef0e 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html @@ -83,6 +83,6 @@ [showRowMenuForCriticalApps]="true" [isDrawerIsOpenForThisRecord]="isDrawerOpenForTableRow" [showAppAtRiskMembers]="showAppAtRiskMembers" - [unmarkAsCriticalApp]="unmarkAsCriticalApp" + [unmarkAsCritical]="unmarkAsCritical" > diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts index 531adc003c7..58e0f648749 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts @@ -106,7 +106,7 @@ export class CriticalApplicationsComponent implements OnInit { ); }; - unmarkAsCriticalApp = async (hostname: string) => { + unmarkAsCritical = async (hostname: string) => { try { await this.criticalAppsService.dropCriticalApp( this.organizationId as OrganizationId, From 1d5115f19050d81d58723b435581188928809bf0 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:51:02 -0400 Subject: [PATCH 21/38] Making accessing a project only use one api call to getbyprojectid (#15854) --- .../guards/project-access.guard.spec.ts | 135 ------------------ .../projects/guards/project-access.guard.ts | 33 ----- .../project/project-secrets.component.html | 8 +- .../project/project-secrets.component.ts | 33 +---- .../projects/project/project.component.html | 2 +- .../projects/projects-routing.module.ts | 2 - 6 files changed, 12 insertions(+), 201 deletions(-) delete mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts delete mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.ts diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts deleted file mode 100644 index 7523fa14a21..00000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Component } from "@angular/core"; -import { TestBed } from "@angular/core/testing"; -import { Router } from "@angular/router"; -import { RouterTestingModule } from "@angular/router/testing"; -import { MockProxy, mock } from "jest-mock-extended"; -import { of } from "rxjs"; - -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; -import { UserId } from "@bitwarden/common/types/guid"; -import { ToastService } from "@bitwarden/components"; -import { RouterService } from "@bitwarden/web-vault/app/core"; - -import { ProjectView } from "../../models/view/project.view"; -import { ProjectService } from "../project.service"; - -import { projectAccessGuard } from "./project-access.guard"; - -@Component({ - template: "", - standalone: false, -}) -export class GuardedRouteTestComponent {} - -@Component({ - template: "", - standalone: false, -}) -export class RedirectTestComponent {} - -describe("Project Redirect Guard", () => { - let organizationService: MockProxy; - let routerService: MockProxy; - let projectServiceMock: MockProxy; - let i18nServiceMock: MockProxy; - let toastService: MockProxy; - let router: Router; - let accountService: FakeAccountService; - const userId = Utils.newGuid() as UserId; - - const smOrg1 = { id: "123", canAccessSecretsManager: true } as Organization; - const projectView = { - id: "123", - organizationId: "123", - name: "project-name", - creationDate: Date.now.toString(), - revisionDate: Date.now.toString(), - read: true, - write: true, - } as ProjectView; - - beforeEach(async () => { - organizationService = mock(); - routerService = mock(); - projectServiceMock = mock(); - i18nServiceMock = mock(); - toastService = mock(); - accountService = mockAccountServiceWith(userId); - - TestBed.configureTestingModule({ - imports: [ - RouterTestingModule.withRoutes([ - { - path: "sm/:organizationId/projects/:projectId", - component: GuardedRouteTestComponent, - canActivate: [projectAccessGuard], - }, - { - path: "sm", - component: RedirectTestComponent, - }, - { - path: "sm/:organizationId/projects", - component: RedirectTestComponent, - }, - ]), - ], - providers: [ - { provide: OrganizationService, useValue: organizationService }, - { provide: AccountService, useValue: accountService }, - { provide: RouterService, useValue: routerService }, - { provide: ProjectService, useValue: projectServiceMock }, - { provide: I18nService, useValue: i18nServiceMock }, - { provide: ToastService, useValue: toastService }, - ], - }); - - router = TestBed.inject(Router); - }); - - it("redirects to sm/{orgId}/projects/{projectId} if project exists", async () => { - // Arrange - organizationService.organizations$.mockReturnValue(of([smOrg1])); - projectServiceMock.getByProjectId.mockReturnValue(Promise.resolve(projectView)); - - // Act - await router.navigateByUrl("sm/123/projects/123"); - - // Assert - expect(router.url).toBe("/sm/123/projects/123"); - }); - - it("redirects to sm/projects if project does not exist", async () => { - // Arrange - organizationService.organizations$.mockReturnValue(of([smOrg1])); - - // Act - await router.navigateByUrl("sm/123/projects/124"); - - // Assert - expect(router.url).toBe("/sm/123/projects"); - }); - - it("redirects to sm/123/projects if exception occurs while looking for Project", async () => { - // Arrange - jest.spyOn(projectServiceMock, "getByProjectId").mockImplementation(() => { - throw new Error("Test error"); - }); - jest.spyOn(i18nServiceMock, "t").mockReturnValue("Project not found"); - - // Act - await router.navigateByUrl("sm/123/projects/123"); - // Assert - expect(toastService.showToast).toHaveBeenCalledWith({ - variant: "error", - title: null, - message: "Project not found", - }); - expect(router.url).toBe("/sm/123/projects"); - }); -}); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.ts deleted file mode 100644 index 2c6723a56a2..00000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.ts +++ /dev/null @@ -1,33 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { inject } from "@angular/core"; -import { ActivatedRouteSnapshot, CanActivateFn, createUrlTreeFromSnapshot } from "@angular/router"; - -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ToastService } from "@bitwarden/components"; - -import { ProjectService } from "../project.service"; - -/** - * Redirects to projects list if the user doesn't have access to project. - */ -export const projectAccessGuard: CanActivateFn = async (route: ActivatedRouteSnapshot) => { - const projectService = inject(ProjectService); - const toastService = inject(ToastService); - const i18nService = inject(I18nService); - - try { - const project = await projectService.getByProjectId(route.params.projectId); - if (project) { - return true; - } - } catch { - toastService.showToast({ - variant: "error", - title: null, - message: i18nService.t("notFound", i18nService.t("project")), - }); - return createUrlTreeFromSnapshot(route, ["/sm", route.params.organizationId, "projects"]); - } - return createUrlTreeFromSnapshot(route, ["/sm", route.params.organizationId, "projects"]); -}; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html index d9919ef6bac..7a2968cfb2c 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html @@ -1,7 +1,7 @@ - - + +
; private organizationEnabled: boolean; + protected project = inject(ROUTER_OUTLET_DATA) as Signal; + readonly writeAccess = computed(() => this.project().write); + readonly projectExists = computed(() => !!this.project()); constructor( private route: ActivatedRoute, - private projectService: ProjectService, private secretService: SecretService, private dialogService: DialogService, private platformUtilsService: PlatformUtilsService, @@ -68,21 +61,9 @@ export class ProjectSecretsComponent implements OnInit { ) {} ngOnInit() { - // Refresh list if project is edited - const currentProjectEdited = this.projectService.project$.pipe( - filter((p) => p?.id === this.projectId), - startWith(null), - ); - - this.project$ = combineLatest([this.route.params, currentProjectEdited]).pipe( - switchMap(([params, _]) => { - return this.projectService.getByProjectId(params.projectId); - }), - ); - this.secrets$ = this.secretService.secret$.pipe( startWith(null), - combineLatestWith(this.route.params, currentProjectEdited), + combineLatestWith(this.route.params), switchMap(async ([_, params]) => { this.organizationId = params.organizationId; this.projectId = params.projectId; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html index d44d7898cac..ef7c8ff1275 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html @@ -36,4 +36,4 @@ {{ "editProject" | i18n }} - + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts index 231486703c9..6078520989a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts @@ -1,7 +1,6 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { projectAccessGuard } from "./guards/project-access.guard"; import { ProjectPeopleComponent } from "./project/project-people.component"; import { ProjectSecretsComponent } from "./project/project-secrets.component"; import { ProjectServiceAccountsComponent } from "./project/project-service-accounts.component"; @@ -16,7 +15,6 @@ const routes: Routes = [ { path: ":projectId", component: ProjectComponent, - canActivate: [projectAccessGuard], children: [ { path: "", From cde4890e5eba782d46a8adc05323c72629d17c73 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Wed, 27 Aug 2025 15:34:35 -0400 Subject: [PATCH 22/38] PM-24791 [Defect] White box behind Save login notification UI (#16112) * PM-24791 * update snapshots --- .../overlay-notifications-content.service.spec.ts.snap | 2 +- .../content/overlay-notifications-content.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap b/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap index 18c3baa876c..7bdde2560d0 100644 --- a/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap +++ b/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap @@ -8,7 +8,7 @@ exports[`OverlayNotificationsContentService opening the notification bar creates