From 4d15f2d43cbd79ac29b35c8e8e3244ecb59ad8e4 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Mon, 12 May 2025 09:00:01 -0400 Subject: [PATCH 001/163] [PM-16057] use Roboto as primary font (#14553) * swap to using roboto variable font --- .../content/components/constants/styles.ts | 6 +++--- .../notification/confirmation/message.ts | 2 +- .../components/notification/header-message.ts | 2 +- .../src/autofill/shared/styles/variables.scss | 2 +- apps/browser/src/popup/images/loading.svg | 2 +- apps/browser/src/popup/scss/variables.scss | 2 +- .../src/app/components/avatar.component.ts | 2 +- apps/desktop/src/images/loading.svg | 2 +- apps/desktop/src/scss/variables.scss | 2 +- .../src/app/billing/services/stripe.service.ts | 2 +- apps/web/src/images/loading-white.svg | 2 +- apps/web/src/images/loading.svg | 2 +- apps/web/src/scss/variables.scss | 2 +- libs/angular/src/scss/webfonts.css | 6 +++--- libs/angular/src/scss/webfonts/dm-sans.woff2 | Bin 88504 -> 0 bytes libs/angular/src/scss/webfonts/roboto.woff2 | Bin 0 -> 209484 bytes libs/components/src/avatar/avatar.component.ts | 2 +- libs/components/src/variables.scss | 2 +- 18 files changed, 20 insertions(+), 20 deletions(-) delete mode 100644 libs/angular/src/scss/webfonts/dm-sans.woff2 create mode 100644 libs/angular/src/scss/webfonts/roboto.woff2 diff --git a/apps/browser/src/autofill/content/components/constants/styles.ts b/apps/browser/src/autofill/content/components/constants/styles.ts index f7c9ffd4d92..08c8671ce14 100644 --- a/apps/browser/src/autofill/content/components/constants/styles.ts +++ b/apps/browser/src/autofill/content/components/constants/styles.ts @@ -144,17 +144,17 @@ export const border = { export const typography = { body1: ` line-height: 24px; - font-family: "DM Sans", sans-serif; + font-family: Roboto, sans-serif; font-size: 16px; `, body2: ` line-height: 20px; - font-family: "DM Sans", sans-serif; + font-family: Roboto, sans-serif; font-size: 14px; `, helperMedium: ` line-height: 16px; - font-family: "DM Sans", sans-serif; + font-family: Roboto, sans-serif; font-size: 12px; `, }; diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts index 521fdf45291..527119aed15 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts @@ -72,7 +72,7 @@ const baseTextStyles = css` text-align: left; text-overflow: ellipsis; line-height: 24px; - font-family: "DM Sans", sans-serif; + font-family: Roboto, sans-serif; font-size: 16px; `; diff --git a/apps/browser/src/autofill/content/components/notification/header-message.ts b/apps/browser/src/autofill/content/components/notification/header-message.ts index ccfa58b8970..47fe8cd2828 100644 --- a/apps/browser/src/autofill/content/components/notification/header-message.ts +++ b/apps/browser/src/autofill/content/components/notification/header-message.ts @@ -19,7 +19,7 @@ const notificationHeaderMessageStyles = (theme: Theme) => css` line-height: 28px; white-space: nowrap; color: ${themes[theme].text.main}; - font-family: "DM Sans", sans-serif; + font-family: Roboto, sans-serif; font-size: 18px; font-weight: 600; `; diff --git a/apps/browser/src/autofill/shared/styles/variables.scss b/apps/browser/src/autofill/shared/styles/variables.scss index ae6a060798a..1e804ed8fd2 100644 --- a/apps/browser/src/autofill/shared/styles/variables.scss +++ b/apps/browser/src/autofill/shared/styles/variables.scss @@ -1,6 +1,6 @@ $dark-icon-themes: "theme_dark"; -$font-family-sans-serif: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; +$font-family-sans-serif: Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif; $font-family-source-code-pro: "Source Code Pro", monospace; $font-size-base: 14px; diff --git a/apps/browser/src/popup/images/loading.svg b/apps/browser/src/popup/images/loading.svg index 3f2033303db..5f4102a5921 100644 --- a/apps/browser/src/popup/images/loading.svg +++ b/apps/browser/src/popup/images/loading.svg @@ -1,5 +1,5 @@  - Loading... diff --git a/apps/browser/src/popup/scss/variables.scss b/apps/browser/src/popup/scss/variables.scss index b78f06f2f3f..aea69e26436 100644 --- a/apps/browser/src/popup/scss/variables.scss +++ b/apps/browser/src/popup/scss/variables.scss @@ -1,6 +1,6 @@ $dark-icon-themes: "theme_dark"; -$font-family-sans-serif: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; +$font-family-sans-serif: Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif; $font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; $font-size-base: 16px; $font-size-large: 18px; diff --git a/apps/desktop/src/app/components/avatar.component.ts b/apps/desktop/src/app/components/avatar.component.ts index 87db2e4b6af..d2660763667 100644 --- a/apps/desktop/src/app/components/avatar.component.ts +++ b/apps/desktop/src/app/components/avatar.component.ts @@ -113,7 +113,7 @@ export class AvatarComponent implements OnChanges, OnInit { textTag.setAttribute("fill", Utils.pickTextColorBasedOnBgColor(color, 135, true)); textTag.setAttribute( "font-family", - '"DM Sans","Helvetica Neue",Helvetica,Arial,' + + 'Roboto,"Helvetica Neue",Helvetica,Arial,' + 'sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"', ); textTag.textContent = character; diff --git a/apps/desktop/src/images/loading.svg b/apps/desktop/src/images/loading.svg index 3f2033303db..5f4102a5921 100644 --- a/apps/desktop/src/images/loading.svg +++ b/apps/desktop/src/images/loading.svg @@ -1,5 +1,5 @@  - Loading... diff --git a/apps/desktop/src/scss/variables.scss b/apps/desktop/src/scss/variables.scss index b8978e284e5..b094be63f8c 100644 --- a/apps/desktop/src/scss/variables.scss +++ b/apps/desktop/src/scss/variables.scss @@ -1,6 +1,6 @@ $dark-icon-themes: "theme_dark"; -$font-family-sans-serif: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; +$font-family-sans-serif: Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif; $font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; $font-size-base: 14px; $font-size-large: 18px; diff --git a/apps/web/src/app/billing/services/stripe.service.ts b/apps/web/src/app/billing/services/stripe.service.ts index 5c35923c1f4..360187ecd1e 100644 --- a/apps/web/src/app/billing/services/stripe.service.ts +++ b/apps/web/src/app/billing/services/stripe.service.ts @@ -148,7 +148,7 @@ export class StripeService { base: { color: null, fontFamily: - '"DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' + + 'Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif, ' + '"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', fontSize: "16px", fontSmoothing: "antialiased", diff --git a/apps/web/src/images/loading-white.svg b/apps/web/src/images/loading-white.svg index 2bebff7daba..ef5970da42e 100644 --- a/apps/web/src/images/loading-white.svg +++ b/apps/web/src/images/loading-white.svg @@ -1,5 +1,5 @@  - Loading... diff --git a/apps/web/src/images/loading.svg b/apps/web/src/images/loading.svg index 3f2033303db..5f4102a5921 100644 --- a/apps/web/src/images/loading.svg +++ b/apps/web/src/images/loading.svg @@ -1,5 +1,5 @@  - Loading... diff --git a/apps/web/src/scss/variables.scss b/apps/web/src/scss/variables.scss index 4b023e12746..66773999c54 100644 --- a/apps/web/src/scss/variables.scss +++ b/apps/web/src/scss/variables.scss @@ -21,7 +21,7 @@ $body-bg: $white; $body-color: #333333; $font-family-sans-serif: - "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; $h1-font-size: 1.7rem; diff --git a/libs/angular/src/scss/webfonts.css b/libs/angular/src/scss/webfonts.css index 04b072e1bf1..cd35271bf80 100644 --- a/libs/angular/src/scss/webfonts.css +++ b/libs/angular/src/scss/webfonts.css @@ -1,8 +1,8 @@ @font-face { - font-family: "DM Sans"; + font-family: Roboto; src: - url("webfonts/dm-sans.woff2") format("woff2 supports variations"), - url("webfonts/dm-sans.woff2") format("woff2-variations"); + url("webfonts/roboto.woff2") format("woff2 supports variations"), + url("webfonts/roboto.woff2") format("woff2-variations"); font-display: swap; font-weight: 100 900; } diff --git a/libs/angular/src/scss/webfonts/dm-sans.woff2 b/libs/angular/src/scss/webfonts/dm-sans.woff2 deleted file mode 100644 index 2375279dbf5a47d05c9b3364961a1f2d768b78cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88504 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�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�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 B`$zx) diff --git a/libs/angular/src/scss/webfonts/roboto.woff2 b/libs/angular/src/scss/webfonts/roboto.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..af8d2343d9c7d72c6fcbf7ffe127d0cc9f3e696c GIT binary patch literal 209484 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Ð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 Z%W3>CI$@P~5SgeBd{mD4Gdn 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%0DZzBc#{kuHQ3xWI`Ear!Re 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 E0Ln!2jQ{`u literal 0 HcmV?d00001 diff --git a/libs/components/src/avatar/avatar.component.ts b/libs/components/src/avatar/avatar.component.ts index 0e3dbd6f1b9..554f55636fc 100644 --- a/libs/components/src/avatar/avatar.component.ts +++ b/libs/components/src/avatar/avatar.component.ts @@ -123,7 +123,7 @@ export class AvatarComponent implements OnChanges { textTag.setAttribute("fill", Utils.pickTextColorBasedOnBgColor(color, 135, true)); textTag.setAttribute( "font-family", - '"DM Sans","Helvetica Neue",Helvetica,Arial,' + + 'Roboto,"Helvetica Neue",Helvetica,Arial,' + 'sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"', ); // Warning do not use innerHTML here, characters are user provided diff --git a/libs/components/src/variables.scss b/libs/components/src/variables.scss index bc9cded4981..e3651f9c37d 100644 --- a/libs/components/src/variables.scss +++ b/libs/components/src/variables.scss @@ -21,7 +21,7 @@ $body-bg: $white; $body-color: #333333; $font-family-sans-serif: - "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; $h1-font-size: 1.7rem; From 87b875c48b2a9c5d463397a4bdefe42412399a42 Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Mon, 12 May 2025 09:29:11 -0400 Subject: [PATCH 002/163] docs(update-auth-approving-clients): [PM-17111] Add Browser to List of Approving Clients - Updated comments. (#14707) --- .../services/platform-utils/browser-platform-utils.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts index c9200ecc1a4..22708d8e425 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts @@ -26,6 +26,10 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic return this.deviceCache; } + // ORDERING MATTERS HERE + // Ordered from most specific to least specific. We try to discern the greatest detail + // for the type of extension the user is on by checking specific cases first and as we go down + // the list we hope to catch all by the most generic clients they could be on. if (BrowserPlatformUtilsService.isFirefox()) { this.deviceCache = DeviceType.FirefoxExtension; } else if (BrowserPlatformUtilsService.isOpera(globalContext)) { From aca8ab8e40f18108ddbf3a1808c545bc1571b1f7 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 12 May 2025 15:31:03 +0200 Subject: [PATCH 003/163] [PM-21001] Move autofill code to new encrypt service interface (#14548) * Move autofill code to new encrypt service interface * Fix test runner --- .../src/native-message.service.ts | 8 ++------ .../src/services/duckduckgo-message-handler.service.ts | 5 ++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/desktop/native-messaging-test-runner/src/native-message.service.ts b/apps/desktop/native-messaging-test-runner/src/native-message.service.ts index c01d581afe8..c2356f93e28 100644 --- a/apps/desktop/native-messaging-test-runner/src/native-message.service.ts +++ b/apps/desktop/native-messaging-test-runner/src/native-message.service.ts @@ -220,7 +220,7 @@ export default class NativeMessageService { const sharedKey = await this.getSharedKeyForKey(key); - return this.encryptService.encrypt(commandDataString, sharedKey); + return this.encryptService.encryptString(commandDataString, sharedKey); } private async decryptResponsePayload( @@ -228,11 +228,7 @@ export default class NativeMessageService { key: string, ): Promise { const sharedKey = await this.getSharedKeyForKey(key); - const decrypted = await this.encryptService.decryptToUtf8( - payload, - sharedKey, - "native-messaging-session", - ); + const decrypted = await this.encryptService.decryptString(payload, sharedKey); return JSON.parse(decrypted); } diff --git a/apps/desktop/src/services/duckduckgo-message-handler.service.ts b/apps/desktop/src/services/duckduckgo-message-handler.service.ts index e82444be993..6fb91231be1 100644 --- a/apps/desktop/src/services/duckduckgo-message-handler.service.ts +++ b/apps/desktop/src/services/duckduckgo-message-handler.service.ts @@ -168,7 +168,7 @@ export class DuckDuckGoMessageHandlerService { payload: DecryptedCommandData, key: SymmetricCryptoKey, ): Promise { - return await this.encryptService.encrypt(JSON.stringify(payload), key); + return await this.encryptService.encryptString(JSON.stringify(payload), key); } private async decryptPayload(message: EncryptedMessage): Promise { @@ -188,10 +188,9 @@ export class DuckDuckGoMessageHandlerService { } try { - let decryptedResult = await this.encryptService.decryptToUtf8( + let decryptedResult = await this.encryptService.decryptString( message.encryptedCommand as EncString, this.duckduckgoSharedSecret, - "ddg-shared-key", ); decryptedResult = this.trimNullCharsFromMessage(decryptedResult); From 57ed1e7285f8a263429140a19ae0e0ff32258dc6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 10:09:40 -0400 Subject: [PATCH 004/163] [deps] Platform: Update @types/node to v22.15.3 (#14723) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../native-messaging-test-runner/package-lock.json | 8 ++++---- apps/desktop/native-messaging-test-runner/package.json | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index d506e109e94..8b39fd9805e 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -17,7 +17,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "22.14.1", + "@types/node": "22.15.3", "typescript": "5.4.2" } }, @@ -101,9 +101,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", - "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "version": "22.15.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", + "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index f67ab259d3b..ea6b1b3e7a8 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -22,7 +22,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "22.14.1", + "@types/node": "22.15.3", "typescript": "5.4.2" }, "_moduleAliases": { diff --git a/package-lock.json b/package-lock.json index 1e7e0741cd8..a15275a695e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -110,7 +110,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.14.1", + "@types/node": "22.15.3", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/papaparse": "5.3.15", @@ -11656,9 +11656,9 @@ } }, "node_modules/@types/node": { - "version": "22.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", - "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "version": "22.15.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", + "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 81f1c92d5a5..904ae121cc9 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.14.1", + "@types/node": "22.15.3", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/papaparse": "5.3.15", From 89b9ba21cabbd4ddaf71bba9f267cd646bd4fc9b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 07:15:06 -0700 Subject: [PATCH 005/163] [deps] Platform: Update Rust crate arboard to v3.5.0 (#14484) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/desktop/desktop_native/Cargo.lock | 153 ++++++++----------------- apps/desktop/desktop_native/Cargo.toml | 2 +- 2 files changed, 46 insertions(+), 109 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index cfd29303510..1b442fbbb8a 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -120,9 +120,9 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "arboard" -version = "3.4.1" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" +checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70" dependencies = [ "clipboard-win", "log", @@ -130,6 +130,7 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "parking_lot", + "percent-encoding", "wl-clipboard-rs", "x11rb", ] @@ -465,15 +466,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2", -] - [[package]] name = "blocking" version = "1.6.1" @@ -565,12 +557,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "cfg_aliases" version = "0.2.1" @@ -867,17 +853,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derive-new" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "desktop_core" version = "0.0.0" @@ -1007,6 +982,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1409,7 +1394,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bdbbd5bc8c5749697ccaa352fa45aff8730cf21c68029c0eef1ffed7c3d6ba2" dependencies = [ "cfg-if", - "nix 0.29.0", + "nix", "widestring", "windows 0.57.0", ] @@ -1839,18 +1824,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "nix" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases 0.1.1", - "libc", -] - [[package]] name = "nix" version = "0.29.0" @@ -1859,7 +1832,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "memoffset", ] @@ -1990,47 +1963,24 @@ dependencies = [ "libc", ] -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - [[package]] name = "objc2" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" dependencies = [ - "objc-sys", "objc2-encode", ] [[package]] name = "objc2-app-kit" -version = "0.2.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ "bitflags", - "block2", - "libc", - "objc2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-core-data" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" -dependencies = [ - "bitflags", - "block2", "objc2", + "objc2-core-graphics", "objc2-foundation", ] @@ -2041,18 +1991,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ "bitflags", + "dispatch2", + "objc2", ] [[package]] -name = "objc2-core-image" -version = "0.2.2" +name = "objc2-core-graphics" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ - "block2", + "bitflags", + "dispatch2", "objc2", - "objc2-foundation", - "objc2-metal", + "objc2-core-foundation", + "objc2-io-surface", ] [[package]] @@ -2063,14 +2016,13 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" -version = "0.2.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ "bitflags", - "block2", - "libc", "objc2", + "objc2-core-foundation", ] [[package]] @@ -2084,28 +2036,14 @@ dependencies = [ ] [[package]] -name = "objc2-metal" -version = "0.2.2" +name = "objc2-io-surface" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ "bitflags", - "block2", "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags", - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", + "objc2-core-foundation", ] [[package]] @@ -3487,9 +3425,9 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.31.2" +version = "0.32.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" dependencies = [ "bitflags", "wayland-backend", @@ -3499,9 +3437,9 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.2.0" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" dependencies = [ "bitflags", "wayland-backend", @@ -3982,15 +3920,14 @@ dependencies = [ [[package]] name = "wl-clipboard-rs" -version = "0.8.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b41773911497b18ca8553c3daaf8ec9fe9819caf93d451d3055f69de028adb" +checksum = "2a083daad7e8a4b8805ad73947ccadabe62afe37ce0e9787a56ff373d34762c7" dependencies = [ - "derive-new", "libc", "log", - "nix 0.28.0", "os_pipe", + "rustix", "tempfile", "thiserror 1.0.69", "tree_magic_mini", @@ -4085,7 +4022,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix 0.29.0", + "nix", "ordered-stream", "rand 0.8.5", "serde", @@ -4115,7 +4052,7 @@ dependencies = [ "futures-core", "futures-lite", "hex", - "nix 0.29.0", + "nix", "ordered-stream", "serde", "serde_repr", diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index f613744f42a..fafaf02eca3 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -11,7 +11,7 @@ publish = false [workspace.dependencies] aes = "=0.8.4" anyhow = "=1.0.94" -arboard = { version = "=3.4.1", default-features = false } +arboard = { version = "=3.5.0", default-features = false } argon2 = "=0.5.3" base64 = "=0.22.1" bindgen = "0.71.1" From 07725853a287c679964a608f3b23695c54030575 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 12 May 2025 17:00:53 +0200 Subject: [PATCH 006/163] Add tests for masterpasswordservice (#14728) * Add tests for masterpasswordservice * Fix tests --- .../services/master-password.service.spec.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/libs/common/src/key-management/master-password/services/master-password.service.spec.ts b/libs/common/src/key-management/master-password/services/master-password.service.spec.ts index 93439ac8caa..b55e770f865 100644 --- a/libs/common/src/key-management/master-password/services/master-password.service.spec.ts +++ b/libs/common/src/key-management/master-password/services/master-password.service.spec.ts @@ -2,12 +2,16 @@ import { mock, MockProxy } from "jest-mock-extended"; import { of } from "rxjs"; import * as rxjs from "rxjs"; +import { makeSymmetricCryptoKey } from "../../../../spec"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; import { LogService } from "../../../platform/abstractions/log.service"; import { StateService } from "../../../platform/abstractions/state.service"; +import { EncString } from "../../../platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; +import { MasterKey } from "../../../types/key"; import { EncryptService } from "../../crypto/abstractions/encrypt.service"; import { MasterPasswordService } from "./master-password.service"; @@ -27,6 +31,14 @@ describe("MasterPasswordService", () => { update: jest.fn().mockResolvedValue(null), }; + const testUserKey: SymmetricCryptoKey = makeSymmetricCryptoKey(64, 1); + const testMasterKey: MasterKey = makeSymmetricCryptoKey(32, 2); + const testStretchedMasterKey: SymmetricCryptoKey = makeSymmetricCryptoKey(64, 3); + const testMasterKeyEncryptedKey = + "0.gbauOANURUHqvhLTDnva1A==|nSW+fPumiuTaDB/s12+JO88uemV6rhwRSR+YR1ZzGr5j6Ei3/h+XEli2Unpz652NlZ9NTuRpHxeOqkYYJtp7J+lPMoclgteXuAzUu9kqlRc="; + const testStretchedMasterKeyEncryptedKey = + "2.gbauOANURUHqvhLTDnva1A==|nSW+fPumiuTaDB/s12+JO88uemV6rhwRSR+YR1ZzGr5j6Ei3/h+XEli2Unpz652NlZ9NTuRpHxeOqkYYJtp7J+lPMoclgteXuAzUu9kqlRc=|DeUFkhIwgkGdZA08bDnDqMMNmZk21D+H5g8IostPKAY="; + beforeEach(() => { stateProvider = mock(); stateService = mock(); @@ -45,6 +57,9 @@ describe("MasterPasswordService", () => { encryptService, logService, ); + + encryptService.unwrapSymmetricKey.mockResolvedValue(makeSymmetricCryptoKey(64, 1)); + keyGenerationService.stretchKey.mockResolvedValue(makeSymmetricCryptoKey(64, 3)); }); describe("setForceSetPasswordReason", () => { @@ -101,4 +116,41 @@ describe("MasterPasswordService", () => { expect(mockUserState.update).toHaveBeenCalled(); }); }); + describe("decryptUserKeyWithMasterKey", () => { + it("decrypts a userkey wrapped in AES256-CBC", async () => { + encryptService.unwrapSymmetricKey.mockResolvedValue(testUserKey); + await sut.decryptUserKeyWithMasterKey( + testMasterKey, + userId, + new EncString(testMasterKeyEncryptedKey), + ); + expect(encryptService.unwrapSymmetricKey).toHaveBeenCalledWith( + new EncString(testMasterKeyEncryptedKey), + testMasterKey, + ); + }); + it("decrypts a userkey wrapped in AES256-CBC-HMAC", async () => { + encryptService.unwrapSymmetricKey.mockResolvedValue(testUserKey); + keyGenerationService.stretchKey.mockResolvedValue(testStretchedMasterKey); + await sut.decryptUserKeyWithMasterKey( + testMasterKey, + userId, + new EncString(testStretchedMasterKeyEncryptedKey), + ); + expect(encryptService.unwrapSymmetricKey).toHaveBeenCalledWith( + new EncString(testStretchedMasterKeyEncryptedKey), + testStretchedMasterKey, + ); + expect(keyGenerationService.stretchKey).toHaveBeenCalledWith(testMasterKey); + }); + it("returns null if failed to decrypt", async () => { + encryptService.unwrapSymmetricKey.mockResolvedValue(null); + const result = await sut.decryptUserKeyWithMasterKey( + testMasterKey, + userId, + new EncString(testStretchedMasterKeyEncryptedKey), + ); + expect(result).toBeNull(); + }); + }); }); From 2487e9b98deb32b10a43bd93cc839d4556223ad4 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Mon, 12 May 2025 11:13:49 -0400 Subject: [PATCH 007/163] [PM-20637] Trigger password reprompt when updating a reprompt cipher via notification (#14575) * reprompt user on cipher update when enabled Co-authored-by: Daniel Riera * update tests * add test --------- Co-authored-by: Daniel Riera --- .../notification.background.spec.ts | 57 ++++++++++++++++++- .../background/notification.background.ts | 42 ++++++++++++-- .../overlay-notifications-content.service.ts | 1 + .../overlay-notifications-content.service.ts | 6 +- .../services/abstractions/autofill.service.ts | 6 +- .../src/autofill/services/autofill.service.ts | 9 ++- .../vault-v2/view-v2/view-v2.component.ts | 27 +++++++-- libs/common/src/autofill/constants/index.ts | 3 +- 8 files changed, 134 insertions(+), 17 deletions(-) diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 63ae1193737..009efd7ff36 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; @@ -828,6 +829,7 @@ describe("NotificationBackground", () => { id: "testId", name: "testItemName", login: { username: "testUser" }, + reprompt: CipherRepromptType.None, }); getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); @@ -842,6 +844,7 @@ describe("NotificationBackground", () => { message.edit, sender.tab, "testId", + false, ); expect(updateWithServerSpy).toHaveBeenCalled(); expect(tabSendMessageDataSpy).toHaveBeenCalledWith( @@ -855,6 +858,55 @@ describe("NotificationBackground", () => { ); }); + it("prompts the user for master password entry if the notification message type is for ChangePassword and the cipher reprompt is enabled", async () => { + const tab = createChromeTabMock({ id: 1, url: "https://example.com" }); + const sender = mock({ tab }); + const message: NotificationBackgroundExtensionMessage = { + command: "bgSaveCipher", + edit: false, + folder: "folder-id", + }; + const queueMessage = mock({ + type: NotificationQueueMessageType.ChangePassword, + tab, + domain: "example.com", + newPassword: "newPassword", + }); + notificationBackground["notificationQueue"] = [queueMessage]; + const cipherView = mock({ + id: "testId", + name: "testItemName", + login: { username: "testUser" }, + reprompt: CipherRepromptType.Password, + }); + getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); + + sendMockExtensionMessage(message, sender); + await flushPromises(); + + expect(editItemSpy).not.toHaveBeenCalled(); + expect(autofillService.isPasswordRepromptRequired).toHaveBeenCalled(); + expect(createWithServerSpy).not.toHaveBeenCalled(); + expect(updatePasswordSpy).toHaveBeenCalledWith( + cipherView, + queueMessage.newPassword, + message.edit, + sender.tab, + "testId", + false, + ); + expect(updateWithServerSpy).not.toHaveBeenCalled(); + expect(tabSendMessageDataSpy).not.toHaveBeenCalledWith( + sender.tab, + "saveCipherAttemptCompleted", + { + itemName: "testItemName", + cipherId: cipherView.id, + task: undefined, + }, + ); + }); + it("completes password update notification with a security task notice if any are present for the cipher, and dismisses tasks for the updated cipher", async () => { const mockCipherId = "testId"; const mockOrgId = "testOrgId"; @@ -905,6 +957,7 @@ describe("NotificationBackground", () => { id: mockCipherId, organizationId: mockOrgId, name: "Test Item", + reprompt: CipherRepromptType.None, }); getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); @@ -919,6 +972,7 @@ describe("NotificationBackground", () => { message.edit, sender.tab, mockCipherId, + false, ); expect(updateWithServerSpy).toHaveBeenCalled(); expect(tabSendMessageDataSpy).toHaveBeenCalledWith( @@ -1000,6 +1054,7 @@ describe("NotificationBackground", () => { message.edit, sender.tab, "testId", + false, ); expect(editItemSpy).toHaveBeenCalled(); expect(updateWithServerSpy).not.toHaveBeenCalled(); @@ -1170,7 +1225,7 @@ describe("NotificationBackground", () => { newPassword: "newPassword", }); notificationBackground["notificationQueue"] = [queueMessage]; - const cipherView = mock(); + const cipherView = mock({ reprompt: CipherRepromptType.None }); getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); const errorMessage = "fetch error"; updateWithServerSpy.mockImplementation(() => { diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 5dd15274677..52920ec67a8 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -14,6 +14,7 @@ import { ExtensionCommand, ExtensionCommandType, NOTIFICATION_BAR_LIFESPAN_MS, + UPDATE_PASSWORD, } from "@bitwarden/common/autofill/constants"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; @@ -104,6 +105,8 @@ export default class NotificationBackground { this.removeTabFromNotificationQueue(sender.tab), bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab), bgSaveCipher: ({ message, sender }) => this.handleSaveCipherMessage(message, sender), + bgHandleReprompt: ({ message, sender }: any) => + this.handleCipherUpdateRepromptResponse(message), bgUnlockPopoutOpened: ({ message, sender }) => this.unlockVault(message, sender.tab), checkNotificationQueue: ({ sender }) => this.checkNotificationQueue(sender.tab), collectPageDetailsResponse: ({ message }) => @@ -631,6 +634,17 @@ export default class NotificationBackground { await this.saveOrUpdateCredentials(sender.tab, message.edit, message.folder); } + async handleCipherUpdateRepromptResponse(message: NotificationBackgroundExtensionMessage) { + if (message.success) { + await this.saveOrUpdateCredentials(message.tab, false, undefined, true); + } else { + await BrowserApi.tabSendMessageData(message.tab, "saveCipherAttemptCompleted", { + error: "Password reprompt failed", + }); + return; + } + } + /** * Saves or updates credentials based on the message within the * notification queue that is associated with the specified tab. @@ -639,7 +653,12 @@ export default class NotificationBackground { * @param edit - Identifies if the credentials should be edited or simply added * @param folderId - The folder to add the cipher to */ - private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, edit: boolean, folderId?: string) { + private async saveOrUpdateCredentials( + tab: chrome.tabs.Tab, + edit: boolean, + folderId?: string, + skipReprompt: boolean = false, + ) { for (let i = this.notificationQueue.length - 1; i >= 0; i--) { const queueMessage = this.notificationQueue[i]; if ( @@ -654,18 +673,26 @@ export default class NotificationBackground { continue; } - this.notificationQueue.splice(i, 1); - const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(getOptionalUserId), ); if (queueMessage.type === NotificationQueueMessageType.ChangePassword) { const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId, activeUserId); - await this.updatePassword(cipherView, queueMessage.newPassword, edit, tab, activeUserId); + + await this.updatePassword( + cipherView, + queueMessage.newPassword, + edit, + tab, + activeUserId, + skipReprompt, + ); return; } + this.notificationQueue.splice(i, 1); + // If the vault was locked, check if a cipher needs updating instead of creating a new one if (queueMessage.wasVaultLocked) { const allCiphers = await this.cipherService.getAllDecryptedForUrl( @@ -725,6 +752,7 @@ export default class NotificationBackground { edit: boolean, tab: chrome.tabs.Tab, userId: UserId, + skipReprompt: boolean = false, ) { cipherView.login.password = newPassword; @@ -758,6 +786,12 @@ export default class NotificationBackground { } : undefined; + if (cipherView.reprompt && !skipReprompt) { + await this.autofillService.isPasswordRepromptRequired(cipherView, tab, UPDATE_PASSWORD); + + return; + } + await this.cipherService.updateWithServer(cipher); await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { diff --git a/apps/browser/src/autofill/overlay/notifications/abstractions/overlay-notifications-content.service.ts b/apps/browser/src/autofill/overlay/notifications/abstractions/overlay-notifications-content.service.ts index 82c03cacadf..42d7666e8a7 100644 --- a/apps/browser/src/autofill/overlay/notifications/abstractions/overlay-notifications-content.service.ts +++ b/apps/browser/src/autofill/overlay/notifications/abstractions/overlay-notifications-content.service.ts @@ -15,6 +15,7 @@ export type NotificationsExtensionMessage = { typeData?: NotificationTypeData; height?: number; error?: string; + closedByUser?: boolean; fadeOutNotification?: boolean; }; }; diff --git a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts index 519521feaa9..c21aaa37dd4 100644 --- a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts +++ b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts @@ -106,13 +106,15 @@ export class OverlayNotificationsContentService * @param message - The message containing the data for closing the notification bar. */ private handleCloseNotificationBarMessage(message: NotificationsExtensionMessage) { + const closedByUser = + typeof message.data?.closedByUser === "boolean" ? message.data.closedByUser : true; if (message.data?.fadeOutNotification) { setElementStyles(this.notificationBarIframeElement, { opacity: "0" }, true); - globalThis.setTimeout(() => this.closeNotificationBar(true), 150); + globalThis.setTimeout(() => this.closeNotificationBar(closedByUser), 150); return; } - this.closeNotificationBar(true); + this.closeNotificationBar(closedByUser); } /** diff --git a/apps/browser/src/autofill/services/abstractions/autofill.service.ts b/apps/browser/src/autofill/services/abstractions/autofill.service.ts index 5b1b4b3b8bb..daafd871789 100644 --- a/apps/browser/src/autofill/services/abstractions/autofill.service.ts +++ b/apps/browser/src/autofill/services/abstractions/autofill.service.ts @@ -87,5 +87,9 @@ export abstract class AutofillService { cipherType?: CipherType, ) => Promise; setAutoFillOnPageLoadOrgPolicy: () => Promise; - isPasswordRepromptRequired: (cipher: CipherView, tab: chrome.tabs.Tab) => Promise; + isPasswordRepromptRequired: ( + cipher: CipherView, + tab: chrome.tabs.Tab, + action?: string, + ) => Promise; } diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index da46ceb0864..525010bacc1 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -593,15 +593,20 @@ export default class AutofillService implements AutofillServiceInterface { * * @param cipher - The cipher to autofill * @param tab - The tab to autofill + * @param action - override for default action once reprompt is completed successfully */ - async isPasswordRepromptRequired(cipher: CipherView, tab: chrome.tabs.Tab): Promise { + async isPasswordRepromptRequired( + cipher: CipherView, + tab: chrome.tabs.Tab, + action?: string, + ): Promise { const userHasMasterPasswordAndKeyHash = await this.userVerificationService.hasMasterPasswordAndMasterKeyHash(); if (cipher.reprompt === CipherRepromptType.Password && userHasMasterPasswordAndKeyHash) { if (!this.isDebouncingPasswordRepromptPopout()) { await this.openVaultItemPasswordRepromptPopout(tab, { cipherId: cipher.id, - action: "autofill", + action: action ?? "autofill", }); } diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index 56db47619b0..a834314560b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -19,6 +19,7 @@ import { COPY_USERNAME_ID, COPY_VERIFICATION_CODE_ID, SHOW_AUTOFILL_BUTTON, + UPDATE_PASSWORD, } from "@bitwarden/common/autofill/constants"; import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -49,6 +50,7 @@ import { PasswordRepromptService, } from "@bitwarden/vault"; +import { sendExtensionMessage } from "../../../../../autofill/utils/index"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; @@ -72,7 +74,8 @@ type LoadAction = | typeof SHOW_AUTOFILL_BUTTON | typeof COPY_USERNAME_ID | typeof COPY_PASSWORD_ID - | typeof COPY_VERIFICATION_CODE_ID; + | typeof COPY_VERIFICATION_CODE_ID + | typeof UPDATE_PASSWORD; @Component({ selector: "app-view-v2", @@ -294,7 +297,7 @@ export class ViewV2Component { // Both vaultPopupAutofillService and copyCipherFieldService will perform password re-prompting internally. switch (loadAction) { - case "show-autofill-button": + case SHOW_AUTOFILL_BUTTON: // This action simply shows the cipher view, no need to do anything. if ( this.cipher.reprompt !== CipherRepromptType.None && @@ -303,30 +306,42 @@ export class ViewV2Component { await closeViewVaultItemPopout(`${VaultPopoutType.viewVaultItem}_${this.cipher.id}`); } return; - case "autofill": + case AUTOFILL_ID: actionSuccess = await this.vaultPopupAutofillService.doAutofill(this.cipher, false); break; - case "copy-username": + case COPY_USERNAME_ID: actionSuccess = await this.copyCipherFieldService.copy( this.cipher.login.username, "username", this.cipher, ); break; - case "copy-password": + case COPY_PASSWORD_ID: actionSuccess = await this.copyCipherFieldService.copy( this.cipher.login.password, "password", this.cipher, ); break; - case "copy-totp": + case COPY_VERIFICATION_CODE_ID: actionSuccess = await this.copyCipherFieldService.copy( this.cipher.login.totp, "totp", this.cipher, ); break; + case UPDATE_PASSWORD: { + const repromptSuccess = await this.passwordRepromptService.showPasswordPrompt(); + + await sendExtensionMessage("bgHandleReprompt", { + tab: await chrome.tabs.get(senderTabId), + success: repromptSuccess, + }); + + await closeViewVaultItemPopout(`${VaultPopoutType.viewVaultItem}_${this.cipher.id}`); + + break; + } } if (BrowserPopupUtils.inPopout(window)) { diff --git a/libs/common/src/autofill/constants/index.ts b/libs/common/src/autofill/constants/index.ts index 62ad10e9a90..dc79e27b6aa 100644 --- a/libs/common/src/autofill/constants/index.ts +++ b/libs/common/src/autofill/constants/index.ts @@ -38,7 +38,7 @@ export const ClearClipboardDelay = { FiveMinutes: 300, } as const; -/* Context Menu item Ids */ +/* Ids for context menu items and messaging events */ export const AUTOFILL_CARD_ID = "autofill-card"; export const AUTOFILL_ID = "autofill"; export const SHOW_AUTOFILL_BUTTON = "show-autofill-button"; @@ -54,6 +54,7 @@ export const GENERATE_PASSWORD_ID = "generate-password"; export const NOOP_COMMAND_SUFFIX = "noop"; export const ROOT_ID = "root"; export const SEPARATOR_ID = "separator"; +export const UPDATE_PASSWORD = "update-password"; export const NOTIFICATION_BAR_LIFESPAN_MS = 150000; // 150 seconds From 355bddc6c9d5c110e55fe74c5fcfa86ddd85572c Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Mon, 12 May 2025 11:50:29 -0400 Subject: [PATCH 008/163] [CL-301] Enable Storybook test runner. Run a11y tests also (#14646) * Enable Storybook test runner. Run a11y tests also * no need to return checkA11y function * add back decorator removed in error * add test runner to our ownership * add axe-playwright to our ownership --- .github/renovate.json5 | 2 + .storybook/preview.tsx | 7 + .storybook/test-runner.ts | 50 + package-lock.json | 2149 ++++++++++++++++++++++++++++++++++--- package.json | 4 + tsconfig.eslint.json | 1 + 6 files changed, 2062 insertions(+), 151 deletions(-) create mode 100644 .storybook/test-runner.ts diff --git a/.github/renovate.json5 b/.github/renovate.json5 index c1045739506..e5cd47077fb 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -312,6 +312,7 @@ "@angular/platform-browser", "@angular/platform", "@angular/router", + "axe-playwright", "@compodoc/compodoc", "@ng-select/ng-select", "@storybook/addon-a11y", @@ -320,6 +321,7 @@ "@storybook/addon-essentials", "@storybook/addon-interactions", "@storybook/addon-links", + "@storybook/test-runner", "@storybook/addon-themes", "@storybook/angular", "@storybook/manager-api", diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 6bd28cfe809..19a4fc51dc8 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -26,6 +26,13 @@ const preview: Preview = { wrapperDecorator, ], parameters: { + a11y: { + element: "#storybook-root", + manual: true, + options: { + runOnly: ["section508", "wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"], + }, + }, controls: { matchers: { color: /(background|color)$/i, diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts new file mode 100644 index 00000000000..8584a321090 --- /dev/null +++ b/.storybook/test-runner.ts @@ -0,0 +1,50 @@ +import { + // waitForPageReady, + type TestRunnerConfig, +} from "@storybook/test-runner"; +import { injectAxe, checkA11y } from "axe-playwright"; + +const testRunnerConfig: TestRunnerConfig = { + setup() {}, + + async preVisit(page, context) { + return await injectAxe(page); + }, + + async postVisit(page, context) { + await page.waitForSelector("#storybook-root"); + // https://github.com/abhinaba-ghosh/axe-playwright#parameters-on-checka11y-axerun + await checkA11y( + // Playwright page instance. + page, + + // context + "#storybook-root", + + // axeOptions, see https://www.deque.com/axe/core-documentation/api-documentation/#parameters-axerun + { + detailedReport: true, + detailedReportOptions: { + // Includes the full html for invalid nodes + html: true, + }, + verbose: false, + }, + + // skipFailures + false, + + // reporter "v2" is terminal reporter, "html" writes results to file + "v2", + + // axeHtmlReporterOptions + // NOTE: set reporter param (above) to "html" to activate these options + { + outputDir: "reports/a11y", + reportFileName: `${context.id}.html`, + }, + ); + }, +}; + +export default testRunnerConfig; diff --git a/package-lock.json b/package-lock.json index a15275a695e..81c9384fbbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,6 +91,7 @@ "@storybook/addon-essentials": "8.6.12", "@storybook/addon-interactions": "8.6.12", "@storybook/addon-links": "8.6.12", + "@storybook/test-runner": "0.22.0", "@storybook/addon-themes": "8.6.12", "@storybook/angular": "8.6.12", "@storybook/manager-api": "8.6.12", @@ -123,6 +124,7 @@ "@yao-pkg/pkg": "5.16.1", "angular-eslint": "18.4.3", "autoprefixer": "10.4.21", + "axe-playwright": "2.1.0", "babel-loader": "9.2.1", "base64-loader": "1.0.0", "browserslist": "4.23.2", @@ -3327,7 +3329,6 @@ "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -3453,7 +3454,6 @@ "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, @@ -3580,7 +3580,6 @@ "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, @@ -4714,8 +4713,7 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@bitwarden/admin-console": { "resolved": "libs/admin-console", @@ -7179,7 +7177,6 @@ "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -7197,7 +7194,6 @@ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -7208,7 +7204,6 @@ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -7223,7 +7218,6 @@ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -7238,7 +7232,6 @@ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -7252,7 +7245,6 @@ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-try": "^2.0.0" }, @@ -7269,7 +7261,6 @@ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -7283,7 +7274,6 @@ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -7293,8 +7283,7 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", @@ -7312,7 +7301,6 @@ "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -7331,7 +7319,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -7342,7 +7329,6 @@ "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/reporters": "^29.7.0", @@ -7391,7 +7377,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -7405,7 +7390,6 @@ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -7420,8 +7404,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/core/node_modules/slash": { "version": "3.0.0", @@ -7429,11 +7412,23 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", + "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jest/environment": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", @@ -7456,7 +7451,6 @@ "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" @@ -7502,7 +7496,6 @@ "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -7519,7 +7512,6 @@ "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", @@ -7564,7 +7556,6 @@ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7577,7 +7568,6 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -7599,7 +7589,6 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -7613,7 +7602,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -7637,7 +7625,6 @@ "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", @@ -7653,7 +7640,6 @@ "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/types": "^29.6.3", @@ -7670,7 +7656,6 @@ "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", @@ -7687,7 +7672,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -7698,7 +7682,6 @@ "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -7725,8 +7708,7 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/transform/node_modules/slash": { "version": "3.0.0", @@ -7734,7 +7716,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -10819,6 +10800,43 @@ "storybook": "^8.6.12" } }, + "node_modules/@storybook/test-runner": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@storybook/test-runner/-/test-runner-0.22.0.tgz", + "integrity": "sha512-fKY6MTE/bcvMaulKXy+z0fPmRXJx1REkYMOMcGn8zn6uffyBigGgaVf/sZ+AZfibwvjzg/StWhJ9HvAM8pc14g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5", + "@jest/types": "^29.6.3", + "@storybook/csf": "^0.1.11", + "@swc/core": "^1.5.22", + "@swc/jest": "^0.2.23", + "expect-playwright": "^0.8.0", + "jest": "^29.6.4", + "jest-circus": "^29.6.4", + "jest-environment-node": "^29.6.4", + "jest-junit": "^16.0.0", + "jest-playwright-preset": "^4.0.0", + "jest-runner": "^29.6.4", + "jest-serializer-html": "^7.1.0", + "jest-watch-typeahead": "^2.0.0", + "nyc": "^15.1.0", + "playwright": "^1.14.0" + }, + "bin": { + "test-storybook": "dist/test-storybook.js" + }, + "engines": { + "node": "^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "storybook": "^0.0.0-0 || ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 || ^9.0.0-0" + } + }, "node_modules/@storybook/theming": { "version": "8.6.12", "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.6.12.tgz", @@ -10882,6 +10900,250 @@ "storybook": "^8.6.12" } }, + "node_modules/@swc/core": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz", + "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.21" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.11.24", + "@swc/core-darwin-x64": "1.11.24", + "@swc/core-linux-arm-gnueabihf": "1.11.24", + "@swc/core-linux-arm64-gnu": "1.11.24", + "@swc/core-linux-arm64-musl": "1.11.24", + "@swc/core-linux-x64-gnu": "1.11.24", + "@swc/core-linux-x64-musl": "1.11.24", + "@swc/core-win32-arm64-msvc": "1.11.24", + "@swc/core-win32-ia32-msvc": "1.11.24", + "@swc/core-win32-x64-msvc": "1.11.24" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz", + "integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz", + "integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz", + "integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz", + "integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz", + "integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz", + "integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz", + "integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz", + "integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz", + "integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz", + "integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/jest": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.38.tgz", + "integrity": "sha512-HMoZgXWMqChJwffdDjvplH53g9G2ALQes3HKXDEdliB/b85OQ0CTSbxG8VSeCwiAn7cOaDVEt4mwmZvbHcS52w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/create-cache-key-function": "^29.7.0", + "@swc/counter": "^0.1.3", + "jsonc-parser": "^3.2.0" + }, + "engines": { + "npm": ">= 7.0.0" + }, + "peerDependencies": { + "@swc/core": "*" + } + }, + "node_modules/@swc/types": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", + "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -11102,7 +11364,6 @@ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -11117,7 +11378,6 @@ "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.0.0" } @@ -11128,7 +11388,6 @@ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" @@ -11140,7 +11399,6 @@ "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.20.7" } @@ -11336,7 +11594,6 @@ "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -11506,6 +11763,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/junit-report-builder": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/junit-report-builder/-/junit-report-builder-3.0.2.tgz", + "integrity": "sha512-R5M+SYhMbwBeQcNXYWNCZkl09vkVfAtcPIaCGdzIkkbeaTrVbGQ7HVgi4s+EmM/M1K4ZuWQH0jGcvMvNePfxYA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/keygrip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", @@ -11908,6 +12172,16 @@ "license": "MIT", "optional": true }, + "node_modules/@types/wait-on": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@types/wait-on/-/wait-on-5.3.4.tgz", + "integrity": "sha512-EBsPjFMrFlMbbUFf9D1Fp+PAB2TwmUn7a3YtHyD9RLuTIk1jDd8SxXVAoez2Ciy+8Jsceo2MYEYZzJ/DvorOKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/webpack-env": { "version": "1.18.8", "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.8.tgz", @@ -13845,6 +14119,19 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/archiver": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", @@ -13981,6 +14268,13 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true, + "license": "MIT" + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -14356,6 +14650,39 @@ "node": ">=4" } }, + "node_modules/axe-html-reporter": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/axe-html-reporter/-/axe-html-reporter-2.2.11.tgz", + "integrity": "sha512-WlF+xlNVgNVWiM6IdVrsh+N0Cw7qupe5HT9N6Uyi+aN7f6SSi92RDomiP1noW8OWIV85V6x404m5oKMeqRV3tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mustache": "^4.0.1" + }, + "engines": { + "node": ">=8.9.0" + }, + "peerDependencies": { + "axe-core": ">=3" + } + }, + "node_modules/axe-playwright": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/axe-playwright/-/axe-playwright-2.1.0.tgz", + "integrity": "sha512-tY48SX56XaAp16oHPyD4DXpybz8Jxdz9P7exTjF/4AV70EGUavk+1fUPWirM0OYBR+YyDx6hUeDvuHVA6fB9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/junit-report-builder": "^3.0.2", + "axe-core": "^4.10.1", + "axe-html-reporter": "2.2.11", + "junit-report-builder": "^5.1.1", + "picocolors": "^1.1.1" + }, + "peerDependencies": { + "playwright": ">1.0.0" + } + }, "node_modules/axios": { "version": "1.8.4", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", @@ -14383,7 +14710,6 @@ "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", @@ -14406,7 +14732,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -14435,7 +14760,6 @@ "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -14453,7 +14777,6 @@ "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", @@ -14471,7 +14794,6 @@ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", - "peer": true, "bin": { "semver": "bin/semver.js" } @@ -14482,7 +14804,6 @@ "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", @@ -14600,7 +14921,6 @@ "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", @@ -14628,7 +14948,6 @@ "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" @@ -15090,7 +15409,6 @@ "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "node-int64": "^0.4.0" } @@ -15471,6 +15789,68 @@ "node": ">=8" } }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/caching-transform/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caching-transform/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/caching-transform/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/caching-transform/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -15553,7 +15933,6 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -15670,7 +16049,6 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" } @@ -16037,8 +16415,7 @@ "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/color-convert": { "version": "2.0.1", @@ -16123,6 +16500,13 @@ "node": ">=4.0.0" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, "node_modules/compare-version": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", @@ -16751,7 +17135,6 @@ "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -16942,6 +17325,20 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/cwd": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", + "integrity": "sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-pkg": "^0.1.2", + "fs-exists-sync": "^0.1.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -17120,7 +17517,6 @@ "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -17216,6 +17612,22 @@ "node": ">= 10" } }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -17342,7 +17754,6 @@ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -17385,6 +17796,100 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/diffable-html": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/diffable-html/-/diffable-html-4.1.0.tgz", + "integrity": "sha512-++kyNek+YBLH8cLXS+iTj/Hiy2s5qkRJEJ8kgu/WHbFrVY2vz9xPFUT+fii2zGF0m1CaojDlQJjkfrCt7YWM1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "htmlparser2": "^3.9.2" + } + }, + "node_modules/diffable-html/node_modules/dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/diffable-html/node_modules/dom-serializer/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/diffable-html/node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/diffable-html/node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/diffable-html/node_modules/domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/diffable-html/node_modules/domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/diffable-html/node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/diffable-html/node_modules/htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, "node_modules/dir-compare": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", @@ -18057,7 +18562,6 @@ "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -18382,8 +18886,7 @@ "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/es6-shim": { "version": "0.35.8", @@ -19185,7 +19688,6 @@ "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", "dev": true, - "peer": true, "engines": { "node": ">= 0.8.0" } @@ -19200,6 +19702,19 @@ "node": ">=6" } }, + "node_modules/expand-tilde": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", + "integrity": "sha512-rtmc+cjLZqnu9dSYosX9EWmSJhTwpACgJQTfj4hgg2JjOD/6SIQalZrt4a3aQeh++oNxkazcaxrhPUj6+g5G/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-homedir": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", @@ -19217,6 +19732,24 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/expect-playwright": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/expect-playwright/-/expect-playwright-0.8.0.tgz", + "integrity": "sha512-+kn8561vHAY+dt+0gMqqj1oY+g5xWrsuGMk4QGxotT2WS545nVqqjs37z6hrYfIuucwqthzwJfCJUEYqixyljg==", + "dev": true, + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", @@ -19596,7 +20129,6 @@ "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "bser": "2.1.1" } @@ -19771,6 +20303,58 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-file-up": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz", + "integrity": "sha512-mBxmNbVyjg1LQIIpgO8hN+ybWBgDQK8qjht+EbrTCGmmPV/sc7RF1i9stPTD6bpvXZywBdrwRYxhSdJv867L6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-exists-sync": "^0.1.0", + "resolve-dir": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-pkg": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz", + "integrity": "sha512-0rnQWcFwZr7eO0513HahrWafsc3CTFioEB7DRiEYCUM/70QXSY8f3mCST17HXLcPvEhzH/Ty/Bxd72ZZsr/yvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-file-up": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-process": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.10.tgz", + "integrity": "sha512-ncYFnWEIwL7PzmrK1yZtaccN8GhethD37RzBHG6iOZoFYB4vSmLLXfeWJjeN5nMvCJMjOtBvBBF8OgxEcikiZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "~4.1.2", + "commander": "^12.1.0", + "loglevel": "^1.9.2" + }, + "bin": { + "find-process": "bin/find-process.js" + } + }, + "node_modules/find-process/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -20305,6 +20889,27 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/front-matter": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz", @@ -20349,6 +20954,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fs-exists-sync": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", + "integrity": "sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fs-extra": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", @@ -20527,7 +21142,6 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -20678,6 +21292,56 @@ "node": ">=10.0" } }, + "node_modules/global-modules": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", + "integrity": "sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^0.1.4", + "is-windows": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", + "integrity": "sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.0", + "ini": "^1.3.4", + "is-windows": "^0.2.0", + "which": "^1.2.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -20942,6 +21606,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -20964,6 +21655,19 @@ "he": "bin/he" } }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -21084,8 +21788,7 @@ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/html-loader": { "version": "5.1.0", @@ -22170,7 +22873,6 @@ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -22475,6 +23177,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -22540,6 +23249,16 @@ "dev": true, "license": "MIT" }, + "node_modules/is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -22597,6 +23316,19 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/istanbul-lib-instrument": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", @@ -22614,13 +23346,116 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "license": "ISC", + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -22636,7 +23471,6 @@ "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", @@ -22652,7 +23486,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -22663,7 +23496,6 @@ "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -22737,7 +23569,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -22765,7 +23596,6 @@ "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "execa": "^5.0.0", "jest-util": "^29.7.0", @@ -22781,7 +23611,6 @@ "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -22814,7 +23643,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -22828,7 +23656,6 @@ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -22843,8 +23670,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-circus/node_modules/slash": { "version": "3.0.0", @@ -22852,7 +23678,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -22863,7 +23688,6 @@ "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/test-result": "^29.7.0", @@ -22898,7 +23722,6 @@ "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", @@ -22945,7 +23768,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -22959,7 +23781,6 @@ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -22972,7 +23793,6 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -22994,7 +23814,6 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -23008,7 +23827,6 @@ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -23023,8 +23841,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-config/node_modules/slash": { "version": "3.0.0", @@ -23032,7 +23849,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -23094,7 +23910,6 @@ "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "detect-newline": "^3.0.0" }, @@ -23108,7 +23923,6 @@ "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -23126,7 +23940,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -23140,7 +23953,6 @@ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -23155,8 +23967,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-environment-jsdom": { "version": "29.7.0", @@ -23385,7 +24196,6 @@ "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -23414,7 +24224,6 @@ "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -23467,7 +24276,6 @@ "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" @@ -23482,7 +24290,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -23496,7 +24303,6 @@ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -23511,8 +24317,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-matcher-utils": { "version": "29.7.0", @@ -23660,13 +24465,106 @@ "typescript": "^3.0.0 || ^4.0.0 || ^5.0.0" } }, + "node_modules/jest-playwright-preset": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jest-playwright-preset/-/jest-playwright-preset-4.0.0.tgz", + "integrity": "sha512-+dGZ1X2KqtwXaabVjTGxy0a3VzYfvYsWaRcuO8vMhyclHSOpGSI1+5cmlqzzCwQ3+fv0EjkTc7I5aV9lo08dYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect-playwright": "^0.8.0", + "jest-process-manager": "^0.4.0", + "nyc": "^15.1.0", + "playwright-core": ">=1.2.0", + "rimraf": "^3.0.2", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "jest": "^29.3.1", + "jest-circus": "^29.3.1", + "jest-environment-node": "^29.3.1", + "jest-runner": "^29.3.1" + } + }, + "node_modules/jest-playwright-preset/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-playwright-preset/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-playwright-preset/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-playwright-preset/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-playwright-preset/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" }, @@ -23743,13 +24641,58 @@ "dev": true, "license": "MIT" }, + "node_modules/jest-process-manager": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/jest-process-manager/-/jest-process-manager-0.4.0.tgz", + "integrity": "sha512-80Y6snDyb0p8GG83pDxGI/kQzwVTkCxc7ep5FPe/F6JYdvRDhwr6RzRmPSP7SEwuLhxo80lBS/NqOdUIbHIfhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/wait-on": "^5.2.0", + "chalk": "^4.1.0", + "cwd": "^0.10.0", + "exit": "^0.1.2", + "find-process": "^1.4.4", + "prompts": "^2.4.1", + "signal-exit": "^3.0.3", + "spawnd": "^5.0.0", + "tree-kill": "^1.2.2", + "wait-on": "^7.0.0" + } + }, + "node_modules/jest-process-manager/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-process-manager/node_modules/wait-on": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz", + "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.6.1", + "joi": "^17.11.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.1" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/jest-regex-util": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -23760,7 +24703,6 @@ "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", @@ -23782,7 +24724,6 @@ "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "jest-regex-util": "^29.6.3", "jest-snapshot": "^29.7.0" @@ -23797,7 +24738,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -23808,7 +24748,6 @@ "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/environment": "^29.7.0", @@ -23842,7 +24781,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -23853,7 +24791,6 @@ "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -23865,7 +24802,6 @@ "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -23900,7 +24836,6 @@ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -23913,7 +24848,6 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -23935,7 +24869,6 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -23949,18 +24882,26 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } }, + "node_modules/jest-serializer-html": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/jest-serializer-html/-/jest-serializer-html-7.1.0.tgz", + "integrity": "sha512-xYL2qC7kmoYHJo8MYqJkzrl/Fdlx+fat4U1AqYg+kafqwcKPiMkOcjWHPKhueuNEgr+uemhGc+jqXYiwCyRyLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "diffable-html": "^4.1.0" + } + }, "node_modules/jest-snapshot": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", @@ -23993,7 +24934,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -24007,7 +24947,6 @@ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -24022,8 +24961,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-util": { "version": "29.7.0", @@ -24062,7 +25000,6 @@ "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", @@ -24081,7 +25018,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -24095,7 +25031,6 @@ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -24109,7 +25044,6 @@ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -24124,8 +25058,111 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, + "license": "MIT" + }, + "node_modules/jest-watch-typeahead": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-2.2.2.tgz", + "integrity": "sha512-+QgOFW4o5Xlgd6jGS5X37i08tuuXNW8X0CV9WNFi+3n8ExCIP+E1melYhvYLjv5fE6D0yyzk74vsSO8I6GqtvQ==", + "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "ansi-escapes": "^6.0.0", + "chalk": "^5.2.0", + "jest-regex-util": "^29.0.0", + "jest-watcher": "^29.0.0", + "slash": "^5.0.0", + "string-length": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "jest": "^27.0.0 || ^28.0.0 || ^29.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/ansi-escapes": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", + "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watch-typeahead/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/char-regex": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.2.tgz", + "integrity": "sha512-cbGOjAptfM2LVmWhwRFHEKTPkLwNddVmuqYZQt895yXwAsWsXObCG+YN4DGQ/JBtT4GP1a1lPPdio2z413LmTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/jest-watch-typeahead/node_modules/string-length": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", + "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^2.0.0", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } }, "node_modules/jest-watcher": { "version": "29.7.0", @@ -24133,7 +25170,6 @@ "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", @@ -24154,7 +25190,6 @@ "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -24171,7 +25206,6 @@ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -24518,6 +25552,47 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/junit-report-builder": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/junit-report-builder/-/junit-report-builder-5.1.1.tgz", + "integrity": "sha512-ZNOIIGMzqCGcHQEA2Q4rIQQ3Df6gSIfne+X9Rly9Bc2y55KxAZu8iGv+n2pP0bLf0XAOctJZgeloC54hWzCahQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "make-dir": "^3.1.0", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/junit-report-builder/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/junit-report-builder/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/jwt-decode": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", @@ -24897,7 +25972,6 @@ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -25602,6 +26676,13 @@ "license": "MIT", "peer": true }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -26001,7 +27082,6 @@ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "semver": "^7.5.3" }, @@ -26097,7 +27177,6 @@ "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "tmpl": "1.0.5" } @@ -27587,6 +28666,16 @@ "readable-stream": "^3.6.0" } }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/mute-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", @@ -28164,8 +29253,7 @@ "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/node-machine-id": { "version": "1.1.12", @@ -28173,6 +29261,19 @@ "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", "dev": true }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -28844,6 +29945,350 @@ "node": ">=6" } }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nyc/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nyc/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nyc/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/nyc/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -29113,6 +30558,16 @@ "dev": true, "license": "MIT" }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/os-name": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/os-name/-/os-name-4.0.1.tgz", @@ -29270,6 +30725,22 @@ "node": ">=6" } }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -29551,6 +31022,16 @@ "node": ">= 0.10" } }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/parse5": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", @@ -30112,6 +31593,53 @@ "node": ">=4" } }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -30634,6 +32162,19 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/process-on-spawn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", + "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -30808,8 +32349,7 @@ "url": "https://opencollective.com/fast-check" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/qrcode-parser": { "version": "2.1.3", @@ -31308,6 +32848,19 @@ "node": ">= 0.10" } }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "license": "ISC", + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", @@ -31487,6 +33040,13 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "license": "ISC" + }, "node_modules/requireindex": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", @@ -31550,6 +33110,20 @@ "node": ">=8" } }, + "node_modules/resolve-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", + "integrity": "sha512-QxMPqI6le2u0dCLyiGzgy92kjkkL6zO0XyvHzjdTNH3zM6e5Hz3BwG6+aEyNgiQ5Xz6PwTwgQEj3U50dByPKIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^1.2.2", + "global-modules": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -32444,6 +34018,13 @@ "node": ">= 0.8" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, "node_modules/set-cookie-parser": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", @@ -32888,6 +34469,164 @@ "node": ">=0.10.0" } }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/spawn-wrap/node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/spawn-wrap/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/spawn-wrap/node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-wrap/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/spawn-wrap/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/spawn-wrap/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/spawn-wrap/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/spawn-wrap/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/spawnd": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-5.0.0.tgz", + "integrity": "sha512-28+AJr82moMVWolQvlAIv3JcYDkjkFTEmfDc503wxrF5l2rQ3dFz6DpbXp3kD4zmgGGldfM4xM4v1sFj/ZaIOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "exit": "^0.1.2", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "wait-port": "^0.2.9" + } + }, + "node_modules/spawnd/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -33234,7 +34973,6 @@ "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" @@ -33364,7 +35102,6 @@ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -33927,7 +35664,6 @@ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -33943,7 +35679,6 @@ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -33956,7 +35691,6 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -33978,7 +35712,6 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -34148,8 +35881,7 @@ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -34899,6 +36631,16 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/typescript": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", @@ -35490,7 +37232,6 @@ "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", @@ -35505,8 +37246,7 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", @@ -36131,6 +37871,109 @@ "tslib": "^2.1.0" } }, + "node_modules/wait-port": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-0.2.14.tgz", + "integrity": "sha512-kIzjWcr6ykl7WFbZd0TMae8xovwqcqbx6FM9l+7agOgUByhzdjfzZBPK2CPufldTOMxbUivss//Sh9MFawmPRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2", + "commander": "^3.0.2", + "debug": "^4.1.1" + }, + "bin": { + "wait-port": "bin/wait-port.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wait-port/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/wait-port/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/wait-port/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/wait-port/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/wait-port/node_modules/commander": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", + "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", + "dev": true, + "license": "MIT" + }, + "node_modules/wait-port/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/wait-port/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/wait-port/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/walk-up-path": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", @@ -36144,7 +37987,6 @@ "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "makeerror": "1.0.12" } @@ -36883,6 +38725,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, + "license": "ISC" + }, "node_modules/which-typed-array": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", @@ -37030,7 +38879,6 @@ "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -37044,8 +38892,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/ws": { "version": "8.18.1", diff --git a/package.json b/package.json index 904ae121cc9..8ba1c95df6a 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "storybook": "ng run components:storybook", "build-storybook": "ng run components:build-storybook", "build-storybook:ci": "ng run components:build-storybook --webpack-stats-json", + "test-stories": "test-storybook --url http://localhost:6006", + "test-stories:watch": "test-stories --watch", "postinstall": "patch-package" }, "workspaces": [ @@ -53,6 +55,7 @@ "@storybook/addon-essentials": "8.6.12", "@storybook/addon-interactions": "8.6.12", "@storybook/addon-links": "8.6.12", + "@storybook/test-runner": "0.22.0", "@storybook/addon-themes": "8.6.12", "@storybook/angular": "8.6.12", "@storybook/manager-api": "8.6.12", @@ -85,6 +88,7 @@ "@yao-pkg/pkg": "5.16.1", "angular-eslint": "18.4.3", "autoprefixer": "10.4.21", + "axe-playwright": "2.1.0", "babel-loader": "9.2.1", "base64-loader": "1.0.0", "browserslist": "4.23.2", diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 7ffa34df58c..e8c3f669c0c 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -45,6 +45,7 @@ "files": [ ".storybook/main.ts", ".storybook/manager.js", + ".storybook/test-runner.ts", "apps/browser/src/autofill/content/components/.lit-storybook/main.ts" ], "include": ["apps/**/*", "libs/**/*", "bitwarden_license/**/*", "scripts/**/*"], From a10b9530af5628552583f8afb31cb0d9d6928fd2 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Mon, 12 May 2025 18:20:56 +0100 Subject: [PATCH 009/163] changes to sponsorship is showing in individual vault (#14627) --- .../app/billing/settings/sponsored-families.component.ts | 8 +++++--- .../admin-console/models/data/organization.data.spec.ts | 1 + .../src/admin-console/models/data/organization.data.ts | 2 ++ .../src/admin-console/models/domain/organization.ts | 2 ++ .../models/response/profile-organization.response.ts | 2 ++ 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/billing/settings/sponsored-families.component.ts b/apps/web/src/app/billing/settings/sponsored-families.component.ts index cee08bae8cd..7e493168ce2 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.ts +++ b/apps/web/src/app/billing/settings/sponsored-families.component.ts @@ -112,13 +112,15 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { }); } }); - this.anyOrgsAvailable$ = this.availableSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0)); this.activeSponsorshipOrgs$ = this.organizationService .organizations$(userId) - .pipe(map((orgs) => orgs.filter((o) => o.familySponsorshipFriendlyName !== null))); - + .pipe( + map((orgs) => + orgs.filter((o) => o.familySponsorshipFriendlyName !== null && !o.isAdminInitiated), + ), + ); this.anyActiveSponsorships$ = this.activeSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0)); this.loading = false; diff --git a/libs/common/src/admin-console/models/data/organization.data.spec.ts b/libs/common/src/admin-console/models/data/organization.data.spec.ts index fae24133502..7d66e7bc0d5 100644 --- a/libs/common/src/admin-console/models/data/organization.data.spec.ts +++ b/libs/common/src/admin-console/models/data/organization.data.spec.ts @@ -59,6 +59,7 @@ describe("ORGANIZATIONS state", () => { userIsManagedByOrganization: false, useRiskInsights: false, useAdminSponsoredFamilies: false, + isAdminInitiated: false, }, }; const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult))); diff --git a/libs/common/src/admin-console/models/data/organization.data.ts b/libs/common/src/admin-console/models/data/organization.data.ts index 799d062aefa..e0783957117 100644 --- a/libs/common/src/admin-console/models/data/organization.data.ts +++ b/libs/common/src/admin-console/models/data/organization.data.ts @@ -61,6 +61,7 @@ export class OrganizationData { userIsManagedByOrganization: boolean; useRiskInsights: boolean; useAdminSponsoredFamilies: boolean; + isAdminInitiated: boolean; constructor( response?: ProfileOrganizationResponse, @@ -124,6 +125,7 @@ export class OrganizationData { this.userIsManagedByOrganization = response.userIsManagedByOrganization; this.useRiskInsights = response.useRiskInsights; this.useAdminSponsoredFamilies = response.useAdminSponsoredFamilies; + this.isAdminInitiated = response.isAdminInitiated; this.isMember = options.isMember; this.isProviderUser = options.isProviderUser; diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index 2e51c54b0ad..1864d56649b 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -91,6 +91,7 @@ export class Organization { userIsManagedByOrganization: boolean; useRiskInsights: boolean; useAdminSponsoredFamilies: boolean; + isAdminInitiated: boolean; constructor(obj?: OrganizationData) { if (obj == null) { @@ -150,6 +151,7 @@ export class Organization { this.userIsManagedByOrganization = obj.userIsManagedByOrganization; this.useRiskInsights = obj.useRiskInsights; this.useAdminSponsoredFamilies = obj.useAdminSponsoredFamilies; + this.isAdminInitiated = obj.isAdminInitiated; } get canAccess() { diff --git a/libs/common/src/admin-console/models/response/profile-organization.response.ts b/libs/common/src/admin-console/models/response/profile-organization.response.ts index da97a1034b1..3a86c764eb8 100644 --- a/libs/common/src/admin-console/models/response/profile-organization.response.ts +++ b/libs/common/src/admin-console/models/response/profile-organization.response.ts @@ -56,6 +56,7 @@ export class ProfileOrganizationResponse extends BaseResponse { userIsManagedByOrganization: boolean; useRiskInsights: boolean; useAdminSponsoredFamilies: boolean; + isAdminInitiated: boolean; constructor(response: any) { super(response); @@ -123,5 +124,6 @@ export class ProfileOrganizationResponse extends BaseResponse { this.userIsManagedByOrganization = this.getResponseProperty("UserIsManagedByOrganization"); this.useRiskInsights = this.getResponseProperty("UseRiskInsights"); this.useAdminSponsoredFamilies = this.getResponseProperty("UseAdminSponsoredFamilies"); + this.isAdminInitiated = this.getResponseProperty("IsAdminInitiated"); } } From eed18c929437ab418f35b02b866088a041a83683 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 12 May 2025 14:26:52 -0400 Subject: [PATCH 010/163] chore(view-cache): [PM-21154] Move view-cache its own feature package and adjust imports * Moved view-cache services to directory * Fixed DI for browser extension. * Fixed tests. --- .../view-cache/popup-view-cache.service.ts | 2 +- .../src/popup/services/services.module.ts | 2 +- .../vault-popup-list-filters.service.spec.ts | 5 +- .../vault-popup-list-filters.service.ts | 5 +- libs/angular/src/platform/view-cache/index.ts | 1 + .../src/platform/view-cache/internal.ts | 1 + .../noop-view-cache.service.ts | 6 +- .../src/platform/view-cache/view-cache.md | 130 ++++++++++++++++++ .../view-cache.service.ts | 2 + .../src/services/jslib-services.module.ts | 5 +- ...auth-component-email-cache.service.spec.ts | 2 +- ...auth-email-component-cache.service.spec.ts | 2 +- ...ctor-auth-email-component-cache.service.ts | 2 +- ...actor-auth-component-cache.service.spec.ts | 2 +- ...two-factor-auth-component-cache.service.ts | 2 +- ...gin-via-auth-request-cache.service.spec.ts | 2 +- ...lt-login-via-auth-request-cache.service.ts | 2 +- .../src/cipher-form/cipher-form.stories.ts | 2 +- .../components/cipher-form.component.spec.ts | 2 +- .../default-cipher-form-cache.service.spec.ts | 2 +- .../default-cipher-form-cache.service.ts | 2 +- 21 files changed, 155 insertions(+), 26 deletions(-) create mode 100644 libs/angular/src/platform/view-cache/index.ts create mode 100644 libs/angular/src/platform/view-cache/internal.ts rename libs/angular/src/platform/{services => view-cache}/noop-view-cache.service.ts (87%) create mode 100644 libs/angular/src/platform/view-cache/view-cache.md rename libs/angular/src/platform/{abstractions => view-cache}/view-cache.service.ts (98%) diff --git a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts index ff63b52ab3f..2a946982990 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts @@ -19,7 +19,7 @@ import { FormCacheOptions, SignalCacheOptions, ViewCacheService, -} from "@bitwarden/angular/platform/abstractions/view-cache.service"; +} from "@bitwarden/angular/platform/view-cache"; import { MessageSender } from "@bitwarden/common/platform/messaging"; import { GlobalStateProvider } from "@bitwarden/common/platform/state"; diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 00b8ae81cf9..6ede88dfc13 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -5,9 +5,9 @@ import { Router } from "@angular/router"; import { merge, of, Subject } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; -import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; +import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { CLIENT_TYPE, DEFAULT_VAULT_TIMEOUT, diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index 9498d953808..621ec795157 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -4,6 +4,7 @@ import { FormBuilder } from "@angular/forms"; import { BehaviorSubject, skipWhile } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -20,8 +21,6 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; -import { PopupViewCacheService } from "../../../platform/popup/view-cache/popup-view-cache.service"; - import { CachedFilterState, MY_VAULT_ID, @@ -123,7 +122,7 @@ describe("VaultPopupListFiltersService", () => { useValue: accountService, }, { - provide: PopupViewCacheService, + provide: ViewCacheService, useValue: viewCacheService, }, ], diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index c726678c973..db4cfeefe9f 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -15,6 +15,7 @@ import { } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { DynamicTreeNode } from "@bitwarden/angular/vault/vault-filter/models/dynamic-tree-node.model"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -40,8 +41,6 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { ChipSelectOption } from "@bitwarden/components"; -import { PopupViewCacheService } from "../../../platform/popup/view-cache/popup-view-cache.service"; - const FILTER_VISIBILITY_KEY = new KeyDefinition(VAULT_SETTINGS_DISK, "filterVisibility", { deserializer: (obj) => obj, }); @@ -178,7 +177,7 @@ export class VaultPopupListFiltersService { private policyService: PolicyService, private stateProvider: StateProvider, private accountService: AccountService, - private viewCacheService: PopupViewCacheService, + private viewCacheService: ViewCacheService, ) { this.filterForm.controls.organization.valueChanges .pipe(takeUntilDestroyed()) diff --git a/libs/angular/src/platform/view-cache/index.ts b/libs/angular/src/platform/view-cache/index.ts new file mode 100644 index 00000000000..79deef6aa5b --- /dev/null +++ b/libs/angular/src/platform/view-cache/index.ts @@ -0,0 +1 @@ +export { ViewCacheService, FormCacheOptions, SignalCacheOptions } from "./view-cache.service"; diff --git a/libs/angular/src/platform/view-cache/internal.ts b/libs/angular/src/platform/view-cache/internal.ts new file mode 100644 index 00000000000..6e0992eecbf --- /dev/null +++ b/libs/angular/src/platform/view-cache/internal.ts @@ -0,0 +1 @@ +export { NoopViewCacheService } from "./noop-view-cache.service"; diff --git a/libs/angular/src/platform/services/noop-view-cache.service.ts b/libs/angular/src/platform/view-cache/noop-view-cache.service.ts similarity index 87% rename from libs/angular/src/platform/services/noop-view-cache.service.ts rename to libs/angular/src/platform/view-cache/noop-view-cache.service.ts index 9953e80b3b0..f83a0fc0b04 100644 --- a/libs/angular/src/platform/services/noop-view-cache.service.ts +++ b/libs/angular/src/platform/view-cache/noop-view-cache.service.ts @@ -1,11 +1,7 @@ import { Injectable, signal, WritableSignal } from "@angular/core"; import type { FormGroup } from "@angular/forms"; -import { - FormCacheOptions, - SignalCacheOptions, - ViewCacheService, -} from "../abstractions/view-cache.service"; +import { FormCacheOptions, SignalCacheOptions, ViewCacheService } from "./view-cache.service"; /** * The functionality of the {@link ViewCacheService} is only needed in the browser extension popup, diff --git a/libs/angular/src/platform/view-cache/view-cache.md b/libs/angular/src/platform/view-cache/view-cache.md new file mode 100644 index 00000000000..c1f80da5800 --- /dev/null +++ b/libs/angular/src/platform/view-cache/view-cache.md @@ -0,0 +1,130 @@ +# Extension Persistence + +By default, when the browser extension popup closes, the user's current view and any data entered +without saving is lost. This introduces friction in several workflows within our client, such as: + +- Performing actions that require email OTP entry, since the user must navigate from the popup to + get to their email inbox +- Entering information to create a new vault item from a browser tab +- And many more + +Previously, we have recommended that users "pop out" the extension into its own window to persist +the extension context, but this introduces additional user actions and may leave the extension open +(and unlocked) for longer than a user intends. + +In order to provide a better user experience, we have introduced two levels of persistence to the +Bitwarden extension client: + +- We persist the route history, allowing us to re-open the last route when the popup re-opens, and +- We offer a service for teams to use to persist component-specific form data or state to survive a + popup close/re-open cycle + +## Persistence lifetime + +Since we are persisting data, it is important that the lifetime of that data be well-understood and +well-constrained. The cache of route history and form data is cleared when any of the following +events occur: + +- The account is locked +- The account is logged out +- Account switching is used to switch the active account +- The extension popup has been closed for 2 minutes + +In addition, cached form data is cleared when a browser extension navigation event occurs (e.g. +switching between tabs in the extension). + +## Types of persistence + +### Route history persistence + +Route history is persisted on the extension automatically, with no specific implementation required +on any component. + +The persistence layer ensures that the popup will open at the same route as was active when it +closed, provided that none of the lifetime expiration events have occurred. + +:::tip Excluding a route + +If a particular route should be excluded from the history and not persisted, add +`doNotSaveUrl: true` to the `data` property on the route. + +::: + +### View data persistence + +Route persistence ensures that the user will land back on the route that they were on when the popup +closed, but it does not persist any state or form data that the user may have modified. In order to +persist that data, the component is responsible for registering that data with the +[`ViewCacheService`](./view-cache.service.ts). +This is done prescriptively to ensure that only necessary data is cached and that it is done with +intention by the component. + +The `ViewCacheService` provides an interface for caching both individual state and `FormGroup`s. + +#### Caching individual data elements + +For individual pieces of state, use the `signal()` method on the `ViewCacheService` to create a +writeable [signal](https://angular.dev/guide/signals) wrapper around the desired state. + +```typescript +const mySignal = this.viewCacheService.signal({ + key: "my-state-key" + initialValue: null +}); +``` + +If a cached value exists, the returned signal will contain the cached data. + +Setting the value should be done through the signal's `set()` method: + +```typescript +const mySignal = this.viewCacheService.signal({ + key: "my-state-key" + initialValue: null +}); +mySignal.set("value") +``` + +:::note Equality comparison + +By default, signals use `Object.is` to determine equality, and `set()` will only trigger updates if +the updated value is not equal to the current signal state. See documentation +[here](https://angular.dev/guide/signals#signal-equality-functions). + +::: + +Putting this together, the most common implementation pattern would be: + +1. **Register the signal** using `ViewCacheService.signal()` on initialization of the component or + service responsible for the state being persisted. +2. **Restore state from the signal:** If cached data exists, the signal will contain that data. The + component or service should use this data to re-create the state from prior to the popup closing. +3. **Set new state** in the cache when it changes. Ensure that any updates to the data are persisted + to the cache with `set()`, so that the cache reflects the latest state. + +#### Caching form data + +For persisting form data, the `ViewCacheService` supplies a `formGroup()` method, which manages the +persistence of any entered form data to the cache and the initialization of the form from the cached +data. You can supply the `FormGroup` in the `control` parameter of the method, and the +`ViewCacheService` will: + +- Initialize the form the a cached value, if it exists +- Save form value to cache when it changes +- Mark the form dirty if the restored value is not `undefined`. + +```typescript +this.loginDetailsForm = this.viewCacheService.formGroup({ + key: "my-form", + control: this.formBuilder.group({ + username: [""], + email: [""], + }), +}); +``` + +## What about other clients? + +The `ViewCacheService` is designed to be injected into shared, client-agnostic components. A +`NoopViewCacheService` is provided and injected for non-extension clients, preserving a single +interface for your components. diff --git a/libs/angular/src/platform/abstractions/view-cache.service.ts b/libs/angular/src/platform/view-cache/view-cache.service.ts similarity index 98% rename from libs/angular/src/platform/abstractions/view-cache.service.ts rename to libs/angular/src/platform/view-cache/view-cache.service.ts index c5ae6c77d1f..498a29aa242 100644 --- a/libs/angular/src/platform/abstractions/view-cache.service.ts +++ b/libs/angular/src/platform/view-cache/view-cache.service.ts @@ -42,6 +42,8 @@ export type FormCacheOptions = BaseCacheOptions< /** * Cache for temporary component state * + * [Read more](./view-cache.md) + * * #### Implementations * - browser extension popup: used to persist UI between popup open and close * - all other clients: noop diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 4e7c558a0f0..470115ae3f0 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -325,13 +325,14 @@ import { import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction"; import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; -import { ViewCacheService } from "../platform/abstractions/view-cache.service"; import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service"; import { LoggingErrorHandler } from "../platform/services/logging-error-handler"; -import { NoopViewCacheService } from "../platform/services/noop-view-cache.service"; import { AngularThemingService } from "../platform/services/theming/angular-theming.service"; import { AbstractThemingService } from "../platform/services/theming/theming.service.abstraction"; import { safeProvider, SafeProvider } from "../platform/utils/safe-provider"; +import { ViewCacheService } from "../platform/view-cache"; +// eslint-disable-next-line no-restricted-imports -- Needed for DI +import { NoopViewCacheService } from "../platform/view-cache/internal"; import { CLIENT_TYPE, diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-component-email-cache.service.spec.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-component-email-cache.service.spec.ts index f3b904a4ea6..d2d86710b72 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-component-email-cache.service.spec.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-component-email-cache.service.spec.ts @@ -2,7 +2,7 @@ import { TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; -import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.spec.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.spec.ts index 1613c0e4af8..36d99ee56ac 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.spec.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.spec.ts @@ -2,7 +2,7 @@ import { TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; -import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.ts index e32b6cd1385..d274b8003d7 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.ts @@ -1,7 +1,7 @@ import { inject, Injectable, WritableSignal } from "@angular/core"; import { Jsonify } from "type-fest"; -import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.spec.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.spec.ts index 0993954fde1..5b5d486556b 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.spec.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.spec.ts @@ -2,7 +2,7 @@ import { TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; -import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.ts index 61b44aa98dd..2d9fcaa5633 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.ts @@ -1,7 +1,7 @@ import { inject, Injectable, WritableSignal } from "@angular/core"; import { Jsonify } from "type-fest"; -import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; diff --git a/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.spec.ts b/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.spec.ts index 89b78500f1f..ab82b98f614 100644 --- a/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.spec.ts @@ -1,7 +1,7 @@ import { signal } from "@angular/core"; import { TestBed } from "@angular/core/testing"; -import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; diff --git a/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.ts b/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.ts index 493fea5c14b..8fbdb925e7c 100644 --- a/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.ts +++ b/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.ts @@ -1,6 +1,6 @@ import { inject, Injectable, WritableSignal } from "@angular/core"; -import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; diff --git a/libs/vault/src/cipher-form/cipher-form.stories.ts b/libs/vault/src/cipher-form/cipher-form.stories.ts index 9943f07292d..7090502ef14 100644 --- a/libs/vault/src/cipher-form/cipher-form.stories.ts +++ b/libs/vault/src/cipher-form/cipher-form.stories.ts @@ -13,7 +13,7 @@ import { import { BehaviorSubject } from "rxjs"; import { CollectionView } from "@bitwarden/admin-console/common"; -import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts b/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts index 4c61ad5d2d5..da687f33ef9 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ReactiveFormsModule } from "@angular/forms"; import { mock } from "jest-mock-extended"; -import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.spec.ts b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.spec.ts index 6236e2d3dac..0dec46e1b20 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.spec.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.spec.ts @@ -1,7 +1,7 @@ import { signal } from "@angular/core"; import { TestBed } from "@angular/core/testing"; -import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts index 268b2db306b..b4a8138e025 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts @@ -1,6 +1,6 @@ import { inject, Injectable } from "@angular/core"; -import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; From a8fb456329ab1d5eaa32684e43dffc6a8585b829 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Mon, 12 May 2025 14:38:08 -0400 Subject: [PATCH 011/163] Uif/remove a11y manual flag (#14744) * remove comment left in error * remove runOnly options and manual flag --- .storybook/preview.tsx | 4 ---- .storybook/test-runner.ts | 5 +---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 19a4fc51dc8..a948fce0428 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -28,10 +28,6 @@ const preview: Preview = { parameters: { a11y: { element: "#storybook-root", - manual: true, - options: { - runOnly: ["section508", "wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"], - }, }, controls: { matchers: { diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index 8584a321090..208adf96214 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -1,7 +1,4 @@ -import { - // waitForPageReady, - type TestRunnerConfig, -} from "@storybook/test-runner"; +import { type TestRunnerConfig } from "@storybook/test-runner"; import { injectAxe, checkA11y } from "axe-playwright"; const testRunnerConfig: TestRunnerConfig = { From 90d8e822ff09d6c4013cc346972712e902c0f797 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 12 May 2025 14:08:48 -0500 Subject: [PATCH 012/163] chore: remove vault-bulk-management-action flag for the third time, refs PM-13112 (#14747) --- libs/common/src/enums/feature-flag.enum.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 08de8ada25a..aa9a9ab5d6e 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -55,7 +55,6 @@ export enum FeatureFlag { /* Vault */ PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge", PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form", - VaultBulkManagementAction = "vault-bulk-management-action", SecurityTasks = "security-tasks", CipherKeyEncryption = "cipher-key-encryption", PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms", @@ -107,7 +106,6 @@ export const DefaultFeatureFlagValue = { /* Vault */ [FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE, [FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE, - [FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.SecurityTasks]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, [FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE, From ce8e123c447c8bdbbbc8a6bb0eb1381e837e4fbe Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 12 May 2025 12:17:15 -0700 Subject: [PATCH 013/163] [PM-21480] - check for privacy granted in isBrowserAutofillSettingOverridden (#14748) * check for privacy granted in isBrowserAutofillSettingOverridden * check for privacy granted in browserAutofillSettingsOverridden --- apps/browser/src/platform/browser/browser-api.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index b27e8ca7c96..4ef72fa0077 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -664,6 +664,10 @@ export class BrowserApi { * Identifies if the browser autofill settings are overridden by the extension. */ static async browserAutofillSettingsOverridden(): Promise { + if (!(await BrowserApi.permissionsGranted(["privacy"]))) { + return false; + } + const checkOverrideStatus = (details: chrome.types.ChromeSettingGetResult) => details.levelOfControl === "controlled_by_this_extension" && !details.value; From b8074a6f730c0179b71f2ccde0b06920d46d57ea Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 12 May 2025 15:18:02 -0400 Subject: [PATCH 014/163] chore(feature-flag): Remove pm-9112-device-approval-persistence (#14718) --- .../login-via-auth-request.component.ts | 34 +++++------ ...gin-via-auth-request-cache.service.spec.ts | 61 ++++--------------- ...lt-login-via-auth-request-cache.service.ts | 27 -------- libs/common/src/enums/feature-flag.enum.ts | 2 - 4 files changed, 27 insertions(+), 97 deletions(-) diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts index ab5b0c09b32..4c95a1eca3e 100644 --- a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts @@ -23,12 +23,10 @@ import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request" import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view"; import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -101,7 +99,6 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { private validationService: ValidationService, private loginSuccessHandlerService: LoginSuccessHandlerService, private loginViaAuthRequestCacheService: LoginViaAuthRequestCacheService, - private configService: ConfigService, ) { this.clientType = this.platformUtilsService.getClientType(); @@ -132,7 +129,6 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { async ngOnInit(): Promise { // Get the authStatus early because we use it in both flows this.authStatus = await firstValueFrom(this.authService.activeAccountStatus$); - await this.loginViaAuthRequestCacheService.init(); const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked; @@ -410,24 +406,22 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { const authRequestResponse: AuthRequestResponse = await this.authRequestApiService.postAuthRequest(authRequest); - if (await this.configService.getFeatureFlag(FeatureFlag.PM9112_DeviceApprovalPersistence)) { - if (!this.authRequestKeyPair.privateKey) { - this.logService.error("No private key when trying to cache the login view."); - return; - } - - if (!this.accessCode) { - this.logService.error("No access code when trying to cache the login view."); - return; - } - - this.loginViaAuthRequestCacheService.cacheLoginView( - authRequestResponse.id, - this.authRequestKeyPair.privateKey, - this.accessCode, - ); + if (!this.authRequestKeyPair.privateKey) { + this.logService.error("No private key when trying to cache the login view."); + return; } + if (!this.accessCode) { + this.logService.error("No access code when trying to cache the login view."); + return; + } + + this.loginViaAuthRequestCacheService.cacheLoginView( + authRequestResponse.id, + this.authRequestKeyPair.privateKey, + this.accessCode, + ); + if (authRequestResponse.id) { await this.anonymousHubService.createHubConnection(authRequestResponse.id); } diff --git a/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.spec.ts b/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.spec.ts index ab82b98f614..67ac605e611 100644 --- a/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.spec.ts @@ -3,7 +3,6 @@ import { TestBed } from "@angular/core/testing"; import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { LoginViaAuthRequestCacheService } from "./default-login-via-auth-request-cache.service"; @@ -14,74 +13,40 @@ describe("LoginViaAuthRequestCache", () => { const cacheSignal = signal(null); const getCacheSignal = jest.fn().mockReturnValue(cacheSignal); - const getFeatureFlag = jest.fn().mockResolvedValue(false); const cacheSetMock = jest.spyOn(cacheSignal, "set"); beforeEach(() => { getCacheSignal.mockClear(); - getFeatureFlag.mockClear(); cacheSetMock.mockClear(); testBed = TestBed.configureTestingModule({ providers: [ { provide: ViewCacheService, useValue: { signal: getCacheSignal } }, - { provide: ConfigService, useValue: { getFeatureFlag } }, LoginViaAuthRequestCacheService, ], }); }); - describe("feature enabled", () => { - beforeEach(() => { - getFeatureFlag.mockResolvedValue(true); - }); + it("`getCachedLoginViaAuthRequestView` returns the cached data", async () => { + cacheSignal.set({ ...buildMockState() }); + service = testBed.inject(LoginViaAuthRequestCacheService); - it("`getCachedLoginViaAuthRequestView` returns the cached data", async () => { - cacheSignal.set({ ...buildMockState() }); - service = testBed.inject(LoginViaAuthRequestCacheService); - await service.init(); - - expect(service.getCachedLoginViaAuthRequestView()).toEqual({ - ...buildMockState(), - }); - }); - - it("updates the signal value", async () => { - service = testBed.inject(LoginViaAuthRequestCacheService); - await service.init(); - - const parameters = buildAuthenticMockAuthView(); - - service.cacheLoginView(parameters.id, parameters.privateKey, parameters.accessCode); - - expect(cacheSignal.set).toHaveBeenCalledWith({ - id: parameters.id, - privateKey: Utils.fromBufferToB64(parameters.privateKey), - accessCode: parameters.accessCode, - }); + expect(service.getCachedLoginViaAuthRequestView()).toEqual({ + ...buildMockState(), }); }); - describe("feature disabled", () => { - beforeEach(async () => { - cacheSignal.set({ ...buildMockState() } as LoginViaAuthRequestView); - getFeatureFlag.mockResolvedValue(false); - cacheSetMock.mockClear(); + it("updates the signal value", async () => { + service = testBed.inject(LoginViaAuthRequestCacheService); - service = testBed.inject(LoginViaAuthRequestCacheService); - await service.init(); - }); + const parameters = buildAuthenticMockAuthView(); - it("`getCachedCipherView` returns null", () => { - expect(service.getCachedLoginViaAuthRequestView()).toBeNull(); - }); + service.cacheLoginView(parameters.id, parameters.privateKey, parameters.accessCode); - it("does not update the signal value", () => { - const params = buildAuthenticMockAuthView(); - - service.cacheLoginView(params.id, params.privateKey, params.accessCode); - - expect(cacheSignal.set).not.toHaveBeenCalled(); + expect(cacheSignal.set).toHaveBeenCalledWith({ + id: parameters.id, + privateKey: Utils.fromBufferToB64(parameters.privateKey), + accessCode: parameters.accessCode, }); }); diff --git a/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.ts b/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.ts index 8fbdb925e7c..80dbafd3159 100644 --- a/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.ts +++ b/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.ts @@ -2,8 +2,6 @@ import { inject, Injectable, WritableSignal } from "@angular/core"; import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; const LOGIN_VIA_AUTH_CACHE_KEY = "login-via-auth-request-form-cache"; @@ -17,10 +15,6 @@ const LOGIN_VIA_AUTH_CACHE_KEY = "login-via-auth-request-form-cache"; @Injectable() export class LoginViaAuthRequestCacheService { private viewCacheService: ViewCacheService = inject(ViewCacheService); - private configService: ConfigService = inject(ConfigService); - - /** True when the `PM9112_DeviceApproval` flag is enabled */ - private featureEnabled: boolean = false; private defaultLoginViaAuthRequestCache: WritableSignal = this.viewCacheService.signal({ @@ -31,23 +25,10 @@ export class LoginViaAuthRequestCacheService { constructor() {} - /** - * Must be called once before interacting with the cached data, otherwise methods will be noop. - */ - async init() { - this.featureEnabled = await this.configService.getFeatureFlag( - FeatureFlag.PM9112_DeviceApprovalPersistence, - ); - } - /** * Update the cache with the new LoginView. */ cacheLoginView(id: string, privateKey: Uint8Array, accessCode: string): void { - if (!this.featureEnabled) { - return; - } - // When the keys get stored they should be converted to a B64 string to ensure // data can be properly formed when json-ified. If not done, they are not stored properly and // will not be parsable by the cryptography library after coming out of storage. @@ -59,10 +40,6 @@ export class LoginViaAuthRequestCacheService { } clearCacheLoginView(): void { - if (!this.featureEnabled) { - return; - } - this.defaultLoginViaAuthRequestCache.set(null); } @@ -70,10 +47,6 @@ export class LoginViaAuthRequestCacheService { * Returns the cached LoginViaAuthRequestView when available. */ getCachedLoginViaAuthRequestView(): LoginViaAuthRequestView | null { - if (!this.featureEnabled) { - return null; - } - return this.defaultLoginViaAuthRequestCache(); } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index aa9a9ab5d6e..a7679a6248a 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -16,7 +16,6 @@ export enum FeatureFlag { SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions", /* Auth */ - PM9112_DeviceApprovalPersistence = "pm-9112-device-approval-persistence", PM9115_TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence", /* Autofill */ @@ -112,7 +111,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.EndUserNotifications]: FALSE, /* Auth */ - [FeatureFlag.PM9112_DeviceApprovalPersistence]: FALSE, [FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE, /* Billing */ From 7d01c42e37e07d6642a2fbda5eb27cc12fdf4f85 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 15:42:21 -0400 Subject: [PATCH 015/163] [deps] Vault: Update multer to v1.4.5-lts.2 (#14469) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: SmithThe4th --- 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 79d4786a23c..0a7e66d7818 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -81,7 +81,7 @@ "koa-json": "2.0.2", "lowdb": "1.0.0", "lunr": "2.3.9", - "multer": "1.4.5-lts.1", + "multer": "1.4.5-lts.2", "node-fetch": "2.6.12", "node-forge": "1.3.1", "open": "8.4.2", diff --git a/package-lock.json b/package-lock.json index 81c9384fbbb..44bebc9cbd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "lit": "3.2.1", "lowdb": "1.0.0", "lunr": "2.3.9", - "multer": "1.4.5-lts.1", + "multer": "1.4.5-lts.2", "ngx-toastr": "19.0.0", "node-fetch": "2.6.12", "node-forge": "1.3.1", @@ -217,7 +217,7 @@ "koa-json": "2.0.2", "lowdb": "1.0.0", "lunr": "2.3.9", - "multer": "1.4.5-lts.1", + "multer": "1.4.5-lts.2", "node-fetch": "2.6.12", "node-forge": "1.3.1", "open": "8.4.2", @@ -28598,9 +28598,9 @@ } }, "node_modules/multer": { - "version": "1.4.5-lts.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", - "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", diff --git a/package.json b/package.json index 8ba1c95df6a..47cede513ed 100644 --- a/package.json +++ b/package.json @@ -190,7 +190,7 @@ "lit": "3.2.1", "lowdb": "1.0.0", "lunr": "2.3.9", - "multer": "1.4.5-lts.1", + "multer": "1.4.5-lts.2", "ngx-toastr": "19.0.0", "node-fetch": "2.6.12", "node-forge": "1.3.1", From 9af7963609a08a5e00dd209e5614022c0d58350a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 14:58:48 -0500 Subject: [PATCH 016/163] [deps] Vault: Update @koa/multer to v3.1.0 (#14495) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 68 ++++++++++++++++++++----------------------- package.json | 2 +- 3 files changed, 33 insertions(+), 39 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 0a7e66d7818..82faa7d40e6 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -64,7 +64,7 @@ ] }, "dependencies": { - "@koa/multer": "3.0.2", + "@koa/multer": "3.1.0", "@koa/router": "13.1.0", "argon2": "0.41.1", "big-integer": "1.6.52", diff --git a/package-lock.json b/package-lock.json index 44bebc9cbd4..8466a4ba4c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@bitwarden/sdk-internal": "0.2.0-main.159", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", - "@koa/multer": "3.0.2", + "@koa/multer": "3.1.0", "@koa/router": "13.1.0", "@microsoft/signalr": "8.0.7", "@microsoft/signalr-protocol-msgpack": "8.0.7", @@ -200,7 +200,7 @@ "version": "2025.4.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { - "@koa/multer": "3.0.2", + "@koa/multer": "3.1.0", "@koa/router": "13.1.0", "argon2": "0.41.1", "big-integer": "1.6.52", @@ -389,6 +389,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2745,6 +2746,7 @@ "version": "7.26.8", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2754,6 +2756,7 @@ "version": "7.24.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", + "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -2784,12 +2787,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, "license": "MIT" }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2799,6 +2804,7 @@ "version": "7.26.10", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.26.10", @@ -2828,6 +2834,7 @@ "version": "7.26.5", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.26.5", @@ -2844,6 +2851,7 @@ "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -2876,6 +2884,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3015,6 +3024,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.25.9", @@ -3045,6 +3055,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -3148,6 +3159,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -3172,6 +3184,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.1", @@ -3280,23 +3293,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -3382,6 +3378,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" @@ -4010,6 +4007,7 @@ "version": "7.26.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.26.0", @@ -7855,15 +7853,12 @@ } }, "node_modules/@koa/multer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@koa/multer/-/multer-3.0.2.tgz", - "integrity": "sha512-Q6WfPpE06mJWyZD1fzxM6zWywaoo+zocAn2YA9QYz4RsecoASr1h/kSzG0c5seDpFVKCMZM9raEfuM7XfqbRLw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@koa/multer/-/multer-3.1.0.tgz", + "integrity": "sha512-ETf4OLpOew9XE9lyU+5HIqk3TCmdGAw9pUXgxzrlYip+PkxLGoU4meiVTxiW4B6lxdBNijb3DFQ7M2woLcDL1g==", "license": "MIT", - "dependencies": { - "fix-esm": "1.0.1" - }, "engines": { - "node": ">= 8" + "node": ">= 14" }, "peerDependencies": { "multer": "*" @@ -15362,6 +15357,7 @@ "version": "4.23.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -15951,6 +15947,7 @@ "version": "1.0.30001717", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -18482,6 +18479,7 @@ "version": "1.5.151", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.151.tgz", "integrity": "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==", + "dev": true, "license": "ISC" }, "node_modules/electron-updater": { @@ -18965,6 +18963,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -20387,17 +20386,6 @@ "micromatch": "^4.0.2" } }, - "node_modules/fix-esm": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fix-esm/-/fix-esm-1.0.1.tgz", - "integrity": "sha512-EZtb7wPXZS54GaGxaWxMlhd1DUDCnAg5srlYdu/1ZVeW+7wwR3Tp59nu52dXByFs3MBRq+SByx1wDOJpRvLEXw==", - "license": "WTFPL OR CC0-1.0", - "dependencies": { - "@babel/core": "^7.14.6", - "@babel/plugin-proposal-export-namespace-from": "^7.14.5", - "@babel/plugin-transform-modules-commonjs": "^7.14.5" - } - }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -21084,6 +21072,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -25452,6 +25441,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -27032,6 +27022,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -29278,6 +29269,7 @@ "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, "license": "MIT" }, "node_modules/nopt": { @@ -37088,6 +37080,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -38970,6 +38963,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, "license": "ISC" }, "node_modules/yaml": { diff --git a/package.json b/package.json index 47cede513ed..2993707313f 100644 --- a/package.json +++ b/package.json @@ -163,7 +163,7 @@ "@bitwarden/sdk-internal": "0.2.0-main.159", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", - "@koa/multer": "3.0.2", + "@koa/multer": "3.1.0", "@koa/router": "13.1.0", "@microsoft/signalr": "8.0.7", "@microsoft/signalr-protocol-msgpack": "8.0.7", From 63b224f3dd08f692c14e18109fba4463062b5c63 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Mon, 12 May 2025 17:03:09 -0400 Subject: [PATCH 017/163] PM-21454 Firefox - "null" text is showing in Add/Update Confirmation notification (#14745) * PM-21454 * Update apps/browser/src/autofill/content/components/lit-stories/mock-data.ts Co-authored-by: Jonathan Prusik * Update apps/browser/src/autofill/content/components/lit-stories/mock-data.ts Co-authored-by: Jonathan Prusik * Update apps/browser/src/autofill/content/components/notification/confirmation/container.ts Co-authored-by: Jonathan Prusik * run lint --------- Co-authored-by: Jonathan Prusik --- apps/browser/src/_locales/en/messages.json | 19 ++---- .../components/lit-stories/mock-data.ts | 4 +- .../notification/confirmation/container.ts | 9 ++- .../notification/confirmation/message.ts | 63 ++++++++++++------- apps/browser/src/autofill/notification/bar.ts | 6 +- 5 files changed, 57 insertions(+), 44 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 57a0e78fd1b..361093b12ef 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1088,22 +1088,13 @@ } } }, - "loginSaveConfirmation": { - "message": "$ITEMNAME$ saved to Bitwarden.", - "placeholders": { - "itemName": { - "content": "$1" - } - }, + "notificationLoginSaveConfirmation": { + "message": "saved to Bitwarden.", + "description": "Shown to user after item is saved." }, - "loginUpdatedConfirmation": { - "message": "$ITEMNAME$ updated in Bitwarden.", - "placeholders": { - "itemName": { - "content": "$1" - } - }, + "notificationLoginUpdatedConfirmation": { + "message": "updated in Bitwarden.", "description": "Shown to user after item is updated." }, "saveAsNewLoginAction": { diff --git a/apps/browser/src/autofill/content/components/lit-stories/mock-data.ts b/apps/browser/src/autofill/content/components/lit-stories/mock-data.ts index b8e35813c6c..d134db3ca6f 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/mock-data.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/mock-data.ts @@ -107,9 +107,9 @@ export const mockI18n = { collection: "Collection", folder: "Folder", loginSaveSuccess: "Login saved", - loginSaveConfirmation: "$ITEMNAME$ saved to Bitwarden.", + notificationLoginSaveConfirmation: "saved to Bitwarden.", loginUpdateSuccess: "Login updated", - loginUpdatedConfirmation: "$ITEMNAME$ updated in Bitwarden.", + notificationLoginUpdatedConfirmation: "updated in Bitwarden.", loginUpdateTaskSuccess: "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", loginUpdateTaskSuccessAdditional: diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/container.ts b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts index 7ad6b4542ec..d4d66c7a7be 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/container.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts @@ -113,9 +113,14 @@ function getConfirmationMessage(i18n: I18n, type?: NotificationType, error?: str if (error) { return i18n.saveFailureDetails; } + + /* @TODO This partial string return and later concatenation with the cipher name is needed + * to handle cipher name overflow cases, but is risky for i18n concerns. Fix concatenation + * with cipher name overflow when a tag replacement solution is available. + */ return type === NotificationTypes.Add - ? i18n.loginSaveConfirmation - : i18n.loginUpdatedConfirmation; + ? i18n.notificationLoginSaveConfirmation + : i18n.notificationLoginUpdatedConfirmation; } function getHeaderMessage(i18n: I18n, type?: NotificationType, error?: string) { diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts index 527119aed15..2bf8caecfff 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts @@ -28,28 +28,31 @@ export function NotificationConfirmationMessage({

${message || buttonText ? html` - ${itemName} - - ${message || nothing} - ${buttonText - ? html` - handleButtonKeyDown(e, () => handleClick(e))} - aria-label=${buttonAria} - tabindex="0" - role="button" - > - ${buttonText} - - ` - : nothing} - +
+ ${itemName} + + ${message || nothing} + ${buttonText + ? html` + + handleButtonKeyDown(e, () => handleClick(e))} + aria-label=${buttonAria} + tabindex="0" + role="button" + > + ${buttonText} + + ` + : nothing} + +
` : nothing} ${messageDetails @@ -61,12 +64,17 @@ export function NotificationConfirmationMessage({ const containerStyles = css` display: flex; - flex-wrap: wrap; - align-items: center; + flex-direction: column; gap: ${spacing[1]}; width: 100%; `; +const singleLineWrapperStyles = css` + display: inline; + white-space: normal; + word-break: break-word; +`; + const baseTextStyles = css` overflow-x: hidden; text-align: left; @@ -81,6 +89,9 @@ const notificationConfirmationMessageStyles = (theme: Theme) => css` color: ${themes[theme].text.main}; font-weight: 400; + white-space: normal; + word-break: break-word; + display: inline; `; const itemNameStyles = (theme: Theme) => css` @@ -90,6 +101,10 @@ const itemNameStyles = (theme: Theme) => css` font-weight: 400; white-space: nowrap; max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + vertical-align: bottom; `; const notificationConfirmationButtonTextStyles = (theme: Theme) => css` diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 3fd6016eb33..52cbdcd47d3 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -59,9 +59,7 @@ function getI18n() { collection: chrome.i18n.getMessage("collection"), folder: chrome.i18n.getMessage("folder"), loginSaveSuccess: chrome.i18n.getMessage("loginSaveSuccess"), - loginSaveConfirmation: chrome.i18n.getMessage("loginSaveConfirmation"), loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"), - loginUpdatedConfirmation: chrome.i18n.getMessage("loginUpdatedConfirmation"), loginUpdateTaskSuccess: chrome.i18n.getMessage("loginUpdateTaskSuccess"), loginUpdateTaskSuccessAdditional: chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional"), nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"), @@ -74,6 +72,10 @@ function getI18n() { notificationUpdate: chrome.i18n.getMessage("notificationChangeSave"), notificationEdit: chrome.i18n.getMessage("edit"), notificationEditTooltip: chrome.i18n.getMessage("notificationEditTooltip"), + notificationLoginSaveConfirmation: chrome.i18n.getMessage("notificationLoginSaveConfirmation"), + notificationLoginUpdatedConfirmation: chrome.i18n.getMessage( + "notificationLoginUpdatedConfirmation", + ), notificationUnlock: chrome.i18n.getMessage("notificationUnlock"), notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"), notificationViewAria: chrome.i18n.getMessage("notificationViewAria"), From 02cbc37ac56eccefacfb1d35a6a4c7e8f59c0cea Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Mon, 12 May 2025 17:23:06 -0400 Subject: [PATCH 018/163] PM-20397 Misc notification bar UI tweaks (#14558) * PM-20637 * turncate save button --- .../content/components/notification/container.ts | 2 +- .../content/components/notification/footer.ts | 15 +++++++++------ .../content/components/rows/button-row.ts | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts index c7c7ae16cde..1a78854f5dc 100644 --- a/apps/browser/src/autofill/content/components/notification/container.ts +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -94,7 +94,7 @@ const notificationContainerStyles = (theme: Theme) => css` } [class*="${notificationBodyClassPrefix}-"] { - margin: ${spacing["3"]} 0 ${spacing["1.5"]} ${spacing["3"]}; + margin: ${spacing["3"]} 0 0 ${spacing["3"]}; padding-right: ${spacing["3"]}; } `; diff --git a/apps/browser/src/autofill/content/components/notification/footer.ts b/apps/browser/src/autofill/content/components/notification/footer.ts index 73fe9084595..03d0d475e85 100644 --- a/apps/browser/src/autofill/content/components/notification/footer.ts +++ b/apps/browser/src/autofill/content/components/notification/footer.ts @@ -8,7 +8,7 @@ import { NotificationTypes, } from "../../../notification/abstractions/notification-bar"; import { OrgView, FolderView, I18n, CollectionView } from "../common-types"; -import { spacing, themes } from "../constants/styles"; +import { spacing } from "../constants/styles"; import { NotificationButtonRow } from "./button-row"; @@ -37,7 +37,7 @@ export function NotificationFooter({ const primaryButtonText = i18n.saveAction; return html` -
+
${!isChangeNotification ? NotificationButtonRow({ collections, @@ -56,13 +56,16 @@ export function NotificationFooter({ `; } -const notificationFooterStyles = ({ theme }: { theme: Theme }) => css` +const notificationFooterStyles = ({ + isChangeNotification, +}: { + isChangeNotification: boolean; +}) => css` display: flex; - background-color: ${themes[theme].background.alt}; - padding: 0 ${spacing[3]} ${spacing[3]} ${spacing[3]}; + padding: ${spacing[2]} ${spacing[4]} ${isChangeNotification ? spacing[1] : spacing[4]} + ${spacing[4]}; :last-child { border-radius: 0 0 ${spacing["4"]} ${spacing["4"]}; - padding-bottom: ${spacing[4]}; } `; diff --git a/apps/browser/src/autofill/content/components/rows/button-row.ts b/apps/browser/src/autofill/content/components/rows/button-row.ts index 3527d050b81..7e81d846cb7 100644 --- a/apps/browser/src/autofill/content/components/rows/button-row.ts +++ b/apps/browser/src/autofill/content/components/rows/button-row.ts @@ -62,7 +62,7 @@ const buttonRowStyles = css` > button { max-width: min-content; - flex: 1 1 50%; + flex: 1 1 25%; } > div { From 7a51161d50da1b1b4e2428de943a8a83991bde82 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 13 May 2025 12:43:26 +0200 Subject: [PATCH 019/163] Move selectable avatar to auth (#14704) --- .../settings/account}/selectable-avatar.component.ts | 0 apps/web/src/app/shared/loose-components.module.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename apps/web/src/app/{components => auth/settings/account}/selectable-avatar.component.ts (100%) diff --git a/apps/web/src/app/components/selectable-avatar.component.ts b/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts similarity index 100% rename from apps/web/src/app/components/selectable-avatar.component.ts rename to apps/web/src/app/auth/settings/account/selectable-avatar.component.ts diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 30d1dd1af75..ab6c6cc5d72 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -22,6 +22,7 @@ import { DangerZoneComponent } from "../auth/settings/account/danger-zone.compon import { DeauthorizeSessionsComponent } from "../auth/settings/account/deauthorize-sessions.component"; import { DeleteAccountDialogComponent } from "../auth/settings/account/delete-account-dialog.component"; import { ProfileComponent } from "../auth/settings/account/profile.component"; +import { SelectableAvatarComponent } from "../auth/settings/account/selectable-avatar.component"; import { EmergencyAccessConfirmComponent } from "../auth/settings/emergency-access/confirm/emergency-access-confirm.component"; import { EmergencyAccessAddEditComponent } from "../auth/settings/emergency-access/emergency-access-add-edit.component"; import { EmergencyAccessComponent } from "../auth/settings/emergency-access/emergency-access.component"; @@ -39,7 +40,6 @@ import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.comp import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-families.component"; import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component"; import { DynamicAvatarComponent } from "../components/dynamic-avatar.component"; -import { SelectableAvatarComponent } from "../components/selectable-avatar.component"; import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../dirt/reports/pages/organizations/exposed-passwords-report.component"; import { InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent } from "../dirt/reports/pages/organizations/inactive-two-factor-report.component"; import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } from "../dirt/reports/pages/organizations/reused-passwords-report.component"; From 4cf6e19b305c693e250f9e4a29008e8f3d06c464 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 13 May 2025 09:28:25 -0400 Subject: [PATCH 020/163] Estimate tax for trial initiation flow when trial length is 0 (#14674) --- .../trial-billing-step.component.html | 41 +++++++- .../trial-billing-step.component.ts | 94 ++++++++++++++++++- .../abstractions/tax.service.abstraction.ts | 6 ++ .../src/billing/models/request/tax/index.ts | 1 + ...x-amount-for-organization-trial.request.ts | 11 +++ .../src/billing/models/response/tax/index.ts | 1 + .../tax/preview-tax-amount.response.ts | 11 +++ .../src/billing/services/tax.service.ts | 15 +++ 8 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 libs/common/src/billing/models/request/tax/index.ts create mode 100644 libs/common/src/billing/models/request/tax/preview-tax-amount-for-organization-trial.request.ts create mode 100644 libs/common/src/billing/models/response/tax/index.ts create mode 100644 libs/common/src/billing/models/response/tax/preview-tax-amount.response.ts diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html index d947ea96dfb..64a9781b7cf 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html @@ -51,8 +51,38 @@

{{ "paymentType" | i18n }}

+ + @if (trialLength === 0) { + @let priceLabel = + subscriptionProduct === SubscriptionProduct.PasswordManager + ? "passwordManagerPlanPrice" + : "secretsManagerPlanPrice"; + +
+
+ {{ priceLabel | i18n }}: {{ getPriceFor(formGroup.value.cadence) | currency: "USD $" }} +
+ {{ "estimatedTax" | i18n }}: + @if (fetchingTaxAmount) { + + } @else { + {{ taxAmount | currency: "USD $" }} + } +
+
+
+

+ {{ "total" | i18n }}: + @if (fetchingTaxAmount) { + + } @else { + {{ total | currency: "USD $" }}/{{ interval | i18n }} + } +

+
+ }
+ + + + {{ "loading" | i18n }} + diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts index c6248a06a89..614d8bf5f97 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts @@ -1,7 +1,16 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core"; +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + ViewChild, +} from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; +import { from, Subject, switchMap, takeUntil } from "rxjs"; import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -12,7 +21,14 @@ import { PaymentInformation, PlanInformation, } from "@bitwarden/common/billing/abstractions/organization-billing.service"; -import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; +import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; +import { + PaymentMethodType, + PlanType, + ProductTierType, + ProductType, +} from "@bitwarden/common/billing/enums"; +import { PreviewTaxAmountForOrganizationTrialRequest } from "@bitwarden/common/billing/models/request/tax"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -50,7 +66,7 @@ export enum SubscriptionProduct { imports: [BillingSharedModule], standalone: true, }) -export class TrialBillingStepComponent implements OnInit { +export class TrialBillingStepComponent implements OnInit, OnDestroy { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; @ViewChild(ManageTaxInformationComponent) taxInfoComponent: ManageTaxInformationComponent; @Input() organizationInfo: OrganizationInfo; @@ -60,6 +76,7 @@ export class TrialBillingStepComponent implements OnInit { @Output() organizationCreated = new EventEmitter(); loading = true; + fetchingTaxAmount = false; annualCadence = SubscriptionCadence.Annual; monthlyCadence = SubscriptionCadence.Monthly; @@ -73,6 +90,12 @@ export class TrialBillingStepComponent implements OnInit { annualPlan?: PlanResponse; monthlyPlan?: PlanResponse; + taxAmount = 0; + + private destroy$ = new Subject(); + + protected readonly SubscriptionProduct = SubscriptionProduct; + constructor( private apiService: ApiService, private i18nService: I18nService, @@ -80,6 +103,7 @@ export class TrialBillingStepComponent implements OnInit { private messagingService: MessagingService, private organizationBillingService: OrganizationBillingService, private toastService: ToastService, + private taxService: TaxServiceAbstraction, ) {} async ngOnInit(): Promise { @@ -87,9 +111,26 @@ export class TrialBillingStepComponent implements OnInit { this.applicablePlans = plans.data.filter(this.isApplicable); this.annualPlan = this.findPlanFor(SubscriptionCadence.Annual); this.monthlyPlan = this.findPlanFor(SubscriptionCadence.Monthly); + + if (this.trialLength === 0) { + this.formGroup.controls.cadence.valueChanges + .pipe( + switchMap((cadence) => from(this.previewTaxAmount(cadence))), + takeUntil(this.destroy$), + ) + .subscribe((taxAmount) => { + this.taxAmount = taxAmount; + }); + } + this.loading = false; } + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + async submit(): Promise { if (!this.taxInfoComponent.validate()) { return; @@ -115,7 +156,11 @@ export class TrialBillingStepComponent implements OnInit { this.messagingService.send("organizationCreated", { organizationId }); } - protected changedCountry() { + async onTaxInformationChanged() { + if (this.trialLength === 0) { + this.taxAmount = await this.previewTaxAmount(this.formGroup.value.cadence); + } + this.paymentComponent.showBankAccount = this.taxInfoComponent.getTaxInformation().country === "US"; if ( @@ -250,4 +295,45 @@ export class TrialBillingStepComponent implements OnInit { const notDisabledOrLegacy = !plan.disabled && !plan.legacyYear; return hasCorrectProductType && notDisabledOrLegacy; } + + private previewTaxAmount = async (cadence: SubscriptionCadence): Promise => { + this.fetchingTaxAmount = true; + + if (!this.taxInfoComponent.validate()) { + return 0; + } + + const plan = this.findPlanFor(cadence); + + const productType = + this.subscriptionProduct === SubscriptionProduct.PasswordManager + ? ProductType.PasswordManager + : ProductType.SecretsManager; + + const taxInformation = this.taxInfoComponent.getTaxInformation(); + + const request: PreviewTaxAmountForOrganizationTrialRequest = { + planType: plan.type, + productType, + taxInformation: { + ...taxInformation, + }, + }; + + const response = await this.taxService.previewTaxAmountForOrganizationTrial(request); + this.fetchingTaxAmount = false; + return response.taxAmount; + }; + + get price() { + return this.getPriceFor(this.formGroup.value.cadence); + } + + get total() { + return this.price + this.taxAmount; + } + + get interval() { + return this.formGroup.value.cadence === SubscriptionCadence.Annual ? "year" : "month"; + } } diff --git a/libs/common/src/billing/abstractions/tax.service.abstraction.ts b/libs/common/src/billing/abstractions/tax.service.abstraction.ts index 7a744dae856..73dc848c95f 100644 --- a/libs/common/src/billing/abstractions/tax.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/tax.service.abstraction.ts @@ -1,7 +1,9 @@ import { CountryListItem } from "../models/domain"; import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request"; import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request"; +import { PreviewTaxAmountForOrganizationTrialRequest } from "../models/request/tax"; import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response"; +import { PreviewTaxAmountResponse } from "../models/response/tax"; export abstract class TaxServiceAbstraction { abstract getCountries(): CountryListItem[]; @@ -15,4 +17,8 @@ export abstract class TaxServiceAbstraction { abstract previewOrganizationInvoice( request: PreviewOrganizationInvoiceRequest, ): Promise; + + abstract previewTaxAmountForOrganizationTrial: ( + request: PreviewTaxAmountForOrganizationTrialRequest, + ) => Promise; } diff --git a/libs/common/src/billing/models/request/tax/index.ts b/libs/common/src/billing/models/request/tax/index.ts new file mode 100644 index 00000000000..cda1930c614 --- /dev/null +++ b/libs/common/src/billing/models/request/tax/index.ts @@ -0,0 +1 @@ +export * from "./preview-tax-amount-for-organization-trial.request"; diff --git a/libs/common/src/billing/models/request/tax/preview-tax-amount-for-organization-trial.request.ts b/libs/common/src/billing/models/request/tax/preview-tax-amount-for-organization-trial.request.ts new file mode 100644 index 00000000000..3f366335a47 --- /dev/null +++ b/libs/common/src/billing/models/request/tax/preview-tax-amount-for-organization-trial.request.ts @@ -0,0 +1,11 @@ +import { PlanType, ProductType } from "../../../enums"; + +export type PreviewTaxAmountForOrganizationTrialRequest = { + planType: PlanType; + productType: ProductType; + taxInformation: { + country: string; + postalCode: string; + taxId?: string; + }; +}; diff --git a/libs/common/src/billing/models/response/tax/index.ts b/libs/common/src/billing/models/response/tax/index.ts new file mode 100644 index 00000000000..525d6d7c80a --- /dev/null +++ b/libs/common/src/billing/models/response/tax/index.ts @@ -0,0 +1 @@ +export * from "./preview-tax-amount.response"; diff --git a/libs/common/src/billing/models/response/tax/preview-tax-amount.response.ts b/libs/common/src/billing/models/response/tax/preview-tax-amount.response.ts new file mode 100644 index 00000000000..cf15156551a --- /dev/null +++ b/libs/common/src/billing/models/response/tax/preview-tax-amount.response.ts @@ -0,0 +1,11 @@ +import { BaseResponse } from "../../../../models/response/base.response"; + +export class PreviewTaxAmountResponse extends BaseResponse { + taxAmount: number; + + constructor(response: any) { + super(response); + + this.taxAmount = this.getResponseProperty("TaxAmount"); + } +} diff --git a/libs/common/src/billing/services/tax.service.ts b/libs/common/src/billing/services/tax.service.ts index aa27c99adc8..2632ca7083b 100644 --- a/libs/common/src/billing/services/tax.service.ts +++ b/libs/common/src/billing/services/tax.service.ts @@ -1,3 +1,6 @@ +import { PreviewTaxAmountForOrganizationTrialRequest } from "@bitwarden/common/billing/models/request/tax"; +import { PreviewTaxAmountResponse } from "@bitwarden/common/billing/models/response/tax"; + import { ApiService } from "../../abstractions/api.service"; import { TaxServiceAbstraction } from "../abstractions/tax.service.abstraction"; import { CountryListItem } from "../models/domain"; @@ -300,4 +303,16 @@ export class TaxService implements TaxServiceAbstraction { ); return new PreviewInvoiceResponse(response); } + + async previewTaxAmountForOrganizationTrial( + request: PreviewTaxAmountForOrganizationTrialRequest, + ): Promise { + return await this.apiService.send( + "POST", + "/tax/preview-amount/organization-trial", + request, + true, + true, + ); + } } From 992b1456a82ef5626501bbdf35277d3b8078a4db Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 13 May 2025 09:31:47 -0400 Subject: [PATCH 021/163] Send SkipTrial = true to organization create when trial length is 0 (#14701) --- .../accounts/trial-initiation/trial-billing-step.component.ts | 1 + .../admin-console/models/request/organization-create.request.ts | 1 + .../src/billing/abstractions/organization-billing.service.ts | 1 + libs/common/src/billing/services/organization-billing.service.ts | 1 + 4 files changed, 4 insertions(+) diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts index 614d8bf5f97..9f910ad9d23 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts @@ -215,6 +215,7 @@ export class TrialBillingStepComponent implements OnInit, OnDestroy { const payment: PaymentInformation = { paymentMethod, billing: this.getBillingInformationFromTaxInfoComponent(), + skipTrial: this.trialLength === 0, }; const response = await this.organizationBillingService.purchaseSubscription({ diff --git a/libs/common/src/admin-console/models/request/organization-create.request.ts b/libs/common/src/admin-console/models/request/organization-create.request.ts index e8561307b20..d9c62f1e20a 100644 --- a/libs/common/src/admin-console/models/request/organization-create.request.ts +++ b/libs/common/src/admin-console/models/request/organization-create.request.ts @@ -6,4 +6,5 @@ import { OrganizationNoPaymentMethodCreateRequest } from "../../../billing/model export class OrganizationCreateRequest extends OrganizationNoPaymentMethodCreateRequest { paymentMethodType: PaymentMethodType; paymentToken: string; + skipTrial?: boolean; } diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 8024a120b0a..58c537c99cc 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -39,6 +39,7 @@ export type BillingInformation = { export type PaymentInformation = { paymentMethod: [string, PaymentMethodType]; billing: BillingInformation; + skipTrial?: boolean; }; export type SubscriptionInformation = { diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index c6bd88d8dd6..fe5623fd5e6 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -174,6 +174,7 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs const [paymentToken, paymentMethodType] = information.paymentMethod; request.paymentToken = paymentToken; request.paymentMethodType = paymentMethodType; + request.skipTrial = information.skipTrial; const billingInformation = information.billing; request.billingAddressPostalCode = billingInformation.postalCode; From b2c118d607c7be1298b39cd6c3ea3012411e5a78 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 13 May 2025 15:41:57 +0200 Subject: [PATCH 022/163] Move admin-console code to new encrypt service interface (#14547) --- .../organization-user-reset-password.service.ts | 2 +- .../providers/services/web-provider.service.ts | 2 +- .../services/default-collection-admin.service.ts | 4 ++-- .../collections/services/default-collection.service.spec.ts | 5 ++++- .../collections/services/default-collection.service.ts | 2 +- .../services/default-vnext-collection.service.spec.ts | 6 ++++++ .../services/default-vnext-collection.service.ts | 2 +- .../models/domain/encrypted-organization-key.ts | 4 ++-- 8 files changed, 18 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts index 78d2d8fd165..ecf4d26eb52 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts @@ -112,7 +112,7 @@ export class OrganizationUserResetPasswordService if (orgSymKey == null) { throw new Error("No org key found"); } - const decPrivateKey = await this.encryptService.decryptToBytes( + const decPrivateKey = await this.encryptService.unwrapDecapsulationKey( new EncString(response.encryptedPrivateKey), orgSymKey, ); diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts index 844c6b779a9..418b7020ff9 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts @@ -74,7 +74,7 @@ export class WebProviderService { const [publicKey, encryptedPrivateKey] = await this.keyService.makeKeyPair(organizationKey); - const encryptedCollectionName = await this.encryptService.encrypt( + const encryptedCollectionName = await this.encryptService.encryptString( this.i18nService.t("defaultCollection"), organizationKey, ); 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 890353d9039..293090ce315 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 @@ -116,7 +116,7 @@ export class DefaultCollectionAdminService implements CollectionAdminService { const promises = collections.map(async (c) => { const view = new CollectionAdminView(); view.id = c.id; - view.name = await this.encryptService.decryptToUtf8(new EncString(c.name), orgKey); + view.name = await this.encryptService.decryptString(new EncString(c.name), orgKey); view.externalId = c.externalId; view.organizationId = c.organizationId; @@ -146,7 +146,7 @@ export class DefaultCollectionAdminService implements CollectionAdminService { } const collection = new CollectionRequest(); collection.externalId = model.externalId; - collection.name = (await this.encryptService.encrypt(model.name, key)).encryptedString; + collection.name = (await this.encryptService.encryptString(model.name, key)).encryptedString; collection.groups = model.groups.map( (group) => new SelectionReadOnlyRequest(group.id, group.readOnly, group.hidePasswords, group.manage), diff --git a/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts b/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts index 7fe81ade4d2..c5f57f38dd3 100644 --- a/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts +++ b/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts @@ -120,9 +120,12 @@ const mockStateProvider = () => { const mockCryptoService = () => { const keyService = mock(); const encryptService = mock(); - encryptService.decryptToUtf8 + encryptService.decryptString .calledWith(expect.any(EncString), expect.anything()) .mockResolvedValue("DECRYPTED_STRING"); + encryptService.decryptToUtf8 + .calledWith(expect.any(EncString), expect.anything(), expect.anything()) + .mockResolvedValue("DECRYPTED_STRING"); (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); 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 1ae58d3eef3..a1dd0419e2c 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 @@ -113,7 +113,7 @@ export class DefaultCollectionService implements CollectionService { collection.organizationId = model.organizationId; collection.readOnly = model.readOnly; collection.externalId = model.externalId; - collection.name = await this.encryptService.encrypt(model.name, key); + collection.name = await this.encryptService.encryptString(model.name, key); return collection; } diff --git a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts index 9700fcb695a..d4bc026b5bd 100644 --- a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts +++ b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts @@ -46,6 +46,11 @@ describe("DefaultvNextCollectionService", () => { keyService.orgKeys$.mockReturnValue(cryptoKeys); // Set up mock decryption + encryptService.decryptString + .calledWith(expect.any(EncString), expect.any(SymmetricCryptoKey)) + .mockImplementation((encString, key) => + Promise.resolve(encString.data.replace("ENC_", "DEC_")), + ); encryptService.decryptToUtf8 .calledWith(expect.any(EncString), expect.any(SymmetricCryptoKey), expect.any(String)) .mockImplementation((encString, key) => @@ -103,6 +108,7 @@ describe("DefaultvNextCollectionService", () => { ]); // Assert that the correct org keys were used for each encrypted string + // This should be replaced with decryptString when the platform PR (https://github.com/bitwarden/clients/pull/14544) is merged expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( expect.objectContaining(new EncString(collection1.name)), orgKey1, diff --git a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts index 0ef8ae99ab3..4dcda795afe 100644 --- a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts +++ b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts @@ -113,7 +113,7 @@ export class DefaultvNextCollectionService implements vNextCollectionService { collection.organizationId = model.organizationId; collection.readOnly = model.readOnly; collection.externalId = model.externalId; - collection.name = await this.encryptService.encrypt(model.name, key); + collection.name = await this.encryptService.encryptString(model.name, key); return collection; } diff --git a/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts b/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts index 984d80ba519..297bcf08d8c 100644 --- a/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts +++ b/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts @@ -56,14 +56,14 @@ export class ProviderEncryptedOrganizationKey implements BaseEncryptedOrganizati ) {} async decrypt(encryptService: EncryptService, providerKeys: Record) { - const decValue = await encryptService.decryptToBytes( + const decValue = await encryptService.unwrapSymmetricKey( new EncString(this.key), providerKeys[this.providerId], ); if (decValue == null) { throw new Error("Failed to decrypt organization key"); } - return new SymmetricCryptoKey(decValue) as OrgKey; + return decValue as OrgKey; } get encryptedOrganizationKey() { From 00beef617caf0bc3aa81efdc5d4f34e53c1f2e08 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 13 May 2025 15:42:48 +0200 Subject: [PATCH 023/163] [PM-21586] Return null in decryptUserKeyWithMasterKey if decrypt fails (#14756) * Return null in decryptUserKeyWithMasterKey if decrypt fails * Show error on invalid master password * Add logs --- .../master-password.service.abstraction.ts | 4 ++-- .../services/master-password.service.ts | 20 ++++++++++++++----- .../src/lock/components/lock.component.ts | 9 +++++++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts b/libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts index 221ce8ed6ef..fded0cea023 100644 --- a/libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts +++ b/libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts @@ -37,13 +37,13 @@ export abstract class MasterPasswordServiceAbstraction { * @param userKey The user's encrypted symmetric key * @throws If either the MasterKey or UserKey are not resolved, or if the UserKey encryption type * is neither AesCbc256_B64 nor AesCbc256_HmacSha256_B64 - * @returns The user key + * @returns The user key or null if the masterkey is wrong */ abstract decryptUserKeyWithMasterKey: ( masterKey: MasterKey, userId: string, userKey?: EncString, - ) => Promise; + ) => Promise; } export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction { diff --git a/libs/common/src/key-management/master-password/services/master-password.service.ts b/libs/common/src/key-management/master-password/services/master-password.service.ts index b9b11d6cbe8..9e58680d453 100644 --- a/libs/common/src/key-management/master-password/services/master-password.service.ts +++ b/libs/common/src/key-management/master-password/services/master-password.service.ts @@ -166,7 +166,7 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr masterKey: MasterKey, userId: UserId, userKey?: EncString, - ): Promise { + ): Promise { userKey ??= await this.getMasterKeyEncryptedUserKey(userId); masterKey ??= await firstValueFrom(this.masterKey$(userId)); @@ -177,16 +177,26 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr let decUserKey: SymmetricCryptoKey; if (userKey.encryptionType === EncryptionType.AesCbc256_B64) { - decUserKey = await this.encryptService.unwrapSymmetricKey(userKey, masterKey); + try { + decUserKey = await this.encryptService.unwrapSymmetricKey(userKey, masterKey); + } catch { + this.logService.warning("Failed to decrypt user key with master key."); + return null; + } } else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) { - const newKey = await this.keyGenerationService.stretchKey(masterKey); - decUserKey = await this.encryptService.unwrapSymmetricKey(userKey, newKey); + try { + const newKey = await this.keyGenerationService.stretchKey(masterKey); + decUserKey = await this.encryptService.unwrapSymmetricKey(userKey, newKey); + } catch { + this.logService.warning("Failed to decrypt user key with stretched master key."); + return null; + } } else { throw new Error("Unsupported encryption type."); } if (decUserKey == null) { - this.logService.warning("Failed to decrypt user key with master key."); + this.logService.warning("Failed to decrypt user key with master key, user key is null."); return null; } diff --git a/libs/key-management-ui/src/lock/components/lock.component.ts b/libs/key-management-ui/src/lock/components/lock.component.ts index 80d64e17b84..3cb0dbaca52 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.ts @@ -556,6 +556,15 @@ export class LockComponent implements OnInit, OnDestroy { masterPasswordVerificationResponse!.masterKey, this.activeAccount.id, ); + if (userKey == null) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidMasterPassword"), + }); + return; + } + await this.setUserKeyAndContinue(userKey, true); } From 9f3310ed7edecb2afc68a284dedd37f68dae72f9 Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Tue, 13 May 2025 09:44:54 -0400 Subject: [PATCH 024/163] Redoing the PR for get by domain due to file move (#14746) --- .../services/risk-insights-report.service.spec.ts | 10 +++++----- .../services/risk-insights-report.service.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts index 705eb1231a9..f9177bf1bf7 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts @@ -50,7 +50,7 @@ describe("RiskInsightsReportService", () => { let testCase = testCaseResults[0]; expect(testCase).toBeTruthy(); expect(testCase.cipherMembers).toHaveLength(2); - expect(testCase.trimmedUris).toHaveLength(3); + expect(testCase.trimmedUris).toHaveLength(2); expect(testCase.weakPasswordDetail).toBeTruthy(); expect(testCase.exposedPasswordDetail).toBeTruthy(); expect(testCase.reusedPasswordCount).toEqual(2); @@ -69,7 +69,7 @@ describe("RiskInsightsReportService", () => { it("should generate the raw data + uri report correctly", async () => { const result = await firstValueFrom(service.generateRawDataUriReport$("orgId")); - expect(result).toHaveLength(9); + expect(result).toHaveLength(8); // Two ciphers that have google.com as their uri. There should be 2 results const googleResults = result.filter((x) => x.trimmedUri === "google.com"); @@ -88,7 +88,7 @@ describe("RiskInsightsReportService", () => { it("should generate applications health report data correctly", async () => { const result = await firstValueFrom(service.generateApplicationsReport$("orgId")); - expect(result).toHaveLength(6); + expect(result).toHaveLength(5); // Two ciphers have google.com associated with them. The first cipher // has 2 members and the second has 4. However, the 2 members in the first @@ -132,7 +132,7 @@ describe("RiskInsightsReportService", () => { expect(reportSummary.totalMemberCount).toEqual(7); expect(reportSummary.totalAtRiskMemberCount).toEqual(6); - expect(reportSummary.totalApplicationCount).toEqual(6); - expect(reportSummary.totalAtRiskApplicationCount).toEqual(5); + expect(reportSummary.totalApplicationCount).toEqual(5); + expect(reportSummary.totalAtRiskApplicationCount).toEqual(4); }); }); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts index 027760f678c..8a6eb5000cd 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts @@ -428,7 +428,7 @@ export class RiskInsightsReportService { const cipherUris: string[] = []; const uris = cipher.login?.uris ?? []; uris.map((u: { uri: string }) => { - const uri = Utils.getHostname(u.uri).replace("www.", ""); + const uri = Utils.getDomain(u.uri); if (!cipherUris.includes(uri)) { cipherUris.push(uri); } From 0b0397c3f05d94e1ea8cbedd97b7706fba968e9b Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Tue, 13 May 2025 10:07:38 -0400 Subject: [PATCH 025/163] fix(enums-eslint): Enum Rule for ESLint (#14650) * fix(enums-eslint): Enum Rule for ESLint - Added enums in the warnings for eslint. * fix(enums-eslint): Enum Rule for ESLint - Updated to error in both places for enums. * fix(enums-eslint): Enum Rule for ESLint - Added new eslint plugin for warning on enums. * fix(enums-eslint): Enum Rule for ESLint - Changed based on suggestion. Co-authored-by: Andreas Coroiu * refactor(browser-platform-utils): Remove Deprecation and Fix Code - Changed usages of firefox to private and moved the usages to the preferred public method and removed the deprecations. * fix(enums-eslint): Enum Rule for ESLint - Updated to error and added disable rules for all other places. * fix(enums-eslint): Enum Rule for ESLint - Undid other changes by accident --- .../autofill/enums/autofill-overlay.enum.ts | 2 + .../fido2/content/messaging/message.ts | 2 + .../tools/popup/send-v2/send-v2.component.ts | 2 + .../at-risk-carousel-dialog.component.ts | 2 + .../vault-generator-dialog.component.ts | 2 + .../components/vault-v2/vault-v2.component.ts | 2 + .../src/ipc.service.ts | 2 + .../src/app/tools/send/send.component.ts | 2 + .../src/autofill/models/ssh-agent-setting.ts | 2 + apps/desktop/src/types/biometric-message.ts | 2 + .../credential-generator-dialog.component.ts | 2 + .../bulk-collections-dialog.component.ts | 2 + .../collections/vault.component.ts | 2 + .../manage/group-add-edit.component.ts | 4 ++ .../member-dialog/member-dialog.component.ts | 4 ++ .../components/reset-password.component.ts | 2 + .../policies/policy-edit.component.ts | 2 + .../delete-organization-dialog.component.ts | 2 + .../access-selector.component.ts | 2 + .../access-selector/access-selector.models.ts | 4 ++ .../collection-dialog.component.ts | 6 ++ ...bauthn-login-credential-prf-status.enum.ts | 2 + .../enums/emergency-access-status-type.ts | 2 + .../enums/emergency-access-type.ts | 2 + .../emergency-access-confirm.component.ts | 2 + .../emergency-access-add-edit.component.ts | 2 + .../emergency-access-takeover.component.ts | 2 + .../create-credential-dialog.component.ts | 2 + .../trial-billing-step.component.ts | 4 ++ .../change-plan-dialog.component.ts | 4 ++ .../download-license.component.ts | 2 + ...ization-subscription-selfhost.component.ts | 2 + .../shared/add-credit-dialog.component.ts | 2 + .../adjust-payment-dialog.component.ts | 2 + .../adjust-storage-dialog.component.ts | 2 + .../shared/offboarding-survey.component.ts | 2 + .../billing/shared/update-license-types.ts | 2 + apps/web/src/app/dirt/reports/reports.ts | 2 + .../reports/shared/models/report-variant.ts | 2 + .../vault-item-dialog.component.ts | 2 + .../web-generator-dialog.component.ts | 2 + .../individual-vault/add-edit-v2.component.ts | 2 + .../bulk-delete-dialog.component.ts | 2 + .../bulk-move-dialog.component.ts | 2 + .../folder-add-edit.component.ts | 2 + .../services/vault-banners.service.ts | 2 + .../models/vault-filter-section.type.ts | 2 + .../vault/individual-vault/view.component.ts | 2 + .../browser-extension-prompt.service.ts | 2 + .../risk-insights/models/password-health.ts | 2 + .../add-edit-member-dialog.component.ts | 2 + ...-existing-organization-dialog.component.ts | 2 + .../clients/create-client-dialog.component.ts | 2 + .../manage-client-name-dialog.component.ts | 2 + ...ge-client-subscription-dialog.component.ts | 2 + .../risk-insights.component.ts | 2 + .../dialog/project-dialog.component.ts | 2 + .../secrets/dialog/secret-dialog.component.ts | 4 ++ .../service-account-dialog.component.ts | 2 + .../models/enums/ap-item.enum.ts | 2 + .../models/enums/ap-permission.enum.ts | 2 + .../bulk-confirmation-dialog.component.ts | 2 + eslint.config.mjs | 1 + .../add-account-credit-dialog.component.ts | 2 + .../src/tools/send/add-edit.component.ts | 2 + .../input-password.component.ts | 2 + .../login-decryption-options.component.ts | 2 + .../login-via-auth-request.component.ts | 2 + .../auth/src/angular/login/login.component.ts | 2 + .../registration-start.component.ts | 2 + .../two-factor-auth-component.service.ts | 4 ++ .../active-client-verification-option.enum.ts | 2 + .../validators/compare-inputs.validator.ts | 2 + .../enums/organization-api-key-type.enum.ts | 2 + .../organization-connection-type.enum.ts | 2 + .../organization-user-status-type.enum.ts | 2 + .../enums/organization-user-type.enum.ts | 2 + .../admin-console/enums/policy-type.enum.ts | 2 + .../enums/provider-status-type.enum.ts | 2 + .../admin-console/enums/provider-type.enum.ts | 2 + .../enums/provider-user-status-type.enum.ts | 2 + .../enums/provider-user-type.enum.ts | 2 + .../enums/scim-provider-type.enum.ts | 2 + .../src/auth/enums/auth-request-type.ts | 2 + .../src/auth/enums/authentication-status.ts | 2 + .../src/auth/enums/authentication-type.ts | 2 + libs/common/src/auth/enums/sso.ts | 12 ++++ .../auth/enums/two-factor-provider-type.ts | 2 + .../src/auth/enums/verification-type.ts | 2 + .../domain/force-set-password-reason.ts | 2 + .../common/src/auth/services/token.service.ts | 2 + .../enums/bitwarden-product-type.enum.ts | 2 + .../billing/enums/payment-method-type.enum.ts | 2 + .../src/billing/enums/plan-interval.enum.ts | 2 + .../enums/plan-sponsorship-type.enum.ts | 2 + .../src/billing/enums/plan-type.enum.ts | 2 + .../billing/enums/product-tier-type.enum.ts | 2 + .../src/billing/enums/product-type.enum.ts | 2 + .../billing/enums/transaction-type.enum.ts | 2 + libs/common/src/enums/client-type.enum.ts | 2 + libs/common/src/enums/device-type.enum.ts | 2 + .../src/enums/event-system-user.enum.ts | 2 + libs/common/src/enums/event-type.enum.ts | 2 + libs/common/src/enums/feature-flag.enum.ts | 2 + .../common/src/enums/http-status-code.enum.ts | 2 + .../common/src/enums/integration-type.enum.ts | 2 + .../enums/native-messaging-version.enum.ts | 2 + .../src/enums/notification-type.enum.ts | 2 + libs/common/src/enums/push-technology.enum.ts | 2 + .../enums/vault-timeout-action.enum.ts | 2 + .../abstractions/environment.service.ts | 2 + ...fido2-authenticator.service.abstraction.ts | 4 ++ .../platform/enums/encryption-type.enum.ts | 2 + .../platform/enums/file-upload-type.enum.ts | 2 + .../src/platform/enums/hash-purpose.enum.ts | 2 + .../enums/html-storage-location.enum.ts | 2 + .../platform/enums/key-suffix-options.enum.ts | 2 + .../src/platform/enums/log-level-type.enum.ts | 2 + .../platform/enums/storage-location.enum.ts | 2 + .../src/platform/enums/theme-type.enum.ts | 2 + .../services/cryptography/initializer-key.ts | 2 + ...8-move-provider-state-to-state-provider.ts | 4 ++ .../30-move-policy-state-to-state-provider.ts | 2 + ...ve-organization-state-to-state-provider.ts | 8 +++ .../migrations/54-move-encrypted-sends.ts | 2 + .../59-move-kdf-config-to-state-provider.ts | 2 + ...-timeout-settings-svc-to-state-provider.ts | 2 + libs/common/src/tools/send/enums/send-type.ts | 2 + .../src/vault/enums/cipher-reprompt-type.ts | 2 + libs/common/src/vault/enums/cipher-type.ts | 2 + .../common/src/vault/enums/field-type.enum.ts | 2 + .../src/vault/enums/linked-id-type.enum.ts | 6 ++ .../src/vault/enums/secure-note-type.enum.ts | 2 + .../tasks/enums/security-task-status.enum.ts | 2 + .../tasks/enums/security-task-type.enum.ts | 2 + .../color-password.component.ts | 2 + libs/eslint/platform/index.mjs | 3 +- libs/eslint/platform/no-enums.mjs | 23 ++++++ libs/eslint/platform/no-enums.spec.mjs | 71 +++++++++++++++++++ .../importers/fsecure/fsecure-fsk-types.ts | 2 + .../lastpass/access/enums/idp-provider.ts | 2 + .../access/enums/lastpass-login-type.ts | 2 + .../lastpass/access/enums/otp-method.ts | 2 + .../lastpass/access/enums/platform.ts | 2 + .../types/onepassword-1pux-importer-types.ts | 4 ++ .../protonpass/types/protonpass-json-type.ts | 2 + .../src/biometrics/biometrics-commands.ts | 2 + .../src/biometrics/biometrics-status.ts | 2 + .../key-management/src/enums/kdf-type.enum.ts | 2 + .../src/enums/encrypted-export-type.enum.ts | 2 + .../send-add-edit-dialog.component.ts | 2 + .../send-details/send-details.component.ts | 2 + .../attachments/attachments-v2.component.ts | 2 + .../add-edit-folder-dialog.component.ts | 2 + .../assign-collections.component.ts | 2 + .../src/services/vault-nudges.service.ts | 2 + 156 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 libs/eslint/platform/no-enums.mjs create mode 100644 libs/eslint/platform/no-enums.spec.mjs diff --git a/apps/browser/src/autofill/enums/autofill-overlay.enum.ts b/apps/browser/src/autofill/enums/autofill-overlay.enum.ts index 9cc457f3c1a..d0b970671a8 100644 --- a/apps/browser/src/autofill/enums/autofill-overlay.enum.ts +++ b/apps/browser/src/autofill/enums/autofill-overlay.enum.ts @@ -21,6 +21,8 @@ export const RedirectFocusDirection = { Next: "next", } as const; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum InlineMenuFillType { AccountCreationUsername = 5, PasswordGeneration = 6, diff --git a/apps/browser/src/autofill/fido2/content/messaging/message.ts b/apps/browser/src/autofill/fido2/content/messaging/message.ts index 5815be9eb60..640af22ab9a 100644 --- a/apps/browser/src/autofill/fido2/content/messaging/message.ts +++ b/apps/browser/src/autofill/fido2/content/messaging/message.ts @@ -5,6 +5,8 @@ import { AssertCredentialResult, } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum MessageType { CredentialCreationRequest, CredentialCreationResponse, diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts index 49804abda5d..def425a51a5 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts @@ -26,6 +26,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum SendState { Empty, NoResults, diff --git a/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts b/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts index fcca125c2b6..0133bccd25c 100644 --- a/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts @@ -10,6 +10,8 @@ import { import { I18nPipe } from "@bitwarden/ui-common"; import { DarkImageSourceDirective, VaultCarouselModule } from "@bitwarden/vault"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum AtRiskCarouselDialogResult { Dismissed = "dismissed", } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts index 137f2a9dac3..4daffa6a9b8 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts @@ -30,6 +30,8 @@ export interface GeneratorDialogResult { generatedValue?: string; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum GeneratorDialogAction { Selected = "selected", Canceled = "canceled", diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index 64805a02394..4a8625f982c 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -55,6 +55,8 @@ import { VaultPageService } from "./vault-page.service"; import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "."; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum VaultState { Empty, NoResults, diff --git a/apps/desktop/native-messaging-test-runner/src/ipc.service.ts b/apps/desktop/native-messaging-test-runner/src/ipc.service.ts index b02ff1a4225..d8616e9757a 100644 --- a/apps/desktop/native-messaging-test-runner/src/ipc.service.ts +++ b/apps/desktop/native-messaging-test-runner/src/ipc.service.ts @@ -15,6 +15,8 @@ const DEFAULT_MESSAGE_TIMEOUT = 10 * 1000; // 10 seconds export type MessageHandler = (MessageCommon) => void; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum IPCConnectionState { Disconnected = "disconnected", Connecting = "connecting", diff --git a/apps/desktop/src/app/tools/send/send.component.ts b/apps/desktop/src/app/tools/send/send.component.ts index cc3007ae133..6c2c3ed53c6 100644 --- a/apps/desktop/src/app/tools/send/send.component.ts +++ b/apps/desktop/src/app/tools/send/send.component.ts @@ -25,6 +25,8 @@ import { SearchBarService } from "../../layout/search/search-bar.service"; import { AddEditComponent } from "./add-edit.component"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum Action { None = "", Add = "add", diff --git a/apps/desktop/src/autofill/models/ssh-agent-setting.ts b/apps/desktop/src/autofill/models/ssh-agent-setting.ts index f332cc93ee1..1775cf35588 100644 --- a/apps/desktop/src/autofill/models/ssh-agent-setting.ts +++ b/apps/desktop/src/autofill/models/ssh-agent-setting.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum SshAgentPromptType { Always = "always", Never = "never", diff --git a/apps/desktop/src/types/biometric-message.ts b/apps/desktop/src/types/biometric-message.ts index f7a7ef0c507..7616b265005 100644 --- a/apps/desktop/src/types/biometric-message.ts +++ b/apps/desktop/src/types/biometric-message.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum BiometricAction { Authenticate = "authenticate", GetStatus = "status", diff --git a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts index 2858d7330e5..204615443ba 100644 --- a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts +++ b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts @@ -31,6 +31,8 @@ export interface CredentialGeneratorDialogResult { generatedValue?: string; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum CredentialGeneratorDialogAction { Selected = "selected", Canceled = "canceled", diff --git a/apps/web/src/app/admin-console/organizations/collections/bulk-collections-dialog/bulk-collections-dialog.component.ts b/apps/web/src/app/admin-console/organizations/collections/bulk-collections-dialog/bulk-collections-dialog.component.ts index dd19c66f21e..147340e6a00 100644 --- a/apps/web/src/app/admin-console/organizations/collections/bulk-collections-dialog/bulk-collections-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/bulk-collections-dialog/bulk-collections-dialog.component.ts @@ -43,6 +43,8 @@ export interface BulkCollectionsDialogParams { collections: CollectionView[]; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum BulkCollectionsDialogResult { Saved = "saved", Canceled = "canceled", diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index 687aef9b671..96c00faceb2 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -132,6 +132,8 @@ import { VaultHeaderComponent } from "./vault-header/vault-header.component"; const BroadcasterSubscriptionId = "OrgVaultComponent"; const SearchTextDebounceInterval = 200; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum AddAccessStatusType { All = 0, AddAccess = 1, diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index 2a5af32ecc2..f29b4b642cb 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -58,6 +58,8 @@ import { AddEditGroupDetail } from "./../core/views/add-edit-group-detail"; /** * Indices for the available tabs in the dialog */ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum GroupAddEditTabType { Info = 0, Members = 1, @@ -82,6 +84,8 @@ export interface GroupAddEditDialogParams { initialTab?: GroupAddEditTabType; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum GroupAddEditDialogResultType { Saved = "saved", Canceled = "canceled", diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index c90a2a657e7..8349b44c735 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -64,6 +64,8 @@ import { commaSeparatedEmails } from "./validators/comma-separated-emails.valida import { inputEmailLimitValidator } from "./validators/input-email-limit.validator"; import { orgSeatLimitReachedValidator } from "./validators/org-seat-limit-reached.validator"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum MemberDialogTab { Role = 0, Groups = 1, @@ -92,6 +94,8 @@ export interface EditMemberDialogParams extends CommonMemberDialogParams { export type MemberDialogParams = EditMemberDialogParams | AddMemberDialogParams; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum MemberDialogResult { Saved = "saved", Canceled = "canceled", diff --git a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts index f08cb0b7d7c..4e78d4dc91f 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts @@ -50,6 +50,8 @@ export type ResetPasswordDialogData = { organizationId: string; }; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ResetPasswordDialogResult { Ok = "ok", } diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts index 49f4d15a100..4d722840e23 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts @@ -41,6 +41,8 @@ export type PolicyEditDialogData = { organizationId: string; }; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum PolicyEditDialogResult { Saved = "saved", UpgradePlan = "upgrade-plan", diff --git a/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts b/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts index c23dcf2c8f2..e942eecbd37 100644 --- a/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts @@ -71,6 +71,8 @@ export interface DeleteOrganizationDialogParams { requestType: "InvalidFamiliesForEnterprise" | "RegularDelete"; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum DeleteOrganizationDialogResult { Deleted = "deleted", Canceled = "canceled", diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts index edd0bfcaada..1db1fc8a06e 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts @@ -26,6 +26,8 @@ import { Permission, } from "./access-selector.models"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum PermissionMode { /** * No permission controls or column present. No permission values are emitted. diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts index 8702c0f7a6c..884483d32b0 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts @@ -15,6 +15,8 @@ import { GroupView } from "../../../core"; /** * Permission options that replace/correspond with manage, readOnly, and hidePassword server fields. */ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum CollectionPermission { View = "view", ViewExceptPass = "viewExceptPass", @@ -23,6 +25,8 @@ export enum CollectionPermission { Manage = "manage", } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum AccessItemType { Collection, Group, 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 37d0ebbd195..07bff3aba64 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 @@ -65,6 +65,8 @@ import { } from "../access-selector/access-selector.models"; import { AccessSelectorModule } from "../access-selector/access-selector.module"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum CollectionDialogTabType { Info = 0, Access = 1, @@ -76,6 +78,8 @@ export enum CollectionDialogTabType { * @readonly * @enum {string} */ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum ButtonType { /** Displayed when the user has reached the maximum number of collections allowed for the organization. */ Upgrade = "upgrade", @@ -103,6 +107,8 @@ export interface CollectionDialogResult { collection: CollectionResponse | CollectionView; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum CollectionDialogAction { Saved = "saved", Canceled = "canceled", diff --git a/apps/web/src/app/auth/core/enums/webauthn-login-credential-prf-status.enum.ts b/apps/web/src/app/auth/core/enums/webauthn-login-credential-prf-status.enum.ts index 7dc8217fde5..3073917e57b 100644 --- a/apps/web/src/app/auth/core/enums/webauthn-login-credential-prf-status.enum.ts +++ b/apps/web/src/app/auth/core/enums/webauthn-login-credential-prf-status.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum WebauthnLoginCredentialPrfStatus { Enabled = 0, Supported = 1, diff --git a/apps/web/src/app/auth/emergency-access/enums/emergency-access-status-type.ts b/apps/web/src/app/auth/emergency-access/enums/emergency-access-status-type.ts index 94400f34e6e..16aa2546101 100644 --- a/apps/web/src/app/auth/emergency-access/enums/emergency-access-status-type.ts +++ b/apps/web/src/app/auth/emergency-access/enums/emergency-access-status-type.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum EmergencyAccessStatusType { Invited = 0, Accepted = 1, diff --git a/apps/web/src/app/auth/emergency-access/enums/emergency-access-type.ts b/apps/web/src/app/auth/emergency-access/enums/emergency-access-type.ts index 61a366c433e..ecb0c5a3d07 100644 --- a/apps/web/src/app/auth/emergency-access/enums/emergency-access-type.ts +++ b/apps/web/src/app/auth/emergency-access/enums/emergency-access-type.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum EmergencyAccessType { View = 0, Takeover = 1, diff --git a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts index f3fd19a4e8b..95afc167374 100644 --- a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts @@ -8,6 +8,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { DialogConfig, DialogRef, DIALOG_DATA, DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum EmergencyAccessConfirmDialogResult { Confirmed = "confirmed", } diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts index cf52969c244..1a6510ef011 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts @@ -26,6 +26,8 @@ export type EmergencyAccessAddEditDialogData = { readOnly: boolean; }; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum EmergencyAccessAddEditDialogResult { Saved = "saved", Canceled = "canceled", diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts index c80f82ae126..edb85dc0f1a 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts @@ -24,6 +24,8 @@ import { KdfType, KdfConfigService, KeyService } from "@bitwarden/key-management import { EmergencyAccessService } from "../../../emergency-access"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum EmergencyAccessTakeoverResultType { Done = "done", } diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts index 591fe3816dc..8e7e25896ab 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts @@ -19,6 +19,8 @@ import { PendingWebauthnLoginCredentialView } from "../../../core/views/pending- import { CreatePasskeyFailedIcon } from "./create-passkey-failed.icon"; import { CreatePasskeyIcon } from "./create-passkey.icon"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum CreateCredentialDialogResult { Success, } diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts index 9f910ad9d23..63c42139648 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts @@ -50,11 +50,15 @@ export interface OrganizationCreatedEvent { planDescription: string; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum SubscriptionCadence { Annual, Monthly, } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum SubscriptionProduct { PasswordManager, SecretsManager, diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index 8cff90edd5b..49c5bb775b1 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -74,11 +74,15 @@ type ChangePlanDialogParams = { productTierType: ProductTierType; }; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ChangePlanDialogResultType { Closed = "closed", Submitted = "submitted", } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum PlanCardState { Selected = "selected", NotSelected = "not_selected", diff --git a/apps/web/src/app/billing/organizations/download-license.component.ts b/apps/web/src/app/billing/organizations/download-license.component.ts index fecb58a7a9d..66778aec50f 100644 --- a/apps/web/src/app/billing/organizations/download-license.component.ts +++ b/apps/web/src/app/billing/organizations/download-license.component.ts @@ -7,6 +7,8 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { DialogConfig, DIALOG_DATA, DialogRef, DialogService } from "@bitwarden/components"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum DownloadLicenseDialogResult { Cancelled = "cancelled", Downloaded = "downloaded", diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts index e6854a5216b..2189bfa830f 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts @@ -27,6 +27,8 @@ import { DialogService, ToastService } from "@bitwarden/components"; import { BillingSyncKeyComponent } from "./billing-sync-key.component"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum LicenseOptions { SYNC = 0, UPLOAD = 1, diff --git a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts index 45dab542ce8..ec6e251418b 100644 --- a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts +++ b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts @@ -21,6 +21,8 @@ export interface AddCreditDialogData { organizationId: string; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum AddCreditDialogResult { Added = "added", Cancelled = "cancelled", diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts index e7b7cc78250..9d32becd1bb 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts @@ -30,6 +30,8 @@ export interface AdjustPaymentDialogParams { providerId?: string; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum AdjustPaymentDialogResultType { Closed = "closed", Submitted = "submitted", diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts index 3acba414df4..6cd17218f02 100644 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts @@ -22,6 +22,8 @@ export interface AdjustStorageDialogParams { organizationId?: string; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum AdjustStorageDialogResultType { Submitted = "submitted", Closed = "closed", diff --git a/apps/web/src/app/billing/shared/offboarding-survey.component.ts b/apps/web/src/app/billing/shared/offboarding-survey.component.ts index cecbc302f40..62213c1fe94 100644 --- a/apps/web/src/app/billing/shared/offboarding-survey.component.ts +++ b/apps/web/src/app/billing/shared/offboarding-survey.component.ts @@ -25,6 +25,8 @@ type OrganizationOffboardingParams = { export type OffboardingSurveyDialogParams = UserOffboardingParams | OrganizationOffboardingParams; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum OffboardingSurveyDialogResultType { Closed = "closed", Submitted = "submitted", diff --git a/apps/web/src/app/billing/shared/update-license-types.ts b/apps/web/src/app/billing/shared/update-license-types.ts index 8f939ac62a9..8ba13541ba8 100644 --- a/apps/web/src/app/billing/shared/update-license-types.ts +++ b/apps/web/src/app/billing/shared/update-license-types.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum UpdateLicenseDialogResult { Updated = "updated", Cancelled = "cancelled", diff --git a/apps/web/src/app/dirt/reports/reports.ts b/apps/web/src/app/dirt/reports/reports.ts index 500ae23e5cf..c47928af1e9 100644 --- a/apps/web/src/app/dirt/reports/reports.ts +++ b/apps/web/src/app/dirt/reports/reports.ts @@ -7,6 +7,8 @@ import { ReportUnsecuredWebsites } from "./icons/report-unsecured-websites.icon" import { ReportWeakPasswords } from "./icons/report-weak-passwords.icon"; import { ReportEntry } from "./shared"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ReportType { ExposedPasswords = "exposedPasswords", ReusedPasswords = "reusedPasswords", diff --git a/apps/web/src/app/dirt/reports/shared/models/report-variant.ts b/apps/web/src/app/dirt/reports/shared/models/report-variant.ts index 3beba65f7d9..48b213f4cf6 100644 --- a/apps/web/src/app/dirt/reports/shared/models/report-variant.ts +++ b/apps/web/src/app/dirt/reports/shared/models/report-variant.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ReportVariant { Enabled = "Enabled", RequiresPremium = "RequiresPremium", diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 460b8d58d63..10c35f861b9 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -95,6 +95,8 @@ export interface VaultItemDialogParams { restore?: (c: CipherView) => Promise; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum VaultItemDialogResult { /** * A cipher was saved (created or updated). diff --git a/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.ts b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.ts index 2d0aa0231f1..e20efa9dbb8 100644 --- a/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.ts +++ b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.ts @@ -26,6 +26,8 @@ export interface WebVaultGeneratorDialogResult { generatedValue?: string; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum WebVaultGeneratorDialogAction { Selected = "selected", Canceled = "canceled", diff --git a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts index 5dcbf0d4e78..621e0ec88c5 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts @@ -35,6 +35,8 @@ import { WebCipherFormGenerationService } from "../services/web-cipher-form-gene /** * The result of the AddEditCipherDialogV2 component. */ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum AddEditCipherDialogResult { Edited = "edited", Added = "added", diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index f8084b03e33..1650b0f371f 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -29,6 +29,8 @@ export interface BulkDeleteDialogParams { unassignedCiphers?: string[]; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum BulkDeleteDialogResult { Deleted = "deleted", Canceled = "canceled", diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts index 8f0827e4b51..d287c430d49 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts @@ -23,6 +23,8 @@ export interface BulkMoveDialogParams { cipherIds?: string[]; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum BulkMoveDialogResult { Moved = "moved", Canceled = "canceled", diff --git a/apps/web/src/app/vault/individual-vault/folder-add-edit.component.ts b/apps/web/src/app/vault/individual-vault/folder-add-edit.component.ts index ad6cbfad43d..6a3c5663d93 100644 --- a/apps/web/src/app/vault/individual-vault/folder-add-edit.component.ts +++ b/apps/web/src/app/vault/individual-vault/folder-add-edit.component.ts @@ -113,6 +113,8 @@ export interface FolderAddEditDialogParams { folderId: string; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum FolderAddEditDialogResult { Deleted = "deleted", Canceled = "canceled", diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts index 1fa5ae1ad8b..ca16541f88f 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts @@ -17,6 +17,8 @@ import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PBKDF2KdfConfig, KdfConfigService, KdfType } from "@bitwarden/key-management"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum VisibleVaultBanner { KDFSettings = "kdf-settings", OutdatedBrowser = "outdated-browser", diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts index 0f949e17146..7566dbdc507 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts @@ -15,6 +15,8 @@ export type VaultFilterType = | FolderFilter | CollectionFilter; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum VaultFilterLabel { OrganizationFilter = "organizationFilter", TypeFilter = "typeFilter", diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts index e7b06cbb8d6..f52a4da3ffb 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.ts @@ -54,6 +54,8 @@ export interface ViewCipherDialogParams { disableEdit?: boolean; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ViewCipherDialogResult { Edited = "edited", Deleted = "deleted", diff --git a/apps/web/src/app/vault/services/browser-extension-prompt.service.ts b/apps/web/src/app/vault/services/browser-extension-prompt.service.ts index 5617d4aef75..f928404a2a9 100644 --- a/apps/web/src/app/vault/services/browser-extension-prompt.service.ts +++ b/apps/web/src/app/vault/services/browser-extension-prompt.service.ts @@ -7,6 +7,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { Utils } from "@bitwarden/common/platform/misc/utils"; import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum BrowserPromptState { Loading = "loading", Error = "error", diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts index 723d737d5bd..b8333828693 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts @@ -149,6 +149,8 @@ export interface PasswordHealthReportApplicationsRequest { url: string; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum DrawerType { None = 0, AppAtRiskMembers = 1, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts index 0213903c4d9..67f2382cf91 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts @@ -25,6 +25,8 @@ export type AddEditMemberDialogParams = { }; }; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum AddEditMemberDialogResultType { Closed = "closed", Deleted = "deleted", diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts index 0d1602946c7..4bb2c36ef15 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts @@ -18,6 +18,8 @@ export type AddExistingOrganizationDialogParams = { provider: Provider; }; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum AddExistingOrganizationDialogResultType { Closed = "closed", Submitted = "submitted", diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts index d3245916ad7..d71e18cd539 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts @@ -22,6 +22,8 @@ type CreateClientDialogParams = { plans: PlanResponse[]; }; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum CreateClientDialogResultType { Closed = "closed", Submitted = "submitted", diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-name-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-name-dialog.component.ts index bbcd5f7ed63..45abeab1f4a 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-name-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-name-dialog.component.ts @@ -23,6 +23,8 @@ type ManageClientNameDialogParams = { }; }; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ManageClientNameDialogResultType { Closed = "closed", Submitted = "submitted", diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts index b79c887e77e..ced48bfdbea 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts @@ -18,6 +18,8 @@ type ManageClientSubscriptionDialogParams = { provider: Provider; }; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ManageClientSubscriptionDialogResultType { Closed = "closed", Submitted = "submitted", diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts index ab74869bfc9..5aca124a46a 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts @@ -37,6 +37,8 @@ import { PasswordHealthMembersURIComponent } from "./password-health-members-uri import { PasswordHealthMembersComponent } from "./password-health-members.component"; import { PasswordHealthComponent } from "./password-health.component"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum RiskInsightsTabType { AllApps = 0, CriticalApps = 1, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts index 74824de00e0..c96887cc9ac 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts @@ -11,6 +11,8 @@ import { DialogRef, DIALOG_DATA, BitValidators, ToastService } from "@bitwarden/ import { ProjectView } from "../../models/view/project.view"; import { ProjectService } from "../../projects/project.service"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum OperationType { Add, Edit, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts index 88b3e5e2172..09a78e02c44 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts @@ -44,11 +44,15 @@ import { SecretService } from "../secret.service"; import { SecretDeleteDialogComponent, SecretDeleteOperation } from "./secret-delete.component"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum OperationType { Add, Edit, } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum SecretDialogTabType { NameValuePair = 0, People = 1, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts index e1cd7a77b28..241c736fb7a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts @@ -10,6 +10,8 @@ import { DialogRef, DIALOG_DATA, BitValidators, ToastService } from "@bitwarden/ import { ServiceAccountView } from "../../models/view/service-account.view"; import { ServiceAccountService } from "../service-account.service"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum OperationType { Add, Edit, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-item.enum.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-item.enum.ts index 6d060ac255d..6b92fd7458a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-item.enum.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-item.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ApItemEnum { User, Group, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-permission.enum.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-permission.enum.ts index eb442b0af5d..a57f9636178 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-permission.enum.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-permission.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ApPermissionEnum { CanRead = "canRead", CanReadWrite = "canReadWrite", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts index a43207ed75e..935ee1c8518 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts @@ -15,6 +15,8 @@ export interface BulkConfirmationStatus { description: string; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum BulkConfirmationResult { Continue, Cancel, diff --git a/eslint.config.mjs b/eslint.config.mjs index 9d93d1118c0..7928224dc00 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -73,6 +73,7 @@ export default tseslint.config( "@angular-eslint/use-lifecycle-interface": "error", "@angular-eslint/use-pipe-transform-interface": 0, "@bitwarden/platform/required-using": "error", + "@bitwarden/platform/no-enums": "error", "@typescript-eslint/explicit-member-accessibility": ["error", { accessibility: "no-public" }], "@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled "@typescript-eslint/no-floating-promises": "error", diff --git a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts index 7c068c51d86..871895c2ede 100644 --- a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts +++ b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts @@ -22,6 +22,8 @@ export type AddAccountCreditDialogParams = { providerId?: string; }; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum AddAccountCreditDialogResultType { Closed = "closed", Submitted = "submitted", diff --git a/libs/angular/src/tools/send/add-edit.component.ts b/libs/angular/src/tools/send/add-edit.component.ts index 7e6180e5849..0289664c365 100644 --- a/libs/angular/src/tools/send/add-edit.component.ts +++ b/libs/angular/src/tools/send/add-edit.component.ts @@ -36,6 +36,8 @@ import { SendService } from "@bitwarden/common/tools/send/services/send.service. import { DialogService, ToastService } from "@bitwarden/components"; // Value = hours +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum DatePreset { OneHour = 1, OneDay = 24, diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index bffb8a56b5e..dff4eafc3c2 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -36,6 +36,8 @@ import { PasswordInputResult } from "./password-input-result"; /** * Determines which form input elements will be displayed in the UI. */ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum InputPasswordFlow { /** * - Input: New password diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts index 945e6bbaaf5..3ea9416b7e2 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts @@ -41,6 +41,8 @@ import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper import { LoginDecryptionOptionsService } from "./login-decryption-options.service"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum State { NewUser, ExistingUserUntrustedDevice, diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts index 4c95a1eca3e..d74deb443f5 100644 --- a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts @@ -40,6 +40,8 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac import { AuthRequestApiService } from "../../common/abstractions/auth-request-api.service"; import { LoginViaAuthRequestCacheService } from "../../common/services/auth-request/default-login-via-auth-request-cache.service"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum Flow { StandardAuthRequest, // when user clicks "Login with device" from /login or "Approve from your other device" from /login-initiated AdminAuthRequest, // when user clicks "Request admin approval" from /login-initiated diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index cd226cddcec..d5180f56785 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -46,6 +46,8 @@ import { LoginComponentService, PasswordPolicies } from "./login-component.servi const BroadcasterSubscriptionId = "LoginComponent"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum LoginUiState { EMAIL_ENTRY = "EmailEntry", MASTER_PASSWORD_ENTRY = "MasterPasswordEntry", diff --git a/libs/auth/src/angular/registration/registration-start/registration-start.component.ts b/libs/auth/src/angular/registration/registration-start/registration-start.component.ts index e365ff09aa2..44d1d720a8d 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start.component.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start.component.ts @@ -26,6 +26,8 @@ import { RegistrationUserAddIcon } from "../../icons"; import { RegistrationCheckEmailIcon } from "../../icons/registration-check-email.icon"; import { RegistrationEnvSelectorComponent } from "../registration-env-selector/registration-env-selector.component"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum RegistrationStartState { USER_DATA_ENTRY = "UserDataEntry", CHECK_EMAIL = "CheckEmail", diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth-component.service.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component.service.ts index 2bb354a8cc3..c99722fb8e4 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth-component.service.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component.service.ts @@ -1,10 +1,14 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum LegacyKeyMigrationAction { PREVENT_LOGIN_AND_SHOW_REQUIRE_MIGRATION_WARNING, NAVIGATE_TO_MIGRATION_COMPONENT, } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum DuoLaunchAction { DIRECT_LAUNCH, SINGLE_ACTION_POPOUT, diff --git a/libs/auth/src/angular/user-verification/active-client-verification-option.enum.ts b/libs/auth/src/angular/user-verification/active-client-verification-option.enum.ts index bceccc7f965..ef2bd1855c6 100644 --- a/libs/auth/src/angular/user-verification/active-client-verification-option.enum.ts +++ b/libs/auth/src/angular/user-verification/active-client-verification-option.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ActiveClientVerificationOption { MasterPassword = "masterPassword", Pin = "pin", diff --git a/libs/auth/src/angular/validators/compare-inputs.validator.ts b/libs/auth/src/angular/validators/compare-inputs.validator.ts index 24568ade0e3..79d547859a4 100644 --- a/libs/auth/src/angular/validators/compare-inputs.validator.ts +++ b/libs/auth/src/angular/validators/compare-inputs.validator.ts @@ -1,5 +1,7 @@ import { AbstractControl, FormGroup, ValidationErrors, ValidatorFn } from "@angular/forms"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ValidationGoal { InputsShouldMatch, InputsShouldNotMatch, diff --git a/libs/common/src/admin-console/enums/organization-api-key-type.enum.ts b/libs/common/src/admin-console/enums/organization-api-key-type.enum.ts index 44ba7f8391d..bb98ea8718b 100644 --- a/libs/common/src/admin-console/enums/organization-api-key-type.enum.ts +++ b/libs/common/src/admin-console/enums/organization-api-key-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum OrganizationApiKeyType { Default = 0, BillingSync = 1, diff --git a/libs/common/src/admin-console/enums/organization-connection-type.enum.ts b/libs/common/src/admin-console/enums/organization-connection-type.enum.ts index d2f9700a6a0..3cd11a29496 100644 --- a/libs/common/src/admin-console/enums/organization-connection-type.enum.ts +++ b/libs/common/src/admin-console/enums/organization-connection-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum OrganizationConnectionType { CloudBillingSync = 1, Scim = 2, diff --git a/libs/common/src/admin-console/enums/organization-user-status-type.enum.ts b/libs/common/src/admin-console/enums/organization-user-status-type.enum.ts index f5fa0e25c91..df9ccc8b430 100644 --- a/libs/common/src/admin-console/enums/organization-user-status-type.enum.ts +++ b/libs/common/src/admin-console/enums/organization-user-status-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum OrganizationUserStatusType { Invited = 0, Accepted = 1, diff --git a/libs/common/src/admin-console/enums/organization-user-type.enum.ts b/libs/common/src/admin-console/enums/organization-user-type.enum.ts index da50bfbdc20..098374becb4 100644 --- a/libs/common/src/admin-console/enums/organization-user-type.enum.ts +++ b/libs/common/src/admin-console/enums/organization-user-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum OrganizationUserType { Owner = 0, Admin = 1, diff --git a/libs/common/src/admin-console/enums/policy-type.enum.ts b/libs/common/src/admin-console/enums/policy-type.enum.ts index 336b834ca56..42ab798eabf 100644 --- a/libs/common/src/admin-console/enums/policy-type.enum.ts +++ b/libs/common/src/admin-console/enums/policy-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum PolicyType { TwoFactorAuthentication = 0, // Requires users to have 2fa enabled MasterPassword = 1, // Sets minimum requirements for master password complexity diff --git a/libs/common/src/admin-console/enums/provider-status-type.enum.ts b/libs/common/src/admin-console/enums/provider-status-type.enum.ts index 8da60af0eb3..32b730510e0 100644 --- a/libs/common/src/admin-console/enums/provider-status-type.enum.ts +++ b/libs/common/src/admin-console/enums/provider-status-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ProviderStatusType { Pending = 0, Created = 1, diff --git a/libs/common/src/admin-console/enums/provider-type.enum.ts b/libs/common/src/admin-console/enums/provider-type.enum.ts index eb48e362e7d..dbb60fb3638 100644 --- a/libs/common/src/admin-console/enums/provider-type.enum.ts +++ b/libs/common/src/admin-console/enums/provider-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ProviderType { Msp = 0, Reseller = 1, diff --git a/libs/common/src/admin-console/enums/provider-user-status-type.enum.ts b/libs/common/src/admin-console/enums/provider-user-status-type.enum.ts index 38d6c2e4961..253128aa25f 100644 --- a/libs/common/src/admin-console/enums/provider-user-status-type.enum.ts +++ b/libs/common/src/admin-console/enums/provider-user-status-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ProviderUserStatusType { Invited = 0, Accepted = 1, diff --git a/libs/common/src/admin-console/enums/provider-user-type.enum.ts b/libs/common/src/admin-console/enums/provider-user-type.enum.ts index 00490adcfca..d20755c3196 100644 --- a/libs/common/src/admin-console/enums/provider-user-type.enum.ts +++ b/libs/common/src/admin-console/enums/provider-user-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ProviderUserType { ProviderAdmin = 0, ServiceUser = 1, diff --git a/libs/common/src/admin-console/enums/scim-provider-type.enum.ts b/libs/common/src/admin-console/enums/scim-provider-type.enum.ts index 43c518fdfbf..cd33a1cf891 100644 --- a/libs/common/src/admin-console/enums/scim-provider-type.enum.ts +++ b/libs/common/src/admin-console/enums/scim-provider-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ScimProviderType { Default = 0, AzureAd = 1, diff --git a/libs/common/src/auth/enums/auth-request-type.ts b/libs/common/src/auth/enums/auth-request-type.ts index 31db2467861..d23fec03205 100644 --- a/libs/common/src/auth/enums/auth-request-type.ts +++ b/libs/common/src/auth/enums/auth-request-type.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum AuthRequestType { AuthenticateAndUnlock = 0, Unlock = 1, diff --git a/libs/common/src/auth/enums/authentication-status.ts b/libs/common/src/auth/enums/authentication-status.ts index 17b4f1f21e8..6a6f9467ae7 100644 --- a/libs/common/src/auth/enums/authentication-status.ts +++ b/libs/common/src/auth/enums/authentication-status.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum AuthenticationStatus { LoggedOut = 0, Locked = 1, diff --git a/libs/common/src/auth/enums/authentication-type.ts b/libs/common/src/auth/enums/authentication-type.ts index 35b50e6400a..13a3d70ddda 100644 --- a/libs/common/src/auth/enums/authentication-type.ts +++ b/libs/common/src/auth/enums/authentication-type.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum AuthenticationType { Password = 0, Sso = 1, diff --git a/libs/common/src/auth/enums/sso.ts b/libs/common/src/auth/enums/sso.ts index 0c86a27151f..1e5766c7afd 100644 --- a/libs/common/src/auth/enums/sso.ts +++ b/libs/common/src/auth/enums/sso.ts @@ -1,25 +1,35 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum SsoType { None = 0, OpenIdConnect = 1, Saml2 = 2, } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum MemberDecryptionType { MasterPassword = 0, KeyConnector = 1, TrustedDeviceEncryption = 2, } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum OpenIdConnectRedirectBehavior { RedirectGet = 0, FormPost = 1, } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum Saml2BindingType { HttpRedirect = 1, HttpPost = 2, } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum Saml2NameIdFormat { NotConfigured = 0, Unspecified = 1, @@ -32,6 +42,8 @@ export enum Saml2NameIdFormat { Transient = 8, } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum Saml2SigningBehavior { IfIdpWantAuthnRequestsSigned = 0, Always = 1, diff --git a/libs/common/src/auth/enums/two-factor-provider-type.ts b/libs/common/src/auth/enums/two-factor-provider-type.ts index b3308b6c12f..9be22bf90c8 100644 --- a/libs/common/src/auth/enums/two-factor-provider-type.ts +++ b/libs/common/src/auth/enums/two-factor-provider-type.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum TwoFactorProviderType { Authenticator = 0, Email = 1, diff --git a/libs/common/src/auth/enums/verification-type.ts b/libs/common/src/auth/enums/verification-type.ts index c1991162f91..f0ad4c0d19c 100644 --- a/libs/common/src/auth/enums/verification-type.ts +++ b/libs/common/src/auth/enums/verification-type.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum VerificationType { MasterPassword = 0, OTP = 1, diff --git a/libs/common/src/auth/models/domain/force-set-password-reason.ts b/libs/common/src/auth/models/domain/force-set-password-reason.ts index 56d52860443..68392125281 100644 --- a/libs/common/src/auth/models/domain/force-set-password-reason.ts +++ b/libs/common/src/auth/models/domain/force-set-password-reason.ts @@ -2,6 +2,8 @@ * This enum is used to determine if a user should be forced to initially set or reset their password * on login (server flag) or unlock via MP (client evaluation). */ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ForceSetPasswordReason { /** * A password reset should not be forced. diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index c27afa6805a..61c00f69215 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -43,6 +43,8 @@ import { SECURITY_STAMP_MEMORY, } from "./token.state"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum TokenStorageLocation { Disk = "disk", SecureStorage = "secureStorage", diff --git a/libs/common/src/billing/enums/bitwarden-product-type.enum.ts b/libs/common/src/billing/enums/bitwarden-product-type.enum.ts index 76b0899fd9c..4389d283c00 100644 --- a/libs/common/src/billing/enums/bitwarden-product-type.enum.ts +++ b/libs/common/src/billing/enums/bitwarden-product-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum BitwardenProductType { PasswordManager = 0, SecretsManager = 1, diff --git a/libs/common/src/billing/enums/payment-method-type.enum.ts b/libs/common/src/billing/enums/payment-method-type.enum.ts index 9b29263175c..6ea98fdc5ec 100644 --- a/libs/common/src/billing/enums/payment-method-type.enum.ts +++ b/libs/common/src/billing/enums/payment-method-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum PaymentMethodType { Card = 0, BankAccount = 1, diff --git a/libs/common/src/billing/enums/plan-interval.enum.ts b/libs/common/src/billing/enums/plan-interval.enum.ts index 546336748cd..c048160310b 100644 --- a/libs/common/src/billing/enums/plan-interval.enum.ts +++ b/libs/common/src/billing/enums/plan-interval.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum PlanInterval { Monthly = 0, Annually = 1, diff --git a/libs/common/src/billing/enums/plan-sponsorship-type.enum.ts b/libs/common/src/billing/enums/plan-sponsorship-type.enum.ts index 3b4c00467c2..845b698f509 100644 --- a/libs/common/src/billing/enums/plan-sponsorship-type.enum.ts +++ b/libs/common/src/billing/enums/plan-sponsorship-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum PlanSponsorshipType { FamiliesForEnterprise = 0, } diff --git a/libs/common/src/billing/enums/plan-type.enum.ts b/libs/common/src/billing/enums/plan-type.enum.ts index c8977703454..5c356ce42fe 100644 --- a/libs/common/src/billing/enums/plan-type.enum.ts +++ b/libs/common/src/billing/enums/plan-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum PlanType { Free = 0, FamiliesAnnually2019 = 1, diff --git a/libs/common/src/billing/enums/product-tier-type.enum.ts b/libs/common/src/billing/enums/product-tier-type.enum.ts index fadf57ccdc5..e94243de588 100644 --- a/libs/common/src/billing/enums/product-tier-type.enum.ts +++ b/libs/common/src/billing/enums/product-tier-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ProductTierType { Free = 0, Families = 1, diff --git a/libs/common/src/billing/enums/product-type.enum.ts b/libs/common/src/billing/enums/product-type.enum.ts index 3072ad0f96a..0c11eb92c79 100644 --- a/libs/common/src/billing/enums/product-type.enum.ts +++ b/libs/common/src/billing/enums/product-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ProductType { PasswordManager = 0, SecretsManager = 1, diff --git a/libs/common/src/billing/enums/transaction-type.enum.ts b/libs/common/src/billing/enums/transaction-type.enum.ts index 34731a2ec33..d4c48253a39 100644 --- a/libs/common/src/billing/enums/transaction-type.enum.ts +++ b/libs/common/src/billing/enums/transaction-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum TransactionType { Charge = 0, Credit = 1, diff --git a/libs/common/src/enums/client-type.enum.ts b/libs/common/src/enums/client-type.enum.ts index 54653f74462..25e9d6f3371 100644 --- a/libs/common/src/enums/client-type.enum.ts +++ b/libs/common/src/enums/client-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ClientType { Web = "web", Browser = "browser", diff --git a/libs/common/src/enums/device-type.enum.ts b/libs/common/src/enums/device-type.enum.ts index ff6329b9ac4..d5628536ff7 100644 --- a/libs/common/src/enums/device-type.enum.ts +++ b/libs/common/src/enums/device-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum DeviceType { Android = 0, iOS = 1, diff --git a/libs/common/src/enums/event-system-user.enum.ts b/libs/common/src/enums/event-system-user.enum.ts index e2d43a4f113..f4abbb1e3e9 100644 --- a/libs/common/src/enums/event-system-user.enum.ts +++ b/libs/common/src/enums/event-system-user.enum.ts @@ -1,4 +1,6 @@ // Note: the enum key is used to describe the EventSystemUser in the UI. Be careful about changing it. +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum EventSystemUser { SCIM = 1, DomainVerification = 2, diff --git a/libs/common/src/enums/event-type.enum.ts b/libs/common/src/enums/event-type.enum.ts index 51b324bb434..9efe49b1b0f 100644 --- a/libs/common/src/enums/event-type.enum.ts +++ b/libs/common/src/enums/event-type.enum.ts @@ -1,4 +1,6 @@ // Increment by 100 for each new set of events +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum EventType { User_LoggedIn = 1000, User_ChangedPassword = 1001, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index a7679a6248a..ddc75eb0d66 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -7,6 +7,8 @@ import { ServerConfig } from "../platform/abstractions/config/server-config"; * * Flags should be grouped by team to have visibility of ownership and cleanup. */ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum FeatureFlag { /* Admin Console Team */ VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint", diff --git a/libs/common/src/enums/http-status-code.enum.ts b/libs/common/src/enums/http-status-code.enum.ts index 94226c82552..d2dba5d8d4c 100644 --- a/libs/common/src/enums/http-status-code.enum.ts +++ b/libs/common/src/enums/http-status-code.enum.ts @@ -4,6 +4,8 @@ * @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes} * src: https://gist.github.com/RWOverdijk/6cef816cfdf5722228e01cc05fd4b094 */ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum HttpStatusCode { /** * The server has received the request headers and the client should proceed to send the request body diff --git a/libs/common/src/enums/integration-type.enum.ts b/libs/common/src/enums/integration-type.enum.ts index 42c385fe715..ac8bb9c6afa 100644 --- a/libs/common/src/enums/integration-type.enum.ts +++ b/libs/common/src/enums/integration-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum IntegrationType { Integration = "integration", SDK = "sdk", diff --git a/libs/common/src/enums/native-messaging-version.enum.ts b/libs/common/src/enums/native-messaging-version.enum.ts index f7cf411a40a..f83ec8f4b25 100644 --- a/libs/common/src/enums/native-messaging-version.enum.ts +++ b/libs/common/src/enums/native-messaging-version.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum NativeMessagingVersion { One = 1, // Original implementation Latest = One, diff --git a/libs/common/src/enums/notification-type.enum.ts b/libs/common/src/enums/notification-type.enum.ts index 0e4d0bfee3d..6d731253ce3 100644 --- a/libs/common/src/enums/notification-type.enum.ts +++ b/libs/common/src/enums/notification-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum NotificationType { SyncCipherUpdate = 0, SyncCipherCreate = 1, diff --git a/libs/common/src/enums/push-technology.enum.ts b/libs/common/src/enums/push-technology.enum.ts index 9452c144bb7..1bc4e62cc9d 100644 --- a/libs/common/src/enums/push-technology.enum.ts +++ b/libs/common/src/enums/push-technology.enum.ts @@ -1,6 +1,8 @@ /** * The preferred push technology of the server. */ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum PushTechnology { /** * Indicates that we should use SignalR over web sockets to receive push notifications from the server. diff --git a/libs/common/src/key-management/vault-timeout/enums/vault-timeout-action.enum.ts b/libs/common/src/key-management/vault-timeout/enums/vault-timeout-action.enum.ts index 239a7490191..9e70b7ebb12 100644 --- a/libs/common/src/key-management/vault-timeout/enums/vault-timeout-action.enum.ts +++ b/libs/common/src/key-management/vault-timeout/enums/vault-timeout-action.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum VaultTimeoutAction { Lock = "lock", LogOut = "logOut", diff --git a/libs/common/src/platform/abstractions/environment.service.ts b/libs/common/src/platform/abstractions/environment.service.ts index 4a10f856893..b8931656848 100644 --- a/libs/common/src/platform/abstractions/environment.service.ts +++ b/libs/common/src/platform/abstractions/environment.service.ts @@ -17,6 +17,8 @@ export type Urls = { /** * A subset of available regions, additional regions can be loaded through configuration. */ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum Region { US = "US", EU = "EU", diff --git a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts index e9e68ca92c3..15655393362 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts @@ -46,11 +46,15 @@ export abstract class Fido2AuthenticatorService { silentCredentialDiscovery: (rpId: string) => Promise; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum Fido2AlgorithmIdentifier { ES256 = -7, RS256 = -257, } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum Fido2AuthenticatorErrorCode { Unknown = "UnknownError", NotSupported = "NotSupportedError", diff --git a/libs/common/src/platform/enums/encryption-type.enum.ts b/libs/common/src/platform/enums/encryption-type.enum.ts index fd484dc2fdf..7f4b5048a45 100644 --- a/libs/common/src/platform/enums/encryption-type.enum.ts +++ b/libs/common/src/platform/enums/encryption-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum EncryptionType { AesCbc256_B64 = 0, // Type 1 was the unused and removed AesCbc128_HmacSha256_B64 diff --git a/libs/common/src/platform/enums/file-upload-type.enum.ts b/libs/common/src/platform/enums/file-upload-type.enum.ts index 4cf654fd0ff..a94797b9ef3 100644 --- a/libs/common/src/platform/enums/file-upload-type.enum.ts +++ b/libs/common/src/platform/enums/file-upload-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum FileUploadType { Direct = 0, Azure = 1, diff --git a/libs/common/src/platform/enums/hash-purpose.enum.ts b/libs/common/src/platform/enums/hash-purpose.enum.ts index 887931b9edd..4b61db914a1 100644 --- a/libs/common/src/platform/enums/hash-purpose.enum.ts +++ b/libs/common/src/platform/enums/hash-purpose.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum HashPurpose { ServerAuthorization = 1, LocalAuthorization = 2, diff --git a/libs/common/src/platform/enums/html-storage-location.enum.ts b/libs/common/src/platform/enums/html-storage-location.enum.ts index 19c84fe88c7..1d018a72869 100644 --- a/libs/common/src/platform/enums/html-storage-location.enum.ts +++ b/libs/common/src/platform/enums/html-storage-location.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum HtmlStorageLocation { Local = "local", Memory = "memory", diff --git a/libs/common/src/platform/enums/key-suffix-options.enum.ts b/libs/common/src/platform/enums/key-suffix-options.enum.ts index 98fa215be6a..7cc412f563b 100644 --- a/libs/common/src/platform/enums/key-suffix-options.enum.ts +++ b/libs/common/src/platform/enums/key-suffix-options.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum KeySuffixOptions { Auto = "auto", Pin = "pin", diff --git a/libs/common/src/platform/enums/log-level-type.enum.ts b/libs/common/src/platform/enums/log-level-type.enum.ts index 709871dd5e9..b5f84467d6e 100644 --- a/libs/common/src/platform/enums/log-level-type.enum.ts +++ b/libs/common/src/platform/enums/log-level-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum LogLevelType { Debug, Info, diff --git a/libs/common/src/platform/enums/storage-location.enum.ts b/libs/common/src/platform/enums/storage-location.enum.ts index 46d50d1d38d..9f6e22babec 100644 --- a/libs/common/src/platform/enums/storage-location.enum.ts +++ b/libs/common/src/platform/enums/storage-location.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum StorageLocation { Both = "both", Disk = "disk", diff --git a/libs/common/src/platform/enums/theme-type.enum.ts b/libs/common/src/platform/enums/theme-type.enum.ts index d1767c4990a..c0f9aa2d52f 100644 --- a/libs/common/src/platform/enums/theme-type.enum.ts +++ b/libs/common/src/platform/enums/theme-type.enum.ts @@ -1,6 +1,8 @@ /** * @deprecated prefer the `ThemeTypes` constants and `Theme` type over unsafe enum types **/ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ThemeType { System = "system", Light = "light", diff --git a/libs/common/src/platform/services/cryptography/initializer-key.ts b/libs/common/src/platform/services/cryptography/initializer-key.ts index 88e36d90515..59287a62c46 100644 --- a/libs/common/src/platform/services/cryptography/initializer-key.ts +++ b/libs/common/src/platform/services/cryptography/initializer-key.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum InitializerKey { Cipher = 0, CipherView = 1, diff --git a/libs/common/src/state-migrations/migrations/28-move-provider-state-to-state-provider.ts b/libs/common/src/state-migrations/migrations/28-move-provider-state-to-state-provider.ts index 96f0a9acd5d..5ae943438d2 100644 --- a/libs/common/src/state-migrations/migrations/28-move-provider-state-to-state-provider.ts +++ b/libs/common/src/state-migrations/migrations/28-move-provider-state-to-state-provider.ts @@ -5,6 +5,8 @@ import { Jsonify } from "type-fest"; import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; import { Migrator } from "../migrator"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum ProviderUserStatusType { Invited = 0, Accepted = 1, @@ -12,6 +14,8 @@ enum ProviderUserStatusType { Revoked = -1, } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum ProviderUserType { ProviderAdmin = 0, ServiceUser = 1, diff --git a/libs/common/src/state-migrations/migrations/30-move-policy-state-to-state-provider.ts b/libs/common/src/state-migrations/migrations/30-move-policy-state-to-state-provider.ts index 0b4c95cb55c..f188bc0a33e 100644 --- a/libs/common/src/state-migrations/migrations/30-move-policy-state-to-state-provider.ts +++ b/libs/common/src/state-migrations/migrations/30-move-policy-state-to-state-provider.ts @@ -3,6 +3,8 @@ import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; import { Migrator } from "../migrator"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum PolicyType { TwoFactorAuthentication = 0, // Requires users to have 2fa enabled MasterPassword = 1, // Sets minimum requirements for master password complexity diff --git a/libs/common/src/state-migrations/migrations/40-move-organization-state-to-state-provider.ts b/libs/common/src/state-migrations/migrations/40-move-organization-state-to-state-provider.ts index 862fe33bd4e..b23b712aefe 100644 --- a/libs/common/src/state-migrations/migrations/40-move-organization-state-to-state-provider.ts +++ b/libs/common/src/state-migrations/migrations/40-move-organization-state-to-state-provider.ts @@ -7,6 +7,8 @@ import { Migrator } from "../migrator"; // Local declarations of `OrganizationData` and the types of it's properties. // Duplicated to remain frozen in time when migration occurs. +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum OrganizationUserStatusType { Invited = 0, Accepted = 1, @@ -14,6 +16,8 @@ enum OrganizationUserStatusType { Revoked = -1, } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum OrganizationUserType { Owner = 0, Admin = 1, @@ -40,11 +44,15 @@ type PermissionsApi = { manageScim: boolean; }; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum ProviderType { Msp = 0, Reseller = 1, } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum ProductType { Free = 0, Families = 1, diff --git a/libs/common/src/state-migrations/migrations/54-move-encrypted-sends.ts b/libs/common/src/state-migrations/migrations/54-move-encrypted-sends.ts index 7d3a2bebbf9..489fb2258ee 100644 --- a/libs/common/src/state-migrations/migrations/54-move-encrypted-sends.ts +++ b/libs/common/src/state-migrations/migrations/54-move-encrypted-sends.ts @@ -3,6 +3,8 @@ import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; import { Migrator } from "../migrator"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum SendType { Text = 0, File = 1, diff --git a/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts index 81ee193867b..a01ef42a8ff 100644 --- a/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts +++ b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts @@ -3,6 +3,8 @@ import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; import { Migrator } from "../migrator"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum KdfType { PBKDF2_SHA256 = 0, Argon2id = 1, diff --git a/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.ts b/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.ts index 5874a80dc60..3557bfae083 100644 --- a/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.ts +++ b/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.ts @@ -57,6 +57,8 @@ const vaultTimeoutTypeRollbackRecord: Record = { onIdle: -4, }; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ClientType { Web = "web", Browser = "browser", diff --git a/libs/common/src/tools/send/enums/send-type.ts b/libs/common/src/tools/send/enums/send-type.ts index 487930c90c7..5b03c71d22a 100644 --- a/libs/common/src/tools/send/enums/send-type.ts +++ b/libs/common/src/tools/send/enums/send-type.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum SendType { Text = 0, File = 1, diff --git a/libs/common/src/vault/enums/cipher-reprompt-type.ts b/libs/common/src/vault/enums/cipher-reprompt-type.ts index 1d0a523ced0..190a9bad042 100644 --- a/libs/common/src/vault/enums/cipher-reprompt-type.ts +++ b/libs/common/src/vault/enums/cipher-reprompt-type.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum CipherRepromptType { None = 0, Password = 1, diff --git a/libs/common/src/vault/enums/cipher-type.ts b/libs/common/src/vault/enums/cipher-type.ts index 0b7bbf1ee17..30d80cdef7e 100644 --- a/libs/common/src/vault/enums/cipher-type.ts +++ b/libs/common/src/vault/enums/cipher-type.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum CipherType { Login = 1, SecureNote = 2, diff --git a/libs/common/src/vault/enums/field-type.enum.ts b/libs/common/src/vault/enums/field-type.enum.ts index d6deb30e691..df5016890b2 100644 --- a/libs/common/src/vault/enums/field-type.enum.ts +++ b/libs/common/src/vault/enums/field-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum FieldType { Text = 0, Hidden = 1, diff --git a/libs/common/src/vault/enums/linked-id-type.enum.ts b/libs/common/src/vault/enums/linked-id-type.enum.ts index c38ebc1c6e8..b329aecb3f4 100644 --- a/libs/common/src/vault/enums/linked-id-type.enum.ts +++ b/libs/common/src/vault/enums/linked-id-type.enum.ts @@ -1,12 +1,16 @@ export type LinkedIdType = LoginLinkedId | CardLinkedId | IdentityLinkedId; // LoginView +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum LoginLinkedId { Username = 100, Password = 101, } // CardView +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum CardLinkedId { CardholderName = 300, ExpMonth = 301, @@ -17,6 +21,8 @@ export enum CardLinkedId { } // IdentityView +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum IdentityLinkedId { Title = 400, MiddleName = 401, diff --git a/libs/common/src/vault/enums/secure-note-type.enum.ts b/libs/common/src/vault/enums/secure-note-type.enum.ts index 8015236d148..4fbd05e6bd4 100644 --- a/libs/common/src/vault/enums/secure-note-type.enum.ts +++ b/libs/common/src/vault/enums/secure-note-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum SecureNoteType { Generic = 0, } diff --git a/libs/common/src/vault/tasks/enums/security-task-status.enum.ts b/libs/common/src/vault/tasks/enums/security-task-status.enum.ts index 1c6e7decc20..c8c26266e66 100644 --- a/libs/common/src/vault/tasks/enums/security-task-status.enum.ts +++ b/libs/common/src/vault/tasks/enums/security-task-status.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum SecurityTaskStatus { /** * Default status for newly created tasks that have not been completed. diff --git a/libs/common/src/vault/tasks/enums/security-task-type.enum.ts b/libs/common/src/vault/tasks/enums/security-task-type.enum.ts index 264cd88696b..79a2d23c8b3 100644 --- a/libs/common/src/vault/tasks/enums/security-task-type.enum.ts +++ b/libs/common/src/vault/tasks/enums/security-task-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum SecurityTaskType { /** * Task to update a cipher's password that was found to be at-risk by an administrator diff --git a/libs/components/src/color-password/color-password.component.ts b/libs/components/src/color-password/color-password.component.ts index 86ccb41ba3c..2dd78e8525d 100644 --- a/libs/components/src/color-password/color-password.component.ts +++ b/libs/components/src/color-password/color-password.component.ts @@ -2,6 +2,8 @@ import { Component, computed, HostBinding, input } from "@angular/core"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums enum CharacterType { Letter, Emoji, diff --git a/libs/eslint/platform/index.mjs b/libs/eslint/platform/index.mjs index 049d95e2074..c7ea3f1dd89 100644 --- a/libs/eslint/platform/index.mjs +++ b/libs/eslint/platform/index.mjs @@ -1,3 +1,4 @@ import requiredUsing from "./required-using.mjs"; +import noEnums from "./no-enums.mjs"; -export default { rules: { "required-using": requiredUsing } }; +export default { rules: { "required-using": requiredUsing, "no-enums": noEnums } }; diff --git a/libs/eslint/platform/no-enums.mjs b/libs/eslint/platform/no-enums.mjs new file mode 100644 index 00000000000..4f3039f7e34 --- /dev/null +++ b/libs/eslint/platform/no-enums.mjs @@ -0,0 +1,23 @@ +export const errorMessage = "Enums are discouraged, please use a const object instead"; + +export default { + meta: { + type: "suggestion", + docs: { + description: "Enforce using consts instead of enums", + category: "Best Practices", + recommended: false, + }, + schema: [], + }, + create(context) { + return { + TSEnumDeclaration(node) { + context.report({ + node, + message: errorMessage, + }); + }, + }; + }, +}; diff --git a/libs/eslint/platform/no-enums.spec.mjs b/libs/eslint/platform/no-enums.spec.mjs new file mode 100644 index 00000000000..5ded12d6ed3 --- /dev/null +++ b/libs/eslint/platform/no-enums.spec.mjs @@ -0,0 +1,71 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; + +import rule, { errorMessage } from "./no-enums.mjs"; + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + project: [__dirname + "/../tsconfig.spec.json"], + projectService: { + allowDefaultProject: ["*.ts*"], + }, + tsconfigRootDir: __dirname + "/..", + }, + }, +}); + +ruleTester.run("no-enums", rule.default, { + valid: [ + { + name: "Using const instead of enum", + code: ` + const Status = { + Active: "active", + Inactive: "inactive", + } as const; + `, + }, + { + name: "Using const with type", + code: ` + const Status = { + Active: "active", + Inactive: "inactive", + } as const; + type Status = typeof Status[keyof typeof Status]; + `, + }, + ], + invalid: [ + { + name: "Using enum declaration", + code: ` + enum Status { + Active = "active", + Inactive = "inactive", + } + `, + errors: [ + { + message: errorMessage, + }, + ], + }, + { + name: "Using enum with numeric values", + code: ` + enum Direction { + Up = 1, + Down = 2, + Left = 3, + Right = 4, + } + `, + errors: [ + { + message: errorMessage, + }, + ], + }, + ], +}); diff --git a/libs/importer/src/importers/fsecure/fsecure-fsk-types.ts b/libs/importer/src/importers/fsecure/fsecure-fsk-types.ts index 71797a0f8cd..1235426d683 100644 --- a/libs/importer/src/importers/fsecure/fsecure-fsk-types.ts +++ b/libs/importer/src/importers/fsecure/fsecure-fsk-types.ts @@ -6,6 +6,8 @@ export interface Data { [key: string]: FskEntry; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum FskEntryTypesEnum { Login = 1, CreditCard = 2, diff --git a/libs/importer/src/importers/lastpass/access/enums/idp-provider.ts b/libs/importer/src/importers/lastpass/access/enums/idp-provider.ts index 32e74c36ee1..01c4572fcf9 100644 --- a/libs/importer/src/importers/lastpass/access/enums/idp-provider.ts +++ b/libs/importer/src/importers/lastpass/access/enums/idp-provider.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum IdpProvider { Azure = 0, OktaAuthServer = 1, diff --git a/libs/importer/src/importers/lastpass/access/enums/lastpass-login-type.ts b/libs/importer/src/importers/lastpass/access/enums/lastpass-login-type.ts index 611dd0b6dab..a3be36c790e 100644 --- a/libs/importer/src/importers/lastpass/access/enums/lastpass-login-type.ts +++ b/libs/importer/src/importers/lastpass/access/enums/lastpass-login-type.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum LastpassLoginType { MasterPassword = 0, // Not sure what Types 1 and 2 are? diff --git a/libs/importer/src/importers/lastpass/access/enums/otp-method.ts b/libs/importer/src/importers/lastpass/access/enums/otp-method.ts index 6b940486ff0..f1237160179 100644 --- a/libs/importer/src/importers/lastpass/access/enums/otp-method.ts +++ b/libs/importer/src/importers/lastpass/access/enums/otp-method.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum OtpMethod { GoogleAuth, MicrosoftAuth, diff --git a/libs/importer/src/importers/lastpass/access/enums/platform.ts b/libs/importer/src/importers/lastpass/access/enums/platform.ts index 283e0c36ce2..6870fc28c24 100644 --- a/libs/importer/src/importers/lastpass/access/enums/platform.ts +++ b/libs/importer/src/importers/lastpass/access/enums/platform.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum Platform { Desktop, Mobile, diff --git a/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts b/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts index 63a0427b2c7..d7f4dec8f95 100644 --- a/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts +++ b/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts @@ -25,6 +25,8 @@ export interface VaultAttributes { type: string; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum CategoryEnum { Login = "001", CreditCard = "002", @@ -67,6 +69,8 @@ export interface Details { password?: string | null; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum LoginFieldTypeEnum { TextOrHtml = "T", EmailAddress = "E", diff --git a/libs/importer/src/importers/protonpass/types/protonpass-json-type.ts b/libs/importer/src/importers/protonpass/types/protonpass-json-type.ts index 20fa314a314..af2eb15a740 100644 --- a/libs/importer/src/importers/protonpass/types/protonpass-json-type.ts +++ b/libs/importer/src/importers/protonpass/types/protonpass-json-type.ts @@ -27,6 +27,8 @@ export type ProtonPassItem = { pinned: boolean; }; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum ProtonPassItemState { ACTIVE = 1, TRASHED = 2, diff --git a/libs/key-management/src/biometrics/biometrics-commands.ts b/libs/key-management/src/biometrics/biometrics-commands.ts index 81f0ea747e4..1ef31a31fb4 100644 --- a/libs/key-management/src/biometrics/biometrics-commands.ts +++ b/libs/key-management/src/biometrics/biometrics-commands.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum BiometricsCommands { /** Perform biometric authentication for the system's user. Does not require setup, and does not return cryptographic material, only yes or no. */ AuthenticateWithBiometrics = "authenticateWithBiometrics", diff --git a/libs/key-management/src/biometrics/biometrics-status.ts b/libs/key-management/src/biometrics/biometrics-status.ts index fb46ed1c4be..e436574ff7d 100644 --- a/libs/key-management/src/biometrics/biometrics-status.ts +++ b/libs/key-management/src/biometrics/biometrics-status.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum BiometricsStatus { /** For the biometrics interface, this means that biometric unlock is available and can be used. Querying for the user specifically, this means that biometric can be used for to unlock this user */ Available, diff --git a/libs/key-management/src/enums/kdf-type.enum.ts b/libs/key-management/src/enums/kdf-type.enum.ts index 29fcd9f1f8e..4f468e63a29 100644 --- a/libs/key-management/src/enums/kdf-type.enum.ts +++ b/libs/key-management/src/enums/kdf-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum KdfType { PBKDF2_SHA256 = 0, Argon2id = 1, diff --git a/libs/tools/export/vault-export/vault-export-ui/src/enums/encrypted-export-type.enum.ts b/libs/tools/export/vault-export/vault-export-ui/src/enums/encrypted-export-type.enum.ts index 4767869f7d0..2f416e4a49a 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/enums/encrypted-export-type.enum.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/enums/encrypted-export-type.enum.ts @@ -1,3 +1,5 @@ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum EncryptedExportType { AccountEncrypted = 0, FileEncrypted = 1, diff --git a/libs/tools/send/send-ui/src/add-edit/send-add-edit-dialog.component.ts b/libs/tools/send/send-ui/src/add-edit/send-add-edit-dialog.component.ts index 5da2c6f8bed..0bb753d3f37 100644 --- a/libs/tools/send/send-ui/src/add-edit/send-add-edit-dialog.component.ts +++ b/libs/tools/send/send-ui/src/add-edit/send-add-edit-dialog.component.ts @@ -35,6 +35,8 @@ export interface SendItemDialogParams { disableForm?: boolean; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum SendItemDialogResult { /** * A Send was saved (created or updated). diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts index c77c44467d8..9ca9aefb4ac 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts @@ -30,6 +30,8 @@ import { SendFileDetailsComponent } from "./send-file-details.component"; import { SendTextDetailsComponent } from "./send-text-details.component"; // Value = hours +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum DatePreset { OneHour = 1, OneDay = 24, diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts index 68660c4bbd9..b34d0d3a312 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts +++ b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts @@ -24,6 +24,8 @@ export interface AttachmentsDialogParams { /** * Enum representing the possible results of the attachment dialog. */ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum AttachmentDialogResult { Uploaded = "uploaded", Removed = "removed", diff --git a/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts index c8559799eec..dd3cbc4c5c9 100644 --- a/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts +++ b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts @@ -34,6 +34,8 @@ import { } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum AddEditFolderDialogResult { Created = "created", Deleted = "deleted", diff --git a/libs/vault/src/components/assign-collections.component.ts b/libs/vault/src/components/assign-collections.component.ts index 45d25a037e9..6a0c45cfbe3 100644 --- a/libs/vault/src/components/assign-collections.component.ts +++ b/libs/vault/src/components/assign-collections.component.ts @@ -80,6 +80,8 @@ export interface CollectionAssignmentParams { isSingleCipherAdmin?: boolean; } +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum CollectionAssignmentResult { Saved = "saved", Canceled = "canceled", diff --git a/libs/vault/src/services/vault-nudges.service.ts b/libs/vault/src/services/vault-nudges.service.ts index be6e978eaa0..d27cd09e954 100644 --- a/libs/vault/src/services/vault-nudges.service.ts +++ b/libs/vault/src/services/vault-nudges.service.ts @@ -23,6 +23,8 @@ export type NudgeStatus = { /** * Enum to list the various nudge types, to be used by components/badges to show/hide the nudge */ +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums export enum VaultNudgeType { /** Nudge to show when user has no items in their vault * Add future nudges here From 4c68f61d47a2fc8deb4300d9beec568c4a11d555 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 13 May 2025 10:58:48 -0400 Subject: [PATCH 026/163] feat(CLI-SSO-Login): [Auth/PM-21116] CLI - SSO Login - Add SSO Org Identifier option (#14605) * Add --identifier option for SSO on CLI * Add option for identifier * Moved auto-submit after the setting of client arguments * Adjusted comment * Changed to pass in as SSO option * Renamed to orgSsoIdentifier for clarity * Added more changes to orgSsoIdentifier. --- apps/cli/src/auth/commands/login.command.ts | 7 +++++- apps/cli/src/program.ts | 5 +++- libs/auth/src/angular/sso/sso.component.ts | 16 ++++++------- .../sso-redirect/sso-url.service.spec.ts | 23 +++++++++++++++++++ .../services/sso-redirect/sso-url.service.ts | 6 +++++ 5 files changed, 47 insertions(+), 10 deletions(-) diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index 3ad71c62e66..cd5c8ef9bcd 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -106,6 +106,8 @@ export class LoginCommand { return Response.badRequest("client_secret is required."); } } else if (options.sso != null && this.canInteract) { + // If the optional Org SSO Identifier isn't provided, the option value is `true`. + const orgSsoIdentifier = options.sso === true ? null : options.sso; const passwordOptions: any = { type: "password", length: 64, @@ -119,7 +121,7 @@ export class LoginCommand { const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256"); const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); try { - const ssoParams = await this.openSsoPrompt(codeChallenge, state); + const ssoParams = await this.openSsoPrompt(codeChallenge, state, orgSsoIdentifier); ssoCode = ssoParams.ssoCode; orgIdentifier = ssoParams.orgIdentifier; } catch { @@ -664,6 +666,7 @@ export class LoginCommand { private async openSsoPrompt( codeChallenge: string, state: string, + orgSsoIdentifier: string, ): Promise<{ ssoCode: string; orgIdentifier: string }> { const env = await firstValueFrom(this.environmentService.environment$); @@ -712,6 +715,8 @@ export class LoginCommand { this.ssoRedirectUri, state, codeChallenge, + null, + orgSsoIdentifier, ); this.platformUtilsService.launchUri(webAppSsoUrl); }); diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index dca4effcdc9..d85f1b366e6 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -118,7 +118,10 @@ export class Program extends BaseProgram { .description("Log into a user account.") .option("--method ", "Two-step login method.") .option("--code ", "Two-step login code.") - .option("--sso", "Log in with Single-Sign On.") + .option( + "--sso [identifier]", + "Log in with Single-Sign On with optional organization identifier.", + ) .option("--apikey", "Log in with an Api Key.") .option("--passwordenv ", "Environment variable storing your password") .option( diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts index a91a8ed20e9..968a05bf850 100644 --- a/libs/auth/src/angular/sso/sso.component.ts +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -155,7 +155,14 @@ export class SsoComponent implements OnInit { return; } - // Detect if we have landed here but only have an SSO identifier in the URL. + // Detect if we are on the first portion of the SSO flow + // and have been sent here from another client with the info in query params. + // If so, we want to initialize the SSO flow with those values. + if (this.hasParametersFromOtherClientRedirect(qParams)) { + this.initializeFromRedirectFromOtherClient(qParams); + } + + // Detect if we have landed here with an SSO identifier in the URL. // This is used by integrations that want to "short-circuit" the login to send users // directly to their IdP to simulate IdP-initiated SSO, so we submit automatically. if (qParams.identifier != null) { @@ -165,13 +172,6 @@ export class SsoComponent implements OnInit { return; } - // Detect if we are on the first portion of the SSO flow - // and have been sent here from another client with the info in query params. - // If so, we want to initialize the SSO flow with those values. - if (this.hasParametersFromOtherClientRedirect(qParams)) { - this.initializeFromRedirectFromOtherClient(qParams); - } - // Try to determine the identifier using claimed domain or local state // persisted from the user's last login attempt. await this.initializeIdentifierFromEmailOrStorage(); diff --git a/libs/auth/src/common/services/sso-redirect/sso-url.service.spec.ts b/libs/auth/src/common/services/sso-redirect/sso-url.service.spec.ts index 074c3a1e0b1..632a2812cfe 100644 --- a/libs/auth/src/common/services/sso-redirect/sso-url.service.spec.ts +++ b/libs/auth/src/common/services/sso-redirect/sso-url.service.spec.ts @@ -92,4 +92,27 @@ describe("SsoUrlService", () => { ); expect(result).toBe(expectedUrl); }); + + it("should build CLI SSO URL with Org SSO Identifier correctly", () => { + const baseUrl = "https://web-vault.bitwarden.com"; + const clientType = ClientType.Cli; + const redirectUri = "https://localhost:1000"; + const state = "abc123"; + const codeChallenge = "xyz789"; + const email = "test@bitwarden.com"; + const orgSsoIdentifier = "test-org"; + + const expectedUrl = `${baseUrl}/#/sso?clientId=cli&redirectUri=${encodeURIComponent(redirectUri)}&state=${state}&codeChallenge=${codeChallenge}&email=${encodeURIComponent(email)}&identifier=${encodeURIComponent(orgSsoIdentifier)}`; + + const result = service.buildSsoUrl( + baseUrl, + clientType, + redirectUri, + state, + codeChallenge, + email, + orgSsoIdentifier, + ); + expect(result).toBe(expectedUrl); + }); }); diff --git a/libs/auth/src/common/services/sso-redirect/sso-url.service.ts b/libs/auth/src/common/services/sso-redirect/sso-url.service.ts index 667a27ad598..b2d6231db7c 100644 --- a/libs/auth/src/common/services/sso-redirect/sso-url.service.ts +++ b/libs/auth/src/common/services/sso-redirect/sso-url.service.ts @@ -11,6 +11,7 @@ export class SsoUrlService { * @param state A state value that will be peristed through the SSO flow * @param codeChallenge A challenge value that will be used to verify the SSO code after authentication * @param email The optional email adddress of the user initiating SSO, which will be used to look up the org SSO identifier + * @param orgSsoIdentifier The optional SSO identifier of the org that is initiating SSO * @returns The URL for redirecting users to the web app SSO component */ buildSsoUrl( @@ -20,6 +21,7 @@ export class SsoUrlService { state: string, codeChallenge: string, email?: string, + orgSsoIdentifier?: string, ): string { let url = webAppUrl + @@ -36,6 +38,10 @@ export class SsoUrlService { url += "&email=" + encodeURIComponent(email); } + if (orgSsoIdentifier) { + url += "&identifier=" + encodeURIComponent(orgSsoIdentifier); + } + return url; } } From 9a7089594efac32006eacd1e013488447f1f4214 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 13 May 2025 16:59:56 +0200 Subject: [PATCH 027/163] [PM-19878] Add core-js to cli (#14696) * Add core-js to cli * Add core-js to bit-cli --- apps/cli/package.json | 1 + bitwarden_license/bit-cli/src/bw.ts | 2 ++ package-lock.json | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 82faa7d40e6..b01c96b23d1 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -71,6 +71,7 @@ "browser-hrtime": "1.1.8", "chalk": "4.1.2", "commander": "11.1.0", + "core-js": "3.40.0", "form-data": "4.0.1", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", diff --git a/bitwarden_license/bit-cli/src/bw.ts b/bitwarden_license/bit-cli/src/bw.ts index ffbc186d9e0..2e4e945b9c8 100644 --- a/bitwarden_license/bit-cli/src/bw.ts +++ b/bitwarden_license/bit-cli/src/bw.ts @@ -1,3 +1,5 @@ +import "core-js/proposals/explicit-resource-management"; + import { program } from "commander"; import { registerOssPrograms } from "@bitwarden/cli/register-oss-programs"; diff --git a/package-lock.json b/package-lock.json index 8466a4ba4c6..d1378d63ec3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -207,6 +207,7 @@ "browser-hrtime": "1.1.8", "chalk": "4.1.2", "commander": "11.1.0", + "core-js": "3.40.0", "form-data": "4.0.1", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", @@ -16960,7 +16961,6 @@ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz", "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==", "hasInstallScript": true, - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" From 0750ff38f580e85e7ec6296e3883ac7c38676349 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Tue, 13 May 2025 11:21:07 -0400 Subject: [PATCH 028/163] Route to vault page when user doesn't have access (#14633) --- apps/browser/src/_locales/en/messages.json | 3 +++ .../src/vault/guards/at-risk-passwords.guard.ts | 12 ++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 361093b12ef..e1a3234a61f 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -5321,5 +5321,8 @@ "message": "Learn more about SSH agent", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" + }, + "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/guards/at-risk-passwords.guard.ts b/apps/browser/src/vault/guards/at-risk-passwords.guard.ts index 6bcdddfde81..fc302dd6c36 100644 --- a/apps/browser/src/vault/guards/at-risk-passwords.guard.ts +++ b/apps/browser/src/vault/guards/at-risk-passwords.guard.ts @@ -1,6 +1,6 @@ import { inject } from "@angular/core"; -import { CanActivateFn } from "@angular/router"; -import { switchMap, tap } from "rxjs"; +import { CanActivateFn, Router } from "@angular/router"; +import { map, switchMap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -13,18 +13,22 @@ export const canAccessAtRiskPasswords: CanActivateFn = () => { const taskService = inject(TaskService); const toastService = inject(ToastService); const i18nService = inject(I18nService); + const router = inject(Router); return accountService.activeAccount$.pipe( filterOutNullish(), switchMap((user) => taskService.tasksEnabled$(user.id)), - tap((tasksEnabled) => { + map((tasksEnabled) => { if (!tasksEnabled) { toastService.showToast({ variant: "error", title: "", - message: i18nService.t("accessDenied"), + message: i18nService.t("noPermissionsViewPage"), }); + + return router.createUrlTree(["/tabs/vault"]); } + return true; }), ); }; From 1f72dd7710a160058cd39316ac00d97cdbfe5aad Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Tue, 13 May 2025 11:30:36 -0400 Subject: [PATCH 029/163] PM-21286 add aria label to nudge settings badge (#14750) --- apps/browser/src/_locales/en/messages.json | 3 +++ .../src/tools/popup/settings/settings-v2.component.html | 3 +++ 2 files changed, 6 insertions(+) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index e1a3234a61f..dcdfe7df4d6 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -5258,6 +5258,9 @@ "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." }, + "nudgeBadgeAria": { + "message": "1 notification" + }, "emptyVaultNudgeTitle": { "message": "Import existing passwords" }, diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.html b/apps/browser/src/tools/popup/settings/settings-v2.component.html index 22e2d9a28d0..8d31ccf8371 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.html +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html @@ -23,6 +23,7 @@ *ngIf="!isBrowserAutofillSettingOverridden && (showAutofillBadge$ | async)" bitBadge variant="notification" + [attr.aria-label]="'nudgeBadgeAria' | i18n" >1
@@ -53,6 +54,7 @@ *ngIf="!(showVaultBadge$ | async)?.hasBadgeDismissed" bitBadge variant="notification" + [attr.aria-label]="'nudgeBadgeAria' | i18n" >1 @@ -83,6 +85,7 @@ *ngIf="(downloadBitwardenNudgeStatus$ | async)?.hasBadgeDismissed === false" bitBadge variant="notification" + [attr.aria-label]="'nudgeBadgeAria' | i18n" >1 From 5fb46df3415aefced0b52f2db86c873962255448 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 13 May 2025 16:49:06 +0100 Subject: [PATCH 030/163] [PM 21106]Remove button not responsive for admin Console Remove Sponorship (#14743) * Resolve the remove button inactive * Resolve the lint error --- .../free-bitwarden-families.component.ts | 2 +- .../settings/sponsoring-org-row.component.ts | 2 +- .../src/services/jslib-services.module.ts | 2 +- libs/common/src/abstractions/api.service.ts | 1 - ...ion-sponsorship-api.service.abstraction.ts | 5 ++++ .../organization-sponsorship-api.service.ts | 23 ++++++++++++++++++- libs/common/src/services/api.service.ts | 12 ---------- 7 files changed, 30 insertions(+), 17 deletions(-) diff --git a/apps/web/src/app/billing/members/free-bitwarden-families.component.ts b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts index eb6bfdf368b..b482007e30b 100644 --- a/apps/web/src/app/billing/members/free-bitwarden-families.component.ts +++ b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts @@ -179,7 +179,7 @@ export class FreeBitwardenFamiliesComponent implements OnInit { return; } - await this.apiService.deleteRevokeSponsorship(this.organizationId); + await this.organizationSponsorshipApiService.deleteRevokeSponsorship(this.organizationId, true); this.toastService.showToast({ variant: "success", diff --git a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts index 33e6334c577..39a7531082a 100644 --- a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts +++ b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts @@ -109,7 +109,7 @@ export class SponsoringOrgRowComponent implements OnInit { return; } - await this.apiService.deleteRevokeSponsorship(this.sponsoringOrg.id); + await this.organizationSponsorshipApiService.deleteRevokeSponsorship(this.sponsoringOrg.id); this.toastService.showToast({ variant: "success", title: null, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 470115ae3f0..3ffca776034 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1079,7 +1079,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: OrganizationSponsorshipApiServiceAbstraction, useClass: OrganizationSponsorshipApiService, - deps: [ApiServiceAbstraction], + deps: [ApiServiceAbstraction, PlatformUtilsServiceAbstraction], }), safeProvider({ provide: OrganizationBillingApiServiceAbstraction, diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 1e13a3064f4..e4453359015 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -475,7 +475,6 @@ export abstract class ApiService { getSponsorshipSyncStatus: ( sponsoredOrgId: string, ) => Promise; - deleteRevokeSponsorship: (sponsoringOrganizationId: string) => Promise; deleteRemoveSponsorship: (sponsoringOrgId: string) => Promise; postPreValidateSponsorshipToken: ( sponsorshipToken: string, diff --git a/libs/common/src/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction.ts b/libs/common/src/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction.ts index 7bd6aac17bd..5e69f57ca19 100644 --- a/libs/common/src/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction.ts @@ -10,4 +10,9 @@ export abstract class OrganizationSponsorshipApiServiceAbstraction { sponsoringOrgId: string, friendlyName?: string, ): Promise; + + abstract deleteRevokeSponsorship: ( + sponsoringOrganizationId: string, + isAdminInitiated?: boolean, + ) => Promise; } diff --git a/libs/common/src/billing/services/organization/organization-sponsorship-api.service.ts b/libs/common/src/billing/services/organization/organization-sponsorship-api.service.ts index 22ab3ec86ee..99440b10dec 100644 --- a/libs/common/src/billing/services/organization/organization-sponsorship-api.service.ts +++ b/libs/common/src/billing/services/organization/organization-sponsorship-api.service.ts @@ -1,12 +1,16 @@ import { ApiService } from "../../../abstractions/api.service"; import { ListResponse } from "../../../models/response/list.response"; +import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { OrganizationSponsorshipApiServiceAbstraction } from "../../abstractions/organizations/organization-sponsorship-api.service.abstraction"; import { OrganizationSponsorshipInvitesResponse } from "../../models/response/organization-sponsorship-invites.response"; export class OrganizationSponsorshipApiService implements OrganizationSponsorshipApiServiceAbstraction { - constructor(private apiService: ApiService) {} + constructor( + private apiService: ApiService, + private platformUtilsService: PlatformUtilsService, + ) {} async getOrganizationSponsorship( sponsoredOrgId: string, ): Promise> { @@ -33,4 +37,21 @@ export class OrganizationSponsorshipApiService return await this.apiService.send("POST", url, null, true, false); } + + async deleteRevokeSponsorship( + sponsoringOrganizationId: string, + isAdminInitiated: boolean = false, + ): Promise { + const basePath = "/organization/sponsorship/"; + const hostPath = this.platformUtilsService.isSelfHost() ? "self-hosted/" : ""; + const queryParam = `?isAdminInitiated=${isAdminInitiated}`; + + return await this.apiService.send( + "DELETE", + basePath + hostPath + sponsoringOrganizationId + queryParam, + null, + true, + false, + ); + } } diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 5c4bcdedb26..639daa7c658 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -1621,18 +1621,6 @@ export class ApiService implements ApiServiceAbstraction { return new OrganizationSponsorshipSyncStatusResponse(response); } - async deleteRevokeSponsorship(sponsoringOrganizationId: string): Promise { - return await this.send( - "DELETE", - "/organization/sponsorship/" + - (this.platformUtilsService.isSelfHost() ? "self-hosted/" : "") + - sponsoringOrganizationId, - null, - true, - false, - ); - } - async deleteRemoveSponsorship(sponsoringOrgId: string): Promise { return await this.send( "DELETE", From b3df8a6c13781a41ce5b82b83b35c90c5e32e0f2 Mon Sep 17 00:00:00 2001 From: Vicki League Date: Tue, 13 May 2025 14:16:18 -0400 Subject: [PATCH 031/163] [PM-17091][PM-17043] Support system zoom in browser extension (#14435) --- .../popup/layout/popup-size.service.ts | 37 +++++++++++--- apps/browser/src/popup/app.component.ts | 3 ++ apps/browser/src/popup/scss/base.scss | 49 ++++++++++++------- 3 files changed, 63 insertions(+), 26 deletions(-) diff --git a/apps/browser/src/platform/popup/layout/popup-size.service.ts b/apps/browser/src/platform/popup/layout/popup-size.service.ts index 3ae9a633cab..69d3102d24e 100644 --- a/apps/browser/src/platform/popup/layout/popup-size.service.ts +++ b/apps/browser/src/platform/popup/layout/popup-size.service.ts @@ -50,17 +50,40 @@ export class PopupSizeService { PopupSizeService.setStyle(width); localStorage.setItem(PopupSizeService.LocalStorageKey, width); }); + } + async setHeight() { const isInChromeTab = await BrowserPopupUtils.isInTab(); + /** + * To support both browser default zoom and system default zoom, we need to take into account + * the full screen height. When system default zoom is >100%, window.innerHeight still outputs + * a height equivalent to what it would be at 100%, which can cause the extension window to + * render as too tall. So if the screen height is smaller than the max possible extension height, + * we should use that to set our extension height. Otherwise, we want to use the window.innerHeight + * to support browser zoom. + * + * This is basically a workaround for what we consider a bug with browsers reporting the wrong + * available innerHeight when system zoom is turned on. If that gets fixed, we can remove the code + * checking the screen height. + */ + const MAX_EXT_HEIGHT = 600; + const extensionInnerHeight = window.innerHeight; + // Use a 100px offset when calculating screen height to account for browser container elements + const screenAvailHeight = window.screen.availHeight - 100; + const availHeight = + screenAvailHeight < MAX_EXT_HEIGHT ? screenAvailHeight : extensionInnerHeight; + if (!BrowserPopupUtils.inPopup(window) || isInChromeTab) { - window.document.body.classList.add("body-full"); - } else if (window.innerHeight < 400) { - window.document.body.classList.add("body-xxs"); - } else if (window.innerHeight < 500) { - window.document.body.classList.add("body-xs"); - } else if (window.innerHeight < 600) { - window.document.body.classList.add("body-sm"); + window.document.documentElement.classList.add("body-full"); + } else if (availHeight < 300) { + window.document.documentElement.classList.add("body-3xs"); + } else if (availHeight < 400) { + window.document.documentElement.classList.add("body-xxs"); + } else if (availHeight < 500) { + window.document.documentElement.classList.add("body-xs"); + } else if (availHeight < 600) { + window.document.documentElement.classList.add("body-sm"); } } diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 49579f889b3..a480e1d6ba3 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -26,6 +26,7 @@ import { import { BiometricsService, BiometricStateService } from "@bitwarden/key-management"; import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-mode.service"; +import { PopupSizeService } from "../platform/popup/layout/popup-size.service"; import { initPopupClosedListener } from "../platform/services/popup-view-cache-background.service"; import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service"; @@ -71,6 +72,7 @@ export class AppComponent implements OnInit, OnDestroy { private biometricStateService: BiometricStateService, private biometricsService: BiometricsService, private deviceTrustToastService: DeviceTrustToastService, + private popupSizeService: PopupSizeService, ) { this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); } @@ -79,6 +81,7 @@ export class AppComponent implements OnInit, OnDestroy { initPopupClosedListener(); this.compactModeService.init(); + await this.popupSizeService.setHeight(); // Component states must not persist between closing and reopening the popup, otherwise they become dead objects // Clear them aggressively to make sure this doesn't occur diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss index 59893b5050d..80ada61f868 100644 --- a/apps/browser/src/popup/scss/base.scss +++ b/apps/browser/src/popup/scss/base.scss @@ -8,6 +8,34 @@ html { overflow: hidden; + min-height: 600px; + height: 100%; + + &.body-sm { + min-height: 500px; + } + + &.body-xs { + min-height: 400px; + } + + &.body-xxs { + min-height: 300px; + } + + &.body-3xs { + min-height: 240px; + } + + &.body-full { + min-height: unset; + width: 100%; + height: 100%; + + & body { + width: 100%; + } + } } html, @@ -20,9 +48,9 @@ body { body { width: 380px; - height: 600px; + height: 100%; position: relative; - min-height: 100vh; + min-height: inherit; overflow: hidden; color: $text-color; background-color: $background-color; @@ -31,23 +59,6 @@ body { color: themed("textColor"); background-color: themed("backgroundColor"); } - - &.body-sm { - height: 500px; - } - - &.body-xs { - height: 400px; - } - - &.body-xxs { - height: 300px; - } - - &.body-full { - width: 100%; - height: 100%; - } } h1, From 896c9bd583f772242accdd9a57dc12f1520b8d15 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Tue, 13 May 2025 13:59:03 -0500 Subject: [PATCH 032/163] [PM-20997] Part 1 of PM-20117 - Styling Changes (#14527) --- .../organization-layout.component.html | 1 + apps/web/src/locales/en/messages.json | 4 ++-- .../risk-insights/models/password-health.ts | 2 ++ .../services/risk-insights-report.service.ts | 2 ++ .../access-intelligence-routing.module.ts | 1 + .../all-applications.component.html | 5 ++--- .../all-applications.component.ts | 4 +++- .../app-table-row-scrollable.component.html | 4 ++++ .../risk-insights.component.html | 19 +++++++++++-------- 9 files changed, 28 insertions(+), 14 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index 4e7c2403893..24be9a16090 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -6,6 +6,7 @@ icon="bwi-filter" *ngIf="organization.useRiskInsights" [text]="'accessIntelligence' | i18n" + route="access-intelligence" > org.useRiskInsights)], diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html index c9d0ba11eee..6f8e738fdc3 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.html @@ -56,16 +56,15 @@ [formControl]="searchControl" > diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts index f53272845f2..c586882a1e0 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts @@ -26,6 +26,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { + IconButtonModule, Icons, NoItemsModule, SearchModule, @@ -53,6 +54,7 @@ import { ApplicationsLoadingComponent } from "./risk-insights-loading.component" NoItemsModule, SharedModule, AppTableRowScrollableComponent, + IconButtonModule, ], }) export class AllApplicationsComponent implements OnInit { @@ -160,7 +162,7 @@ export class AllApplicationsComponent implements OnInit { this.toastService.showToast({ variant: "success", title: "", - message: this.i18nService.t("appsMarkedAsCritical"), + message: this.i18nService.t("applicationsMarkedAsCriticalSuccess"), }); } finally { this.selectedUrls.clear(); 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 b575d40076c..ff38ab5687e 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 @@ -2,6 +2,7 @@ + {{ "application" | i18n }} {{ "atRiskPasswords" | i18n }} {{ "totalPasswords" | i18n }} @@ -30,6 +31,9 @@ > + + + -
- {{ "accessIntelligence" | i18n }} -

{{ "riskInsights" | i18n }}

-
+
{{ "reviewAtRiskPasswords" | i18n }}
-
{{ "email" | i18n }}
-
{{ "atRiskPasswords" | i18n }}
+
{{ "email" | i18n }}
+
+ {{ "atRiskPasswords" | i18n }} +
@@ -117,8 +116,12 @@ "atRiskApplicationsDescription" | i18n }}
-
{{ "application" | i18n }}
-
{{ "atRiskPasswords" | i18n }}
+
+ {{ "application" | i18n }} +
+
+ {{ "atRiskPasswords" | i18n }} +
From 1cc06fd3b9ffbb1bfbfff74f286d849212aeccd7 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Tue, 13 May 2025 14:29:23 -0500 Subject: [PATCH 033/163] [PM-20999] Styling corrections to Access Intelligence - Part 2 (#14552) --- apps/web/src/locales/en/messages.json | 15 +++++ .../app-table-row-scrollable.component.html | 2 +- .../risk-insights.component.html | 63 +++++++++++-------- 3 files changed, 54 insertions(+), 26 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b5725d4eddf..cf2174cc1db 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -140,9 +140,15 @@ "atRiskMembersDescription": { "message": "These members are logging into applications with weak, exposed, or reused passwords." }, + "atRiskMembersDescriptionNone": { + "message": "These are no members logging into applications with weak, exposed, or reused passwords." + }, "atRiskApplicationsDescription": { "message": "These applications have weak, exposed, or reused passwords." }, + "atRiskApplicationsDescriptionNone": { + "message": "These are no applications with weak, exposed, or reused passwords." + }, "atRiskMembersDescriptionWithApp": { "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", "placeholders": { @@ -152,6 +158,15 @@ } } }, + "atRiskMembersDescriptionWithAppNone": { + "message": "There are no at risk members for $APPNAME$.", + "placeholders": { + "appname": { + "content": "$1", + "example": "Salesforce" + } + } + }, "totalMembers": { "message": "Total members" }, 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 ff38ab5687e..10dbb179519 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 @@ -89,7 +89,7 @@ diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html index fea0b32e959..f759e483bd0 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html @@ -68,19 +68,24 @@ {{ - "atRiskMembersDescription" | i18n + (dataService.atRiskMemberDetails.length > 0 + ? "atRiskMembersDescription" + : "atRiskMembersDescriptionNone" + ) | i18n }} -
-
{{ "email" | i18n }}
-
- {{ "atRiskPasswords" | i18n }} -
-
- -
-
{{ member.email }}
-
{{ member.atRiskPasswordCount }}
+ +
+
{{ "email" | i18n }}
+
+ {{ "atRiskPasswords" | i18n }} +
+ +
+
{{ member.email }}
+
{{ member.atRiskPasswordCount }}
+
+
@@ -94,7 +99,10 @@
{{ - "atRiskMembersDescriptionWithApp" | i18n: dataService.appAtRiskMembers.applicationName + (dataService.appAtRiskMembers.members.length > 0 + ? "atRiskMembersDescriptionWithApp" + : "atRiskMembersDescriptionWithAppNone" + ) | i18n: dataService.appAtRiskMembers.applicationName }}
@@ -113,21 +121,26 @@ {{ - "atRiskApplicationsDescription" | i18n + (dataService.atRiskAppDetails.length > 0 + ? "atRiskApplicationsDescription" + : "atRiskApplicationsDescriptionNone" + ) | i18n }} -
-
- {{ "application" | i18n }} -
-
- {{ "atRiskPasswords" | i18n }} -
-
- -
-
{{ app.applicationName }}
-
{{ app.atRiskPasswordCount }}
+ +
+
+ {{ "application" | i18n }} +
+
+ {{ "atRiskPasswords" | i18n }} +
+ +
+
{{ app.applicationName }}
+
{{ app.atRiskPasswordCount }}
+
+
From bacd1fb999744fdc92b99ae5563333cb2336daef Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 13 May 2025 21:36:26 +0200 Subject: [PATCH 034/163] [PM-17407] [CL-665] Remove jQuery and Popper.js (#14621) Now that the last usages of ModalService is removed from the web portions we can finally remove jQuery and Popper.js. This PR also removes bootstrap js imports since it would drag in jQuery as a peer dependency. Note: Both dependencies still exists in the lockfile as they are peer dependencies of boostrap. --- .github/renovate.json5 | 3 -- apps/web/src/app/app.component.ts | 24 +---------- apps/web/src/app/core/core.module.ts | 7 ---- apps/web/src/app/core/modal.service.ts | 56 -------------------------- apps/web/src/main.ts | 4 -- bitwarden_license/bit-web/src/main.ts | 4 -- package-lock.json | 24 ++--------- package.json | 3 -- 8 files changed, 4 insertions(+), 121 deletions(-) delete mode 100644 apps/web/src/app/core/modal.service.ts diff --git a/.github/renovate.json5 b/.github/renovate.json5 index e5cd47077fb..d0066ddd7ba 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -222,7 +222,6 @@ "@types/chrome", "@types/firefox-webext-browser", "@types/glob", - "@types/jquery", "@types/lowdb", "@types/node", "@types/node-forge", @@ -330,9 +329,7 @@ "autoprefixer", "bootstrap", "chromatic", - "jquery", "ngx-toastr", - "popper.js", "react", "react-dom", "remark-gfm", diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 55e2595e0f7..b94ce004313 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -3,25 +3,20 @@ import { DOCUMENT } from "@angular/common"; import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { NavigationEnd, Router } from "@angular/router"; -import * as jq from "jquery"; +import { Router } from "@angular/router"; import { Subject, filter, firstValueFrom, map, takeUntil, timeout } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; -import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout"; -import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -29,11 +24,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { NotificationsService } from "@bitwarden/common/platform/notifications"; import { StateEventRunnerService } from "@bitwarden/common/platform/state"; -import { SyncService } from "@bitwarden/common/platform/sync"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KeyService, BiometricStateService } from "@bitwarden/key-management"; import { PolicyListService } from "./admin-console/core/policy-list.service"; @@ -69,8 +62,6 @@ export class AppComponent implements OnDestroy, OnInit { @Inject(DOCUMENT) private document: Document, private broadcasterService: BroadcasterService, private folderService: InternalFolderService, - private syncService: SyncService, - private passwordGenerationService: PasswordGenerationServiceAbstraction, private cipherService: CipherService, private authService: AuthService, private router: Router, @@ -85,17 +76,13 @@ export class AppComponent implements OnDestroy, OnInit { private notificationsService: NotificationsService, private stateService: StateService, private eventUploadService: EventUploadService, - private policyService: InternalPolicyService, protected policyListService: PolicyListService, - private keyConnectorService: KeyConnectorService, protected configService: ConfigService, private dialogService: DialogService, private biometricStateService: BiometricStateService, private stateEventRunnerService: StateEventRunnerService, private organizationService: InternalOrganizationServiceAbstraction, private accountService: AccountService, - private apiService: ApiService, - private appIdService: AppIdService, private processReloadService: ProcessReloadServiceAbstraction, private deviceTrustToastService: DeviceTrustToastService, ) { @@ -247,15 +234,6 @@ export class AppComponent implements OnDestroy, OnInit { }); }); - this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event) => { - if (event instanceof NavigationEnd) { - const modals = Array.from(document.querySelectorAll(".modal")); - for (const modal of modals) { - (jq(modal) as any).modal("hide"); - } - } - }); - this.policyListService.addPolicies([ new TwoFactorAuthenticationPolicy(), new MasterPasswordPolicy(), diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 06a91895eb8..48e884f252c 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -26,7 +26,6 @@ import { WINDOW, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; -import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service"; import { RegistrationFinishService as RegistrationFinishServiceAbstraction, LoginComponentService, @@ -136,7 +135,6 @@ import { WebStorageServiceProvider } from "../platform/web-storage-service.provi import { EventService } from "./event.service"; import { InitService } from "./init.service"; import { ENV_URLS } from "./injection-tokens"; -import { ModalService } from "./modal.service"; import { RouterService } from "./router.service"; import { WebPlatformUtilsService } from "./web-platform-utils.service"; @@ -195,11 +193,6 @@ const safeProviders: SafeProvider[] = [ useClass: WebPlatformUtilsService, useAngularDecorators: true, }), - safeProvider({ - provide: ModalServiceAbstraction, - useClass: ModalService, - useAngularDecorators: true, - }), safeProvider({ provide: FileDownloadService, useClass: WebFileDownloadService, diff --git a/apps/web/src/app/core/modal.service.ts b/apps/web/src/app/core/modal.service.ts deleted file mode 100644 index 14ea6044f36..00000000000 --- a/apps/web/src/app/core/modal.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable, Injector } from "@angular/core"; -import * as jq from "jquery"; -import { first } from "rxjs/operators"; - -import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; -import { ModalService as BaseModalService } from "@bitwarden/angular/services/modal.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; - -@Injectable() -export class ModalService extends BaseModalService { - el: any = null; - modalOpen = false; - - constructor( - injector: Injector, - private messagingService: MessagingService, - ) { - super(injector); - } - - protected setupHandlers(modalRef: ModalRef) { - modalRef.onCreated.pipe(first()).subscribe(() => { - const modals = Array.from(document.querySelectorAll(".modal")); - if (modals.length > 0) { - this.el = jq(modals[0]); - this.el.modal("show"); - - this.el.on("show.bs.modal", () => { - modalRef.show(); - this.messagingService.send("modalShow"); - }); - this.el.on("shown.bs.modal", () => { - modalRef.shown(); - this.messagingService.send("modalShown"); - if (!Utils.isMobileBrowser) { - this.el.find("*[appAutoFocus]").focus(); - } - }); - this.el.on("hide.bs.modal", () => { - this.messagingService.send("modalClose"); - }); - this.el.on("hidden.bs.modal", () => { - modalRef.closed(); - this.messagingService.send("modalClosed"); - }); - } - }); - - modalRef.onClose.pipe(first()).subscribe(() => { - if (this.el != null) { - this.el.modal("hide"); - } - }); - } -} diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index b202a170d26..572d3968f3d 100644 --- a/apps/web/src/main.ts +++ b/apps/web/src/main.ts @@ -1,10 +1,6 @@ import { enableProdMode } from "@angular/core"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; -import "bootstrap"; -import "jquery"; -import "popper.js"; - import { AppModule } from "./app/app.module"; if (process.env.NODE_ENV === "production") { diff --git a/bitwarden_license/bit-web/src/main.ts b/bitwarden_license/bit-web/src/main.ts index b202a170d26..572d3968f3d 100644 --- a/bitwarden_license/bit-web/src/main.ts +++ b/bitwarden_license/bit-web/src/main.ts @@ -1,10 +1,6 @@ import { enableProdMode } from "@angular/core"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; -import "bootstrap"; -import "jquery"; -import "popper.js"; - import { AppModule } from "./app/app.module"; if (process.env.NODE_ENV === "production") { diff --git a/package-lock.json b/package-lock.json index d1378d63ec3..ddcdd1c9df9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,6 @@ "form-data": "4.0.1", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", - "jquery": "3.7.1", "jsdom": "26.1.0", "jszip": "3.10.1", "koa": "2.16.1", @@ -62,7 +61,6 @@ "open": "8.4.2", "papaparse": "5.5.2", "patch-package": "8.0.0", - "popper.js": "1.16.1", "proper-lockfile": "4.1.2", "qrcode-parser": "2.1.3", "qrious": "4.0.2", @@ -102,7 +100,6 @@ "@types/firefox-webext-browser": "120.0.4", "@types/inquirer": "8.2.10", "@types/jest": "29.5.12", - "@types/jquery": "3.5.32", "@types/jsdom": "21.1.7", "@types/koa": "2.15.0", "@types/koa__multer": "2.0.7", @@ -11723,16 +11720,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/jquery": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.32.tgz", - "integrity": "sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/sizzle": "*" - } - }, "node_modules/@types/jsdom": { "version": "21.1.7", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", @@ -12099,13 +12086,6 @@ "@types/send": "*" } }, - "node_modules/@types/sizzle": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", - "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/sockjs": { "version": "0.3.36", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", @@ -25233,7 +25213,8 @@ "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/js-tokens": { "version": "4.0.0", @@ -31666,6 +31647,7 @@ "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" diff --git a/package.json b/package.json index 2993707313f..93361275494 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "@types/firefox-webext-browser": "120.0.4", "@types/inquirer": "8.2.10", "@types/jest": "29.5.12", - "@types/jquery": "3.5.32", "@types/jsdom": "21.1.7", "@types/koa": "2.15.0", "@types/koa__multer": "2.0.7", @@ -181,7 +180,6 @@ "form-data": "4.0.1", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", - "jquery": "3.7.1", "jsdom": "26.1.0", "jszip": "3.10.1", "koa": "2.16.1", @@ -198,7 +196,6 @@ "open": "8.4.2", "papaparse": "5.5.2", "patch-package": "8.0.0", - "popper.js": "1.16.1", "proper-lockfile": "4.1.2", "qrcode-parser": "2.1.3", "qrious": "4.0.2", From d50db0d0dded04deb261901a9672b193779d5198 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Tue, 13 May 2025 16:08:43 -0400 Subject: [PATCH 035/163] [PM-21441] Defect - Notification bar sometimes gets cut off in fill dev (#14764) * PM-21441 * revert default value --- .../overlay-notifications-content.service.ts | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts index c21aaa37dd4..a2e1d6e49a0 100644 --- a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts +++ b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts @@ -17,7 +17,7 @@ export class OverlayNotificationsContentService private notificationBarIframeElement: HTMLIFrameElement | null = null; private currentNotificationBarType: string | null = null; private removeTabFromNotificationQueueTypes = new Set(["add", "change"]); - private notificationRefreshFlag: boolean; + private notificationRefreshFlag: boolean = false; private notificationBarElementStyles: Partial = { height: "82px", width: "430px", @@ -57,6 +57,7 @@ export class OverlayNotificationsContentService void sendExtensionMessage("checkNotificationQueue"); void sendExtensionMessage("notificationRefreshFlagValue").then((notificationRefreshFlag) => { this.notificationRefreshFlag = !!notificationRefreshFlag; + this.setNotificationRefreshBarHeight(); }); } @@ -223,15 +224,31 @@ export class OverlayNotificationsContentService this.notificationBarElement.id = "bit-notification-bar"; setElementStyles(this.notificationBarElement, this.notificationBarElementStyles, true); - - if (this.notificationRefreshFlag) { - setElementStyles(this.notificationBarElement, { height: "400px", right: "0" }, true); - } + this.setNotificationRefreshBarHeight(); this.notificationBarElement.appendChild(this.notificationBarIframeElement); } } + /** + * Sets the height of the notification bar based on the value of `notificationRefreshFlag`. + * If the flag is `true`, the bar is expanded to 400px and aligned right. + * If the flag is `false`, `null`, or `undefined`, it defaults to height of 82px. + * Skips if the notification bar element has not yet been created. + * + */ + private setNotificationRefreshBarHeight() { + const isNotificationV3 = !!this.notificationRefreshFlag; + + if (!this.notificationBarElement) { + return; + } + + if (isNotificationV3) { + setElementStyles(this.notificationBarElement, { height: "400px", right: "0" }, true); + } + } + /** * Sets up the message listener for the initialization of the notification bar. * This will send the initialization data to the notification bar iframe. From 393926beece21a23037f055bb75175dd6ff718df Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Tue, 13 May 2025 16:10:33 -0400 Subject: [PATCH 036/163] PM-21605 Remove Login text from error notification (#14767) --- .../content/components/notification/confirmation/body.ts | 1 + .../components/notification/confirmation/message.ts | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts index caea38718fd..8286202b498 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts @@ -48,6 +48,7 @@ export function NotificationConfirmationBody({ ? NotificationConfirmationMessage({ buttonAria, buttonText, + error, itemName, message: confirmationMessage, messageDetails, diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts index 2bf8caecfff..8fdda593382 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts @@ -8,6 +8,7 @@ import { spacing, themes, typography } from "../../constants/styles"; export type NotificationConfirmationMessageProps = { buttonAria?: string; buttonText?: string; + error?: string; itemName?: string; message?: string; messageDetails?: string; @@ -18,6 +19,7 @@ export type NotificationConfirmationMessageProps = { export function NotificationConfirmationMessage({ buttonAria, buttonText, + error, itemName, message, messageDetails, @@ -29,7 +31,11 @@ export function NotificationConfirmationMessage({ ${message || buttonText ? html`
- ${itemName} + ${!error && itemName + ? html` + ${itemName} + ` + : nothing} Date: Tue, 13 May 2025 16:49:41 -0400 Subject: [PATCH 037/163] [PM-21395] Vault Nudges Bugs (#14737) * updates to empty vault and has items nudges --- apps/browser/src/_locales/en/messages.json | 10 +++- .../vault-v2/vault-v2.component.html | 6 ++- .../components/vault-v2/vault-v2.component.ts | 11 +++- .../spotlight/spotlight.component.html | 8 ++- .../spotlight/spotlight.component.ts | 2 +- .../empty-vault-nudge.service.ts | 5 +- .../has-items-nudge.service.ts | 50 +++++++++++++------ 7 files changed, 69 insertions(+), 23 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index dcdfe7df4d6..fa300b4253b 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -5273,8 +5273,14 @@ "hasItemsVaultNudgeTitle": { "message": "Welcome to your vault!" }, - "hasItemsVaultNudgeBody": { - "message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else" + "hasItemsVaultNudgeBodyOne": { + "message": "Autofill items for the current page" + }, + "hasItemsVaultNudgeBodyTwo": { + "message": "Favorite items for easy access" + }, + "hasItemsVaultNudgeBodyThree": { + "message": "Search your vault for something else" }, "newLoginNudgeTitle": { "message": "Save time with autofill" diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html index 894f27245b2..b46002d645e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html @@ -44,9 +44,13 @@
+
    +
  • {{ "hasItemsVaultNudgeBodyOne" | i18n }}
  • +
  • {{ "hasItemsVaultNudgeBodyTwo" | i18n }}
  • +
  • {{ "hasItemsVaultNudgeBodyThree" | i18n }}
  • +

{{ title }}

-

+

+
- + +

{{ "autofillOptions" | i18n }} @@ -38,4 +38,4 @@ - +

diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.html b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.html index 485f8f79856..7fda078b066 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.html +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.html @@ -1,4 +1,4 @@ - +

{{ getSectionHeading() }} @@ -71,4 +71,4 @@ > - +

diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html index 0dc5e3f6ac0..98cc6489bbd 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html @@ -1,4 +1,8 @@ - +

{{ "customFields" | i18n }}

@@ -116,4 +120,4 @@ - +
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 40a8954b05a..5fd3e08f22d 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 @@ -1,4 +1,4 @@ - +

{{ "itemDetails" | i18n }}

diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.html b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.html index e31be492f93..585f11c2ffe 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.html +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.html @@ -1,4 +1,4 @@ - +

{{ "loginCredentials" | i18n }} @@ -127,6 +127,6 @@ > - +

diff --git a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html index 4e1c0c5cfd9..b919ed69f0d 100644 --- a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html +++ b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.html @@ -1,4 +1,4 @@ - +

{{ "typeSshKey" | i18n }} @@ -35,4 +35,4 @@ - +

diff --git a/libs/vault/src/cipher-view/additional-options/additional-options.component.html b/libs/vault/src/cipher-view/additional-options/additional-options.component.html index cc74d4e3a68..aa6d339dcd7 100644 --- a/libs/vault/src/cipher-view/additional-options/additional-options.component.html +++ b/libs/vault/src/cipher-view/additional-options/additional-options.component.html @@ -1,4 +1,4 @@ - +

{{ "additionalOptions" | i18n }}

@@ -18,4 +18,4 @@ > - +
diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html index a794946cb89..67ded3f8358 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html +++ b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html @@ -1,4 +1,4 @@ - +

{{ "attachments" | i18n }}

@@ -21,4 +21,4 @@ - +
diff --git a/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.html b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.html index aa3e05b9aab..22049b2a72e 100644 --- a/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.html +++ b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.html @@ -1,4 +1,4 @@ - +

{{ "autofillOptions" | i18n }}

@@ -41,4 +41,4 @@ - +
diff --git a/libs/vault/src/cipher-view/card-details/card-details-view.component.html b/libs/vault/src/cipher-view/card-details/card-details-view.component.html index ff61addd7db..9d2fa45ba9e 100644 --- a/libs/vault/src/cipher-view/card-details/card-details-view.component.html +++ b/libs/vault/src/cipher-view/card-details/card-details-view.component.html @@ -1,4 +1,4 @@ - +

{{ setSectionTitle }}

@@ -93,4 +93,4 @@ > - +
diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html index 2492ed0cd81..7c60d35965f 100644 --- a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html @@ -1,4 +1,4 @@ - +

{{ "customFields" | i18n }}

@@ -115,4 +115,4 @@
- + diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.html b/libs/vault/src/cipher-view/item-details/item-details-v2.component.html index 5ba535d0436..32bf1befb66 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.html +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.html @@ -1,4 +1,4 @@ - +

{{ "itemDetails" | i18n }}

@@ -80,4 +80,4 @@ - +
diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html index dc1168b7f01..256aec34b50 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html @@ -1,4 +1,4 @@ - +

{{ "loginCredentials" | i18n }}

@@ -164,4 +164,4 @@ > - +
diff --git a/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.html b/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.html index 20390c0a285..f7c28ceb3f0 100644 --- a/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.html +++ b/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.html @@ -1,4 +1,4 @@ - +

{{ "typeSshKey" | i18n }}

@@ -66,4 +66,4 @@ > - +
diff --git a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html index d2154abd098..1b0a1f48f05 100644 --- a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html +++ b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html @@ -1,4 +1,4 @@ - +

{{ "personalDetails" | i18n }}

@@ -64,9 +64,9 @@ > - +
- +

{{ "identification" | i18n }}

@@ -153,9 +153,9 @@ > - +
- +

{{ "contactInfo" | i18n }}

@@ -212,4 +212,4 @@ > - +
From 3e0cc7ca7f9d1576c9122a66bff3e7e20de234ba Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Wed, 14 May 2025 09:26:47 -0500 Subject: [PATCH 039/163] [PM-18627] Remove customization settings popover (#14713) * chore: remove customization popover strings, refs PM-18627 * chore: delete new settings callout ts/html component, refs PM-18627 * chore: remove new customization code from vault-v2 component, refs PM-18627: :q * chore: delete vault-page service, refs PM-18627 * chore: add state migration to remove data, refs PM-18627 --- apps/browser/src/_locales/en/messages.json | 9 --- .../new-settings-callout.component.html | 29 ------- .../new-settings-callout.component.ts | 81 ------------------- .../components/vault-v2/vault-page.service.ts | 35 -------- .../vault-v2/vault-v2.component.html | 1 - .../components/vault-v2/vault-v2.component.ts | 5 -- libs/common/src/state-migrations/migrate.ts | 6 +- ...mization-options-callout-dismissed.spec.ts | 50 ++++++++++++ ...customization-options-callout-dismissed.ts | 23 ++++++ 9 files changed, 77 insertions(+), 162 deletions(-) delete mode 100644 apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html delete mode 100644 apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.ts delete mode 100644 apps/browser/src/vault/popup/components/vault-v2/vault-page.service.ts create mode 100644 libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index fa300b4253b..df35facff3c 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2208,15 +2208,6 @@ "vaultTimeoutAction1": { "message": "Timeout action" }, - "newCustomizationOptionsCalloutTitle": { - "message": "New customization options" - }, - "newCustomizationOptionsCalloutContent": { - "message": "Customize your vault experience with quick copy actions, compact mode, and more!" - }, - "newCustomizationOptionsCalloutLink": { - "message": "View all Appearance settings" - }, "lock": { "message": "Lock", "description": "Verb form: to make secure or inaccessible by" diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html b/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html deleted file mode 100644 index 6cc60eed6d5..00000000000 --- a/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html +++ /dev/null @@ -1,29 +0,0 @@ - - - -
- {{ "newCustomizationOptionsCalloutContent" | i18n }} - - {{ "newCustomizationOptionsCalloutLink" | i18n }} - -
-
-
diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.ts b/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.ts deleted file mode 100644 index 713dc21c424..00000000000 --- a/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { UserId } from "@bitwarden/common/types/guid"; -import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; -import { ButtonModule, PopoverModule } from "@bitwarden/components"; - -import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service"; -import { VaultPageService } from "../vault-page.service"; - -@Component({ - selector: "new-settings-callout", - templateUrl: "new-settings-callout.component.html", - standalone: true, - imports: [PopoverModule, JslibModule, CommonModule, ButtonModule], - providers: [VaultPageService], -}) -export class NewSettingsCalloutComponent implements OnInit, OnDestroy { - protected showNewCustomizationSettingsCallout = false; - protected activeUserId: UserId | null = null; - - constructor( - private accountService: AccountService, - private vaultProfileService: VaultProfileService, - private vaultPageService: VaultPageService, - private router: Router, - private logService: LogService, - private copyButtonService: VaultPopupCopyButtonsService, - private vaultSettingsService: VaultSettingsService, - ) {} - - async ngOnInit() { - this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - - const showQuickCopyActions = await firstValueFrom(this.copyButtonService.showQuickCopyActions$); - const clickItemsToAutofillVaultView = await firstValueFrom( - this.vaultSettingsService.clickItemsToAutofillVaultView$, - ); - - let profileCreatedDate: Date; - - try { - profileCreatedDate = await this.vaultProfileService.getProfileCreationDate(this.activeUserId); - } catch (e) { - this.logService.error("Error getting profile creation date", e); - // Default to before the cutoff date to ensure the callout is shown - profileCreatedDate = new Date("2024-12-24"); - } - - const hasCalloutBeenDismissed = await firstValueFrom( - this.vaultPageService.isCalloutDismissed(this.activeUserId), - ); - - this.showNewCustomizationSettingsCallout = - !showQuickCopyActions && - !clickItemsToAutofillVaultView && - !hasCalloutBeenDismissed && - profileCreatedDate < new Date("2024-12-25"); - } - - async goToAppearance() { - await this.router.navigate(["/appearance"]); - } - - async dismissCallout() { - if (this.activeUserId) { - await this.vaultPageService.dismissCallout(this.activeUserId); - } - } - - async ngOnDestroy() { - await this.dismissCallout(); - } -} diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-page.service.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-page.service.ts deleted file mode 100644 index a7c52ed4c51..00000000000 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-page.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { inject, Injectable } from "@angular/core"; -import { map, Observable } from "rxjs"; - -import { - BANNERS_DISMISSED_DISK, - StateProvider, - UserKeyDefinition, -} from "@bitwarden/common/platform/state"; -import { UserId } from "@bitwarden/common/types/guid"; - -export const NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY = new UserKeyDefinition( - BANNERS_DISMISSED_DISK, - "newCustomizationOptionsCalloutDismissed", - { - deserializer: (calloutDismissed) => calloutDismissed, - clearOn: [], // Do not clear dismissed callouts - }, -); - -@Injectable() -export class VaultPageService { - private stateProvider = inject(StateProvider); - - isCalloutDismissed(userId: UserId): Observable { - return this.stateProvider - .getUser(userId, NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY) - .state$.pipe(map((dismissed) => !!dismissed)); - } - - async dismissCallout(userId: UserId): Promise { - await this.stateProvider - .getUser(userId, NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY) - .update(() => true); - } -} diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html index b46002d645e..43a96fc616e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html @@ -107,5 +107,4 @@ >
- diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index 58a6ba0000b..ec4f3939204 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -56,9 +56,7 @@ import { NewItemDropdownV2Component, NewItemInitialValues, } from "./new-item-dropdown/new-item-dropdown-v2.component"; -import { NewSettingsCalloutComponent } from "./new-settings-callout/new-settings-callout.component"; import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component"; -import { VaultPageService } from "./vault-page.service"; import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "."; @@ -90,12 +88,10 @@ enum VaultState { ScrollingModule, VaultHeaderV2Component, AtRiskPasswordCalloutComponent, - NewSettingsCalloutComponent, SpotlightComponent, RouterModule, TypographyModule, ], - providers: [VaultPageService], }) export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { @ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement; @@ -152,7 +148,6 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { protected noResultsIcon = Icons.NoResults; protected VaultStateEnum = VaultState; - protected showNewCustomizationSettingsCallout = false; constructor( private vaultPopupItemsService: VaultPopupItemsService, diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index b409f52d936..bea79963b0b 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -69,12 +69,13 @@ import { MoveLastSyncDate } from "./migrations/68-move-last-sync-date"; import { MigrateIncorrectFolderKey } from "./migrations/69-migrate-incorrect-folder-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { RemoveAcBannersDismissed } from "./migrations/70-remove-ac-banner-dismissed"; +import { RemoveNewCustomizationOptionsCalloutDismissed } from "./migrations/71-remove-new-customization-options-callout-dismissed"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 70; +export const CURRENT_VERSION = 71; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -146,7 +147,8 @@ export function createMigrationBuilder() { .with(RemoveUnassignedItemsBannerDismissed, 66, 67) .with(MoveLastSyncDate, 67, 68) .with(MigrateIncorrectFolderKey, 68, 69) - .with(RemoveAcBannersDismissed, 69, CURRENT_VERSION); + .with(RemoveAcBannersDismissed, 69, 70) + .with(RemoveNewCustomizationOptionsCalloutDismissed, 70, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.spec.ts b/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.spec.ts new file mode 100644 index 00000000000..f2c83346a62 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.spec.ts @@ -0,0 +1,50 @@ +import { runMigrator } from "../migration-helper.spec"; +import { IRREVERSIBLE } from "../migrator"; + +import { RemoveNewCustomizationOptionsCalloutDismissed } from "./71-remove-new-customization-options-callout-dismissed"; + +describe("RemoveNewCustomizationOptionsCalloutDismissed", () => { + const sut = new RemoveNewCustomizationOptionsCalloutDismissed(70, 71); + + describe("migrate", () => { + it("deletes new customization options callout dismissed from all users", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + }, + user_user1_bannersDismissed_newCustomizationOptionsCalloutDismissed: true, + user_user2_bannersDismissed_newCustomizationOptionsCalloutDismissed: true, + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + }, + }); + }); + }); + + describe("rollback", () => { + it("is irreversible", async () => { + await expect(runMigrator(sut, {}, "rollback")).rejects.toThrow(IRREVERSIBLE); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.ts b/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.ts new file mode 100644 index 00000000000..7260048daf6 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.ts @@ -0,0 +1,23 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { IRREVERSIBLE, Migrator } from "../migrator"; + +export const SHOW_CALLOUT_KEY: KeyDefinitionLike = { + key: "newCustomizationOptionsCalloutDismissed", + stateDefinition: { name: "bannersDismissed" }, +}; + +export class RemoveNewCustomizationOptionsCalloutDismissed extends Migrator<70, 71> { + async migrate(helper: MigrationHelper): Promise { + await Promise.all( + (await helper.getAccounts()).map(async ({ userId }) => { + if (helper.getFromUser(userId, SHOW_CALLOUT_KEY) != null) { + await helper.removeFromUser(userId, SHOW_CALLOUT_KEY); + } + }), + ); + } + + async rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } +} From ad3121f5359900c16c172f3a33af31a7681a4647 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Wed, 14 May 2025 10:30:01 -0400 Subject: [PATCH 040/163] [PM-12423] Migrate Cipher Decryption to Use SDK (#14206) * Created mappings for client domain object to SDK * Add abstract decrypt observable * Added todo for future consideration * Added implementation to cipher service * Added adapter and unit tests * Created cipher encryption abstraction and service * Register cipher encryption service * Added tests for the cipher encryption service * changed signature * Updated feature flag name * added new function to be used for decrypting ciphers * Added new encryptedKey field * added new function to be used for decrypting ciphers * Manually set fields * Added encrypted key in attachment view * Fixed test * Updated references to use decrypt with feature flag * Added dependency * updated package.json * lint fix * fixed tests * Fixed small mapping issues * Fixed test * Added function to decrypt fido2 key value * Added function to decrypt fido2 key value and updated test * updated to use sdk function without prociding the key * updated localdata sdk type change * decrypt attachment content using sdk * Fixed dependency issues * updated package.json * Refactored service to handle getting decrypted buffer using the legacy and sdk implementations * updated services and component to use refactored version * Updated decryptCiphersWithSdk to use decryptManyLegacy for batch decryption, ensuring the SDK is only called once per batch * Fixed merge conflicts * Fixed merge conflicts * Fixed merge conflicts * Fixed lint issues * Moved getDecryptedAttachmentBuffer to cipher service * Moved getDecryptedAttachmentBuffer to cipher service * ensure CipherView properties are null instead of undefined * Fixed test * ensure AttachmentView properties are null instead of undefined * Linked ticket in comment * removed unused orgKey --- .../background/notification.background.ts | 4 +- .../autofill/popup/fido2/fido2.component.ts | 8 +- .../browser/src/background/main.background.ts | 9 + .../assign-collections.component.ts | 7 +- .../open-attachments.component.spec.ts | 1 + .../open-attachments.component.ts | 4 +- .../vault-password-history-v2.component.ts | 4 +- .../view-v2/view-v2.component.spec.ts | 1 + .../vault-v2/view-v2/view-v2.component.ts | 4 +- .../admin-console/commands/share.command.ts | 8 +- apps/cli/src/commands/edit.command.ts | 15 +- apps/cli/src/commands/get.command.ts | 4 +- .../service-container/service-container.ts | 9 + apps/cli/src/vault/create.command.ts | 8 +- .../services/desktop-autofill.service.ts | 4 +- .../encrypted-message-handler.service.ts | 4 +- .../vault/app/vault/attachments.component.ts | 3 + .../src/vault/app/vault/view.component.ts | 3 +- .../vault-item-dialog.component.ts | 8 +- .../components/collections.component.ts | 4 +- .../angular/src/components/share.component.ts | 8 +- .../src/services/jslib-services.module.ts | 10 + .../vault/components/add-edit.component.ts | 4 +- .../vault/components/attachments.component.ts | 49 ++- .../components/password-history.component.ts | 4 +- .../src/vault/components/view.component.ts | 23 +- libs/common/spec/utils.ts | 14 + libs/common/src/enums/feature-flag.enum.ts | 2 + .../fido2/fido2-authenticator.service.spec.ts | 8 + .../fido2/fido2-authenticator.service.ts | 4 +- .../abstractions/cipher-encryption.service.ts | 60 ++++ .../src/vault/abstractions/cipher.service.ts | 25 ++ .../models/api/cipher-permissions.api.ts | 17 + .../src/vault/models/data/cipher.data.ts | 2 +- .../vault/models/domain/attachment.spec.ts | 17 + .../src/vault/models/domain/attachment.ts | 18 + .../src/vault/models/domain/card.spec.ts | 17 + libs/common/src/vault/models/domain/card.ts | 18 + .../src/vault/models/domain/cipher.spec.ts | 167 ++++++++- libs/common/src/vault/models/domain/cipher.ts | 70 ++++ .../models/domain/fido2-credential.spec.ts | 39 ++ .../vault/models/domain/fido2-credential.ts | 25 ++ .../src/vault/models/domain/field.spec.ts | 24 +- libs/common/src/vault/models/domain/field.ts | 17 + .../src/vault/models/domain/identity.spec.ts | 28 ++ .../src/vault/models/domain/identity.ts | 30 ++ .../src/vault/models/domain/login-uri.spec.ts | 15 + .../src/vault/models/domain/login-uri.ts | 15 + .../src/vault/models/domain/login.spec.ts | 48 +++ libs/common/src/vault/models/domain/login.ts | 19 + .../src/vault/models/domain/password.spec.ts | 13 + .../src/vault/models/domain/password.ts | 14 + .../vault/models/domain/secure-note.spec.ts | 13 + .../src/vault/models/domain/secure-note.ts | 13 + .../src/vault/models/domain/ssh-key.spec.ts | 13 + .../common/src/vault/models/domain/ssh-key.ts | 15 + .../vault/models/view/attachment.view.spec.ts | 55 +++ .../src/vault/models/view/attachment.view.ts | 40 +++ .../common/src/vault/models/view/card.view.ts | 13 + .../src/vault/models/view/cipher.view.spec.ts | 132 ++++++- .../src/vault/models/view/cipher.view.ts | 68 +++- .../models/view/fido2-credential.view.ts | 27 ++ .../src/vault/models/view/field.view.ts | 19 + .../src/vault/models/view/identity.view.ts | 13 + .../vault/models/view/login-uri-view.spec.ts | 22 ++ .../src/vault/models/view/login-uri.view.ts | 17 + .../src/vault/models/view/login.view.spec.ts | 35 +- .../src/vault/models/view/login.view.ts | 25 ++ .../models/view/password-history.view.spec.ts | 23 ++ .../models/view/password-history.view.ts | 17 + .../src/vault/models/view/secure-note.view.ts | 13 + .../src/vault/models/view/ssh-key.view.ts | 17 + .../src/vault/services/cipher.service.spec.ts | 87 ++++- .../src/vault/services/cipher.service.ts | 89 ++++- .../default-cipher-encryption.service.spec.ts | 334 ++++++++++++++++++ .../default-cipher-encryption.service.ts | 190 ++++++++++ .../bitwarden/bitwarden-json-importer.ts | 4 +- .../individual-vault-export.service.spec.ts | 15 +- .../individual-vault-export.service.ts | 36 +- .../src/services/org-vault-export.service.ts | 9 +- .../cipher-attachments.component.spec.ts | 1 + .../cipher-attachments.component.ts | 8 +- .../services/default-cipher-form.service.ts | 14 +- .../download-attachment.component.spec.ts | 35 +- .../download-attachment.component.ts | 41 +-- 85 files changed, 2171 insertions(+), 218 deletions(-) create mode 100644 libs/common/src/vault/abstractions/cipher-encryption.service.ts create mode 100644 libs/common/src/vault/services/default-cipher-encryption.service.spec.ts create mode 100644 libs/common/src/vault/services/default-cipher-encryption.service.ts diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 52920ec67a8..a73141b7e4d 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -894,9 +894,7 @@ export default class NotificationBackground { private async getDecryptedCipherById(cipherId: string, userId: UserId) { const cipher = await this.cipherService.get(cipherId, userId); if (cipher != null && cipher.type === CipherType.Login) { - return await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, userId), - ); + return await this.cipherService.decrypt(cipher, userId); } return null; } diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.ts b/apps/browser/src/autofill/popup/fido2/fido2.component.ts index 0471d460fd5..6b7d9120195 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.ts @@ -216,9 +216,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.ciphers = await Promise.all( message.cipherIds.map(async (cipherId) => { const cipher = await this.cipherService.get(cipherId, activeUserId); - return cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + return this.cipherService.decrypt(cipher, activeUserId); }), ); @@ -237,9 +235,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.ciphers = await Promise.all( message.existingCipherIds.map(async (cipherId) => { const cipher = await this.cipherService.get(cipherId, activeUserId); - return cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + return this.cipherService.decrypt(cipher, activeUserId); }), ); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 85a9cd27c57..a724f857cd1 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -183,6 +183,7 @@ import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-st import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { InternalSendService as InternalSendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; +import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; @@ -199,6 +200,7 @@ import { DefaultCipherAuthorizationService, } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; @@ -408,6 +410,7 @@ export default class MainBackground { endUserNotificationService: EndUserNotificationService; inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; taskService: TaskService; + cipherEncryptionService: CipherEncryptionService; ipcContentScriptManagerService: IpcContentScriptManagerService; ipcService: IpcService; @@ -856,6 +859,11 @@ export default class MainBackground { this.bulkEncryptService = new FallbackBulkEncryptService(this.encryptService); + this.cipherEncryptionService = new DefaultCipherEncryptionService( + this.sdkService, + this.logService, + ); + this.cipherService = new CipherService( this.keyService, this.domainSettingsService, @@ -871,6 +879,7 @@ export default class MainBackground { this.stateProvider, this.accountService, this.logService, + this.cipherEncryptionService, ); this.folderService = new FolderService( this.keyService, diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts index 27f3b7e5e18..7052be5ea62 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts @@ -11,7 +11,6 @@ import { CollectionService } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { OrgKey, UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { @@ -66,11 +65,7 @@ export class AssignCollections { route.queryParams.pipe( switchMap(async ({ cipherId }) => { const cipherDomain = await this.cipherService.get(cipherId, userId); - const key: UserKey | OrgKey = await this.cipherService.getKeyForCipherKeyDecryption( - cipherDomain, - userId, - ); - return cipherDomain.decrypt(key); + return await this.cipherService.decrypt(cipherDomain, userId); }), ), ), diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts index 66d9096cd5c..ec5c93feb9e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts @@ -81,6 +81,7 @@ describe("OpenAttachmentsComponent", () => { useValue: { get: getCipher, getKeyForCipherKeyDecryption: () => Promise.resolve(null), + decrypt: jest.fn().mockResolvedValue(cipherView), }, }, { diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts index 1bc7e22e6d5..9189ea51313 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts @@ -81,9 +81,7 @@ export class OpenAttachmentsComponent implements OnInit { this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId); - const cipher = await cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId), - ); + const cipher = await this.cipherService.decrypt(cipherDomain, activeUserId); if (!cipher.organizationId) { this.cipherIsAPartOfFreeOrg = false; diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts index 5d315775b10..d0eef20f044 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts @@ -69,8 +69,6 @@ export class PasswordHistoryV2Component implements OnInit { const activeUserId = activeAccount.id as UserId; const cipher = await this.cipherService.get(cipherId, activeUserId); - this.cipher = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(cipher, activeUserId); } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts index 44874221a59..3222f39a162 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts @@ -82,6 +82,7 @@ describe("ViewV2Component", () => { getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}), deleteWithServer: jest.fn().mockResolvedValue(undefined), softDeleteWithServer: jest.fn().mockResolvedValue(undefined), + decrypt: jest.fn().mockResolvedValue(mockCipher), }; beforeEach(async () => { diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index a834314560b..0a71caf5aee 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -203,9 +203,7 @@ export class ViewV2Component { async getCipherData(id: string, userId: UserId) { const cipher = await this.cipherService.get(id, userId); - return await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, userId), - ); + return await this.cipherService.decrypt(cipher, userId); } async editCipher() { diff --git a/apps/cli/src/admin-console/commands/share.command.ts b/apps/cli/src/admin-console/commands/share.command.ts index 6d9e6c8b6c0..540bc2659c9 100644 --- a/apps/cli/src/admin-console/commands/share.command.ts +++ b/apps/cli/src/admin-console/commands/share.command.ts @@ -59,15 +59,11 @@ export class ShareCommand { return Response.badRequest("This item already belongs to an organization."); } - const cipherView = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + const cipherView = await this.cipherService.decrypt(cipher, activeUserId); try { await this.cipherService.shareWithServer(cipherView, organizationId, req, activeUserId); const updatedCipher = await this.cipherService.get(cipher.id, activeUserId); - const decCipher = await updatedCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), - ); + const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId); const res = new CipherResponse(decCipher); return Response.success(res); } catch (e) { diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 2d4a854135d..4dcf805661d 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -90,9 +90,7 @@ export class EditCommand { return Response.notFound(); } - let cipherView = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + let cipherView = await this.cipherService.decrypt(cipher, activeUserId); if (cipherView.isDeleted) { return Response.badRequest("You may not edit a deleted item. Use the restore command first."); } @@ -100,9 +98,7 @@ export class EditCommand { const encCipher = await this.cipherService.encrypt(cipherView, activeUserId); try { const updatedCipher = await this.cipherService.updateWithServer(encCipher); - const decCipher = await updatedCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), - ); + const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId); const res = new CipherResponse(decCipher); return Response.success(res); } catch (e) { @@ -132,12 +128,7 @@ export class EditCommand { cipher, activeUserId, ); - const decCipher = await updatedCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption( - updatedCipher, - await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)), - ), - ); + const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId); const res = new CipherResponse(decCipher); return Response.success(res); } catch (e) { diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index 1bdbd051585..c3ba6044f8a 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -116,9 +116,7 @@ export class GetCommand extends DownloadCommand { if (Utils.isGuid(id)) { const cipher = await this.cipherService.get(id, activeUserId); if (cipher != null) { - decCipher = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + decCipher = await this.cipherService.decrypt(cipher, activeUserId); } } else if (id.trim() !== "") { let ciphers = await this.cipherService.getAllDecrypted(activeUserId); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index fe2f506f229..cdf6c4bbfda 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -139,12 +139,14 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider"; import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherAuthorizationService, DefaultCipherAuthorizationService, } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; @@ -284,6 +286,7 @@ export class ServiceContainer { ssoUrlService: SsoUrlService; masterPasswordApiService: MasterPasswordApiServiceAbstraction; bulkEncryptService: FallbackBulkEncryptService; + cipherEncryptionService: CipherEncryptionService; constructor() { let p = null; @@ -679,6 +682,11 @@ export class ServiceContainer { this.accountService, ); + this.cipherEncryptionService = new DefaultCipherEncryptionService( + this.sdkService, + this.logService, + ); + this.cipherService = new CipherService( this.keyService, this.domainSettingsService, @@ -694,6 +702,7 @@ export class ServiceContainer { this.stateProvider, this.accountService, this.logService, + this.cipherEncryptionService, ); this.folderService = new FolderService( diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index 5b34d2cb507..b1536e23748 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -93,9 +93,7 @@ export class CreateCommand { const cipher = await this.cipherService.encrypt(CipherExport.toView(req), activeUserId); try { const newCipher = await this.cipherService.createWithServer(cipher); - const decCipher = await newCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(newCipher, activeUserId), - ); + const decCipher = await this.cipherService.decrypt(newCipher, activeUserId); const res = new CipherResponse(decCipher); return Response.success(res); } catch (e) { @@ -162,9 +160,7 @@ export class CreateCommand { new Uint8Array(fileBuf).buffer, activeUserId, ); - const decCipher = await updatedCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), - ); + const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId); return Response.success(new CipherResponse(decCipher)); } catch (e) { return Response.error(e); diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index e88e16c2ffc..d6dddf3b23f 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -199,9 +199,7 @@ export class DesktopAutofillService implements OnDestroy { return; } - const decrypted = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + const decrypted = await this.cipherService.decrypt(cipher, activeUserId); const fido2Credential = decrypted.login.fido2Credentials?.[0]; if (!fido2Credential) { diff --git a/apps/desktop/src/services/encrypted-message-handler.service.ts b/apps/desktop/src/services/encrypted-message-handler.service.ts index 591ff6fa8cf..37a8114c1d1 100644 --- a/apps/desktop/src/services/encrypted-message-handler.service.ts +++ b/apps/desktop/src/services/encrypted-message-handler.service.ts @@ -207,9 +207,7 @@ export class EncryptedMessageHandlerService { return { status: "failure" }; } - const cipherView = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + const cipherView = await this.cipherService.decrypt(cipher, activeUserId); cipherView.name = credentialUpdatePayload.name; cipherView.login.password = credentialUpdatePayload.password; cipherView.login.username = credentialUpdatePayload.userName; diff --git a/apps/desktop/src/vault/app/vault/attachments.component.ts b/apps/desktop/src/vault/app/vault/attachments.component.ts index ea4f49b8431..a2cea5f2722 100644 --- a/apps/desktop/src/vault/app/vault/attachments.component.ts +++ b/apps/desktop/src/vault/app/vault/attachments.component.ts @@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -33,6 +34,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { billingAccountProfileStateService: BillingAccountProfileStateService, accountService: AccountService, toastService: ToastService, + configService: ConfigService, ) { super( cipherService, @@ -49,6 +51,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { billingAccountProfileStateService, accountService, toastService, + configService, ); } } diff --git a/apps/desktop/src/vault/app/vault/view.component.ts b/apps/desktop/src/vault/app/vault/view.component.ts index e5f677cbca6..e74b07445da 100644 --- a/apps/desktop/src/vault/app/vault/view.component.ts +++ b/apps/desktop/src/vault/app/vault/view.component.ts @@ -72,7 +72,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro accountService: AccountService, toastService: ToastService, cipherAuthorizationService: CipherAuthorizationService, - private configService: ConfigService, + configService: ConfigService, ) { super( cipherService, @@ -100,6 +100,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro billingAccountProfileStateService, toastService, cipherAuthorizationService, + configService, ); } diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 10c35f861b9..aa457e97093 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -481,9 +481,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { activeUserId, ); - updatedCipherView = await updatedCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), - ); + updatedCipherView = await this.cipherService.decrypt(updatedCipher, activeUserId); } this.cipherFormComponent.patchCipher((currentCipher) => { @@ -520,9 +518,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { return; } const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - return await config.originalCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(config.originalCipher, activeUserId), - ); + return await this.cipherService.decrypt(config.originalCipher, activeUserId); } private updateTitle() { diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 5f39966468f..8ae90705f92 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -50,9 +50,7 @@ export class CollectionsComponent implements OnInit { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); this.cipherDomain = await this.loadCipher(activeUserId); this.collectionIds = this.loadCipherCollections(); - this.cipher = await this.cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId); this.collections = await this.loadCollections(); this.collections.forEach((c) => ((c as any).checked = false)); diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts index e785441b8e4..198cc7dc3a5 100644 --- a/libs/angular/src/components/share.component.ts +++ b/libs/angular/src/components/share.component.ts @@ -76,9 +76,7 @@ export class ShareComponent implements OnInit, OnDestroy { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId); - this.cipher = await cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(cipherDomain, activeUserId); } filterCollections() { @@ -105,9 +103,7 @@ export class ShareComponent implements OnInit, OnDestroy { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId); - const cipherView = await cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId), - ); + const cipherView = await this.cipherService.decrypt(cipherDomain, activeUserId); const orgs = await firstValueFrom(this.organizations$); const orgName = orgs.find((o) => o.id === this.organizationId)?.name ?? this.i18nService.t("organization"); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 3ffca776034..920d35a1017 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -263,6 +263,7 @@ import { InternalSendService, SendService as SendServiceAbstraction, } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; @@ -281,6 +282,7 @@ import { DefaultCipherAuthorizationService, } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; @@ -509,6 +511,7 @@ const safeProviders: SafeProvider[] = [ stateProvider: StateProvider, accountService: AccountServiceAbstraction, logService: LogService, + cipherEncryptionService: CipherEncryptionService, ) => new CipherService( keyService, @@ -525,6 +528,7 @@ const safeProviders: SafeProvider[] = [ stateProvider, accountService, logService, + cipherEncryptionService, ), deps: [ KeyService, @@ -541,6 +545,7 @@ const safeProviders: SafeProvider[] = [ StateProvider, AccountServiceAbstraction, LogService, + CipherEncryptionService, ], }), safeProvider({ @@ -1528,6 +1533,11 @@ const safeProviders: SafeProvider[] = [ useClass: MasterPasswordApiService, deps: [ApiServiceAbstraction, LogService], }), + safeProvider({ + provide: CipherEncryptionService, + useClass: DefaultCipherEncryptionService, + deps: [SdkService, LogService], + }), ]; @NgModule({ diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index b9defa8383d..b04adc1fdfb 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -269,9 +269,7 @@ export class AddEditComponent implements OnInit, OnDestroy { if (this.cipher == null) { if (this.editMode) { const cipher = await this.loadCipher(activeUserId); - this.cipher = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(cipher, activeUserId); // Adjust Cipher Name if Cloning if (this.cloneMode) { diff --git a/libs/angular/src/vault/components/attachments.component.ts b/libs/angular/src/vault/components/attachments.component.ts index 9e9450c587e..e4b01d3aac1 100644 --- a/libs/angular/src/vault/components/attachments.component.ts +++ b/libs/angular/src/vault/components/attachments.component.ts @@ -9,13 +9,13 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; -import { UserId } from "@bitwarden/common/types/guid"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; @@ -56,6 +56,7 @@ export class AttachmentsComponent implements OnInit { protected billingAccountProfileStateService: BillingAccountProfileStateService, protected accountService: AccountService, protected toastService: ToastService, + protected configService: ConfigService, ) {} async ngOnInit() { @@ -88,9 +89,7 @@ export class AttachmentsComponent implements OnInit { const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.formPromise = this.saveCipherAttachment(files[0], activeUserId); this.cipherDomain = await this.formPromise; - this.cipher = await this.cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId); this.toastService.showToast({ variant: "success", title: null, @@ -130,9 +129,7 @@ export class AttachmentsComponent implements OnInit { const updatedCipher = await this.deletePromises[attachment.id]; const cipher = new Cipher(updatedCipher); - this.cipher = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(cipher, activeUserId); this.toastService.showToast({ variant: "success", @@ -197,12 +194,14 @@ export class AttachmentsComponent implements OnInit { } try { - const encBuf = await EncArrayBuffer.fromResponse(response); - const key = - attachment.key != null - ? attachment.key - : await this.keyService.getOrgKey(this.cipher.organizationId); - const decBuf = await this.encryptService.decryptFileData(encBuf, key); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const decBuf = await this.cipherService.getDecryptedAttachmentBuffer( + this.cipherDomain.id as CipherId, + attachment, + response, + activeUserId, + ); + this.fileDownloadService.download({ fileName: attachment.fileName, blobData: decBuf, @@ -228,9 +227,7 @@ export class AttachmentsComponent implements OnInit { protected async init() { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); this.cipherDomain = await this.loadCipher(activeUserId); - this.cipher = await this.cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId); const canAccessPremium = await firstValueFrom( this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), @@ -276,15 +273,17 @@ export class AttachmentsComponent implements OnInit { try { // 2. Resave - const encBuf = await EncArrayBuffer.fromResponse(response); - const key = - attachment.key != null - ? attachment.key - : await this.keyService.getOrgKey(this.cipher.organizationId); - const decBuf = await this.encryptService.decryptFileData(encBuf, key); const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(getUserId), ); + + const decBuf = await this.cipherService.getDecryptedAttachmentBuffer( + this.cipherDomain.id as CipherId, + attachment, + response, + activeUserId, + ); + this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer( this.cipherDomain, attachment.fileName, @@ -292,9 +291,7 @@ export class AttachmentsComponent implements OnInit { activeUserId, admin, ); - this.cipher = await this.cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), - ); + this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId); // 3. Delete old this.deletePromises[attachment.id] = this.deleteCipherAttachment( diff --git a/libs/angular/src/vault/components/password-history.component.ts b/libs/angular/src/vault/components/password-history.component.ts index 4df9f4bd24d..acb89b82191 100644 --- a/libs/angular/src/vault/components/password-history.component.ts +++ b/libs/angular/src/vault/components/password-history.component.ts @@ -42,9 +42,7 @@ export class PasswordHistoryComponent implements OnInit { protected async init() { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const cipher = await this.cipherService.get(this.cipherId, activeUserId); - const decCipher = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + const decCipher = await this.cipherService.decrypt(cipher, activeUserId); this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory; } } diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index 9d5a8fe9e62..8915cb6b671 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -34,13 +34,13 @@ import { EventType } from "@bitwarden/common/enums"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; -import { CollectionId, UserId } from "@bitwarden/common/types/guid"; +import { CipherId, CollectionId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; @@ -137,6 +137,7 @@ export class ViewComponent implements OnDestroy, OnInit { private billingAccountProfileStateService: BillingAccountProfileStateService, protected toastService: ToastService, private cipherAuthorizationService: CipherAuthorizationService, + protected configService: ConfigService, ) {} ngOnInit() { @@ -458,19 +459,19 @@ export class ViewComponent implements OnDestroy, OnInit { } try { - const encBuf = await EncArrayBuffer.fromResponse(response); - const key = - attachment.key != null - ? attachment.key - : await this.keyService.getOrgKey(this.cipher.organizationId); - const decBuf = await this.encryptService.decryptFileData(encBuf, key); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const decBuf = await this.cipherService.getDecryptedAttachmentBuffer( + this.cipher.id as CipherId, + attachment, + response, + activeUserId, + ); + this.fileDownloadService.download({ fileName: attachment.fileName, blobData: decBuf, }); - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { + } catch { this.toastService.showToast({ variant: "error", title: null, diff --git a/libs/common/spec/utils.ts b/libs/common/spec/utils.ts index 51db65d0ce0..2b9b2567895 100644 --- a/libs/common/spec/utils.ts +++ b/libs/common/spec/utils.ts @@ -64,6 +64,20 @@ export function makeSymmetricCryptoKey( */ export const mockFromJson = (stub: any) => (stub + "_fromJSON") as any; +/** + * Use to mock a return value of a static fromSdk method. + */ +export const mockFromSdk = (stub: any) => { + if (typeof stub === "object") { + return { + ...stub, + __fromSdk: true, + }; + } + + return `${stub}_fromSdk`; +}; + /** * Tracks the emissions of the given observable. * diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index ddc75eb0d66..d349703bddf 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -57,6 +57,7 @@ export enum FeatureFlag { PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge", PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form", SecurityTasks = "security-tasks", + PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk", CipherKeyEncryption = "cipher-key-encryption", PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms", EndUserNotifications = "pm-10609-end-user-notifications", @@ -111,6 +112,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.CipherKeyEncryption]: FALSE, [FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE, [FeatureFlag.EndUserNotifications]: FALSE, + [FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE, /* Auth */ [FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE, diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts index 3ea86a1f504..5c377e1a980 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts @@ -152,6 +152,7 @@ describe("FidoAuthenticatorService", () => { id === excludedCipher.id ? ({ decrypt: () => excludedCipher } as any) : undefined, ); cipherService.getAllDecrypted.mockResolvedValue([excludedCipher]); + cipherService.decrypt.mockResolvedValue(excludedCipher); }); /** @@ -220,6 +221,7 @@ describe("FidoAuthenticatorService", () => { id === existingCipher.id ? ({ decrypt: () => existingCipher } as any) : undefined, ); cipherService.getAllDecrypted.mockResolvedValue([existingCipher]); + cipherService.decrypt.mockResolvedValue(existingCipher); }); /** @@ -306,6 +308,11 @@ describe("FidoAuthenticatorService", () => { const encryptedCipher = { ...existingCipher, reprompt: CipherRepromptType.Password }; cipherService.get.mockResolvedValue(encryptedCipher as unknown as Cipher); + cipherService.decrypt.mockResolvedValue({ + ...existingCipher, + reprompt: CipherRepromptType.Password, + } as unknown as CipherView); + const result = async () => await authenticator.makeCredential(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); @@ -347,6 +354,7 @@ describe("FidoAuthenticatorService", () => { cipherId === cipher.id ? ({ decrypt: () => cipher } as any) : undefined, ); cipherService.getAllDecrypted.mockResolvedValue([await cipher]); + cipherService.decrypt.mockResolvedValue(cipher); cipherService.encrypt.mockImplementation(async (cipher) => { cipher.login.fido2Credentials[0].credentialId = credentialId; // Replace id for testability return {} as any; diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts index 76bd19b2876..a605e466338 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts @@ -151,9 +151,7 @@ export class Fido2AuthenticatorService ); const encrypted = await this.cipherService.get(cipherId, activeUserId); - cipher = await encrypted.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(encrypted, activeUserId), - ); + cipher = await this.cipherService.decrypt(encrypted, activeUserId); if ( !userVerified && diff --git a/libs/common/src/vault/abstractions/cipher-encryption.service.ts b/libs/common/src/vault/abstractions/cipher-encryption.service.ts new file mode 100644 index 00000000000..6b2a8e8943e --- /dev/null +++ b/libs/common/src/vault/abstractions/cipher-encryption.service.ts @@ -0,0 +1,60 @@ +import { CipherListView } from "@bitwarden/sdk-internal"; + +import { UserId } from "../../types/guid"; +import { Cipher } from "../models/domain/cipher"; +import { AttachmentView } from "../models/view/attachment.view"; +import { CipherView } from "../models/view/cipher.view"; + +/** + * Service responsible for encrypting and decrypting ciphers. + */ +export abstract class CipherEncryptionService { + /** + * Decrypts a cipher using the SDK for the given userId. + * + * @param cipher The encrypted cipher object + * @param userId The user ID whose key will be used for decryption + * + * @returns A promise that resolves to the decrypted cipher view + */ + abstract decrypt(cipher: Cipher, userId: UserId): Promise; + /** + * Decrypts many ciphers using the SDK for the given userId. + * + * For bulk decryption, prefer using `decryptMany`, which returns a more efficient + * `CipherListView` object. + * + * @param ciphers The encrypted cipher objects + * @param userId The user ID whose key will be used for decryption + * + * @deprecated Use `decryptMany` for bulk decryption instead. + * + * @returns A promise that resolves to an array of decrypted cipher views + */ + abstract decryptManyLegacy(ciphers: Cipher[], userId: UserId): Promise; + /** + * Decrypts many ciphers using the SDK for the given userId. + * + * @param ciphers The encrypted cipher objects + * @param userId The user ID whose key will be used for decryption + * + * @returns A promise that resolves to an array of decrypted cipher list views + */ + abstract decryptMany(ciphers: Cipher[], userId: UserId): Promise; + /** + * Decrypts an attachment's content from a response object. + * + * @param cipher The encrypted cipher object that owns the attachment + * @param attachment The attachment view object + * @param encryptedContent The encrypted content of the attachment + * @param userId The user ID whose key will be used for decryption + * + * @returns A promise that resolves to the decrypted content + */ + abstract decryptAttachmentContent( + cipher: Cipher, + attachment: AttachmentView, + encryptedContent: Uint8Array, + userId: UserId, + ): Promise; +} diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index b12488a5e03..a67dfcef8b9 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -14,6 +14,7 @@ import { LocalData } from "../models/data/local.data"; import { Cipher } from "../models/domain/cipher"; import { Field } from "../models/domain/field"; import { CipherWithIdRequest } from "../models/request/cipher-with-id.request"; +import { AttachmentView } from "../models/view/attachment.view"; import { CipherView } from "../models/view/cipher.view"; import { FieldView } from "../models/view/field.view"; import { AddEditCipherInfo } from "../types/add-edit-cipher-info"; @@ -215,4 +216,28 @@ export abstract class CipherService implements UserKeyRotationDataProvider; abstract getNextCardCipher(userId: UserId): Promise; abstract getNextIdentityCipher(userId: UserId): Promise; + + /** + * Decrypts a cipher using either the SDK or the legacy method based on the feature flag. + * @param cipher The cipher to decrypt. + * @param userId The user ID to use for decryption. + * @returns A promise that resolves to the decrypted cipher view. + */ + abstract decrypt(cipher: Cipher, userId: UserId): Promise; + /** + * Decrypts an attachment's content from a response object. + * + * @param cipherId The ID of the cipher that owns the attachment + * @param attachment The attachment view object + * @param response The response object containing the encrypted content + * @param userId The user ID whose key will be used for decryption + * + * @returns A promise that resolves to the decrypted content + */ + abstract getDecryptedAttachmentBuffer( + cipherId: CipherId, + attachment: AttachmentView, + response: Response, + userId: UserId, + ): Promise; } diff --git a/libs/common/src/vault/models/api/cipher-permissions.api.ts b/libs/common/src/vault/models/api/cipher-permissions.api.ts index 4df7f891e26..b7341d39b1d 100644 --- a/libs/common/src/vault/models/api/cipher-permissions.api.ts +++ b/libs/common/src/vault/models/api/cipher-permissions.api.ts @@ -1,5 +1,7 @@ import { Jsonify } from "type-fest"; +import { CipherPermissions as SdkCipherPermissions } from "@bitwarden/sdk-internal"; + import { BaseResponse } from "../../../models/response/base.response"; export class CipherPermissionsApi extends BaseResponse { @@ -18,4 +20,19 @@ export class CipherPermissionsApi extends BaseResponse { static fromJSON(obj: Jsonify) { return Object.assign(new CipherPermissionsApi(), obj); } + + /** + * Converts the SDK CipherPermissionsApi to a CipherPermissionsApi. + */ + static fromSdkCipherPermissions(obj: SdkCipherPermissions): CipherPermissionsApi | undefined { + if (!obj) { + return undefined; + } + + const permissions = new CipherPermissionsApi(); + permissions.delete = obj.delete; + permissions.restore = obj.restore; + + return permissions; + } } diff --git a/libs/common/src/vault/models/data/cipher.data.ts b/libs/common/src/vault/models/data/cipher.data.ts index ee5e5b3e72b..1be70283fb3 100644 --- a/libs/common/src/vault/models/data/cipher.data.ts +++ b/libs/common/src/vault/models/data/cipher.data.ts @@ -39,7 +39,7 @@ export class CipherData { passwordHistory?: PasswordHistoryData[]; collectionIds?: string[]; creationDate: string; - deletedDate: string; + deletedDate: string | null; reprompt: CipherRepromptType; key: string; diff --git a/libs/common/src/vault/models/domain/attachment.spec.ts b/libs/common/src/vault/models/domain/attachment.spec.ts index 40d7ea7f05c..eab67320679 100644 --- a/libs/common/src/vault/models/domain/attachment.spec.ts +++ b/libs/common/src/vault/models/domain/attachment.spec.ts @@ -153,4 +153,21 @@ describe("Attachment", () => { expect(Attachment.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkAttachment", () => { + it("should map to SDK Attachment", () => { + const attachment = new Attachment(data); + + const sdkAttachment = attachment.toSdkAttachment(); + + expect(sdkAttachment).toEqual({ + id: "id", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "fileName", + key: "key", + }); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/attachment.ts b/libs/common/src/vault/models/domain/attachment.ts index 15ce06e0881..4339f31a2e1 100644 --- a/libs/common/src/vault/models/domain/attachment.ts +++ b/libs/common/src/vault/models/domain/attachment.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Attachment as SdkAttachment } from "@bitwarden/sdk-internal"; + import { Utils } from "../../../platform/misc/utils"; import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; @@ -113,4 +115,20 @@ export class Attachment extends Domain { fileName, }); } + + /** + * Maps to SDK Attachment + * + * @returns {SdkAttachment} - The SDK Attachment object + */ + toSdkAttachment(): SdkAttachment { + return { + id: this.id, + url: this.url, + size: this.size, + sizeName: this.sizeName, + fileName: this.fileName?.toJSON(), + key: this.key?.toJSON(), + }; + } } diff --git a/libs/common/src/vault/models/domain/card.spec.ts b/libs/common/src/vault/models/domain/card.spec.ts index a7011966d94..19546ddcb05 100644 --- a/libs/common/src/vault/models/domain/card.spec.ts +++ b/libs/common/src/vault/models/domain/card.spec.ts @@ -99,4 +99,21 @@ describe("Card", () => { expect(Card.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkCard", () => { + it("should map to SDK Card", () => { + const card = new Card(data); + + const sdkCard = card.toSdkCard(); + + expect(sdkCard).toEqual({ + cardholderName: "encHolder", + brand: "encBrand", + number: "encNumber", + expMonth: "encMonth", + expYear: "encYear", + code: "encCode", + }); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/card.ts b/libs/common/src/vault/models/domain/card.ts index 3d73a8f527c..43068012ef6 100644 --- a/libs/common/src/vault/models/domain/card.ts +++ b/libs/common/src/vault/models/domain/card.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Card as SdkCard } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -85,4 +87,20 @@ export class Card extends Domain { code, }); } + + /** + * Maps Card to SDK format. + * + * @returns {SdkCard} The SDK card object. + */ + toSdkCard(): SdkCard { + return { + cardholderName: this.cardholderName?.toJSON(), + brand: this.brand?.toJSON(), + number: this.number?.toJSON(), + expMonth: this.expMonth?.toJSON(), + expYear: this.expYear?.toJSON(), + code: this.code?.toJSON(), + }; + } } diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index 0ef2233120a..a889f0b969c 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -3,6 +3,12 @@ import { Jsonify } from "type-fest"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { KeyService } from "@bitwarden/key-management"; +import { + CipherType as SdkCipherType, + UriMatchType, + CipherRepromptType as SdkCipherRepromptType, + LoginLinkedIdType, +} from "@bitwarden/sdk-internal"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; @@ -12,7 +18,7 @@ import { ContainerService } from "../../../platform/services/container.service"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; import { UserId } from "../../../types/guid"; import { CipherService } from "../../abstractions/cipher.service"; -import { FieldType, SecureNoteType } from "../../enums"; +import { FieldType, LoginLinkedId, SecureNoteType } from "../../enums"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherType } from "../../enums/cipher-type"; import { CipherData } from "../../models/data/cipher.data"; @@ -770,6 +776,165 @@ describe("Cipher DTO", () => { expect(Cipher.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkCipher", () => { + it("should map to SDK Cipher", () => { + const lastUsedDate = new Date("2025-04-15T12:00:00.000Z").getTime(); + const lastLaunched = new Date("2025-04-15T12:00:00.000Z").getTime(); + + const cipherData: CipherData = { + id: "id", + organizationId: "orgId", + folderId: "folderId", + edit: true, + permissions: new CipherPermissionsApi(), + viewPassword: true, + organizationUseTotp: true, + favorite: false, + revisionDate: "2022-01-31T12:00:00.000Z", + type: CipherType.Login, + name: "EncryptedString", + notes: "EncryptedString", + creationDate: "2022-01-01T12:00:00.000Z", + deletedDate: null, + reprompt: CipherRepromptType.None, + key: "EncryptedString", + login: { + uris: [ + { + uri: "EncryptedString", + uriChecksum: "EncryptedString", + match: UriMatchStrategy.Domain, + }, + ], + username: "EncryptedString", + password: "EncryptedString", + passwordRevisionDate: "2022-01-31T12:00:00.000Z", + totp: "EncryptedString", + autofillOnPageLoad: false, + }, + passwordHistory: [ + { password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" }, + ], + attachments: [ + { + id: "a1", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + { + id: "a2", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + ], + fields: [ + { + name: "EncryptedString", + value: "EncryptedString", + type: FieldType.Linked, + linkedId: LoginLinkedId.Username, + }, + { + name: "EncryptedString", + value: "EncryptedString", + type: FieldType.Linked, + linkedId: LoginLinkedId.Password, + }, + ], + }; + + const cipher = new Cipher(cipherData, { lastUsedDate, lastLaunched }); + const sdkCipher = cipher.toSdkCipher(); + + expect(sdkCipher).toEqual({ + id: "id", + organizationId: "orgId", + folderId: "folderId", + collectionIds: [], + key: "EncryptedString", + name: "EncryptedString", + notes: "EncryptedString", + type: SdkCipherType.Login, + login: { + username: "EncryptedString", + password: "EncryptedString", + passwordRevisionDate: "2022-01-31T12:00:00.000Z", + uris: [ + { + uri: "EncryptedString", + uriChecksum: "EncryptedString", + match: UriMatchType.Domain, + }, + ], + totp: "EncryptedString", + autofillOnPageLoad: false, + fido2Credentials: undefined, + }, + identity: undefined, + card: undefined, + secureNote: undefined, + sshKey: undefined, + favorite: false, + reprompt: SdkCipherRepromptType.None, + organizationUseTotp: true, + edit: true, + permissions: new CipherPermissionsApi(), + viewPassword: true, + localData: { + lastUsedDate: "2025-04-15T12:00:00.000Z", + lastLaunched: "2025-04-15T12:00:00.000Z", + }, + attachments: [ + { + id: "a1", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + { + id: "a2", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + ], + fields: [ + { + name: "EncryptedString", + value: "EncryptedString", + type: FieldType.Linked, + linkedId: LoginLinkedIdType.Username, + }, + { + name: "EncryptedString", + value: "EncryptedString", + type: FieldType.Linked, + linkedId: LoginLinkedIdType.Password, + }, + ], + passwordHistory: [ + { + password: "EncryptedString", + lastUsedDate: "2022-01-31T12:00:00.000Z", + }, + ], + creationDate: "2022-01-01T12:00:00.000Z", + deletedDate: undefined, + revisionDate: "2022-01-31T12:00:00.000Z", + }); + }); + }); }); const mockUserId = "TestUserId" as UserId; diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index 780690217a8..f647adf198e 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Cipher as SdkCipher } from "@bitwarden/sdk-internal"; + import { Decryptable } from "../../../platform/interfaces/decryptable.interface"; import { Utils } from "../../../platform/misc/utils"; import Domain from "../../../platform/models/domain/domain-base"; @@ -330,4 +332,72 @@ export class Cipher extends Domain implements Decryptable { return domain; } + + /** + * Maps Cipher to SDK format. + * + * @returns {SdkCipher} The SDK cipher object. + */ + toSdkCipher(): SdkCipher { + const sdkCipher: SdkCipher = { + id: this.id, + organizationId: this.organizationId, + folderId: this.folderId, + collectionIds: this.collectionIds || [], + key: this.key?.toJSON(), + name: this.name.toJSON(), + notes: this.notes?.toJSON(), + type: this.type, + favorite: this.favorite, + organizationUseTotp: this.organizationUseTotp, + edit: this.edit, + permissions: this.permissions, + viewPassword: this.viewPassword, + localData: this.localData + ? { + lastUsedDate: this.localData.lastUsedDate + ? new Date(this.localData.lastUsedDate).toISOString() + : undefined, + lastLaunched: this.localData.lastLaunched + ? new Date(this.localData.lastLaunched).toISOString() + : undefined, + } + : undefined, + attachments: this.attachments?.map((a) => a.toSdkAttachment()), + fields: this.fields?.map((f) => f.toSdkField()), + passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistory()), + revisionDate: this.revisionDate?.toISOString(), + creationDate: this.creationDate?.toISOString(), + deletedDate: this.deletedDate?.toISOString(), + reprompt: this.reprompt, + // Initialize all cipher-type-specific properties as undefined + login: undefined, + identity: undefined, + card: undefined, + secureNote: undefined, + sshKey: undefined, + }; + + switch (this.type) { + case CipherType.Login: + sdkCipher.login = this.login.toSdkLogin(); + break; + case CipherType.SecureNote: + sdkCipher.secureNote = this.secureNote.toSdkSecureNote(); + break; + case CipherType.Card: + sdkCipher.card = this.card.toSdkCard(); + break; + case CipherType.Identity: + sdkCipher.identity = this.identity.toSdkIdentity(); + break; + case CipherType.SshKey: + sdkCipher.sshKey = this.sshKey.toSdkSshKey(); + break; + default: + break; + } + + return sdkCipher; + } } diff --git a/libs/common/src/vault/models/domain/fido2-credential.spec.ts b/libs/common/src/vault/models/domain/fido2-credential.spec.ts index e2cddcea3f3..bde29d0e99c 100644 --- a/libs/common/src/vault/models/domain/fido2-credential.spec.ts +++ b/libs/common/src/vault/models/domain/fido2-credential.spec.ts @@ -167,6 +167,45 @@ describe("Fido2Credential", () => { expect(Fido2Credential.fromJSON(null)).toBeNull(); }); }); + + describe("SDK Fido2Credential Mapping", () => { + it("should map to SDK Fido2Credential", () => { + const data: Fido2CredentialData = { + credentialId: "credentialId", + keyType: "public-key", + keyAlgorithm: "ECDSA", + keyCurve: "P-256", + keyValue: "keyValue", + rpId: "rpId", + userHandle: "userHandle", + userName: "userName", + counter: "2", + rpName: "rpName", + userDisplayName: "userDisplayName", + discoverable: "discoverable", + creationDate: mockDate.toISOString(), + }; + + const credential = new Fido2Credential(data); + const sdkCredential = credential.toSdkFido2Credential(); + + expect(sdkCredential).toEqual({ + credentialId: "credentialId", + keyType: "public-key", + keyAlgorithm: "ECDSA", + keyCurve: "P-256", + keyValue: "keyValue", + rpId: "rpId", + userHandle: "userHandle", + userName: "userName", + counter: "2", + rpName: "rpName", + userDisplayName: "userDisplayName", + discoverable: "discoverable", + creationDate: mockDate.toISOString(), + }); + }); + }); }); function createEncryptedEncString(s: string): EncString { diff --git a/libs/common/src/vault/models/domain/fido2-credential.ts b/libs/common/src/vault/models/domain/fido2-credential.ts index 8b0082892e4..7002a58150d 100644 --- a/libs/common/src/vault/models/domain/fido2-credential.ts +++ b/libs/common/src/vault/models/domain/fido2-credential.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Fido2Credential as SdkFido2Credential } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -148,4 +150,27 @@ export class Fido2Credential extends Domain { creationDate, }); } + + /** + * Maps Fido2Credential to SDK format. + * + * @returns {SdkFido2Credential} The SDK Fido2Credential object. + */ + toSdkFido2Credential(): SdkFido2Credential { + return { + credentialId: this.credentialId?.toJSON(), + keyType: this.keyType.toJSON(), + keyAlgorithm: this.keyAlgorithm.toJSON(), + keyCurve: this.keyCurve.toJSON(), + keyValue: this.keyValue.toJSON(), + rpId: this.rpId.toJSON(), + userHandle: this.userHandle.toJSON(), + userName: this.userName.toJSON(), + counter: this.counter.toJSON(), + rpName: this.rpName?.toJSON(), + userDisplayName: this.userDisplayName?.toJSON(), + discoverable: this.discoverable?.toJSON(), + creationDate: this.creationDate.toISOString(), + }; + } } diff --git a/libs/common/src/vault/models/domain/field.spec.ts b/libs/common/src/vault/models/domain/field.spec.ts index 7dc5556e6cf..c0f9713f7ab 100644 --- a/libs/common/src/vault/models/domain/field.spec.ts +++ b/libs/common/src/vault/models/domain/field.spec.ts @@ -1,6 +1,6 @@ import { mockEnc, mockFromJson } from "../../../../spec"; import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; -import { FieldType } from "../../enums"; +import { CardLinkedId, FieldType, IdentityLinkedId, LoginLinkedId } from "../../enums"; import { FieldData } from "../../models/data/field.data"; import { Field } from "../../models/domain/field"; @@ -82,4 +82,26 @@ describe("Field", () => { expect(Field.fromJSON(null)).toBeNull(); }); }); + + describe("SDK Field Mapping", () => { + it("should map to SDK Field", () => { + // Test Login LinkedId + const loginField = new Field(data); + loginField.type = FieldType.Linked; + loginField.linkedId = LoginLinkedId.Username; + expect(loginField.toSdkField().linkedId).toBe(100); + + // Test Card LinkedId + const cardField = new Field(data); + cardField.type = FieldType.Linked; + cardField.linkedId = CardLinkedId.Number; + expect(cardField.toSdkField().linkedId).toBe(305); + + // Test Identity LinkedId + const identityField = new Field(data); + identityField.type = FieldType.Linked; + identityField.linkedId = IdentityLinkedId.LicenseNumber; + expect(identityField.toSdkField().linkedId).toBe(415); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/field.ts b/libs/common/src/vault/models/domain/field.ts index c0f08a38bcc..223c9b39163 100644 --- a/libs/common/src/vault/models/domain/field.ts +++ b/libs/common/src/vault/models/domain/field.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Field as SdkField, LinkedIdType as SdkLinkedIdType } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -73,4 +75,19 @@ export class Field extends Domain { value, }); } + + /** + * Maps Field to SDK format. + * + * @returns {SdkField} The SDK field object. + */ + toSdkField(): SdkField { + return { + name: this.name?.toJSON(), + value: this.value?.toJSON(), + type: this.type, + // Safe type cast: client and SDK LinkedIdType enums have identical values + linkedId: this.linkedId as unknown as SdkLinkedIdType, + }; + } } diff --git a/libs/common/src/vault/models/domain/identity.spec.ts b/libs/common/src/vault/models/domain/identity.spec.ts index 3a95138998b..cf296a6ff08 100644 --- a/libs/common/src/vault/models/domain/identity.spec.ts +++ b/libs/common/src/vault/models/domain/identity.spec.ts @@ -184,4 +184,32 @@ describe("Identity", () => { expect(Identity.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkIdentity", () => { + it("returns the correct SDK Identity object", () => { + const identity = new Identity(data); + const sdkIdentity = identity.toSdkIdentity(); + + expect(sdkIdentity).toEqual({ + title: "enctitle", + firstName: "encfirstName", + middleName: "encmiddleName", + lastName: "enclastName", + address1: "encaddress1", + address2: "encaddress2", + address3: "encaddress3", + city: "enccity", + state: "encstate", + postalCode: "encpostalCode", + country: "enccountry", + company: "enccompany", + email: "encemail", + phone: "encphone", + ssn: "encssn", + username: "encusername", + passportNumber: "encpassportNumber", + licenseNumber: "enclicenseNumber", + }); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/identity.ts b/libs/common/src/vault/models/domain/identity.ts index 5d8c20ef2b3..c7756733a66 100644 --- a/libs/common/src/vault/models/domain/identity.ts +++ b/libs/common/src/vault/models/domain/identity.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Identity as SdkIdentity } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -165,4 +167,32 @@ export class Identity extends Domain { licenseNumber, }); } + + /** + * Maps Identity to SDK format. + * + * @returns {SdkIdentity} The SDK identity object. + */ + toSdkIdentity(): SdkIdentity { + return { + title: this.title?.toJSON(), + firstName: this.firstName?.toJSON(), + middleName: this.middleName?.toJSON(), + lastName: this.lastName?.toJSON(), + address1: this.address1?.toJSON(), + address2: this.address2?.toJSON(), + address3: this.address3?.toJSON(), + city: this.city?.toJSON(), + state: this.state?.toJSON(), + postalCode: this.postalCode?.toJSON(), + country: this.country?.toJSON(), + company: this.company?.toJSON(), + email: this.email?.toJSON(), + phone: this.phone?.toJSON(), + ssn: this.ssn?.toJSON(), + username: this.username?.toJSON(), + passportNumber: this.passportNumber?.toJSON(), + licenseNumber: this.licenseNumber?.toJSON(), + }; + } } diff --git a/libs/common/src/vault/models/domain/login-uri.spec.ts b/libs/common/src/vault/models/domain/login-uri.spec.ts index 6346f38f0de..a0e6b6d7dc9 100644 --- a/libs/common/src/vault/models/domain/login-uri.spec.ts +++ b/libs/common/src/vault/models/domain/login-uri.spec.ts @@ -1,6 +1,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { Jsonify } from "type-fest"; +import { UriMatchType } from "@bitwarden/sdk-internal"; + import { mockEnc, mockFromJson } from "../../../../spec"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { UriMatchStrategy } from "../../../models/domain/domain-service"; @@ -118,4 +120,17 @@ describe("LoginUri", () => { expect(LoginUri.fromJSON(null)).toBeNull(); }); }); + + describe("SDK Login Uri Mapping", () => { + it("should map to SDK login uri", () => { + const loginUri = new LoginUri(data); + const sdkLoginUri = loginUri.toSdkLoginUri(); + + expect(sdkLoginUri).toEqual({ + uri: "encUri", + uriChecksum: "encUriChecksum", + match: UriMatchType.Domain, + }); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/login-uri.ts b/libs/common/src/vault/models/domain/login-uri.ts index 883f8c9a616..b3e6fad70dd 100644 --- a/libs/common/src/vault/models/domain/login-uri.ts +++ b/libs/common/src/vault/models/domain/login-uri.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { LoginUri as SdkLoginUri } from "@bitwarden/sdk-internal"; + import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { Utils } from "../../../platform/misc/utils"; import Domain from "../../../platform/models/domain/domain-base"; @@ -87,4 +89,17 @@ export class LoginUri extends Domain { uriChecksum, }); } + + /** + * Maps LoginUri to SDK format. + * + * @returns {SdkLoginUri} The SDK login uri object. + */ + toSdkLoginUri(): SdkLoginUri { + return { + uri: this.uri.toJSON(), + uriChecksum: this.uriChecksum.toJSON(), + match: this.match, + }; + } } diff --git a/libs/common/src/vault/models/domain/login.spec.ts b/libs/common/src/vault/models/domain/login.spec.ts index 4f9e4546220..84d12e8131f 100644 --- a/libs/common/src/vault/models/domain/login.spec.ts +++ b/libs/common/src/vault/models/domain/login.spec.ts @@ -202,6 +202,54 @@ describe("Login DTO", () => { expect(Login.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkLogin", () => { + it("should map to SDK login", () => { + const data: LoginData = { + uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchStrategy.Domain }], + username: "username", + password: "password", + passwordRevisionDate: "2022-01-31T12:00:00.000Z", + totp: "123", + autofillOnPageLoad: false, + fido2Credentials: [initializeFido2Credential(new Fido2CredentialData())], + }; + const login = new Login(data); + const sdkLogin = login.toSdkLogin(); + + expect(sdkLogin).toEqual({ + username: "username", + password: "password", + passwordRevisionDate: "2022-01-31T12:00:00.000Z", + uris: [ + { + match: 0, + uri: "uri", + uriChecksum: "checksum", + }, + ], + totp: "123", + autofillOnPageLoad: false, + fido2Credentials: [ + { + credentialId: "credentialId", + keyType: "public-key", + keyAlgorithm: "ECDSA", + keyCurve: "P-256", + keyValue: "keyValue", + rpId: "rpId", + userHandle: "userHandle", + userName: "userName", + counter: "counter", + rpName: "rpName", + userDisplayName: "userDisplayName", + discoverable: "discoverable", + creationDate: "2023-01-01T12:00:00.000Z", + }, + ], + }); + }); + }); }); type Fido2CredentialLike = Fido2CredentialData | Fido2CredentialView | Fido2CredentialApi; diff --git a/libs/common/src/vault/models/domain/login.ts b/libs/common/src/vault/models/domain/login.ts index b29b42bf3de..1893212bdaa 100644 --- a/libs/common/src/vault/models/domain/login.ts +++ b/libs/common/src/vault/models/domain/login.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Login as SdkLogin } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -144,4 +146,21 @@ export class Login extends Domain { fido2Credentials, }); } + + /** + * Maps Login to SDK format. + * + * @returns {SdkLogin} The SDK login object. + */ + toSdkLogin(): SdkLogin { + return { + uris: this.uris?.map((u) => u.toSdkLoginUri()), + username: this.username?.toJSON(), + password: this.password?.toJSON(), + passwordRevisionDate: this.passwordRevisionDate?.toISOString(), + totp: this.totp?.toJSON(), + autofillOnPageLoad: this.autofillOnPageLoad, + fido2Credentials: this.fido2Credentials?.map((f) => f.toSdkFido2Credential()), + }; + } } diff --git a/libs/common/src/vault/models/domain/password.spec.ts b/libs/common/src/vault/models/domain/password.spec.ts index 614b9639e52..24163cccf36 100644 --- a/libs/common/src/vault/models/domain/password.spec.ts +++ b/libs/common/src/vault/models/domain/password.spec.ts @@ -70,4 +70,17 @@ describe("Password", () => { expect(Password.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkPasswordHistory", () => { + it("returns the correct SDK PasswordHistory object", () => { + const password = new Password(data); + + const sdkPasswordHistory = password.toSdkPasswordHistory(); + + expect(sdkPasswordHistory).toEqual({ + password: "encPassword", + lastUsedDate: new Date("2022-01-31T12:00:00.000Z").toISOString(), + }); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/password.ts b/libs/common/src/vault/models/domain/password.ts index 8573c224416..b69a61a95a9 100644 --- a/libs/common/src/vault/models/domain/password.ts +++ b/libs/common/src/vault/models/domain/password.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { PasswordHistory } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -57,4 +59,16 @@ export class Password extends Domain { lastUsedDate, }); } + + /** + * Maps Password to SDK format. + * + * @returns {PasswordHistory} The SDK password history object. + */ + toSdkPasswordHistory(): PasswordHistory { + return { + password: this.password.toJSON(), + lastUsedDate: this.lastUsedDate.toISOString(), + }; + } } diff --git a/libs/common/src/vault/models/domain/secure-note.spec.ts b/libs/common/src/vault/models/domain/secure-note.spec.ts index 719cf59f136..ff71e53238d 100644 --- a/libs/common/src/vault/models/domain/secure-note.spec.ts +++ b/libs/common/src/vault/models/domain/secure-note.spec.ts @@ -50,4 +50,17 @@ describe("SecureNote", () => { expect(SecureNote.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkSecureNote", () => { + it("returns the correct SDK SecureNote object", () => { + const secureNote = new SecureNote(); + secureNote.type = SecureNoteType.Generic; + + const sdkSecureNote = secureNote.toSdkSecureNote(); + + expect(sdkSecureNote).toEqual({ + type: 0, + }); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/secure-note.ts b/libs/common/src/vault/models/domain/secure-note.ts index 693ae38d9fb..ac7977b0e46 100644 --- a/libs/common/src/vault/models/domain/secure-note.ts +++ b/libs/common/src/vault/models/domain/secure-note.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { SecureNote as SdkSecureNote } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SecureNoteType } from "../../enums"; @@ -41,4 +43,15 @@ export class SecureNote extends Domain { return Object.assign(new SecureNote(), obj); } + + /** + * Maps Secure note to SDK format. + * + * @returns {SdkSecureNote} The SDK secure note object. + */ + toSdkSecureNote(): SdkSecureNote { + return { + type: this.type, + }; + } } diff --git a/libs/common/src/vault/models/domain/ssh-key.spec.ts b/libs/common/src/vault/models/domain/ssh-key.spec.ts index f56d738fde8..6576d1a41e9 100644 --- a/libs/common/src/vault/models/domain/ssh-key.spec.ts +++ b/libs/common/src/vault/models/domain/ssh-key.spec.ts @@ -64,4 +64,17 @@ describe("Sshkey", () => { expect(SshKey.fromJSON(null)).toBeNull(); }); }); + + describe("toSdkSshKey", () => { + it("returns the correct SDK SshKey object", () => { + const sshKey = new SshKey(data); + const sdkSshKey = sshKey.toSdkSshKey(); + + expect(sdkSshKey).toEqual({ + privateKey: "privateKey", + publicKey: "publicKey", + fingerprint: "keyFingerprint", + }); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/ssh-key.ts b/libs/common/src/vault/models/domain/ssh-key.ts index f32a1a913ca..96a1c9e58de 100644 --- a/libs/common/src/vault/models/domain/ssh-key.ts +++ b/libs/common/src/vault/models/domain/ssh-key.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { SshKey as SdkSshKey } from "@bitwarden/sdk-internal"; + import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -70,4 +72,17 @@ export class SshKey extends Domain { keyFingerprint, }); } + + /** + * Maps SSH key to SDK format. + * + * @returns {SdkSshKey} The SDK SSH key object. + */ + toSdkSshKey(): SdkSshKey { + return { + privateKey: this.privateKey.toJSON(), + publicKey: this.publicKey.toJSON(), + fingerprint: this.keyFingerprint.toJSON(), + }; + } } diff --git a/libs/common/src/vault/models/view/attachment.view.spec.ts b/libs/common/src/vault/models/view/attachment.view.spec.ts index 7cb291f2714..8ae836e1265 100644 --- a/libs/common/src/vault/models/view/attachment.view.spec.ts +++ b/libs/common/src/vault/models/view/attachment.view.spec.ts @@ -1,4 +1,7 @@ +import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal"; + import { mockFromJson } from "../../../../spec"; +import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { AttachmentView } from "./attachment.view"; @@ -15,4 +18,56 @@ describe("AttachmentView", () => { expect(actual.key).toEqual("encKeyB64_fromJSON"); }); + + describe("fromSdkAttachmentView", () => { + it("should return undefined when the input is null", () => { + const result = AttachmentView.fromSdkAttachmentView(null as unknown as any); + expect(result).toBeUndefined(); + }); + + it("should return an AttachmentView from an SdkAttachmentView", () => { + const sdkAttachmentView = { + id: "id", + url: "url", + size: "size", + sizeName: "sizeName", + fileName: "fileName", + key: "encKeyB64_fromString", + } as SdkAttachmentView; + + const result = AttachmentView.fromSdkAttachmentView(sdkAttachmentView); + + expect(result).toMatchObject({ + id: "id", + url: "url", + size: "size", + sizeName: "sizeName", + fileName: "fileName", + key: null, + encryptedKey: new EncString(sdkAttachmentView.key as string), + }); + }); + }); + + describe("toSdkAttachmentView", () => { + it("should convert AttachmentView to SdkAttachmentView", () => { + const attachmentView = new AttachmentView(); + attachmentView.id = "id"; + attachmentView.url = "url"; + attachmentView.size = "size"; + attachmentView.sizeName = "sizeName"; + attachmentView.fileName = "fileName"; + attachmentView.encryptedKey = new EncString("encKeyB64"); + + const result = attachmentView.toSdkAttachmentView(); + expect(result).toEqual({ + id: "id", + url: "url", + size: "size", + sizeName: "sizeName", + fileName: "fileName", + key: "encKeyB64", + }); + }); + }); }); diff --git a/libs/common/src/vault/models/view/attachment.view.ts b/libs/common/src/vault/models/view/attachment.view.ts index 09839ed2fef..2ef4280d97a 100644 --- a/libs/common/src/vault/models/view/attachment.view.ts +++ b/libs/common/src/vault/models/view/attachment.view.ts @@ -2,7 +2,10 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal"; + import { View } from "../../../models/view/view"; +import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { Attachment } from "../domain/attachment"; @@ -13,6 +16,10 @@ export class AttachmentView implements View { sizeName: string = null; fileName: string = null; key: SymmetricCryptoKey = null; + /** + * The SDK returns an encrypted key for the attachment. + */ + encryptedKey: EncString | undefined; constructor(a?: Attachment) { if (!a) { @@ -40,4 +47,37 @@ export class AttachmentView implements View { const key = obj.key == null ? null : SymmetricCryptoKey.fromJSON(obj.key); return Object.assign(new AttachmentView(), obj, { key: key }); } + + /** + * Converts the AttachmentView to a SDK AttachmentView. + */ + toSdkAttachmentView(): SdkAttachmentView { + return { + id: this.id, + url: this.url, + size: this.size, + sizeName: this.sizeName, + fileName: this.fileName, + key: this.encryptedKey?.toJSON(), + }; + } + + /** + * Converts the SDK AttachmentView to a AttachmentView. + */ + static fromSdkAttachmentView(obj: SdkAttachmentView): AttachmentView | undefined { + if (!obj) { + return undefined; + } + + const view = new AttachmentView(); + view.id = obj.id ?? null; + view.url = obj.url ?? null; + view.size = obj.size ?? null; + view.sizeName = obj.sizeName ?? null; + view.fileName = obj.fileName ?? null; + view.encryptedKey = new EncString(obj.key); + + return view; + } } diff --git a/libs/common/src/vault/models/view/card.view.ts b/libs/common/src/vault/models/view/card.view.ts index 9eeb4dabf4d..2adfbb39e89 100644 --- a/libs/common/src/vault/models/view/card.view.ts +++ b/libs/common/src/vault/models/view/card.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { CardView as SdkCardView } from "@bitwarden/sdk-internal"; + import { normalizeExpiryYearFormat } from "../../../autofill/utils"; import { CardLinkedId as LinkedId } from "../../enums"; import { linkedFieldOption } from "../../linked-field-option.decorator"; @@ -146,4 +148,15 @@ export class CardView extends ItemView { return null; } + + /** + * Converts an SDK CardView to a CardView. + */ + static fromSdkCardView(obj: SdkCardView): CardView | undefined { + if (obj == null) { + return undefined; + } + + return Object.assign(new CardView(), obj); + } } diff --git a/libs/common/src/vault/models/view/cipher.view.spec.ts b/libs/common/src/vault/models/view/cipher.view.spec.ts index 3ab2706d356..b9d3e42aa62 100644 --- a/libs/common/src/vault/models/view/cipher.view.spec.ts +++ b/libs/common/src/vault/models/view/cipher.view.spec.ts @@ -1,4 +1,16 @@ -import { mockFromJson } from "../../../../spec"; +import { + CipherView as SdkCipherView, + CipherType as SdkCipherType, + CipherRepromptType as SdkCipherRepromptType, + AttachmentView as SdkAttachmentView, + LoginUriView as SdkLoginUriView, + LoginView as SdkLoginView, + FieldView as SdkFieldView, + FieldType as SdkFieldType, +} from "@bitwarden/sdk-internal"; + +import { mockFromJson, mockFromSdk } from "../../../../spec"; +import { CipherRepromptType } from "../../enums"; import { CipherType } from "../../enums/cipher-type"; import { AttachmentView } from "./attachment.view"; @@ -9,6 +21,7 @@ import { IdentityView } from "./identity.view"; import { LoginView } from "./login.view"; import { PasswordHistoryView } from "./password-history.view"; import { SecureNoteView } from "./secure-note.view"; +import { SshKeyView } from "./ssh-key.view"; jest.mock("../../models/view/login.view"); jest.mock("../../models/view/attachment.view"); @@ -73,4 +86,121 @@ describe("CipherView", () => { expect(actual).toMatchObject(expected); }); }); + + describe("fromSdkCipherView", () => { + let sdkCipherView: SdkCipherView; + + beforeEach(() => { + jest.spyOn(CardView, "fromSdkCardView").mockImplementation(mockFromSdk); + jest.spyOn(IdentityView, "fromSdkIdentityView").mockImplementation(mockFromSdk); + jest.spyOn(LoginView, "fromSdkLoginView").mockImplementation(mockFromSdk); + jest.spyOn(SecureNoteView, "fromSdkSecureNoteView").mockImplementation(mockFromSdk); + jest.spyOn(SshKeyView, "fromSdkSshKeyView").mockImplementation(mockFromSdk); + jest.spyOn(AttachmentView, "fromSdkAttachmentView").mockImplementation(mockFromSdk); + jest.spyOn(FieldView, "fromSdkFieldView").mockImplementation(mockFromSdk); + + sdkCipherView = { + id: "id", + organizationId: "orgId", + folderId: "folderId", + collectionIds: ["collectionId"], + key: undefined, + name: "name", + notes: undefined, + type: SdkCipherType.Login, + favorite: true, + edit: true, + reprompt: SdkCipherRepromptType.None, + organizationUseTotp: false, + viewPassword: true, + localData: undefined, + permissions: undefined, + attachments: [{ id: "attachmentId", url: "attachmentUrl" } as SdkAttachmentView], + login: { + username: "username", + password: "password", + uris: [{ uri: "bitwarden.com" } as SdkLoginUriView], + totp: "totp", + autofillOnPageLoad: true, + } as SdkLoginView, + identity: undefined, + card: undefined, + secureNote: undefined, + sshKey: undefined, + fields: [ + { + name: "fieldName", + value: "fieldValue", + type: SdkFieldType.Linked, + linkedId: 100, + } as SdkFieldView, + ], + passwordHistory: undefined, + creationDate: "2022-01-01T12:00:00.000Z", + revisionDate: "2022-01-02T12:00:00.000Z", + deletedDate: undefined, + }; + }); + + it("returns undefined when input is null", () => { + expect(CipherView.fromSdkCipherView(null as unknown as SdkCipherView)).toBeUndefined(); + }); + + it("maps properties correctly", () => { + const result = CipherView.fromSdkCipherView(sdkCipherView); + + expect(result).toMatchObject({ + id: "id", + organizationId: "orgId", + folderId: "folderId", + collectionIds: ["collectionId"], + name: "name", + notes: null, + type: CipherType.Login, + favorite: true, + edit: true, + reprompt: CipherRepromptType.None, + organizationUseTotp: false, + viewPassword: true, + localData: undefined, + permissions: undefined, + attachments: [ + { + id: "attachmentId", + url: "attachmentUrl", + __fromSdk: true, + }, + ], + login: { + username: "username", + password: "password", + uris: [ + { + uri: "bitwarden.com", + }, + ], + totp: "totp", + autofillOnPageLoad: true, + __fromSdk: true, + }, + identity: new IdentityView(), + card: new CardView(), + secureNote: new SecureNoteView(), + sshKey: new SshKeyView(), + fields: [ + { + name: "fieldName", + value: "fieldValue", + type: SdkFieldType.Linked, + linkedId: 100, + __fromSdk: true, + }, + ], + passwordHistory: null, + creationDate: new Date("2022-01-01T12:00:00.000Z"), + revisionDate: new Date("2022-01-02T12:00:00.000Z"), + deletedDate: null, + }); + }); + }); }); diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index 7ddba9e2ed5..1f73903a5bc 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal"; + import { View } from "../../../models/view/view"; import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; @@ -110,7 +112,7 @@ export class CipherView implements View, InitializerMetadata { get hasOldAttachments(): boolean { if (this.hasAttachments) { for (let i = 0; i < this.attachments.length; i++) { - if (this.attachments[i].key == null) { + if (this.attachments[i].key == null && this.attachments[i].encryptedKey == null) { return true; } } @@ -222,4 +224,68 @@ export class CipherView implements View, InitializerMetadata { return view; } + + /** + * Creates a CipherView from the SDK CipherView. + */ + static fromSdkCipherView(obj: SdkCipherView): CipherView | undefined { + if (obj == null) { + return undefined; + } + + const cipherView = new CipherView(); + cipherView.id = obj.id ?? null; + cipherView.organizationId = obj.organizationId ?? null; + cipherView.folderId = obj.folderId ?? null; + cipherView.name = obj.name; + cipherView.notes = obj.notes ?? null; + cipherView.type = obj.type; + cipherView.favorite = obj.favorite; + cipherView.organizationUseTotp = obj.organizationUseTotp; + cipherView.permissions = CipherPermissionsApi.fromSdkCipherPermissions(obj.permissions); + cipherView.edit = obj.edit; + cipherView.viewPassword = obj.viewPassword; + cipherView.localData = obj.localData + ? { + lastUsedDate: obj.localData.lastUsedDate + ? new Date(obj.localData.lastUsedDate).getTime() + : undefined, + lastLaunched: obj.localData.lastLaunched + ? new Date(obj.localData.lastLaunched).getTime() + : undefined, + } + : undefined; + cipherView.attachments = + obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)) ?? null; + cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)) ?? null; + cipherView.passwordHistory = + obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)) ?? null; + cipherView.collectionIds = obj.collectionIds ?? null; + cipherView.revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate); + cipherView.creationDate = obj.creationDate == null ? null : new Date(obj.creationDate); + cipherView.deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate); + cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None; + + switch (obj.type) { + case CipherType.Card: + cipherView.card = CardView.fromSdkCardView(obj.card); + break; + case CipherType.Identity: + cipherView.identity = IdentityView.fromSdkIdentityView(obj.identity); + break; + case CipherType.Login: + cipherView.login = LoginView.fromSdkLoginView(obj.login); + break; + case CipherType.SecureNote: + cipherView.secureNote = SecureNoteView.fromSdkSecureNoteView(obj.secureNote); + break; + case CipherType.SshKey: + cipherView.sshKey = SshKeyView.fromSdkSshKeyView(obj.sshKey); + break; + default: + break; + } + + return cipherView; + } } diff --git a/libs/common/src/vault/models/view/fido2-credential.view.ts b/libs/common/src/vault/models/view/fido2-credential.view.ts index b364d63b8ea..bf1d324d22d 100644 --- a/libs/common/src/vault/models/view/fido2-credential.view.ts +++ b/libs/common/src/vault/models/view/fido2-credential.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { Fido2CredentialView as SdkFido2CredentialView } from "@bitwarden/sdk-internal"; + import { ItemView } from "./item.view"; export class Fido2CredentialView extends ItemView { @@ -29,4 +31,29 @@ export class Fido2CredentialView extends ItemView { creationDate, }); } + + /** + * Converts the SDK Fido2CredentialView to a Fido2CredentialView. + */ + static fromSdkFido2CredentialView(obj: SdkFido2CredentialView): Fido2CredentialView | undefined { + if (!obj) { + return undefined; + } + + const view = new Fido2CredentialView(); + view.credentialId = obj.credentialId; + view.keyType = obj.keyType as "public-key"; + view.keyAlgorithm = obj.keyAlgorithm as "ECDSA"; + view.keyCurve = obj.keyCurve as "P-256"; + view.rpId = obj.rpId; + view.userHandle = obj.userHandle; + view.userName = obj.userName; + view.counter = parseInt(obj.counter); + view.rpName = obj.rpName; + view.userDisplayName = obj.userDisplayName; + view.discoverable = obj.discoverable?.toLowerCase() === "true" ? true : false; + view.creationDate = obj.creationDate ? new Date(obj.creationDate) : null; + + return view; + } } diff --git a/libs/common/src/vault/models/view/field.view.ts b/libs/common/src/vault/models/view/field.view.ts index ef8c5113fd0..770150f8a63 100644 --- a/libs/common/src/vault/models/view/field.view.ts +++ b/libs/common/src/vault/models/view/field.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { FieldView as SdkFieldView } from "@bitwarden/sdk-internal"; + import { View } from "../../../models/view/view"; import { FieldType, LinkedIdType } from "../../enums"; import { Field } from "../domain/field"; @@ -31,4 +33,21 @@ export class FieldView implements View { static fromJSON(obj: Partial>): FieldView { return Object.assign(new FieldView(), obj); } + + /** + * Converts the SDK FieldView to a FieldView. + */ + static fromSdkFieldView(obj: SdkFieldView): FieldView | undefined { + if (!obj) { + return undefined; + } + + const view = new FieldView(); + view.name = obj.name; + view.value = obj.value; + view.type = obj.type; + view.linkedId = obj.linkedId as unknown as LinkedIdType; + + return view; + } } diff --git a/libs/common/src/vault/models/view/identity.view.ts b/libs/common/src/vault/models/view/identity.view.ts index 247e5cfec86..a75d11efd95 100644 --- a/libs/common/src/vault/models/view/identity.view.ts +++ b/libs/common/src/vault/models/view/identity.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { IdentityView as SdkIdentityView } from "@bitwarden/sdk-internal"; + import { Utils } from "../../../platform/misc/utils"; import { IdentityLinkedId as LinkedId } from "../../enums"; import { linkedFieldOption } from "../../linked-field-option.decorator"; @@ -158,4 +160,15 @@ export class IdentityView extends ItemView { static fromJSON(obj: Partial>): IdentityView { return Object.assign(new IdentityView(), obj); } + + /** + * Converts the SDK IdentityView to an IdentityView. + */ + static fromSdkIdentityView(obj: SdkIdentityView): IdentityView | undefined { + if (obj == null) { + return undefined; + } + + return Object.assign(new IdentityView(), obj); + } } diff --git a/libs/common/src/vault/models/view/login-uri-view.spec.ts b/libs/common/src/vault/models/view/login-uri-view.spec.ts index efc75096295..155d3d59f7c 100644 --- a/libs/common/src/vault/models/view/login-uri-view.spec.ts +++ b/libs/common/src/vault/models/view/login-uri-view.spec.ts @@ -1,3 +1,5 @@ +import { LoginUriView as SdkLoginUriView, UriMatchType } from "@bitwarden/sdk-internal"; + import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { Utils } from "../../../platform/misc/utils"; @@ -184,6 +186,26 @@ describe("LoginUriView", () => { }); }); }); + + describe("fromSdkLoginUriView", () => { + it("should return undefined when the input is null", () => { + const result = LoginUriView.fromSdkLoginUriView(null as unknown as SdkLoginUriView); + expect(result).toBeUndefined(); + }); + + it("should create a LoginUriView from a SdkLoginUriView", () => { + const sdkLoginUriView = { + uri: "https://example.com", + match: UriMatchType.Host, + } as SdkLoginUriView; + + const loginUriView = LoginUriView.fromSdkLoginUriView(sdkLoginUriView); + + expect(loginUriView).toBeInstanceOf(LoginUriView); + expect(loginUriView!.uri).toBe(sdkLoginUriView.uri); + expect(loginUriView!.match).toBe(sdkLoginUriView.match); + }); + }); }); function uriFactory(match: UriMatchStrategySetting, uri: string) { diff --git a/libs/common/src/vault/models/view/login-uri.view.ts b/libs/common/src/vault/models/view/login-uri.view.ts index 315adb87c75..43d47aa4a3c 100644 --- a/libs/common/src/vault/models/view/login-uri.view.ts +++ b/libs/common/src/vault/models/view/login-uri.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { LoginUriView as SdkLoginUriView } from "@bitwarden/sdk-internal"; + import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { View } from "../../../models/view/view"; import { SafeUrls } from "../../../platform/misc/safe-urls"; @@ -112,6 +114,21 @@ export class LoginUriView implements View { return Object.assign(new LoginUriView(), obj); } + /** + * Converts a LoginUriView object from the SDK to a LoginUriView object. + */ + static fromSdkLoginUriView(obj: SdkLoginUriView): LoginUriView | undefined { + if (obj == null) { + return undefined; + } + + const view = new LoginUriView(); + view.uri = obj.uri; + view.match = obj.match; + + return view; + } + matchesUri( targetUri: string, equivalentDomains: Set, diff --git a/libs/common/src/vault/models/view/login.view.spec.ts b/libs/common/src/vault/models/view/login.view.spec.ts index 728a62deb9d..57e82faf7f1 100644 --- a/libs/common/src/vault/models/view/login.view.spec.ts +++ b/libs/common/src/vault/models/view/login.view.spec.ts @@ -1,4 +1,6 @@ -import { mockFromJson } from "../../../../spec"; +import { LoginView as SdkLoginView } from "@bitwarden/sdk-internal"; + +import { mockFromJson, mockFromSdk } from "../../../../spec"; import { LoginUriView } from "./login-uri.view"; import { LoginView } from "./login.view"; @@ -25,4 +27,35 @@ describe("LoginView", () => { uris: ["uri1_fromJSON", "uri2_fromJSON", "uri3_fromJSON"], }); }); + + describe("fromSdkLoginView", () => { + it("should return undefined when the input is null", () => { + const result = LoginView.fromSdkLoginView(null as unknown as SdkLoginView); + expect(result).toBeUndefined(); + }); + + it("should return a LoginView from an SdkLoginView", () => { + jest.spyOn(LoginUriView, "fromSdkLoginUriView").mockImplementation(mockFromSdk); + + const sdkLoginView = { + username: "username", + password: "password", + passwordRevisionDate: "2025-01-01T01:06:40.441Z", + uris: [{ uri: "bitwarden.com" } as any], + totp: "totp", + autofillOnPageLoad: true, + } as SdkLoginView; + + const result = LoginView.fromSdkLoginView(sdkLoginView); + + expect(result).toMatchObject({ + username: "username", + password: "password", + passwordRevisionDate: new Date("2025-01-01T01:06:40.441Z"), + uris: [expect.objectContaining({ uri: "bitwarden.com", __fromSdk: true })], + totp: "totp", + autofillOnPageLoad: true, + }); + }); + }); }); diff --git a/libs/common/src/vault/models/view/login.view.ts b/libs/common/src/vault/models/view/login.view.ts index 228f3a60c34..41568f643d5 100644 --- a/libs/common/src/vault/models/view/login.view.ts +++ b/libs/common/src/vault/models/view/login.view.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { LoginView as SdkLoginView } from "@bitwarden/sdk-internal"; + import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { Utils } from "../../../platform/misc/utils"; import { DeepJsonify } from "../../../types/deep-jsonify"; @@ -100,4 +102,27 @@ export class LoginView extends ItemView { fido2Credentials, }); } + + /** + * Converts the SDK LoginView to a LoginView. + * + * Note: FIDO2 credentials remain encrypted at this stage. + * Unlike other fields that are decrypted as part of the LoginView, the SDK maintains + * the FIDO2 credentials in encrypted form. We can decrypt them later using a separate + * call to client.vault().ciphers().decrypt_fido2_credentials(). + */ + static fromSdkLoginView(obj: SdkLoginView): LoginView | undefined { + if (obj == null) { + return undefined; + } + + const passwordRevisionDate = + obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); + const uris = obj.uris?.map((uri) => LoginUriView.fromSdkLoginUriView(uri)); + + return Object.assign(new LoginView(), obj, { + passwordRevisionDate, + uris, + }); + } } diff --git a/libs/common/src/vault/models/view/password-history.view.spec.ts b/libs/common/src/vault/models/view/password-history.view.spec.ts index 7349e44454d..81894ec7493 100644 --- a/libs/common/src/vault/models/view/password-history.view.spec.ts +++ b/libs/common/src/vault/models/view/password-history.view.spec.ts @@ -1,3 +1,5 @@ +import { PasswordHistoryView as SdkPasswordHistoryView } from "@bitwarden/sdk-internal"; + import { PasswordHistoryView } from "./password-history.view"; describe("PasswordHistoryView", () => { @@ -10,4 +12,25 @@ describe("PasswordHistoryView", () => { expect(actual.lastUsedDate).toEqual(lastUsedDate); }); + + describe("fromSdkPasswordHistoryView", () => { + it("should return undefined when the input is null", () => { + const result = PasswordHistoryView.fromSdkPasswordHistoryView(null as unknown as any); + expect(result).toBeUndefined(); + }); + + it("should return a PasswordHistoryView from an SdkPasswordHistoryView", () => { + const sdkPasswordHistoryView = { + password: "password", + lastUsedDate: "2023-10-01T00:00:00Z", + } as SdkPasswordHistoryView; + + const result = PasswordHistoryView.fromSdkPasswordHistoryView(sdkPasswordHistoryView); + + expect(result).toMatchObject({ + password: "password", + lastUsedDate: new Date("2023-10-01T00:00:00Z"), + }); + }); + }); }); diff --git a/libs/common/src/vault/models/view/password-history.view.ts b/libs/common/src/vault/models/view/password-history.view.ts index 3ab360d5e09..31f05f4cc71 100644 --- a/libs/common/src/vault/models/view/password-history.view.ts +++ b/libs/common/src/vault/models/view/password-history.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { PasswordHistoryView as SdkPasswordHistoryView } from "@bitwarden/sdk-internal"; + import { View } from "../../../models/view/view"; import { Password } from "../domain/password"; @@ -24,4 +26,19 @@ export class PasswordHistoryView implements View { lastUsedDate: lastUsedDate, }); } + + /** + * Converts the SDK PasswordHistoryView to a PasswordHistoryView. + */ + static fromSdkPasswordHistoryView(obj: SdkPasswordHistoryView): PasswordHistoryView | undefined { + if (!obj) { + return undefined; + } + + const view = new PasswordHistoryView(); + view.password = obj.password; + view.lastUsedDate = obj.lastUsedDate == null ? null : new Date(obj.lastUsedDate); + + return view; + } } diff --git a/libs/common/src/vault/models/view/secure-note.view.ts b/libs/common/src/vault/models/view/secure-note.view.ts index c7dd4f8932d..075e4dfc520 100644 --- a/libs/common/src/vault/models/view/secure-note.view.ts +++ b/libs/common/src/vault/models/view/secure-note.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { SecureNoteView as SdkSecureNoteView } from "@bitwarden/sdk-internal"; + import { SecureNoteType } from "../../enums"; import { SecureNote } from "../domain/secure-note"; @@ -26,4 +28,15 @@ export class SecureNoteView extends ItemView { static fromJSON(obj: Partial>): SecureNoteView { return Object.assign(new SecureNoteView(), obj); } + + /** + * Converts the SDK SecureNoteView to a SecureNoteView. + */ + static fromSdkSecureNoteView(obj: SdkSecureNoteView): SecureNoteView | undefined { + if (!obj) { + return undefined; + } + + return Object.assign(new SecureNoteView(), obj); + } } diff --git a/libs/common/src/vault/models/view/ssh-key.view.ts b/libs/common/src/vault/models/view/ssh-key.view.ts index 8f1a9c5a65a..a3d091e4c07 100644 --- a/libs/common/src/vault/models/view/ssh-key.view.ts +++ b/libs/common/src/vault/models/view/ssh-key.view.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { SshKeyView as SdkSshKeyView } from "@bitwarden/sdk-internal"; + import { SshKey } from "../domain/ssh-key"; import { ItemView } from "./item.view"; @@ -44,4 +46,19 @@ export class SshKeyView extends ItemView { static fromJSON(obj: Partial>): SshKeyView { return Object.assign(new SshKeyView(), obj); } + + /** + * Converts the SDK SshKeyView to a SshKeyView. + */ + static fromSdkSshKeyView(obj: SdkSshKeyView): SshKeyView | undefined { + if (!obj) { + return undefined; + } + + const keyFingerprint = obj.fingerprint; + + return Object.assign(new SshKeyView(), obj, { + keyFingerprint, + }); + } } diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index a8b37e8adc6..b15bc4a9112 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -6,7 +6,7 @@ import { CipherDecryptionKeys, KeyService } from "@bitwarden/key-management"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; -import { makeStaticByteArray } from "../../../spec/utils"; +import { makeStaticByteArray, makeSymmetricCryptoKey } from "../../../spec/utils"; import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service"; @@ -24,6 +24,7 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt import { ContainerService } from "../../platform/services/container.service"; import { CipherId, UserId } from "../../types/guid"; import { CipherKey, OrgKey, UserKey } from "../../types/key"; +import { CipherEncryptionService } from "../abstractions/cipher-encryption.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { FieldType } from "../enums"; import { CipherRepromptType } from "../enums/cipher-reprompt-type"; @@ -34,6 +35,7 @@ import { Cipher } from "../models/domain/cipher"; import { CipherCreateRequest } from "../models/request/cipher-create.request"; import { CipherPartialRequest } from "../models/request/cipher-partial.request"; import { CipherRequest } from "../models/request/cipher.request"; +import { AttachmentView } from "../models/view/attachment.view"; import { CipherView } from "../models/view/cipher.view"; import { LoginUriView } from "../models/view/login-uri.view"; @@ -124,6 +126,7 @@ describe("Cipher Service", () => { accountService = mockAccountServiceWith(mockUserId); const logService = mock(); const stateProvider = new FakeStateProvider(accountService); + const cipherEncryptionService = mock(); const userId = "TestUserId" as UserId; @@ -151,6 +154,7 @@ describe("Cipher Service", () => { stateProvider, accountService, logService, + cipherEncryptionService, ); cipherObj = new Cipher(cipherData); @@ -478,4 +482,85 @@ describe("Cipher Service", () => { ).rejects.toThrow("Cannot rotate ciphers when decryption failures are present"); }); }); + + describe("decrypt", () => { + it("should call decrypt method of CipherEncryptionService when feature flag is true", async () => { + configService.getFeatureFlag.mockResolvedValue(true); + cipherEncryptionService.decrypt.mockResolvedValue(new CipherView(cipherObj)); + + const result = await cipherService.decrypt(cipherObj, userId); + + expect(result).toEqual(new CipherView(cipherObj)); + expect(cipherEncryptionService.decrypt).toHaveBeenCalledWith(cipherObj, userId); + }); + + it("should call legacy decrypt when feature flag is false", async () => { + const mockUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey; + configService.getFeatureFlag.mockResolvedValue(false); + cipherService.getKeyForCipherKeyDecryption = jest.fn().mockResolvedValue(mockUserKey); + encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); + jest.spyOn(cipherObj, "decrypt").mockResolvedValue(new CipherView(cipherObj)); + + const result = await cipherService.decrypt(cipherObj, userId); + + expect(result).toEqual(new CipherView(cipherObj)); + expect(cipherObj.decrypt).toHaveBeenCalledWith(mockUserKey); + }); + }); + + describe("getDecryptedAttachmentBuffer", () => { + const mockEncryptedContent = new Uint8Array([1, 2, 3]); + const mockDecryptedContent = new Uint8Array([4, 5, 6]); + + it("should use SDK when feature flag is enabled", async () => { + const cipher = new Cipher(cipherData); + const attachment = new AttachmentView(cipher.attachments![0]); + configService.getFeatureFlag.mockResolvedValue(true); + + jest.spyOn(cipherService, "ciphers$").mockReturnValue(of({ [cipher.id]: cipherData })); + cipherEncryptionService.decryptAttachmentContent.mockResolvedValue(mockDecryptedContent); + const mockResponse = { + arrayBuffer: jest.fn().mockResolvedValue(mockEncryptedContent.buffer), + } as unknown as Response; + + const result = await cipherService.getDecryptedAttachmentBuffer( + cipher.id as CipherId, + attachment, + mockResponse, + userId, + ); + + expect(result).toEqual(mockDecryptedContent); + expect(cipherEncryptionService.decryptAttachmentContent).toHaveBeenCalledWith( + cipher, + attachment, + mockEncryptedContent, + userId, + ); + }); + + it("should use legacy decryption when feature flag is enabled", async () => { + configService.getFeatureFlag.mockResolvedValue(false); + const cipher = new Cipher(cipherData); + const attachment = new AttachmentView(cipher.attachments![0]); + attachment.key = makeSymmetricCryptoKey(64); + + const mockResponse = { + arrayBuffer: jest.fn().mockResolvedValue(mockEncryptedContent.buffer), + } as unknown as Response; + const mockEncBuf = {} as EncArrayBuffer; + EncArrayBuffer.fromResponse = jest.fn().mockResolvedValue(mockEncBuf); + encryptService.decryptFileData.mockResolvedValue(mockDecryptedContent); + + const result = await cipherService.getDecryptedAttachmentBuffer( + cipher.id as CipherId, + attachment, + mockResponse, + userId, + ); + + expect(result).toEqual(mockDecryptedContent); + expect(encryptService.decryptFileData).toHaveBeenCalledWith(mockEncBuf, attachment.key); + }); + }); }); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 169568d44e9..6bea56baa5e 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -29,7 +29,8 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt import { StateProvider } from "../../platform/state"; import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid"; import { OrgKey, UserKey } from "../../types/key"; -import { perUserCache$ } from "../../vault/utils/observable-utilities"; +import { filterOutNullish, perUserCache$ } from "../../vault/utils/observable-utilities"; +import { CipherEncryptionService } from "../abstractions/cipher-encryption.service"; import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { FieldType } from "../enums"; @@ -103,6 +104,7 @@ export class CipherService implements CipherServiceAbstraction { private stateProvider: StateProvider, private accountService: AccountService, private logService: LogService, + private cipherEncryptionService: CipherEncryptionService, ) {} localData$(userId: UserId): Observable> { @@ -424,13 +426,21 @@ export class CipherService implements CipherServiceAbstraction { ciphers: Cipher[], userId: UserId, ): Promise<[CipherView[], CipherView[]] | null> { - const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true)); + if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) { + const decryptStartTime = new Date().getTime(); + const decrypted = await this.decryptCiphersWithSdk(ciphers, userId); + this.logService.info( + `[CipherService] Decrypting ${decrypted.length} ciphers took ${new Date().getTime() - decryptStartTime}ms`, + ); + // With SDK, failed ciphers are not returned + return [decrypted, []]; + } + const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true)); if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) { // return early if there are no keys to decrypt with return null; } - // Group ciphers by orgId or under 'null' for the user's ciphers const grouped = ciphers.reduce( (agg, c) => { @@ -440,7 +450,6 @@ export class CipherService implements CipherServiceAbstraction { }, {} as Record, ); - const decryptStartTime = new Date().getTime(); const allCipherViews = ( await Promise.all( @@ -464,7 +473,6 @@ export class CipherService implements CipherServiceAbstraction { this.logService.info( `[CipherService] Decrypting ${allCipherViews.length} ciphers took ${new Date().getTime() - decryptStartTime}ms`, ); - // Split ciphers into two arrays, one for successfully decrypted ciphers and one for ciphers that failed to decrypt return allCipherViews.reduce( (acc, c) => { @@ -479,6 +487,21 @@ export class CipherService implements CipherServiceAbstraction { ); } + /** + * Decrypts a cipher using either the SDK or the legacy method based on the feature flag. + * @param cipher The cipher to decrypt. + * @param userId The user ID to use for decryption. + * @returns A promise that resolves to the decrypted cipher view. + */ + async decrypt(cipher: Cipher, userId: UserId): Promise { + if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) { + return await this.cipherEncryptionService.decrypt(cipher, userId); + } else { + const encKey = await this.getKeyForCipherKeyDecryption(cipher, userId); + return await cipher.decrypt(encKey); + } + } + private async reindexCiphers(userId: UserId) { const reindexRequired = this.searchService != null && @@ -895,7 +918,7 @@ export class CipherService implements CipherServiceAbstraction { //then we rollback to using the user key as the main key of encryption of the item //in order to keep item and it's attachments with the same encryption level if (cipher.key != null && !cipherKeyEncryptionEnabled) { - const model = await cipher.decrypt(await this.getKeyForCipherKeyDecryption(cipher, userId)); + const model = await this.decrypt(cipher, userId); cipher = await this.encrypt(model, userId); await this.updateWithServer(cipher); } @@ -1381,6 +1404,43 @@ export class CipherService implements CipherServiceAbstraction { return encryptedCiphers; } + async getDecryptedAttachmentBuffer( + cipherId: CipherId, + attachment: AttachmentView, + response: Response, + userId: UserId, + ): Promise { + const useSdkDecryption = await this.configService.getFeatureFlag( + FeatureFlag.PM19941MigrateCipherDomainToSdk, + ); + + const cipherDomain = await firstValueFrom( + this.ciphers$(userId).pipe(map((ciphersData) => new Cipher(ciphersData[cipherId]))), + ); + + if (useSdkDecryption) { + const encryptedContent = await response.arrayBuffer(); + return this.cipherEncryptionService.decryptAttachmentContent( + cipherDomain, + attachment, + new Uint8Array(encryptedContent), + userId, + ); + } + + const encBuf = await EncArrayBuffer.fromResponse(response); + const key = + attachment.key != null + ? attachment.key + : await firstValueFrom( + this.keyService.orgKeys$(userId).pipe( + filterOutNullish(), + map((orgKeys) => orgKeys[cipherDomain.organizationId as OrganizationId] as OrgKey), + ), + ); + return await this.encryptService.decryptFileData(encBuf, key); + } + /** * @returns a SingleUserState */ @@ -1430,9 +1490,7 @@ export class CipherService implements CipherServiceAbstraction { originalCipher: Cipher, userId: UserId, ): Promise { - const existingCipher = await originalCipher.decrypt( - await this.getKeyForCipherKeyDecryption(originalCipher, userId), - ); + const existingCipher = await this.decrypt(originalCipher, userId); model.passwordHistory = existingCipher.passwordHistory || []; if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) { if ( @@ -1852,4 +1910,17 @@ export class CipherService implements CipherServiceAbstraction { ); return featureEnabled && meetsServerVersion; } + + /** + * Decrypts the provided ciphers using the SDK. + * @param ciphers The ciphers to decrypt. + * @param userId The user ID to use for decryption. + * @returns The decrypted ciphers. + * @private + */ + private async decryptCiphersWithSdk(ciphers: Cipher[], userId: UserId): Promise { + const decryptedViews = await this.cipherEncryptionService.decryptManyLegacy(ciphers, userId); + + return decryptedViews.sort(this.getLocaleSortingFunction()); + } } diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts new file mode 100644 index 00000000000..c0b3d3be85f --- /dev/null +++ b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts @@ -0,0 +1,334 @@ +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { + Fido2Credential, + Cipher as SdkCipher, + CipherType as SdkCipherType, + CipherView as SdkCipherView, + CipherListView, + Attachment as SdkAttachment, +} from "@bitwarden/sdk-internal"; + +import { mockEnc } from "../../../spec"; +import { UriMatchStrategy } from "../../models/domain/domain-service"; +import { LogService } from "../../platform/abstractions/log.service"; +import { SdkService } from "../../platform/abstractions/sdk/sdk.service"; +import { UserId } from "../../types/guid"; +import { CipherRepromptType, CipherType } from "../enums"; +import { CipherPermissionsApi } from "../models/api/cipher-permissions.api"; +import { CipherData } from "../models/data/cipher.data"; +import { Cipher } from "../models/domain/cipher"; +import { AttachmentView } from "../models/view/attachment.view"; +import { CipherView } from "../models/view/cipher.view"; +import { Fido2CredentialView } from "../models/view/fido2-credential.view"; + +import { DefaultCipherEncryptionService } from "./default-cipher-encryption.service"; + +const cipherData: CipherData = { + id: "id", + organizationId: "orgId", + folderId: "folderId", + edit: true, + viewPassword: true, + organizationUseTotp: true, + favorite: false, + revisionDate: "2022-01-31T12:00:00.000Z", + type: CipherType.Login, + name: "EncryptedString", + notes: "EncryptedString", + creationDate: "2022-01-01T12:00:00.000Z", + deletedDate: null, + permissions: new CipherPermissionsApi(), + key: "EncKey", + reprompt: CipherRepromptType.None, + login: { + uris: [ + { uri: "EncryptedString", uriChecksum: "EncryptedString", match: UriMatchStrategy.Domain }, + ], + username: "EncryptedString", + password: "EncryptedString", + passwordRevisionDate: "2022-01-31T12:00:00.000Z", + totp: "EncryptedString", + autofillOnPageLoad: false, + }, + passwordHistory: [{ password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" }], + attachments: [ + { + id: "a1", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + { + id: "a2", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + ], +}; + +describe("DefaultCipherEncryptionService", () => { + let cipherEncryptionService: DefaultCipherEncryptionService; + const sdkService = mock(); + const logService = mock(); + let sdkCipherView: SdkCipherView; + + const mockSdkClient = { + vault: jest.fn().mockReturnValue({ + ciphers: jest.fn().mockReturnValue({ + decrypt: jest.fn(), + decrypt_list: jest.fn(), + decrypt_fido2_credentials: jest.fn(), + }), + attachments: jest.fn().mockReturnValue({ + decrypt_buffer: jest.fn(), + }), + }), + }; + const mockRef = { + value: mockSdkClient, + [Symbol.dispose]: jest.fn(), + }; + const mockSdk = { + take: jest.fn().mockReturnValue(mockRef), + }; + + const userId = "user-id" as UserId; + + let cipherObj: Cipher; + + beforeEach(() => { + sdkService.userClient$ = jest.fn((userId: UserId) => of(mockSdk)) as any; + cipherEncryptionService = new DefaultCipherEncryptionService(sdkService, logService); + cipherObj = new Cipher(cipherData); + + jest.spyOn(cipherObj, "toSdkCipher").mockImplementation(() => { + return { id: cipherData.id } as SdkCipher; + }); + + sdkCipherView = { + id: "test-id", + type: SdkCipherType.Login, + name: "test-name", + login: { + username: "test-username", + password: "test-password", + }, + } as SdkCipherView; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("decrypt", () => { + it("should decrypt a cipher successfully", async () => { + const expectedCipherView: CipherView = { + id: "test-id", + type: CipherType.Login, + name: "test-name", + login: { + username: "test-username", + password: "test-password", + }, + } as CipherView; + + mockSdkClient.vault().ciphers().decrypt.mockReturnValue(sdkCipherView); + jest.spyOn(CipherView, "fromSdkCipherView").mockReturnValue(expectedCipherView); + + const result = await cipherEncryptionService.decrypt(cipherObj, userId); + + expect(result).toEqual(expectedCipherView); + expect(cipherObj.toSdkCipher).toHaveBeenCalledTimes(1); + expect(mockSdkClient.vault().ciphers().decrypt).toHaveBeenCalledWith({ id: cipherData.id }); + expect(CipherView.fromSdkCipherView).toHaveBeenCalledWith(sdkCipherView); + expect(mockSdkClient.vault().ciphers().decrypt_fido2_credentials).not.toHaveBeenCalled(); + }); + + it("should decrypt FIDO2 credentials if present", async () => { + const fido2Credentials = [ + { + credentialId: mockEnc("credentialId"), + keyType: mockEnc("keyType"), + keyAlgorithm: mockEnc("keyAlgorithm"), + keyCurve: mockEnc("keyCurve"), + keyValue: mockEnc("keyValue"), + rpId: mockEnc("rpId"), + userHandle: mockEnc("userHandle"), + userName: mockEnc("userName"), + counter: mockEnc("2"), + rpName: mockEnc("rpName"), + userDisplayName: mockEnc("userDisplayName"), + discoverable: mockEnc("true"), + creationDate: new Date("2023-01-01T12:00:00.000Z"), + }, + ] as unknown as Fido2Credential[]; + + sdkCipherView.login!.fido2Credentials = fido2Credentials; + + const expectedCipherView: CipherView = { + id: "test-id", + type: CipherType.Login, + name: "test-name", + login: { + username: "test-username", + password: "test-password", + fido2Credentials: [], + }, + } as unknown as CipherView; + + const fido2CredentialView: Fido2CredentialView = { + credentialId: "credentialId", + keyType: "keyType", + keyAlgorithm: "keyAlgorithm", + keyCurve: "keyCurve", + keyValue: "decrypted-key-value", + rpId: "rpId", + userHandle: "userHandle", + userName: "userName", + counter: 2, + rpName: "rpName", + userDisplayName: "userDisplayName", + discoverable: true, + creationDate: new Date("2023-01-01T12:00:00.000Z"), + } as unknown as Fido2CredentialView; + + mockSdkClient.vault().ciphers().decrypt.mockReturnValue(sdkCipherView); + mockSdkClient.vault().ciphers().decrypt_fido2_credentials.mockReturnValue(fido2Credentials); + mockSdkClient.vault().ciphers().decrypt_fido2_private_key = jest + .fn() + .mockReturnValue("decrypted-key-value"); + + jest.spyOn(CipherView, "fromSdkCipherView").mockReturnValue(expectedCipherView); + jest + .spyOn(Fido2CredentialView, "fromSdkFido2CredentialView") + .mockReturnValueOnce(fido2CredentialView); + + const result = await cipherEncryptionService.decrypt(cipherObj, userId); + + expect(result).toBe(expectedCipherView); + expect(result.login?.fido2Credentials).toEqual([fido2CredentialView]); + expect(mockSdkClient.vault().ciphers().decrypt_fido2_credentials).toHaveBeenCalledWith( + sdkCipherView, + ); + expect(mockSdkClient.vault().ciphers().decrypt_fido2_private_key).toHaveBeenCalledWith( + sdkCipherView, + ); + expect(Fido2CredentialView.fromSdkFido2CredentialView).toHaveBeenCalledTimes(1); + }); + }); + + describe("decryptManyLegacy", () => { + it("should decrypt multiple ciphers successfully", async () => { + const ciphers = [new Cipher(cipherData), new Cipher(cipherData)]; + + const expectedViews = [ + { + id: "test-id-1", + name: "test-name-1", + } as CipherView, + { + id: "test-id-2", + name: "test-name-2", + } as CipherView, + ]; + + mockSdkClient + .vault() + .ciphers() + .decrypt.mockReturnValueOnce({ id: "test-id-1", name: "test-name-1" } as SdkCipherView) + .mockReturnValueOnce({ id: "test-id-2", name: "test-name-2" } as SdkCipherView); + + jest + .spyOn(CipherView, "fromSdkCipherView") + .mockReturnValueOnce(expectedViews[0]) + .mockReturnValueOnce(expectedViews[1]); + + const result = await cipherEncryptionService.decryptManyLegacy(ciphers, userId); + + expect(result).toEqual(expectedViews); + expect(mockSdkClient.vault().ciphers().decrypt).toHaveBeenCalledTimes(2); + expect(CipherView.fromSdkCipherView).toHaveBeenCalledTimes(2); + }); + + it("should throw EmptyError when SDK is not available", async () => { + sdkService.userClient$ = jest.fn().mockReturnValue(of(null)) as any; + + await expect( + cipherEncryptionService.decryptManyLegacy([cipherObj], userId), + ).rejects.toThrow(); + + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to decrypt ciphers"), + ); + }); + }); + + describe("decryptMany", () => { + it("should decrypt multiple ciphers to list views", async () => { + const ciphers = [new Cipher(cipherData), new Cipher(cipherData)]; + + const expectedListViews = [ + { id: "list1", name: "List 1" } as CipherListView, + { id: "list2", name: "List 2" } as CipherListView, + ]; + + mockSdkClient.vault().ciphers().decrypt_list.mockReturnValue(expectedListViews); + + const result = await cipherEncryptionService.decryptMany(ciphers, userId); + + expect(result).toEqual(expectedListViews); + expect(mockSdkClient.vault().ciphers().decrypt_list).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ id: cipherData.id }), + expect.objectContaining({ id: cipherData.id }), + ]), + ); + }); + + it("should throw EmptyError when SDK is not available", async () => { + sdkService.userClient$ = jest.fn().mockReturnValue(of(null)) as any; + + await expect(cipherEncryptionService.decryptMany([cipherObj], userId)).rejects.toThrow(); + + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to decrypt cipher list"), + ); + }); + }); + + describe("decryptAttachmentContent", () => { + it("should decrypt attachment content successfully", async () => { + const cipher = new Cipher(cipherData); + const attachment = new AttachmentView(cipher.attachments![0]); + const encryptedContent = new Uint8Array([1, 2, 3, 4]); + const expectedDecryptedContent = new Uint8Array([5, 6, 7, 8]); + + jest.spyOn(cipher, "toSdkCipher").mockReturnValue({ id: "id" } as SdkCipher); + jest.spyOn(attachment, "toSdkAttachmentView").mockReturnValue({ id: "a1" } as SdkAttachment); + mockSdkClient.vault().attachments().decrypt_buffer.mockReturnValue(expectedDecryptedContent); + + const result = await cipherEncryptionService.decryptAttachmentContent( + cipher, + attachment, + encryptedContent, + userId, + ); + + expect(result).toEqual(expectedDecryptedContent); + expect(cipher.toSdkCipher).toHaveBeenCalled(); + expect(attachment.toSdkAttachmentView).toHaveBeenCalled(); + expect(mockSdkClient.vault().attachments().decrypt_buffer).toHaveBeenCalledWith( + { id: "id" }, + { id: "a1" }, + encryptedContent, + ); + }); + }); +}); diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.ts b/libs/common/src/vault/services/default-cipher-encryption.service.ts new file mode 100644 index 00000000000..2c57df6f5bb --- /dev/null +++ b/libs/common/src/vault/services/default-cipher-encryption.service.ts @@ -0,0 +1,190 @@ +import { EMPTY, catchError, firstValueFrom, map } from "rxjs"; + +import { CipherListView } from "@bitwarden/sdk-internal"; + +import { LogService } from "../../platform/abstractions/log.service"; +import { SdkService } from "../../platform/abstractions/sdk/sdk.service"; +import { UserId } from "../../types/guid"; +import { CipherEncryptionService } from "../abstractions/cipher-encryption.service"; +import { CipherType } from "../enums"; +import { Cipher } from "../models/domain/cipher"; +import { AttachmentView } from "../models/view/attachment.view"; +import { CipherView } from "../models/view/cipher.view"; +import { Fido2CredentialView } from "../models/view/fido2-credential.view"; + +export class DefaultCipherEncryptionService implements CipherEncryptionService { + constructor( + private sdkService: SdkService, + private logService: LogService, + ) {} + + async decrypt(cipher: Cipher, userId: UserId): Promise { + return firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map((sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + + using ref = sdk.take(); + const sdkCipherView = ref.value.vault().ciphers().decrypt(cipher.toSdkCipher()); + + const clientCipherView = CipherView.fromSdkCipherView(sdkCipherView)!; + + // Decrypt Fido2 credentials if available + if ( + clientCipherView.type === CipherType.Login && + sdkCipherView.login?.fido2Credentials?.length + ) { + const fido2CredentialViews = ref.value + .vault() + .ciphers() + .decrypt_fido2_credentials(sdkCipherView); + + // TEMPORARY: Manually decrypt the keyValue for Fido2 credentials + // since we don't currently use the SDK for Fido2 Authentication. + const decryptedKeyValue = ref.value + .vault() + .ciphers() + .decrypt_fido2_private_key(sdkCipherView); + + clientCipherView.login.fido2Credentials = fido2CredentialViews + .map((f) => { + const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!; + + return { + ...view, + keyValue: decryptedKeyValue, + }; + }) + .filter((view): view is Fido2CredentialView => view !== undefined); + } + + return clientCipherView; + }), + catchError((error: unknown) => { + this.logService.error(`Failed to decrypt cipher ${error}`); + return EMPTY; + }), + ), + ); + } + + decryptManyLegacy(ciphers: Cipher[], userId: UserId): Promise { + return firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map((sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + + using ref = sdk.take(); + + return ciphers.map((cipher) => { + const sdkCipherView = ref.value.vault().ciphers().decrypt(cipher.toSdkCipher()); + const clientCipherView = CipherView.fromSdkCipherView(sdkCipherView)!; + + // Handle FIDO2 credentials if present + if ( + clientCipherView.type === CipherType.Login && + sdkCipherView.login?.fido2Credentials?.length + ) { + const fido2CredentialViews = ref.value + .vault() + .ciphers() + .decrypt_fido2_credentials(sdkCipherView); + + // TODO (PM-21259): Remove manual keyValue decryption for FIDO2 credentials. + // This is a temporary workaround until we can use the SDK for FIDO2 authentication. + const decryptedKeyValue = ref.value + .vault() + .ciphers() + .decrypt_fido2_private_key(sdkCipherView); + + clientCipherView.login.fido2Credentials = fido2CredentialViews + .map((f) => { + const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!; + return { + ...view, + keyValue: decryptedKeyValue, + }; + }) + .filter((view): view is Fido2CredentialView => view !== undefined); + } + + return clientCipherView; + }); + }), + catchError((error: unknown) => { + this.logService.error(`Failed to decrypt ciphers: ${error}`); + return EMPTY; + }), + ), + ); + } + + async decryptMany(ciphers: Cipher[], userId: UserId): Promise { + return firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map((sdk) => { + if (!sdk) { + throw new Error("SDK is undefined"); + } + + using ref = sdk.take(); + + return ref.value + .vault() + .ciphers() + .decrypt_list(ciphers.map((cipher) => cipher.toSdkCipher())); + }), + catchError((error: unknown) => { + this.logService.error(`Failed to decrypt cipher list: ${error}`); + return EMPTY; + }), + ), + ); + } + + /** + * Decrypts an attachment's content from a response object. + * + * @param cipher The encrypted cipher object that owns the attachment + * @param attachment The encrypted attachment object + * @param encryptedContent The encrypted content as a Uint8Array + * @param userId The user ID whose key will be used for decryption + * + * @returns A promise that resolves to the decrypted content + */ + async decryptAttachmentContent( + cipher: Cipher, + attachment: AttachmentView, + encryptedContent: Uint8Array, + userId: UserId, + ): Promise { + return firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map((sdk) => { + if (!sdk) { + throw new Error("SDK is undefined"); + } + + using ref = sdk.take(); + + return ref.value + .vault() + .attachments() + .decrypt_buffer( + cipher.toSdkCipher(), + attachment.toSdkAttachmentView(), + encryptedContent, + ); + }), + catchError((error: unknown) => { + this.logService.error(`Failed to decrypt cipher buffer: ${error}`); + return EMPTY; + }), + ), + ); + } +} diff --git a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts index 9284718a063..af29d8263c6 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts @@ -118,9 +118,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); - const view = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + const view = await this.cipherService.decrypt(cipher, activeUserId); this.cleanupCipher(view); this.result.ciphers.push(view); } diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index ae408af421b..6ed4caa3f8d 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -10,7 +10,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { UserId } from "@bitwarden/common/types/guid"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -172,6 +172,8 @@ describe("VaultExportService", () => { let apiService: MockProxy; let fetchMock: jest.Mock; + const userId = "" as UserId; + beforeEach(() => { cryptoFunctionService = mock(); cipherService = mock(); @@ -185,7 +187,6 @@ describe("VaultExportService", () => { keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any)); - const userId = "" as UserId; const accountInfo: AccountInfo = { email: "", emailVerified: true, @@ -338,7 +339,9 @@ describe("VaultExportService", () => { cipherService.getAllDecrypted.mockResolvedValue([cipherView]); folderService.getAllDecryptedFromState.mockResolvedValue([]); - encryptService.decryptFileData.mockResolvedValue(new Uint8Array(255)); + cipherService.getDecryptedAttachmentBuffer.mockRejectedValue( + new Error("Error decrypting attachment"), + ); global.fetch = jest.fn(() => Promise.resolve({ @@ -356,13 +359,17 @@ describe("VaultExportService", () => { it("contains attachments with folders", async () => { const cipherData = new CipherData(); cipherData.id = "mock-id"; + const cipherRecord: Record = { + ["mock-id" as CipherId]: cipherData, + }; const cipherView = new CipherView(new Cipher(cipherData)); const attachmentView = new AttachmentView(new Attachment(new AttachmentData())); attachmentView.fileName = "mock-file-name"; cipherView.attachments = [attachmentView]; + cipherService.ciphers$.mockReturnValue(of(cipherRecord)); cipherService.getAllDecrypted.mockResolvedValue([cipherView]); folderService.getAllDecryptedFromState.mockResolvedValue([]); - encryptService.decryptFileData.mockResolvedValue(new Uint8Array(255)); + cipherService.getDecryptedAttachmentBuffer.mockResolvedValue(new Uint8Array(255)); global.fetch = jest.fn(() => Promise.resolve({ status: 200, diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index 8b66580d4cd..537585aac7e 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -12,14 +12,12 @@ import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/a import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; -import { UserId } from "@bitwarden/common/types/guid"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Folder } from "@bitwarden/common/vault/models/domain/folder"; -import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; @@ -118,8 +116,19 @@ export class IndividualVaultExportService const cipherFolder = attachmentsFolder.folder(cipher.id); for (const attachment of cipher.attachments) { const response = await this.downloadAttachment(cipher.id, attachment.id); - const decBuf = await this.decryptAttachment(cipher, attachment, response); - cipherFolder.file(attachment.fileName, decBuf); + + try { + const decBuf = await this.cipherService.getDecryptedAttachmentBuffer( + cipher.id as CipherId, + attachment, + response, + activeUserId, + ); + + cipherFolder.file(attachment.fileName, decBuf); + } catch { + throw new Error("Error decrypting attachment"); + } } } @@ -146,23 +155,6 @@ export class IndividualVaultExportService return response; } - private async decryptAttachment( - cipher: CipherView, - attachment: AttachmentView, - response: Response, - ) { - try { - const encBuf = await EncArrayBuffer.fromResponse(response); - const key = - attachment.key != null - ? attachment.key - : await this.keyService.getOrgKey(cipher.organizationId); - return await this.encryptService.decryptFileData(encBuf, key); - } catch { - throw new Error("Error decrypting attachment"); - } - } - private async getDecryptedExport( activeUserId: UserId, format: "json" | "csv", diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts index fc46915c15d..4f30f299062 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts @@ -155,12 +155,9 @@ export class OrganizationVaultExportService .forEach(async (c) => { const cipher = new Cipher(new CipherData(c)); exportPromises.push( - this.cipherService - .getKeyForCipherKeyDecryption(cipher, activeUserId) - .then((key) => cipher.decrypt(key)) - .then((decCipher) => { - decCiphers.push(decCipher); - }), + this.cipherService.decrypt(cipher, activeUserId).then((decCipher) => { + decCiphers.push(decCipher); + }), ); }); } diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts index 0fe358cd89b..da827addf67 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts @@ -80,6 +80,7 @@ describe("CipherAttachmentsComponent", () => { get: cipherServiceGet, saveAttachmentWithServer, getKeyForCipherKeyDecryption: () => Promise.resolve(null), + decrypt: jest.fn().mockResolvedValue(cipherView), }, }, { diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts index 29a80c826c6..aa9769ec392 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts @@ -137,9 +137,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { this.organization = await this.getOrganization(); this.cipherDomain = await this.getCipher(this.cipherId); - this.cipher = await this.cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, this.activeUserId), - ); + this.cipher = await this.cipherService.decrypt(this.cipherDomain, this.activeUserId); // Update the initial state of the submit button if (this.submitBtn) { @@ -210,9 +208,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { ); // re-decrypt the cipher to update the attachments - this.cipher = await this.cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, this.activeUserId), - ); + this.cipher = await this.cipherService.decrypt(this.cipherDomain, this.activeUserId); // Reset reactive form and input element this.fileInput.nativeElement.value = ""; diff --git a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts index 98286e4bbb2..68eac4f0da2 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts @@ -3,7 +3,6 @@ import { inject, Injectable } from "@angular/core"; import { firstValueFrom } from "rxjs"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -21,13 +20,10 @@ function isSetEqual(a: Set, b: Set) { export class DefaultCipherFormService implements CipherFormService { private cipherService: CipherService = inject(CipherService); private accountService: AccountService = inject(AccountService); - private apiService: ApiService = inject(ApiService); async decryptCipher(cipher: Cipher): Promise { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - return await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + return await this.cipherService.decrypt(cipher, activeUserId); } async saveCipher(cipher: CipherView, config: CipherFormConfig): Promise { @@ -46,9 +42,7 @@ export class DefaultCipherFormService implements CipherFormService { // Creating a new cipher if (cipher.id == null) { savedCipher = await this.cipherService.createWithServer(encryptedCipher, config.admin); - return await savedCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(savedCipher, activeUserId), - ); + return await this.cipherService.decrypt(savedCipher, activeUserId); } if (config.originalCipher == null) { @@ -100,8 +94,6 @@ export class DefaultCipherFormService implements CipherFormService { return null; } - return await savedCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(savedCipher, activeUserId), - ); + return await this.cipherService.decrypt(savedCipher, activeUserId); } } diff --git a/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts b/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts index f621ca63101..8a4e962707d 100644 --- a/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts +++ b/libs/vault/src/components/download-attachment/download-attachment.component.spec.ts @@ -6,15 +6,16 @@ import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; import { StateProvider } from "@bitwarden/common/platform/state"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; import { PasswordRepromptService } from "../../services/password-reprompt.service"; @@ -51,6 +52,21 @@ describe("DownloadAttachmentComponent", () => { }, } as CipherView; + const ciphers$ = new BehaviorSubject({ + "5555-444-3333": { + id: "5555-444-3333", + attachments: [ + { + id: "222-3333-4444", + fileName: "encrypted-filename", + key: "encrypted-key", + }, + ], + }, + }); + + const getFeatureFlag = jest.fn().mockResolvedValue(false); + beforeEach(async () => { showToast.mockClear(); getAttachmentData.mockClear(); @@ -60,13 +76,22 @@ describe("DownloadAttachmentComponent", () => { imports: [DownloadAttachmentComponent], providers: [ { provide: EncryptService, useValue: mock() }, - { provide: KeyService, useValue: mock() }, { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: StateProvider, useValue: { activeUserId$ } }, { provide: ToastService, useValue: { showToast } }, { provide: ApiService, useValue: { getAttachmentData } }, { provide: FileDownloadService, useValue: { download } }, { provide: PasswordRepromptService, useValue: mock() }, + { + provide: ConfigService, + useValue: { + getFeatureFlag, + }, + }, + { + provide: CipherService, + useValue: { ciphers$: () => ciphers$, getDecryptedAttachmentBuffer: jest.fn() }, + }, ], }).compileComponents(); }); @@ -128,10 +153,12 @@ describe("DownloadAttachmentComponent", () => { }); }); - it("shows an error toast when EncArrayBuffer fails", async () => { + it("shows an error toast when getDecryptedAttachmentBuffer fails", async () => { getAttachmentData.mockResolvedValue({ url: "https://www.downloadattachement.com" }); fetchMock.mockResolvedValue({ status: 200 }); - EncArrayBuffer.fromResponse = jest.fn().mockRejectedValue({}); + + const cipherService = TestBed.inject(CipherService) as jest.Mocked; + cipherService.getDecryptedAttachmentBuffer.mockRejectedValue(new Error()); await component.download(); diff --git a/libs/vault/src/components/download-attachment/download-attachment.component.ts b/libs/vault/src/components/download-attachment/download-attachment.component.ts index e64777ebb8e..f06d6db582a 100644 --- a/libs/vault/src/components/download-attachment/download-attachment.component.ts +++ b/libs/vault/src/components/download-attachment/download-attachment.component.ts @@ -2,23 +2,19 @@ // @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { NEVER, switchMap } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; import { StateProvider } from "@bitwarden/common/platform/state"; -import { EmergencyAccessId, OrganizationId } from "@bitwarden/common/types/guid"; -import { OrgKey } from "@bitwarden/common/types/key"; +import { CipherId, EmergencyAccessId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { AsyncActionsModule, IconButtonModule, ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; @Component({ standalone: true, @@ -42,29 +38,14 @@ export class DownloadAttachmentComponent { /** When owners/admins can mange all items and when accessing from the admin console, use the admin endpoint */ @Input() admin?: boolean = false; - /** The organization key if the cipher is associated with one */ - private orgKey: OrgKey | null = null; - constructor( private i18nService: I18nService, private apiService: ApiService, private fileDownloadService: FileDownloadService, private toastService: ToastService, - private encryptService: EncryptService, private stateProvider: StateProvider, - private keyService: KeyService, - ) { - this.stateProvider.activeUserId$ - .pipe( - switchMap((userId) => (userId !== null ? this.keyService.orgKeys$(userId) : NEVER)), - takeUntilDestroyed(), - ) - .subscribe((data: Record | null) => { - if (data) { - this.orgKey = data[this.cipher.organizationId as OrganizationId]; - } - }); - } + private cipherService: CipherService, + ) {} /** Download the attachment */ download = async () => { @@ -100,9 +81,15 @@ export class DownloadAttachmentComponent { } try { - const encBuf = await EncArrayBuffer.fromResponse(response); - const key = this.attachment.key != null ? this.attachment.key : this.orgKey; - const decBuf = await this.encryptService.decryptFileData(encBuf, key); + const userId = await firstValueFrom(this.stateProvider.activeUserId$); + + const decBuf = await this.cipherService.getDecryptedAttachmentBuffer( + this.cipher.id as CipherId, + this.attachment, + response, + userId, + ); + this.fileDownloadService.download({ fileName: this.attachment.fileName, blobData: decBuf, From 1a1481bbd643169a554e72437c3f8b78264cfbbc Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 14 May 2025 16:01:03 +0100 Subject: [PATCH 041/163] Resolve the tiny line issue (#14758) --- .../billing/members/free-bitwarden-families.component.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/billing/members/free-bitwarden-families.component.html b/apps/web/src/app/billing/members/free-bitwarden-families.component.html index ee21909beec..243cf612c73 100644 --- a/apps/web/src/app/billing/members/free-bitwarden-families.component.html +++ b/apps/web/src/app/billing/members/free-bitwarden-families.component.html @@ -67,7 +67,9 @@ {{ "resendInvitation" | i18n }} -
+ +
+
diff --git a/libs/components/src/form-field/form-field.component.ts b/libs/components/src/form-field/form-field.component.ts index 4f7c2a67483..e810aaec8cb 100644 --- a/libs/components/src/form-field/form-field.component.ts +++ b/libs/components/src/form-field/form-field.component.ts @@ -91,7 +91,7 @@ export class BitFormFieldComponent implements AfterContentChecked { protected defaultContentIsFocused = signal(false); @HostListener("focusin", ["$event.target"]) onFocusIn(target: HTMLElement) { - this.defaultContentIsFocused.set(target.matches(".default-content *:focus-visible")); + this.defaultContentIsFocused.set(target.matches("[data-default-content] *:focus-visible")); } @HostListener("focusout") onFocusOut() { diff --git a/libs/components/src/item/item-content.component.ts b/libs/components/src/item/item-content.component.ts index 2a6e06291fd..76fa3996210 100644 --- a/libs/components/src/item/item-content.component.ts +++ b/libs/components/src/item/item-content.component.ts @@ -25,7 +25,8 @@ import { TypographyModule } from "../typography"; * y-axis padding should be kept in sync with `item-action.component.ts`'s `top` and `bottom` units. * we want this to be the same height as the `item-action`'s `:after` element */ - "fvw-target tw-outline-none tw-text-main hover:tw-text-main tw-no-underline hover:tw-no-underline tw-text-base tw-py-2 tw-px-4 bit-compact:tw-py-1.5 bit-compact:tw-px-2 tw-bg-transparent tw-w-full tw-border-none tw-flex tw-gap-4 tw-items-center tw-justify-between", + "tw-outline-none tw-text-main hover:tw-text-main tw-no-underline hover:tw-no-underline tw-text-base tw-py-2 tw-px-4 bit-compact:tw-py-1.5 bit-compact:tw-px-2 tw-bg-transparent tw-w-full tw-border-none tw-flex tw-gap-4 tw-items-center tw-justify-between", + "data-fvw-target": "", }, changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/libs/components/src/item/item.component.html b/libs/components/src/item/item.component.html index 812d6b0315b..2863bb2891b 100644 --- a/libs/components/src/item/item.component.html +++ b/libs/components/src/item/item.component.html @@ -1,4 +1,4 @@ - + diff --git a/libs/components/src/item/item.component.ts b/libs/components/src/item/item.component.ts index 1ef4a4af1fa..0c45f98139e 100644 --- a/libs/components/src/item/item.component.ts +++ b/libs/components/src/item/item.component.ts @@ -19,7 +19,7 @@ import { ItemActionComponent } from "./item-action.component"; providers: [{ provide: A11yRowDirective, useExisting: ItemComponent }], host: { class: - "tw-block tw-box-border tw-overflow-hidden tw-flex tw-bg-background [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-cursor-pointer [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-bg-primary-100 tw-text-main tw-border-solid tw-border-b tw-border-0 [&:not(bit-layout_*)]:tw-rounded-lg bit-compact:[&:not(bit-layout_*)]:tw-rounded-none bit-compact:[&:not(bit-layout_*)]:last-of-type:tw-rounded-b-lg bit-compact:[&:not(bit-layout_*)]:first-of-type:tw-rounded-t-lg tw-min-h-9 tw-mb-1.5 bit-compact:tw-mb-0", + "tw-block tw-box-border tw-overflow-hidden tw-flex tw-bg-background [&:has([data-item-main-content]_button:hover,[data-item-main-content]_a:hover)]:tw-cursor-pointer [&:has([data-item-main-content]_button:hover,[data-item-main-content]_a:hover)]:tw-bg-primary-100 tw-text-main tw-border-solid tw-border-b tw-border-0 [&:not(bit-layout_*)]:tw-rounded-lg bit-compact:[&:not(bit-layout_*)]:tw-rounded-none bit-compact:[&:not(bit-layout_*)]:last-of-type:tw-rounded-b-lg bit-compact:[&:not(bit-layout_*)]:first-of-type:tw-rounded-t-lg tw-min-h-9 tw-mb-1.5 bit-compact:tw-mb-0", }, }) export class ItemComponent extends A11yRowDirective { @@ -29,7 +29,7 @@ export class ItemComponent extends A11yRowDirective { protected focusVisibleWithin = signal(false); @HostListener("focusin", ["$event.target"]) onFocusIn(target: HTMLElement) { - this.focusVisibleWithin.set(target.matches(".fvw-target:focus-visible")); + this.focusVisibleWithin.set(target.matches("[data-fvw-target]:focus-visible")); } @HostListener("focusout") onFocusOut() { diff --git a/libs/components/src/navigation/nav-item.component.html b/libs/components/src/navigation/nav-item.component.html index c3cf559389f..595ddf5a99f 100644 --- a/libs/components/src/navigation/nav-item.component.html +++ b/libs/components/src/navigation/nav-item.component.html @@ -70,10 +70,11 @@ - + `, + template: ` + + + + + `, }) class StoryDialogComponent { constructor(public dialogService: DialogService) {} @@ -31,6 +41,14 @@ class StoryDialogComponent { }, }); } + + openDrawer() { + this.dialogService.openDrawer(StoryDialogContentComponent, { + data: { + animal: "panda", + }, + }); + } } @Component({ @@ -65,25 +83,37 @@ export default { title: "Component Library/Dialogs/Service", component: StoryDialogComponent, decorators: [ + positionFixedWrapperDecorator(), moduleMetadata({ declarations: [StoryDialogContentComponent], imports: [ SharedModule, ButtonModule, + NoopAnimationsModule, DialogModule, IconButtonModule, DialogCloseDirective, DialogComponent, DialogTitleContainerDirective, + RouterTestingModule, + LayoutComponent, ], + providers: [DialogService], + }), + applicationConfig({ providers: [ - DialogService, { provide: I18nService, useFactory: () => { return new I18nMockService({ close: "Close", - loading: "Loading", + search: "Search", + skipToContent: "Skip to content", + submenu: "submenu", + toggleCollapse: "toggle collapse", + toggleSideNavigation: "Toggle side navigation", + yes: "Yes", + no: "No", }); }, }, @@ -100,4 +130,21 @@ export default { type Story = StoryObj; -export const Default: Story = {}; +export const Default: Story = { + play: async (context) => { + const canvas = context.canvasElement; + + const button = getAllByRole(canvas, "button")[0]; + await userEvent.click(button); + }, +}; + +/** Drawers must be a descendant of `bit-layout`. */ +export const Drawer: Story = { + play: async (context) => { + const canvas = context.canvasElement; + + const button = getAllByRole(canvas, "button")[1]; + await userEvent.click(button); + }, +}; diff --git a/libs/components/src/dialog/dialog.service.ts b/libs/components/src/dialog/dialog.service.ts index 83aaaff470e..409bf0a5b55 100644 --- a/libs/components/src/dialog/dialog.service.ts +++ b/libs/components/src/dialog/dialog.service.ts @@ -1,31 +1,25 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { - DEFAULT_DIALOG_CONFIG, - Dialog, - DialogConfig, - DialogRef, - DIALOG_SCROLL_STRATEGY, + Dialog as CdkDialog, + DialogConfig as CdkDialogConfig, + DialogRef as CdkDialogRefBase, + DIALOG_DATA, + DialogCloseOptions, } from "@angular/cdk/dialog"; -import { ComponentType, Overlay, OverlayContainer, ScrollStrategy } from "@angular/cdk/overlay"; -import { - Inject, - Injectable, - Injector, - OnDestroy, - Optional, - SkipSelf, - TemplateRef, -} from "@angular/core"; +import { ComponentType, ScrollStrategy } from "@angular/cdk/overlay"; +import { ComponentPortal, Portal } from "@angular/cdk/portal"; +import { Injectable, Injector, TemplateRef, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router } from "@angular/router"; -import { filter, firstValueFrom, Subject, switchMap, takeUntil } from "rxjs"; +import { filter, firstValueFrom, map, Observable, Subject, switchMap } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DrawerService } from "../drawer/drawer.service"; + import { SimpleConfigurableDialogComponent } from "./simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component"; -import { SimpleDialogOptions, Translation } from "./simple-dialog/types"; +import { SimpleDialogOptions } from "./simple-dialog/types"; /** * The default `BlockScrollStrategy` does not work well with virtual scrolling. @@ -48,61 +42,163 @@ class CustomBlockScrollStrategy implements ScrollStrategy { detach() {} } +export abstract class DialogRef + implements Pick, "close" | "closed" | "disableClose" | "componentInstance"> +{ + abstract readonly isDrawer?: boolean; + + // --- From CdkDialogRef --- + abstract close(result?: R, options?: DialogCloseOptions): void; + abstract readonly closed: Observable; + abstract disableClose: boolean | undefined; + /** + * @deprecated + * Does not work with drawer dialogs. + **/ + abstract componentInstance: C | null; +} + +export type DialogConfig = Pick< + CdkDialogConfig, + "data" | "disableClose" | "ariaModal" | "positionStrategy" | "height" | "width" +>; + +class DrawerDialogRef implements DialogRef { + readonly isDrawer = true; + + private _closed = new Subject(); + closed = this._closed.asObservable(); + disableClose = false; + + /** The portal containing the drawer */ + portal?: Portal; + + constructor(private drawerService: DrawerService) {} + + close(result?: R, _options?: DialogCloseOptions): void { + if (this.disableClose) { + return; + } + this.drawerService.close(this.portal!); + this._closed.next(result); + this._closed.complete(); + } + + componentInstance: C | null = null; +} + +/** + * DialogRef that delegates functionality to the CDK implementation + **/ +export class CdkDialogRef implements DialogRef { + readonly isDrawer = false; + + /** This is not available until after construction, @see DialogService.open. */ + cdkDialogRefBase!: CdkDialogRefBase; + + // --- Delegated to CdkDialogRefBase --- + + close(result?: R, options?: DialogCloseOptions): void { + this.cdkDialogRefBase.close(result, options); + } + + get closed(): Observable { + return this.cdkDialogRefBase.closed; + } + + get disableClose(): boolean | undefined { + return this.cdkDialogRefBase.disableClose; + } + set disableClose(value: boolean | undefined) { + this.cdkDialogRefBase.disableClose = value; + } + + // Delegate the `componentInstance` property to the CDK DialogRef + get componentInstance(): C | null { + return this.cdkDialogRefBase.componentInstance; + } +} + @Injectable() -export class DialogService extends Dialog implements OnDestroy { - private _destroy$ = new Subject(); +export class DialogService { + private dialog = inject(CdkDialog); + private drawerService = inject(DrawerService); + private injector = inject(Injector); + private router = inject(Router, { optional: true }); + private authService = inject(AuthService, { optional: true }); + private i18nService = inject(I18nService); private backDropClasses = ["tw-fixed", "tw-bg-black", "tw-bg-opacity-30", "tw-inset-0"]; - private defaultScrollStrategy = new CustomBlockScrollStrategy(); + private activeDrawer: DrawerDialogRef | null = null; - constructor( - /** Parent class constructor */ - _overlay: Overlay, - _injector: Injector, - @Optional() @Inject(DEFAULT_DIALOG_CONFIG) _defaultOptions: DialogConfig, - @Optional() @SkipSelf() _parentDialog: Dialog, - _overlayContainer: OverlayContainer, - @Inject(DIALOG_SCROLL_STRATEGY) scrollStrategy: any, - - /** Not in parent class */ - @Optional() router: Router, - @Optional() authService: AuthService, - - protected i18nService: I18nService, - ) { - super(_overlay, _injector, _defaultOptions, _parentDialog, _overlayContainer, scrollStrategy); - + constructor() { + /** + * TODO: This logic should exist outside of `libs/components`. + * @see https://bitwarden.atlassian.net/browse/CL-657 + **/ /** Close all open dialogs if the vault locks */ - if (router && authService) { - router.events + if (this.router && this.authService) { + this.router.events .pipe( filter((event) => event instanceof NavigationEnd), - switchMap(() => authService.getAuthStatus()), + switchMap(() => this.authService!.getAuthStatus()), filter((v) => v !== AuthenticationStatus.Unlocked), - takeUntil(this._destroy$), + takeUntilDestroyed(), ) .subscribe(() => this.closeAll()); } } - override ngOnDestroy(): void { - this._destroy$.next(); - this._destroy$.complete(); - super.ngOnDestroy(); - } - - override open( + open( componentOrTemplateRef: ComponentType | TemplateRef, config?: DialogConfig>, ): DialogRef { - config = { + /** + * This is a bit circular in nature: + * We need the DialogRef instance for the DI injector that is passed *to* `Dialog.open`, + * but we get the base CDK DialogRef instance *from* `Dialog.open`. + * + * To break the circle, we define CDKDialogRef as a wrapper for the CDKDialogRefBase. + * This allows us to create the class instance and provide the base instance later, almost like "deferred inheritance". + **/ + const ref = new CdkDialogRef(); + const injector = this.createInjector({ + data: config?.data, + dialogRef: ref, + }); + + // Merge the custom config with the default config + const _config = { backdropClass: this.backDropClasses, scrollStrategy: this.defaultScrollStrategy, + injector, ...config, }; - return super.open(componentOrTemplateRef, config); + ref.cdkDialogRefBase = this.dialog.open(componentOrTemplateRef, _config); + return ref; + } + + /** Opens a dialog in the side drawer */ + openDrawer( + component: ComponentType, + config?: DialogConfig>, + ): DialogRef { + this.activeDrawer?.close(); + /** + * This is also circular. When creating the DrawerDialogRef, we do not yet have a portal instance to provide to the injector. + * Similar to `this.open`, we get around this with mutability. + */ + this.activeDrawer = new DrawerDialogRef(this.drawerService); + const portal = new ComponentPortal( + component, + null, + this.createInjector({ data: config?.data, dialogRef: this.activeDrawer }), + ); + this.activeDrawer.portal = portal; + this.drawerService.open(portal); + return this.activeDrawer; } /** @@ -113,8 +209,7 @@ export class DialogService extends Dialog implements OnDestroy { */ async openSimpleDialog(simpleDialogOptions: SimpleDialogOptions): Promise { const dialogRef = this.openSimpleDialogRef(simpleDialogOptions); - - return firstValueFrom(dialogRef.closed); + return firstValueFrom(dialogRef.closed.pipe(map((v: boolean | undefined) => !!v))); } /** @@ -134,20 +229,29 @@ export class DialogService extends Dialog implements OnDestroy { }); } - protected translate(translation: string | Translation, defaultKey?: string): string { - if (translation == null && defaultKey == null) { - return null; - } + /** Close all open dialogs */ + closeAll(): void { + return this.dialog.closeAll(); + } - if (translation == null) { - return this.i18nService.t(defaultKey); - } - - // Translation interface use implies we must localize. - if (typeof translation === "object") { - return this.i18nService.t(translation.key, ...(translation.placeholders ?? [])); - } - - return translation; + /** The injector that is passed to the opened dialog */ + private createInjector(opts: { data: unknown; dialogRef: DialogRef }): Injector { + return Injector.create({ + providers: [ + { + provide: DIALOG_DATA, + useValue: opts.data, + }, + { + provide: DialogRef, + useValue: opts.dialogRef, + }, + { + provide: CdkDialogRefBase, + useValue: opts.dialogRef, + }, + ], + parent: this.injector, + }); } } diff --git a/libs/components/src/dialog/dialog/dialog.component.html b/libs/components/src/dialog/dialog/dialog.component.html index 01f05985127..eaf7fc2beec 100644 --- a/libs/components/src/dialog/dialog/dialog.component.html +++ b/libs/components/src/dialog/dialog/dialog.component.html @@ -1,12 +1,22 @@ +@let isDrawer = dialogRef?.isDrawer;
+ @let showHeaderBorder = !isDrawer || background === "alt" || bodyHasScrolledFrom().top;
-

} -

+
+ @let showFooterBorder = !isDrawer || background === "alt" || bodyHasScrolledFrom().bottom;
diff --git a/libs/components/src/dialog/dialog/dialog.component.ts b/libs/components/src/dialog/dialog/dialog.component.ts index 504dbd3a1ea..8cf9cd18c78 100644 --- a/libs/components/src/dialog/dialog/dialog.component.ts +++ b/libs/components/src/dialog/dialog/dialog.component.ts @@ -1,14 +1,18 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { CdkTrapFocus } from "@angular/cdk/a11y"; import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { CdkScrollable } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; -import { Component, HostBinding, Input } from "@angular/core"; +import { Component, HostBinding, Input, inject, viewChild } from "@angular/core"; import { I18nPipe } from "@bitwarden/ui-common"; import { BitIconButtonComponent } from "../../icon-button/icon-button.component"; import { TypographyDirective } from "../../typography/typography.directive"; +import { hasScrolledFrom } from "../../utils/has-scrolled-from"; import { fadeIn } from "../animations"; +import { DialogRef } from "../dialog.service"; import { DialogCloseDirective } from "../directives/dialog-close.directive"; import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive"; @@ -17,6 +21,9 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai templateUrl: "./dialog.component.html", animations: [fadeIn], standalone: true, + host: { + "(keydown.esc)": "handleEsc($event)", + }, imports: [ CommonModule, DialogTitleContainerDirective, @@ -24,9 +31,15 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai BitIconButtonComponent, DialogCloseDirective, I18nPipe, + CdkTrapFocus, + CdkScrollable, ], }) export class DialogComponent { + protected dialogRef = inject(DialogRef, { optional: true }); + private scrollableBody = viewChild.required(CdkScrollable); + protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody); + /** Background color */ @Input() background: "default" | "alt" = "default"; @@ -64,21 +77,31 @@ export class DialogComponent { @HostBinding("class") get classes() { // `tw-max-h-[90vh]` is needed to prevent dialogs from overlapping the desktop header - return ["tw-flex", "tw-flex-col", "tw-w-screen", "tw-p-4", "tw-max-h-[90vh]"].concat( - this.width, - ); + return ["tw-flex", "tw-flex-col", "tw-w-screen"] + .concat( + this.width, + this.dialogRef?.isDrawer + ? ["tw-min-h-screen", "md:tw-w-[23rem]"] + : ["tw-p-4", "tw-w-screen", "tw-max-h-[90vh]"], + ) + .flat(); + } + + handleEsc(event: Event) { + this.dialogRef?.close(); + event.stopPropagation(); } get width() { switch (this.dialogSize) { case "small": { - return "tw-max-w-sm"; + return "md:tw-max-w-sm"; } case "large": { - return "tw-max-w-3xl"; + return "md:tw-max-w-3xl"; } default: { - return "tw-max-w-xl"; + return "md:tw-max-w-xl"; } } } diff --git a/libs/components/src/dialog/dialogs.mdx b/libs/components/src/dialog/dialogs.mdx index 63df0bfc131..3f44f31a5eb 100644 --- a/libs/components/src/dialog/dialogs.mdx +++ b/libs/components/src/dialog/dialogs.mdx @@ -22,6 +22,9 @@ For alerts or simple confirmation actions, like speedbumps, use the Dialogs's should be used sparingly as they do call extra attention to themselves and can be interruptive if overused. +For non-blocking, supplementary content, open dialogs as a +[Drawer](?path=/story/component-library-dialogs-service--drawer) (requires `bit-layout`). + ## Placement Dialogs should be centered vertically and horizontally on screen. Dialogs height should expand to diff --git a/libs/components/src/dialog/index.ts b/libs/components/src/dialog/index.ts index 0ab9a5d9e67..fb4c2721b81 100644 --- a/libs/components/src/dialog/index.ts +++ b/libs/components/src/dialog/index.ts @@ -1,4 +1,4 @@ export * from "./dialog.module"; export * from "./simple-dialog/types"; export * from "./dialog.service"; -export { DialogConfig, DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +export { DIALOG_DATA } from "@angular/cdk/dialog"; diff --git a/libs/components/src/drawer/drawer-body.component.ts b/libs/components/src/drawer/drawer-body.component.ts index 9bd2adcffbc..563987dd8b0 100644 --- a/libs/components/src/drawer/drawer-body.component.ts +++ b/libs/components/src/drawer/drawer-body.component.ts @@ -1,7 +1,7 @@ import { CdkScrollable } from "@angular/cdk/scrolling"; -import { ChangeDetectionStrategy, Component, Signal, inject } from "@angular/core"; -import { toSignal } from "@angular/core/rxjs-interop"; -import { map } from "rxjs"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +import { hasScrolledFrom } from "../utils/has-scrolled-from"; /** * Body container for `bit-drawer` @@ -14,7 +14,7 @@ import { map } from "rxjs"; host: { class: "tw-p-4 tw-pt-0 tw-block tw-overflow-auto tw-border-solid tw-border tw-border-transparent tw-transition-colors tw-duration-200", - "[class.tw-border-t-secondary-300]": "isScrolled()", + "[class.tw-border-t-secondary-300]": "this.hasScrolledFrom().top", }, hostDirectives: [ { @@ -24,13 +24,5 @@ import { map } from "rxjs"; template: ` `, }) export class DrawerBodyComponent { - private scrollable = inject(CdkScrollable); - - /** TODO: share this utility with browser popup header? */ - protected isScrolled: Signal = toSignal( - this.scrollable - .elementScrolled() - .pipe(map(() => this.scrollable.measureScrollOffset("top") > 0)), - { initialValue: false }, - ); + protected hasScrolledFrom = hasScrolledFrom(); } diff --git a/libs/components/src/drawer/drawer.component.ts b/libs/components/src/drawer/drawer.component.ts index ccabb6f0b6e..351be57e07b 100644 --- a/libs/components/src/drawer/drawer.component.ts +++ b/libs/components/src/drawer/drawer.component.ts @@ -10,7 +10,7 @@ import { viewChild, } from "@angular/core"; -import { DrawerHostDirective } from "./drawer-host.directive"; +import { DrawerService } from "./drawer.service"; /** * A drawer is a panel of supplementary content that is adjacent to the page's main content. @@ -25,7 +25,7 @@ import { DrawerHostDirective } from "./drawer-host.directive"; templateUrl: "drawer.component.html", }) export class DrawerComponent { - private drawerHost = inject(DrawerHostDirective); + private drawerHost = inject(DrawerService); private portal = viewChild.required(CdkPortal); /** diff --git a/libs/components/src/drawer/drawer.mdx b/libs/components/src/drawer/drawer.mdx index 57d618cfe95..bc99fa290d6 100644 --- a/libs/components/src/drawer/drawer.mdx +++ b/libs/components/src/drawer/drawer.mdx @@ -12,6 +12,8 @@ import { DrawerComponent } from "@bitwarden/components"; # Drawer +**Note: `bit-drawer` is deprecated. Use `bit-dialog` and `DialogService.openDrawer(...)` instead.** + A drawer is a panel of supplementary content that is adjacent to the page's main content. diff --git a/libs/components/src/drawer/drawer.service.ts b/libs/components/src/drawer/drawer.service.ts new file mode 100644 index 00000000000..dd8575efee8 --- /dev/null +++ b/libs/components/src/drawer/drawer.service.ts @@ -0,0 +1,20 @@ +import { Portal } from "@angular/cdk/portal"; +import { Injectable, signal } from "@angular/core"; + +@Injectable({ providedIn: "root" }) +export class DrawerService { + private _portal = signal | undefined>(undefined); + + /** The portal to display */ + portal = this._portal.asReadonly(); + + open(portal: Portal) { + this._portal.set(portal); + } + + close(portal: Portal) { + if (portal === this.portal()) { + this._portal.set(undefined); + } + } +} diff --git a/libs/components/src/layout/index.ts b/libs/components/src/layout/index.ts index 6994a4f639f..a257a4dde85 100644 --- a/libs/components/src/layout/index.ts +++ b/libs/components/src/layout/index.ts @@ -1 +1,2 @@ export * from "./layout.component"; +export * from "./scroll-layout.directive"; diff --git a/libs/components/src/layout/layout.component.html b/libs/components/src/layout/layout.component.html index 33b8de81572..19280c99756 100644 --- a/libs/components/src/layout/layout.component.html +++ b/libs/components/src/layout/layout.component.html @@ -1,43 +1,52 @@ -
+@let mainContentId = "main-content";
- -
- +
+ + +
+ - - @if ( - { - open: sideNavService.open$ | async, - }; - as data - ) { -
- @if (data.open) { -
- } -
- } -
- + + @if ( + { + open: sideNavService.open$ | async, + }; + as data + ) { +
+ @if (data.open) { +
+ } +
+ } +
+
+
+ +
diff --git a/libs/components/src/layout/layout.component.ts b/libs/components/src/layout/layout.component.ts index 7bf8a6ad173..6ee7dd8222e 100644 --- a/libs/components/src/layout/layout.component.ts +++ b/libs/components/src/layout/layout.component.ts @@ -1,9 +1,10 @@ +import { A11yModule, CdkTrapFocus } from "@angular/cdk/a11y"; import { PortalModule } from "@angular/cdk/portal"; import { CommonModule } from "@angular/common"; -import { Component, inject } from "@angular/core"; +import { Component, ElementRef, inject, viewChild } from "@angular/core"; import { RouterModule } from "@angular/router"; -import { DrawerHostDirective } from "../drawer/drawer-host.directive"; +import { DrawerService } from "../drawer/drawer.service"; import { LinkModule } from "../link"; import { SideNavService } from "../navigation/side-nav.service"; import { SharedModule } from "../shared"; @@ -12,16 +13,23 @@ import { SharedModule } from "../shared"; selector: "bit-layout", templateUrl: "layout.component.html", standalone: true, - imports: [CommonModule, SharedModule, LinkModule, RouterModule, PortalModule], - hostDirectives: [DrawerHostDirective], + imports: [ + CommonModule, + SharedModule, + LinkModule, + RouterModule, + PortalModule, + A11yModule, + CdkTrapFocus, + ], }) export class LayoutComponent { - protected mainContentId = "main-content"; - protected sideNavService = inject(SideNavService); - protected drawerPortal = inject(DrawerHostDirective).portal; + protected drawerPortal = inject(DrawerService).portal; - focusMainContent() { - document.getElementById(this.mainContentId)?.focus(); + private mainContent = viewChild.required>("main"); + + protected focusMainContent() { + this.mainContent().nativeElement.focus(); } } diff --git a/libs/components/src/layout/scroll-layout.directive.ts b/libs/components/src/layout/scroll-layout.directive.ts new file mode 100644 index 00000000000..d3ea2bf557b --- /dev/null +++ b/libs/components/src/layout/scroll-layout.directive.ts @@ -0,0 +1,35 @@ +import { Directionality } from "@angular/cdk/bidi"; +import { CdkVirtualScrollable, ScrollDispatcher, VIRTUAL_SCROLLABLE } from "@angular/cdk/scrolling"; +import { Directive, ElementRef, NgZone, Optional } from "@angular/core"; + +@Directive({ + selector: "cdk-virtual-scroll-viewport[bitScrollLayout]", + standalone: true, + providers: [{ provide: VIRTUAL_SCROLLABLE, useExisting: ScrollLayoutDirective }], +}) +export class ScrollLayoutDirective extends CdkVirtualScrollable { + private mainRef: ElementRef; + + constructor(scrollDispatcher: ScrollDispatcher, ngZone: NgZone, @Optional() dir: Directionality) { + const mainEl = document.querySelector("main")!; + if (!mainEl) { + // eslint-disable-next-line no-console + console.error("HTML main element must be an ancestor of [bitScrollLayout]"); + } + const mainRef = new ElementRef(mainEl); + super(mainRef, scrollDispatcher, ngZone, dir); + this.mainRef = mainRef; + } + + override getElementRef(): ElementRef { + return this.mainRef; + } + + override measureBoundingClientRectWithScrollOffset( + from: "left" | "top" | "right" | "bottom", + ): number { + return ( + this.mainRef.nativeElement.getBoundingClientRect()[from] - this.measureScrollOffset(from) + ); + } +} diff --git a/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts index 7709506f050..904b9e11c3a 100644 --- a/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts @@ -3,15 +3,23 @@ import { Component, OnInit } from "@angular/core"; import { DialogModule, DialogService } from "../../../dialog"; import { IconButtonModule } from "../../../icon-button"; +import { ScrollLayoutDirective } from "../../../layout"; import { SectionComponent } from "../../../section"; import { TableDataSource, TableModule } from "../../../table"; @Component({ selector: "dialog-virtual-scroll-block", standalone: true, - imports: [DialogModule, IconButtonModule, SectionComponent, TableModule, ScrollingModule], + imports: [ + DialogModule, + IconButtonModule, + SectionComponent, + TableModule, + ScrollingModule, + ScrollLayoutDirective, + ], template: /*html*/ ` - + diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts index 70f56d2e28d..b62e669d369 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts @@ -12,8 +12,69 @@ import { KitchenSinkToggleList } from "./kitchen-sink-toggle-list.component"; standalone: true, imports: [KitchenSinkSharedModule], template: ` - - Dialog body text goes here. + + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+ + What did foo say to bar? + + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+
@@ -90,72 +151,6 @@ class KitchenSinkDialog {
- - - - -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

- - What did foo say to bar? - - -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-
-
`, }) export class KitchenSinkMainComponent { @@ -168,7 +163,7 @@ export class KitchenSinkMainComponent { } openDrawer() { - this.drawerOpen.set(true); + this.dialogService.openDrawer(KitchenSinkDialog); } navItems = [ diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts index f57a9de4e68..d318e1b5f0e 100644 --- a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts @@ -14,7 +14,6 @@ import { import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogService } from "../../dialog"; import { LayoutComponent } from "../../layout"; import { I18nMockService } from "../../utils/i18n-mock.service"; import { positionFixedWrapperDecorator } from "../storybook-decorators"; @@ -39,8 +38,20 @@ export default { KitchenSinkTable, KitchenSinkToggleList, ], + }), + applicationConfig({ providers: [ - DialogService, + provideNoopAnimations(), + importProvidersFrom( + RouterModule.forRoot( + [ + { path: "", redirectTo: "bitwarden", pathMatch: "full" }, + { path: "bitwarden", component: KitchenSinkMainComponent }, + { path: "virtual-scroll", component: DialogVirtualScrollBlockComponent }, + ], + { useHash: true }, + ), + ), { provide: I18nService, useFactory: () => { @@ -58,21 +69,6 @@ export default { }, ], }), - applicationConfig({ - providers: [ - provideNoopAnimations(), - importProvidersFrom( - RouterModule.forRoot( - [ - { path: "", redirectTo: "bitwarden", pathMatch: "full" }, - { path: "bitwarden", component: KitchenSinkMainComponent }, - { path: "virtual-scroll", component: DialogVirtualScrollBlockComponent }, - ], - { useHash: true }, - ), - ), - ], - }), ], } as Meta; diff --git a/libs/components/src/table/table-scroll.component.html b/libs/components/src/table/table-scroll.component.html index 8f2c88ba3ad..523912cd7ac 100644 --- a/libs/components/src/table/table-scroll.component.html +++ b/libs/components/src/table/table-scroll.component.html @@ -1,5 +1,5 @@ diff --git a/libs/components/src/table/table-scroll.component.ts b/libs/components/src/table/table-scroll.component.ts index 34cd8c5d9ca..e198da1aba0 100644 --- a/libs/components/src/table/table-scroll.component.ts +++ b/libs/components/src/table/table-scroll.component.ts @@ -21,6 +21,8 @@ import { TrackByFunction, } from "@angular/core"; +import { ScrollLayoutDirective } from "../layout"; + import { RowDirective } from "./row.directive"; import { TableComponent } from "./table.component"; @@ -58,6 +60,7 @@ export class BitRowDef { CdkFixedSizeVirtualScroll, CdkVirtualForOf, RowDirective, + ScrollLayoutDirective, ], }) export class TableScrollComponent diff --git a/libs/components/src/table/table.mdx b/libs/components/src/table/table.mdx index 8d784190ed9..59bf5b773a3 100644 --- a/libs/components/src/table/table.mdx +++ b/libs/components/src/table/table.mdx @@ -142,7 +142,7 @@ dataSource.filter = (data) => data.orgType === "family"; Rudimentary string filtering is supported out of the box with `TableDataSource.simpleStringFilter`. It works by converting each entry into a string of it's properties. The provided string is then -compared against the filter value using a simple `indexOf` check. For convienence, you can also just +compared against the filter value using a simple `indexOf` check. For convenience, you can also just pass a string directly. ```ts @@ -153,7 +153,7 @@ dataSource.filter = "search value"; ### Virtual Scrolling -It's heavily adviced to use virtual scrolling if you expect the table to have any significant amount +It's heavily advised to use virtual scrolling if you expect the table to have any significant amount of data. This is done by using the `bit-table-scroll` component instead of the `bit-table` component. This component behaves slightly different from the `bit-table` component. Instead of using the `*ngFor` directive to render the rows, you provide a `bitRowDef` template that will be @@ -178,6 +178,14 @@ height and align vertically. ``` +#### Deprecated approach + +Before `bit-table-scroll` was introduced, virtual scroll in tables was implemented manually via +constructs from Angular CDK. This included wrapping the table with a `cdk-virtual-scroll-viewport` +and targeting with `bit-layout`'s scroll container with the `bitScrollLayout` directive. + +This pattern is deprecated in favor of `bit-table-scroll`. + ## Accessibility - Always include a row or column header with your table; this allows assistive technology to better diff --git a/libs/components/src/table/table.stories.ts b/libs/components/src/table/table.stories.ts index e8ab24ee8b7..d696e6077dd 100644 --- a/libs/components/src/table/table.stories.ts +++ b/libs/components/src/table/table.stories.ts @@ -1,6 +1,13 @@ +import { RouterTestingModule } from "@angular/router/testing"; import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + import { countries } from "../form/countries"; +import { LayoutComponent } from "../layout"; +import { mockLayoutI18n } from "../layout/mocks"; +import { positionFixedWrapperDecorator } from "../stories/storybook-decorators"; +import { I18nMockService } from "../utils"; import { TableDataSource } from "./table-data-source"; import { TableModule } from "./table.module"; @@ -8,8 +15,17 @@ import { TableModule } from "./table.module"; export default { title: "Component Library/Table", decorators: [ + positionFixedWrapperDecorator(), moduleMetadata({ - imports: [TableModule], + imports: [TableModule, LayoutComponent, RouterTestingModule], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService(mockLayoutI18n); + }, + }, + ], }), ], argTypes: { @@ -116,18 +132,20 @@ export const Scrollable: Story = { trackBy: (index: number, item: any) => item.id, }, template: ` - - - Id - Name - Other - - - {{ row.id }} - {{ row.name }} - {{ row.other }} - - + + + + Id + Name + Other + + + {{ row.id }} + {{ row.name }} + {{ row.other }} + + + `, }), }; @@ -144,17 +162,19 @@ export const Filterable: Story = { sortFn: (a: any, b: any) => a.id - b.id, }, template: ` - - - - Name - Value - - - {{ row.name }} - {{ row.value }} - - + + + + + Name + Value + + + {{ row.name }} + {{ row.value }} + + + `, }), }; diff --git a/libs/components/src/utils/has-scrolled-from.ts b/libs/components/src/utils/has-scrolled-from.ts new file mode 100644 index 00000000000..44c73465bdd --- /dev/null +++ b/libs/components/src/utils/has-scrolled-from.ts @@ -0,0 +1,41 @@ +import { CdkScrollable } from "@angular/cdk/scrolling"; +import { Signal, inject, signal } from "@angular/core"; +import { toObservable, toSignal } from "@angular/core/rxjs-interop"; +import { map, startWith, switchMap } from "rxjs"; + +export type ScrollState = { + /** `true` when the scrollbar is not at the top-most position */ + top: boolean; + + /** `true` when the scrollbar is not at the bottom-most position */ + bottom: boolean; +}; + +/** + * Check if a `CdkScrollable` instance has been scrolled + * @param scrollable The instance to check, defaults to the one provided by the current injector + * @returns {Signal} + */ +export const hasScrolledFrom = (scrollable?: Signal): Signal => { + const _scrollable = scrollable ?? signal(inject(CdkScrollable)); + const scrollable$ = toObservable(_scrollable); + + const scrollState$ = scrollable$.pipe( + switchMap((_scrollable) => + _scrollable.elementScrolled().pipe( + startWith(null), + map(() => ({ + top: _scrollable.measureScrollOffset("top") > 0, + bottom: _scrollable.measureScrollOffset("bottom") > 0, + })), + ), + ), + ); + + return toSignal(scrollState$, { + initialValue: { + top: false, + bottom: false, + }, + }); +}; From 623deea4fcf6543e34b95950ee2b5d6eb0893e56 Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Thu, 15 May 2025 10:36:57 -0400 Subject: [PATCH 053/163] Adding at risk cipher ids for accurate notifications (#14784) --- .../src/dirt/reports/risk-insights/models/password-health.ts | 1 + .../risk-insights/services/risk-insights-report.service.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts index 27e135667b4..d24d8386ecd 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts @@ -27,6 +27,7 @@ export type ApplicationHealthReportDetail = { applicationName: string; passwordCount: number; atRiskPasswordCount: number; + atRiskCipherIds: string[]; memberCount: number; atRiskMemberCount: number; memberDetails: MemberDetailsFlat[]; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts index a503d24cad7..e4fece801b6 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts @@ -354,12 +354,15 @@ export class RiskInsightsReportService { : newUriDetail.cipherMembers, atRiskMemberDetails: existingUriDetail ? existingUriDetail.atRiskMemberDetails : [], atRiskPasswordCount: existingUriDetail ? existingUriDetail.atRiskPasswordCount : 0, + atRiskCipherIds: existingUriDetail ? existingUriDetail.atRiskCipherIds : [], atRiskMemberCount: existingUriDetail ? existingUriDetail.atRiskMemberDetails.length : 0, cipher: newUriDetail.cipher, } as ApplicationHealthReportDetail; if (isAtRisk) { reportDetail.atRiskPasswordCount = reportDetail.atRiskPasswordCount + 1; + reportDetail.atRiskCipherIds.push(newUriDetail.cipherId); + reportDetail.atRiskMemberDetails = this.getUniqueMembers( reportDetail.atRiskMemberDetails.concat(newUriDetail.cipherMembers), ); From ac49e594c1957734bfc14b5d0c04738d501d65f9 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 15 May 2025 16:44:07 +0200 Subject: [PATCH 054/163] Add standalone false to all non migrated (#14797) Adds standalone: false to all components since Angular is changing the default to true and we'd rather not have the angular PR change 300+ files. --- .../extension-anon-layout-wrapper.stories.ts | 4 ++++ apps/browser/src/auth/popup/set-password.component.ts | 1 + .../popup/settings/vault-timeout-input.component.ts | 1 + .../src/auth/popup/update-temp-password.component.ts | 1 + .../key-connector/remove-password.component.ts | 1 + .../popup/view-cache/popup-router-cache.spec.ts | 5 ++++- .../platform/popup/view-cache/popup-view-cache.spec.ts | 10 ++++++++-- apps/browser/src/popup/app.component.ts | 1 + .../popup/components/user-verification.component.ts | 1 + apps/browser/src/popup/tabs-v2.component.ts | 1 + apps/desktop/src/app/accounts/settings.component.ts | 1 + .../src/app/accounts/vault-timeout-input.component.ts | 1 + apps/desktop/src/app/app.component.ts | 1 + apps/desktop/src/app/components/avatar.component.ts | 1 + .../src/app/layout/account-switcher.component.ts | 1 + apps/desktop/src/app/layout/header.component.ts | 1 + apps/desktop/src/app/layout/search/search.component.ts | 1 + apps/desktop/src/auth/set-password.component.ts | 1 + .../desktop/src/auth/update-temp-password.component.ts | 1 + .../src/billing/app/accounts/premium.component.ts | 1 + .../key-connector/remove-password.component.ts | 1 + .../app/vault/add-edit-custom-fields.component.ts | 1 + apps/desktop/src/vault/app/vault/add-edit.component.ts | 1 + .../src/vault/app/vault/attachments.component.ts | 1 + .../src/vault/app/vault/collections.component.ts | 1 + .../src/vault/app/vault/folder-add-edit.component.ts | 1 + .../src/vault/app/vault/password-history.component.ts | 1 + apps/desktop/src/vault/app/vault/share.component.ts | 1 + .../filters/collection-filter.component.ts | 1 + .../vault-filter/filters/folder-filter.component.ts | 1 + .../filters/organization-filter.component.ts | 1 + .../vault-filter/filters/status-filter.component.ts | 1 + .../vault-filter/filters/type-filter.component.ts | 1 + .../app/vault/vault-filter/vault-filter.component.ts | 1 + .../src/vault/app/vault/vault-items.component.ts | 1 + apps/desktop/src/vault/app/vault/vault.component.ts | 1 + .../vault/app/vault/view-custom-fields.component.ts | 1 + apps/desktop/src/vault/app/vault/view.component.ts | 1 + .../group-badge/group-name-badge.component.ts | 1 + .../collections/vault-filter/vault-filter.component.ts | 1 + .../create/organization-information.component.ts | 1 + .../guards/is-enterprise-org.guard.spec.ts | 3 +++ .../organizations/guards/is-paid-org.guard.spec.ts | 3 +++ .../organizations/guards/org-redirect.guard.spec.ts | 3 +++ .../organizations/manage/events.component.ts | 1 + .../organizations/manage/group-add-edit.component.ts | 1 + .../organizations/manage/groups.component.ts | 1 + .../organizations/manage/user-confirm.component.ts | 1 + .../components/bulk/bulk-confirm-dialog.component.ts | 1 + .../components/bulk/bulk-delete-dialog.component.ts | 1 + .../components/bulk/bulk-enable-sm-dialog.component.ts | 1 + .../components/bulk/bulk-remove-dialog.component.ts | 1 + .../components/bulk/bulk-restore-revoke.component.ts | 1 + .../members/components/bulk/bulk-status.component.ts | 1 + .../member-dialog/member-dialog.component.ts | 1 + .../member-dialog/nested-checkbox.component.ts | 1 + .../members/components/reset-password.component.ts | 1 + .../organizations/members/members.component.ts | 1 + .../organizations/policies/disable-send.component.ts | 1 + .../policies/master-password.component.ts | 1 + .../policies/password-generator.component.ts | 1 + .../policies/personal-ownership.component.ts | 1 + .../organizations/policies/policies.component.ts | 1 + .../organizations/policies/policy-edit.component.ts | 1 + .../policies/remove-unlock-with-pin.component.ts | 1 + .../organizations/policies/require-sso.component.ts | 1 + .../organizations/policies/reset-password.component.ts | 1 + .../organizations/policies/send-options.component.ts | 1 + .../organizations/policies/single-org.component.ts | 1 + .../policies/two-factor-authentication.component.ts | 1 + .../organizations/reporting/reports-home.component.ts | 1 + .../organizations/settings/account.component.ts | 1 + .../settings/two-factor-setup.component.ts | 1 + .../access-selector/access-selector.component.ts | 1 + .../components/access-selector/user-type.pipe.ts | 1 + .../accept-family-sponsorship.component.ts | 1 + apps/web/src/app/app.component.ts | 1 + apps/web/src/app/auth/guards/deep-link.guard.spec.ts | 3 +++ .../login-via-webauthn/login-via-webauthn.component.ts | 1 + .../accept-organization.component.ts | 1 + apps/web/src/app/auth/recover-delete.component.ts | 1 + apps/web/src/app/auth/recover-two-factor.component.ts | 1 + apps/web/src/app/auth/set-password.component.ts | 1 + .../src/app/auth/settings/account/account.component.ts | 1 + .../settings/account/change-avatar-dialog.component.ts | 1 + .../auth/settings/account/change-email.component.ts | 1 + .../settings/account/deauthorize-sessions.component.ts | 1 + .../account/delete-account-dialog.component.ts | 1 + .../src/app/auth/settings/account/profile.component.ts | 1 + .../settings/account/selectable-avatar.component.ts | 1 + .../src/app/auth/settings/change-password.component.ts | 1 + .../confirm/emergency-access-confirm.component.ts | 1 + .../emergency-access-add-edit.component.ts | 1 + .../emergency-access/emergency-access.component.ts | 1 + .../takeover/emergency-access-takeover.component.ts | 1 + .../view/emergency-access-view.component.ts | 1 + .../app/auth/settings/security/api-key.component.ts | 1 + .../change-kdf/change-kdf-confirmation.component.ts | 1 + .../security/change-kdf/change-kdf.component.ts | 1 + .../auth/settings/security/security-keys.component.ts | 1 + .../app/auth/settings/security/security.component.ts | 1 + .../create-credential-dialog.component.ts | 1 + .../delete-credential-dialog.component.ts | 1 + .../enable-encryption-dialog.component.ts | 1 + .../webauthn-login-settings.component.ts | 1 + .../user-verification-prompt.component.ts | 1 + .../user-verification/user-verification.component.ts | 1 + apps/web/src/app/auth/update-password.component.ts | 1 + .../web/src/app/auth/update-temp-password.component.ts | 1 + apps/web/src/app/auth/verify-email-token.component.ts | 1 + .../src/app/auth/verify-recover-delete.component.ts | 1 + .../individual/billing-history-view.component.ts | 1 + .../billing/individual/premium/premium.component.ts | 1 + .../app/billing/individual/subscription.component.ts | 1 + .../billing/individual/user-subscription.component.ts | 1 + .../members/free-bitwarden-families.component.ts | 1 + .../organizations/adjust-subscription.component.ts | 1 + .../organizations/billing-sync-api-key.component.ts | 1 + .../organizations/billing-sync-key.component.ts | 1 + .../app/billing/organizations/change-plan.component.ts | 1 + .../organizations/download-license.component.ts | 1 + .../organization-billing-history-view.component.ts | 1 + .../organization-subscription-cloud.component.ts | 1 + .../organization-subscription-selfhost.component.ts | 1 + .../organization-payment-method.component.ts | 1 + .../organizations/sm-adjust-subscription.component.ts | 1 + .../organizations/sm-subscribe-standalone.component.ts | 1 + .../organizations/subscription-hidden.component.ts | 1 + .../organizations/subscription-status.component.ts | 1 + .../billing/settings/sponsored-families.component.ts | 1 + .../billing/settings/sponsoring-org-row.component.ts | 1 + .../app/billing/shared/add-credit-dialog.component.ts | 1 + .../adjust-payment-dialog.component.ts | 1 + .../adjust-storage-dialog.component.ts | 1 + .../app/billing/shared/billing-history.component.ts | 1 + .../app/billing/shared/offboarding-survey.component.ts | 1 + .../src/app/billing/shared/payment-method.component.ts | 1 + ...dividual-self-hosting-license-uploader.component.ts | 1 + ...nization-self-hosting-license-uploader.component.ts | 1 + .../src/app/billing/shared/sm-subscribe.component.ts | 1 + .../billing/shared/update-license-dialog.component.ts | 1 + .../src/app/billing/shared/update-license.component.ts | 1 + .../complete-trial-initiation.component.ts | 1 + .../trial-initiation/confirmation-details.component.ts | 1 + .../vertical-step-content.component.ts | 1 + .../vertical-stepper/vertical-step.component.ts | 1 + .../vertical-stepper/vertical-stepper.component.ts | 1 + .../environment-selector.component.ts | 1 + .../app/dirt/reports/pages/breach-report.component.ts | 1 + .../pages/exposed-passwords-report.component.ts | 1 + .../pages/inactive-two-factor-report.component.ts | 1 + .../exposed-passwords-report.component.ts | 1 + .../inactive-two-factor-report.component.ts | 1 + .../organizations/reused-passwords-report.component.ts | 1 + .../unsecured-websites-report.component.ts | 1 + .../organizations/weak-passwords-report.component.ts | 1 + .../app/dirt/reports/pages/reports-home.component.ts | 1 + .../reports/pages/reused-passwords-report.component.ts | 1 + .../pages/unsecured-websites-report.component.ts | 1 + .../reports/pages/weak-passwords-report.component.ts | 1 + .../src/app/dirt/reports/reports-layout.component.ts | 1 + .../shared/report-card/report-card.component.ts | 1 + .../shared/report-list/report-list.component.ts | 1 + .../key-connector/remove-password.component.ts | 1 + apps/web/src/app/layouts/frontend-layout.component.ts | 1 + .../web/src/app/layouts/header/web-header.component.ts | 1 + .../navigation-switcher.component.ts | 1 + .../navigation-switcher/navigation-switcher.stories.ts | 4 ++++ .../product-switcher-content.component.ts | 1 + .../product-switcher/product-switcher.component.ts | 1 + .../product-switcher/product-switcher.stories.ts | 4 ++++ apps/web/src/app/settings/domain-rules.component.ts | 1 + apps/web/src/app/settings/preferences.component.ts | 1 + .../components/onboarding/onboarding-task.component.ts | 1 + .../components/onboarding/onboarding.component.ts | 1 + .../app/vault/components/premium-badge.component.ts | 1 + .../vault-items/vault-cipher-row.component.ts | 1 + .../vault-items/vault-collection-row.component.ts | 1 + .../components/vault-items/vault-items.component.ts | 3 +-- .../bulk-delete-dialog/bulk-delete-dialog.component.ts | 1 + .../bulk-move-dialog/bulk-move-dialog.component.ts | 1 + .../individual-vault/folder-add-edit.component.ts | 1 + .../organization-name-badge.component.ts | 1 + .../individual-vault/pipes/get-group-name.pipe.ts | 1 + .../pipes/get-organization-name.pipe.ts | 1 + .../components/organization-options.component.ts | 1 + .../vault-filter/components/vault-filter.component.ts | 1 + .../components/vault-filter-section.component.ts | 1 + .../src/app/vault/settings/purge-vault.component.ts | 1 + .../domain-add-edit-dialog.component.ts | 1 + .../domain-verification.component.ts | 1 + .../organizations/manage/scim.component.ts | 1 + .../policies/activate-autofill.component.ts | 1 + .../policies/automatic-app-login.component.ts | 1 + .../disable-personal-vault-export.component.ts | 1 + .../policies/maximum-vault-timeout.component.ts | 1 + .../providers/clients/add-organization.component.ts | 1 + .../providers/clients/create-organization.component.ts | 1 + .../providers/manage/accept-provider.component.ts | 1 + .../manage/dialogs/add-edit-member-dialog.component.ts | 1 + .../manage/dialogs/bulk-confirm-dialog.component.ts | 1 + .../manage/dialogs/bulk-remove-dialog.component.ts | 1 + .../admin-console/providers/manage/events.component.ts | 1 + .../providers/manage/members.component.ts | 1 + .../app/admin-console/providers/providers.component.ts | 1 + .../providers/settings/account.component.ts | 1 + .../providers/setup/setup-provider.component.ts | 1 + .../admin-console/providers/setup/setup.component.ts | 1 + .../verify-recover-delete-provider.component.ts | 1 + bitwarden_license/bit-web/src/app/app.component.ts | 1 + .../bit-web/src/app/auth/sso/sso.component.ts | 1 + .../policies/free-families-sponsorship.component.ts | 1 + .../provider-billing-history.component.ts | 1 + .../add-existing-organization-dialog.component.ts | 1 + .../clients/create-client-dialog.component.ts | 1 + .../clients/manage-client-name-dialog.component.ts | 1 + .../manage-client-subscription-dialog.component.ts | 1 + .../providers/setup/setup-business-unit.component.ts | 1 + .../provider-subscription-status.component.ts | 1 + .../subscription/provider-subscription.component.ts | 1 + .../integrations/integrations.component.spec.ts | 2 ++ .../integrations/integrations.component.ts | 1 + .../src/app/secrets-manager/layout/layout.component.ts | 1 + .../app/secrets-manager/layout/navigation.component.ts | 1 + .../app/secrets-manager/overview/overview.component.ts | 1 + .../app/secrets-manager/overview/section.component.ts | 1 + .../projects/dialog/project-delete-dialog.component.ts | 1 + .../projects/dialog/project-dialog.component.ts | 1 + .../projects/guards/project-access.guard.spec.ts | 2 ++ .../projects/project/project-people.component.ts | 1 + .../projects/project/project-secrets.component.ts | 1 + .../project/project-service-accounts.component.ts | 1 + .../projects/project/project.component.ts | 1 + .../projects/projects/projects.component.ts | 1 + .../secrets/dialog/secret-delete.component.ts | 1 + .../secrets/dialog/secret-dialog.component.ts | 1 + .../secrets/dialog/secret-view-dialog.component.ts | 1 + .../app/secrets-manager/secrets/secrets.component.ts | 1 + .../service-accounts/access/access-list.component.ts | 1 + .../service-accounts/access/access-tokens.component.ts | 1 + .../dialogs/access-token-create-dialog.component.ts | 1 + .../access/dialogs/access-token-dialog.component.ts | 1 + .../access/dialogs/expiration-options.component.ts | 1 + .../service-accounts/config/config.component.ts | 1 + .../dialog/service-account-delete-dialog.component.ts | 1 + .../dialog/service-account-dialog.component.ts | 1 + .../event-logs/service-accounts-events.component.ts | 1 + .../guards/service-account-access.guard.spec.ts | 2 ++ .../people/service-account-people.component.ts | 1 + .../projects/service-account-projects.component.ts | 1 + .../service-accounts/service-account.component.ts | 1 + .../service-accounts-list.component.ts | 1 + .../service-accounts/service-accounts.component.ts | 1 + .../dialog/sm-import-error-dialog.component.ts | 1 + .../settings/porting/sm-export.component.ts | 1 + .../settings/porting/sm-import.component.ts | 1 + .../access-policy-selector.component.ts | 1 + .../dialogs/bulk-confirmation-dialog.component.ts | 1 + .../shared/dialogs/bulk-status-dialog.component.ts | 1 + .../app/secrets-manager/shared/new-menu.component.ts | 1 + .../secrets-manager/shared/org-suspended.component.ts | 1 + .../secrets-manager/shared/projects-list.component.ts | 1 + .../secrets-manager/shared/secrets-list.component.ts | 1 + .../trash/dialog/secret-hard-delete.component.ts | 1 + .../trash/dialog/secret-restore.component.ts | 1 + .../src/app/secrets-manager/trash/trash.component.ts | 1 + .../auth/components/environment-selector.component.ts | 1 + .../src/auth/components/two-factor-icon.component.ts | 1 + .../src/auth/components/user-verification.component.ts | 1 + .../add-account-credit-dialog.component.ts | 1 + .../billing/components/invoices/invoices.component.ts | 1 + .../components/invoices/no-invoices.component.ts | 1 + .../manage-tax-information.component.ts | 1 + .../src/billing/directives/not-premium.directive.ts | 1 + libs/angular/src/components/callout.component.ts | 1 + .../src/components/modal/dynamic-modal.component.ts | 1 + libs/angular/src/directives/a11y-invalid.directive.ts | 1 + libs/angular/src/directives/api-action.directive.ts | 1 + libs/angular/src/directives/box-row.directive.ts | 1 + libs/angular/src/directives/copy-text.directive.ts | 1 + libs/angular/src/directives/fallback-src.directive.ts | 1 + libs/angular/src/directives/if-feature.directive.ts | 1 + .../src/directives/input-strip-spaces.directive.ts | 1 + .../angular/src/directives/input-verbatim.directive.ts | 1 + libs/angular/src/directives/launch-click.directive.ts | 1 + libs/angular/src/directives/stop-click.directive.ts | 1 + libs/angular/src/directives/stop-prop.directive.ts | 1 + .../src/directives/true-false-value.directive.ts | 1 + libs/angular/src/pipes/color-password-count.pipe.ts | 5 ++++- libs/angular/src/pipes/color-password.pipe.ts | 5 ++++- libs/angular/src/pipes/credit-card-number.pipe.ts | 5 ++++- libs/angular/src/pipes/search-ciphers.pipe.ts | 1 + libs/angular/src/pipes/search.pipe.ts | 1 + libs/angular/src/pipes/user-name.pipe.ts | 1 + libs/angular/src/pipes/user-type.pipe.ts | 1 + libs/angular/src/platform/pipes/ellipsis.pipe.ts | 1 + libs/angular/src/platform/pipes/fingerprint.pipe.ts | 1 + libs/angular/src/platform/pipes/i18n.pipe.ts | 1 + .../password-strength/password-strength.component.ts | 1 + libs/angular/src/vault/components/icon.component.ts | 1 + libs/components/src/app/app.component.ts | 1 + libs/components/src/input/autofocus.directive.ts | 1 + .../components/src/catchall-settings.component.ts | 1 + .../components/src/credential-generator.component.ts | 1 + .../components/src/forwarder-settings.component.ts | 1 + .../components/src/passphrase-settings.component.ts | 1 + .../components/src/password-generator.component.ts | 1 + .../components/src/password-settings.component.ts | 1 + .../components/src/subaddress-settings.component.ts | 1 + .../components/src/username-generator.component.ts | 1 + .../components/src/username-settings.component.ts | 1 + 311 files changed, 350 insertions(+), 8 deletions(-) diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts index a0990485d49..78ca577a69d 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts @@ -168,18 +168,21 @@ type Story = StoryObj; @Component({ selector: "bit-default-primary-outlet-example-component", template: "

Primary Outlet Example:
your primary component goes here

", + standalone: false, }) class DefaultPrimaryOutletExampleComponent {} @Component({ selector: "bit-default-secondary-outlet-example-component", template: "

Secondary Outlet Example:
your secondary component goes here

", + standalone: false, }) class DefaultSecondaryOutletExampleComponent {} @Component({ selector: "bit-default-env-selector-outlet-example-component", template: "

Env Selector Outlet Example:
your env selector component goes here

", + standalone: false, }) class DefaultEnvSelectorOutletExampleComponent {} @@ -264,6 +267,7 @@ const changedData: ExtensionAnonLayoutWrapperData = { template: ` `, + standalone: false, }) export class DynamicContentExampleComponent { initialData = true; diff --git a/apps/browser/src/auth/popup/set-password.component.ts b/apps/browser/src/auth/popup/set-password.component.ts index accde2e9a09..2a796854531 100644 --- a/apps/browser/src/auth/popup/set-password.component.ts +++ b/apps/browser/src/auth/popup/set-password.component.ts @@ -5,5 +5,6 @@ import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/ang @Component({ selector: "app-set-password", templateUrl: "set-password.component.html", + standalone: false, }) export class SetPasswordComponent extends BaseSetPasswordComponent {} diff --git a/apps/browser/src/auth/popup/settings/vault-timeout-input.component.ts b/apps/browser/src/auth/popup/settings/vault-timeout-input.component.ts index c56e6578a0b..25a4d01333d 100644 --- a/apps/browser/src/auth/popup/settings/vault-timeout-input.component.ts +++ b/apps/browser/src/auth/popup/settings/vault-timeout-input.component.ts @@ -18,5 +18,6 @@ import { VaultTimeoutInputComponent as VaultTimeoutInputComponentBase } from "@b useExisting: VaultTimeoutInputComponent, }, ], + standalone: false, }) export class VaultTimeoutInputComponent extends VaultTimeoutInputComponentBase {} diff --git a/apps/browser/src/auth/popup/update-temp-password.component.ts b/apps/browser/src/auth/popup/update-temp-password.component.ts index 465bc3f7038..e8cf64b7548 100644 --- a/apps/browser/src/auth/popup/update-temp-password.component.ts +++ b/apps/browser/src/auth/popup/update-temp-password.component.ts @@ -8,6 +8,7 @@ import { postLogoutMessageListener$ } from "./utils/post-logout-message-listener @Component({ selector: "app-update-temp-password", templateUrl: "update-temp-password.component.html", + standalone: false, }) export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent { onSuccessfulChangePassword: () => Promise = this.doOnSuccessfulChangePassword.bind(this); diff --git a/apps/browser/src/key-management/key-connector/remove-password.component.ts b/apps/browser/src/key-management/key-connector/remove-password.component.ts index 3ca9d3a5669..1b07f04ba8a 100644 --- a/apps/browser/src/key-management/key-connector/remove-password.component.ts +++ b/apps/browser/src/key-management/key-connector/remove-password.component.ts @@ -5,5 +5,6 @@ import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitward @Component({ selector: "app-remove-password", templateUrl: "remove-password.component.html", + standalone: false, }) export class RemovePasswordComponent extends BaseRemovePasswordComponent {} diff --git a/apps/browser/src/platform/popup/view-cache/popup-router-cache.spec.ts b/apps/browser/src/platform/popup/view-cache/popup-router-cache.spec.ts index 465a6e6c69c..22fb7bf99b9 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-router-cache.spec.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-router-cache.spec.ts @@ -13,7 +13,10 @@ import { PopupRouterCacheService, popupRouterCacheGuard } from "./popup-router-c const flushPromises = async () => await new Promise(process.nextTick); -@Component({ template: "" }) +@Component({ + template: "", + standalone: false, +}) export class EmptyComponent {} describe("Popup router cache guard", () => { diff --git a/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts index 2ec75791d1b..60baf94eeae 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts @@ -19,10 +19,16 @@ import { import { PopupViewCacheService } from "./popup-view-cache.service"; -@Component({ template: "" }) +@Component({ + template: "", + standalone: false, +}) export class EmptyComponent {} -@Component({ template: "" }) +@Component({ + template: "", + standalone: false, +}) export class TestComponent { private viewCacheService = inject(PopupViewCacheService); diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index a480e1d6ba3..5f7fbc1fad7 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -43,6 +43,7 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn
`, + standalone: false, }) export class AppComponent implements OnInit, OnDestroy { private compactModeService = inject(PopupCompactModeService); diff --git a/apps/browser/src/popup/components/user-verification.component.ts b/apps/browser/src/popup/components/user-verification.component.ts index 6befc8973b0..f6cb6cdff12 100644 --- a/apps/browser/src/popup/components/user-verification.component.ts +++ b/apps/browser/src/popup/components/user-verification.component.ts @@ -22,5 +22,6 @@ import { UserVerificationComponent as BaseComponent } from "@bitwarden/angular/a transition(":enter", [style({ opacity: 0 }), animate("100ms", style({ opacity: 1 }))]), ]), ], + standalone: false, }) export class UserVerificationComponent extends BaseComponent {} diff --git a/apps/browser/src/popup/tabs-v2.component.ts b/apps/browser/src/popup/tabs-v2.component.ts index 63b539fddce..5df8fb85d6d 100644 --- a/apps/browser/src/popup/tabs-v2.component.ts +++ b/apps/browser/src/popup/tabs-v2.component.ts @@ -13,6 +13,7 @@ import { NavButton } from "../platform/popup/layout/popup-tab-navigation.compone @Component({ selector: "app-tabs-v2", templateUrl: "./tabs-v2.component.html", + standalone: false, }) export class TabsV2Component { private hasActiveBadges$ = this.accountService.activeAccount$ diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 2639d819854..83c982fbaba 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -54,6 +54,7 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man @Component({ selector: "app-settings", templateUrl: "settings.component.html", + standalone: false, }) export class SettingsComponent implements OnInit, OnDestroy { // For use in template diff --git a/apps/desktop/src/app/accounts/vault-timeout-input.component.ts b/apps/desktop/src/app/accounts/vault-timeout-input.component.ts index c56e6578a0b..25a4d01333d 100644 --- a/apps/desktop/src/app/accounts/vault-timeout-input.component.ts +++ b/apps/desktop/src/app/accounts/vault-timeout-input.component.ts @@ -18,5 +18,6 @@ import { VaultTimeoutInputComponent as VaultTimeoutInputComponentBase } from "@b useExisting: VaultTimeoutInputComponent, }, ], + standalone: false, }) export class VaultTimeoutInputComponent extends VaultTimeoutInputComponentBase {} diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index c3cfdf49c2c..38c5ca3a2a8 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -93,6 +93,7 @@ const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours `, + standalone: false, }) export class AppComponent implements OnInit, OnDestroy { @ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef; diff --git a/apps/desktop/src/app/components/avatar.component.ts b/apps/desktop/src/app/components/avatar.component.ts index d2660763667..1fba864686c 100644 --- a/apps/desktop/src/app/components/avatar.component.ts +++ b/apps/desktop/src/app/components/avatar.component.ts @@ -8,6 +8,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; @Component({ selector: "app-avatar", template: ``, + standalone: false, }) export class AvatarComponent implements OnChanges, OnInit { @Input() size = 45; diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts index d8ffa5ae546..a54674c3a1e 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.ts +++ b/apps/desktop/src/app/layout/account-switcher.component.ts @@ -54,6 +54,7 @@ type InactiveAccount = ActiveAccount & { transition("* => void", animate("100ms linear", style({ opacity: 0 }))), ]), ], + standalone: false, }) export class AccountSwitcherComponent implements OnInit { activeAccount$: Observable; diff --git a/apps/desktop/src/app/layout/header.component.ts b/apps/desktop/src/app/layout/header.component.ts index 1cf697ad4ed..9aef093423f 100644 --- a/apps/desktop/src/app/layout/header.component.ts +++ b/apps/desktop/src/app/layout/header.component.ts @@ -3,5 +3,6 @@ import { Component } from "@angular/core"; @Component({ selector: "app-header", templateUrl: "header.component.html", + standalone: false, }) export class HeaderComponent {} diff --git a/apps/desktop/src/app/layout/search/search.component.ts b/apps/desktop/src/app/layout/search/search.component.ts index a9faf2a2414..70196d74dda 100644 --- a/apps/desktop/src/app/layout/search/search.component.ts +++ b/apps/desktop/src/app/layout/search/search.component.ts @@ -11,6 +11,7 @@ import { SearchBarService, SearchBarState } from "./search-bar.service"; @Component({ selector: "app-search", templateUrl: "search.component.html", + standalone: false, }) export class SearchComponent implements OnInit, OnDestroy { state: SearchBarState; diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index 5a78fb08c47..48b18d7294c 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -28,6 +28,7 @@ const BroadcasterSubscriptionId = "SetPasswordComponent"; @Component({ selector: "app-set-password", templateUrl: "set-password.component.html", + standalone: false, }) export class SetPasswordComponent extends BaseSetPasswordComponent implements OnInit, OnDestroy { constructor( diff --git a/apps/desktop/src/auth/update-temp-password.component.ts b/apps/desktop/src/auth/update-temp-password.component.ts index f7d952b97f4..ead10660b92 100644 --- a/apps/desktop/src/auth/update-temp-password.component.ts +++ b/apps/desktop/src/auth/update-temp-password.component.ts @@ -5,5 +5,6 @@ import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from " @Component({ selector: "app-update-temp-password", templateUrl: "update-temp-password.component.html", + standalone: false, }) export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {} diff --git a/apps/desktop/src/billing/app/accounts/premium.component.ts b/apps/desktop/src/billing/app/accounts/premium.component.ts index 1b4573fe6d6..5d0fa7a5dde 100644 --- a/apps/desktop/src/billing/app/accounts/premium.component.ts +++ b/apps/desktop/src/billing/app/accounts/premium.component.ts @@ -13,6 +13,7 @@ import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "app-premium", templateUrl: "premium.component.html", + standalone: false, }) export class PremiumComponent extends BasePremiumComponent { constructor( diff --git a/apps/desktop/src/key-management/key-connector/remove-password.component.ts b/apps/desktop/src/key-management/key-connector/remove-password.component.ts index 3ca9d3a5669..1b07f04ba8a 100644 --- a/apps/desktop/src/key-management/key-connector/remove-password.component.ts +++ b/apps/desktop/src/key-management/key-connector/remove-password.component.ts @@ -5,5 +5,6 @@ import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitward @Component({ selector: "app-remove-password", templateUrl: "remove-password.component.html", + standalone: false, }) export class RemovePasswordComponent extends BaseRemovePasswordComponent {} diff --git a/apps/desktop/src/vault/app/vault/add-edit-custom-fields.component.ts b/apps/desktop/src/vault/app/vault/add-edit-custom-fields.component.ts index 6992455a8a6..b4be2406c4b 100644 --- a/apps/desktop/src/vault/app/vault/add-edit-custom-fields.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit-custom-fields.component.ts @@ -7,6 +7,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic @Component({ selector: "app-vault-add-edit-custom-fields", templateUrl: "add-edit-custom-fields.component.html", + standalone: false, }) export class AddEditCustomFieldsComponent extends BaseAddEditCustomFieldsComponent { constructor(i18nService: I18nService, eventCollectionService: EventCollectionService) { diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.ts b/apps/desktop/src/vault/app/vault/add-edit.component.ts index 2c8b5a8321a..eb04003a418 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -30,6 +30,7 @@ const BroadcasterSubscriptionId = "AddEditComponent"; @Component({ selector: "app-vault-add-edit", templateUrl: "add-edit.component.html", + standalone: false, }) export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnChanges, OnDestroy { @ViewChild("form") diff --git a/apps/desktop/src/vault/app/vault/attachments.component.ts b/apps/desktop/src/vault/app/vault/attachments.component.ts index a2cea5f2722..a116a4d2acb 100644 --- a/apps/desktop/src/vault/app/vault/attachments.component.ts +++ b/apps/desktop/src/vault/app/vault/attachments.component.ts @@ -18,6 +18,7 @@ import { KeyService } from "@bitwarden/key-management"; @Component({ selector: "app-vault-attachments", templateUrl: "attachments.component.html", + standalone: false, }) export class AttachmentsComponent extends BaseAttachmentsComponent { constructor( diff --git a/apps/desktop/src/vault/app/vault/collections.component.ts b/apps/desktop/src/vault/app/vault/collections.component.ts index e7684c3c07a..46455d04cd2 100644 --- a/apps/desktop/src/vault/app/vault/collections.component.ts +++ b/apps/desktop/src/vault/app/vault/collections.component.ts @@ -13,6 +13,7 @@ import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-vault-collections", templateUrl: "collections.component.html", + standalone: false, }) export class CollectionsComponent extends BaseCollectionsComponent { constructor( diff --git a/apps/desktop/src/vault/app/vault/folder-add-edit.component.ts b/apps/desktop/src/vault/app/vault/folder-add-edit.component.ts index cdb879693c0..cecd5cd671c 100644 --- a/apps/desktop/src/vault/app/vault/folder-add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/folder-add-edit.component.ts @@ -14,6 +14,7 @@ import { KeyService } from "@bitwarden/key-management"; @Component({ selector: "app-folder-add-edit", templateUrl: "folder-add-edit.component.html", + standalone: false, }) export class FolderAddEditComponent extends BaseFolderAddEditComponent { constructor( diff --git a/apps/desktop/src/vault/app/vault/password-history.component.ts b/apps/desktop/src/vault/app/vault/password-history.component.ts index 4a87617d8f4..e83ce0d56ea 100644 --- a/apps/desktop/src/vault/app/vault/password-history.component.ts +++ b/apps/desktop/src/vault/app/vault/password-history.component.ts @@ -10,6 +10,7 @@ import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-password-history", templateUrl: "password-history.component.html", + standalone: false, }) export class PasswordHistoryComponent extends BasePasswordHistoryComponent { constructor( diff --git a/apps/desktop/src/vault/app/vault/share.component.ts b/apps/desktop/src/vault/app/vault/share.component.ts index 6926e7e2abf..50842439323 100644 --- a/apps/desktop/src/vault/app/vault/share.component.ts +++ b/apps/desktop/src/vault/app/vault/share.component.ts @@ -13,6 +13,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi @Component({ selector: "app-vault-share", templateUrl: "share.component.html", + standalone: false, }) export class ShareComponent extends BaseShareComponent { constructor( diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.ts b/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.ts index 161c9ae5353..22372410e5b 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.ts @@ -5,5 +5,6 @@ import { CollectionFilterComponent as BaseCollectionFilterComponent } from "@bit @Component({ selector: "app-collection-filter", templateUrl: "collection-filter.component.html", + standalone: false, }) export class CollectionFilterComponent extends BaseCollectionFilterComponent {} diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/folder-filter.component.ts b/apps/desktop/src/vault/app/vault/vault-filter/filters/folder-filter.component.ts index 790d31a65e6..d7364808f6d 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/folder-filter.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/folder-filter.component.ts @@ -5,5 +5,6 @@ import { FolderFilterComponent as BaseFolderFilterComponent } from "@bitwarden/a @Component({ selector: "app-folder-filter", templateUrl: "folder-filter.component.html", + standalone: false, }) export class FolderFilterComponent extends BaseFolderFilterComponent {} diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.ts b/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.ts index 39f1c0200ea..33a47cdc91f 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.ts @@ -12,6 +12,7 @@ import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-organization-filter", templateUrl: "organization-filter.component.html", + standalone: false, }) export class OrganizationFilterComponent extends BaseOrganizationFilterComponent { get show() { diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/status-filter.component.ts b/apps/desktop/src/vault/app/vault/vault-filter/filters/status-filter.component.ts index 5d43fd52d20..276b11d7138 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/status-filter.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/status-filter.component.ts @@ -5,5 +5,6 @@ import { StatusFilterComponent as BaseStatusFilterComponent } from "@bitwarden/a @Component({ selector: "app-status-filter", templateUrl: "status-filter.component.html", + standalone: false, }) export class StatusFilterComponent extends BaseStatusFilterComponent {} diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.ts b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.ts index 5727cc0e9d5..5920233b206 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.ts @@ -5,6 +5,7 @@ import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angul @Component({ selector: "app-type-filter", templateUrl: "type-filter.component.html", + standalone: false, }) export class TypeFilterComponent extends BaseTypeFilterComponent { constructor() { diff --git a/apps/desktop/src/vault/app/vault/vault-filter/vault-filter.component.ts b/apps/desktop/src/vault/app/vault/vault-filter/vault-filter.component.ts index 12ac1fef425..161d22687e8 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/vault-filter.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/vault-filter.component.ts @@ -5,5 +5,6 @@ import { VaultFilterComponent as BaseVaultFilterComponent } from "@bitwarden/ang @Component({ selector: "app-vault-filter", templateUrl: "vault-filter.component.html", + standalone: false, }) export class VaultFilterComponent extends BaseVaultFilterComponent {} diff --git a/apps/desktop/src/vault/app/vault/vault-items.component.ts b/apps/desktop/src/vault/app/vault/vault-items.component.ts index d5838459ff7..2d1ba784753 100644 --- a/apps/desktop/src/vault/app/vault/vault-items.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-items.component.ts @@ -14,6 +14,7 @@ import { SearchBarService } from "../../../app/layout/search/search-bar.service" @Component({ selector: "app-vault-items", templateUrl: "vault-items.component.html", + standalone: false, }) export class VaultItemsComponent extends BaseVaultItemsComponent { constructor( diff --git a/apps/desktop/src/vault/app/vault/vault.component.ts b/apps/desktop/src/vault/app/vault/vault.component.ts index 6c0d5ef81d0..560855347b3 100644 --- a/apps/desktop/src/vault/app/vault/vault.component.ts +++ b/apps/desktop/src/vault/app/vault/vault.component.ts @@ -57,6 +57,7 @@ const BroadcasterSubscriptionId = "VaultComponent"; @Component({ selector: "app-vault", templateUrl: "vault.component.html", + standalone: false, }) export class VaultComponent implements OnInit, OnDestroy { @ViewChild(ViewComponent) viewComponent: ViewComponent; diff --git a/apps/desktop/src/vault/app/vault/view-custom-fields.component.ts b/apps/desktop/src/vault/app/vault/view-custom-fields.component.ts index 249f83c4444..efe61ad1fa7 100644 --- a/apps/desktop/src/vault/app/vault/view-custom-fields.component.ts +++ b/apps/desktop/src/vault/app/vault/view-custom-fields.component.ts @@ -6,6 +6,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve @Component({ selector: "app-vault-view-custom-fields", templateUrl: "view-custom-fields.component.html", + standalone: false, }) export class ViewCustomFieldsComponent extends BaseViewCustomFieldsComponent { constructor(eventCollectionService: EventCollectionService) { diff --git a/apps/desktop/src/vault/app/vault/view.component.ts b/apps/desktop/src/vault/app/vault/view.component.ts index e74b07445da..084a9a747ed 100644 --- a/apps/desktop/src/vault/app/vault/view.component.ts +++ b/apps/desktop/src/vault/app/vault/view.component.ts @@ -42,6 +42,7 @@ const BroadcasterSubscriptionId = "ViewComponent"; @Component({ selector: "app-vault-view", templateUrl: "view.component.html", + standalone: false, }) export class ViewComponent extends BaseViewComponent implements OnInit, OnDestroy, OnChanges { @Output() onViewCipherPasswordHistory = new EventEmitter(); diff --git a/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.ts b/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.ts index 8e5f261bc26..8f703acf9af 100644 --- a/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.ts @@ -10,6 +10,7 @@ import { GroupView } from "../../core"; @Component({ selector: "app-group-badge", templateUrl: "group-name-badge.component.html", + standalone: false, }) export class GroupNameBadgeComponent implements OnChanges { @Input() selectedGroups: SelectionReadOnlyRequest[]; diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts index 13e90d5275c..f7d7acfdc2d 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts @@ -26,6 +26,7 @@ import { CollectionFilter } from "../../../../vault/individual-vault/vault-filte selector: "app-organization-vault-filter", templateUrl: "../../../../vault/individual-vault/vault-filter/components/vault-filter.component.html", + standalone: false, }) export class VaultFilterComponent extends BaseVaultFilterComponent diff --git a/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts b/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts index fc168f842dc..cd14b73a156 100644 --- a/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts +++ b/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts @@ -9,6 +9,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv @Component({ selector: "app-org-info", templateUrl: "organization-information.component.html", + standalone: false, }) export class OrganizationInformationComponent implements OnInit { @Input() nameOnly = false; diff --git a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts index f5fce0e5e42..bc4a942301a 100644 --- a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts @@ -21,16 +21,19 @@ import { isEnterpriseOrgGuard } from "./is-enterprise-org.guard"; @Component({ template: "

This is the home screen!

", + standalone: false, }) export class HomescreenComponent {} @Component({ template: "

This component can only be accessed by a enterprise organization!

", + standalone: false, }) export class IsEnterpriseOrganizationComponent {} @Component({ template: "

This is the organization upgrade screen!

", + standalone: false, }) export class OrganizationUpgradeScreenComponent {} diff --git a/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts index 8efed8cefa2..ab5fd79321a 100644 --- a/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts @@ -20,16 +20,19 @@ import { isPaidOrgGuard } from "./is-paid-org.guard"; @Component({ template: "

This is the home screen!

", + standalone: false, }) export class HomescreenComponent {} @Component({ template: "

This component can only be accessed by a paid organization!

", + standalone: false, }) export class PaidOrganizationOnlyComponent {} @Component({ template: "

This is the organization upgrade screen!

", + standalone: false, }) export class OrganizationUpgradeScreenComponent {} diff --git a/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.spec.ts index fa348867a86..9dc084484f3 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.spec.ts @@ -19,16 +19,19 @@ import { organizationRedirectGuard } from "./org-redirect.guard"; @Component({ template: "

This is the home screen!

", + standalone: false, }) export class HomescreenComponent {} @Component({ template: "

This is the admin console!

", + standalone: false, }) export class AdminConsoleComponent {} @Component({ template: "

This is a subroute of the admin console!

", + standalone: false, }) export class AdminConsoleSubrouteComponent {} diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index 737a38ee2ab..3daa6c17d07 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -48,6 +48,7 @@ const EVENT_SYSTEM_USER_TO_TRANSLATION: Record = { @Component({ selector: "app-org-events", templateUrl: "events.component.html", + standalone: false, }) export class EventsComponent extends BaseEventsComponent implements OnInit, OnDestroy { exportFileName = "org-events"; diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index f29b4b642cb..53a6a3cf196 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -110,6 +110,7 @@ export const openGroupAddEditDialog = ( @Component({ selector: "app-group-add-edit", templateUrl: "group-add-edit.component.html", + standalone: false, }) export class GroupAddEditComponent implements OnInit, OnDestroy { private organization$ = this.accountService.activeAccount$.pipe( diff --git a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts index 669dcc251ca..6459cd1f857 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts @@ -75,6 +75,7 @@ const groupsFilter = (filter: string) => { @Component({ templateUrl: "groups.component.html", + standalone: false, }) export class GroupsComponent { loading = true; diff --git a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts index b5068ba55a6..03b77cfaa71 100644 --- a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts @@ -18,6 +18,7 @@ export type UserConfirmDialogData = { @Component({ selector: "app-user-confirm", templateUrl: "user-confirm.component.html", + standalone: false, }) export class UserConfirmComponent implements OnInit { name: string; diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts index c19984f980d..4ec50799ae0 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts @@ -33,6 +33,7 @@ type BulkConfirmDialogParams = { @Component({ templateUrl: "bulk-confirm-dialog.component.html", + standalone: false, }) export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { organizationId: string; diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts index 51ba98fabb9..8fb60e85b08 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts @@ -18,6 +18,7 @@ type BulkDeleteDialogParams = { @Component({ templateUrl: "bulk-delete-dialog.component.html", + standalone: false, }) export class BulkDeleteDialogComponent { organizationId: string; diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts index e01809789f3..9132625c587 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts @@ -22,6 +22,7 @@ export type BulkEnableSecretsManagerDialogData = { @Component({ templateUrl: `bulk-enable-sm-dialog.component.html`, + standalone: false, }) export class BulkEnableSecretsManagerDialogComponent implements OnInit { protected dataSource = new TableDataSource(); diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts index 00711e355cb..5bbc6f093f0 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts @@ -21,6 +21,7 @@ type BulkRemoveDialogParams = { @Component({ templateUrl: "bulk-remove-dialog.component.html", + standalone: false, }) export class BulkRemoveDialogComponent extends BaseBulkRemoveComponent { organizationId: string; diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts index 5cb2c2e3d4e..ac99a9b51de 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts @@ -18,6 +18,7 @@ type BulkRestoreDialogParams = { @Component({ selector: "app-bulk-restore-revoke", templateUrl: "bulk-restore-revoke.component.html", + standalone: false, }) export class BulkRestoreRevokeComponent { isRevoking: boolean; diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts index b8a2f45053b..078ba6c1fd1 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts @@ -41,6 +41,7 @@ type BulkStatusDialogData = { @Component({ selector: "app-bulk-status", templateUrl: "bulk-status.component.html", + standalone: false, }) export class BulkStatusComponent implements OnInit { users: BulkStatusEntry[]; diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index 8349b44c735..10bbc5cfe52 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -106,6 +106,7 @@ export enum MemberDialogResult { @Component({ templateUrl: "member-dialog.component.html", + standalone: false, }) export class MemberDialogComponent implements OnDestroy { loading = true; diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/nested-checkbox.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/nested-checkbox.component.ts index 648a5a6ff26..9a2025c2b30 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/nested-checkbox.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/nested-checkbox.component.ts @@ -10,6 +10,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; @Component({ selector: "app-nested-checkbox", templateUrl: "nested-checkbox.component.html", + standalone: false, }) export class NestedCheckboxComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); diff --git a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts index 4e78d4dc91f..80f0745f6d5 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts @@ -59,6 +59,7 @@ export enum ResetPasswordDialogResult { @Component({ selector: "app-reset-password", templateUrl: "reset-password.component.html", + standalone: false, }) /** * Used in a dialog for initiating the account recovery process against a diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 6a3ca58b73d..834aa2c7111 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -88,6 +88,7 @@ class MembersTableDataSource extends PeopleTableDataSource @Component({ templateUrl: "members.component.html", + standalone: false, }) export class MembersComponent extends BaseMembersComponent { userType = OrganizationUserType; diff --git a/apps/web/src/app/admin-console/organizations/policies/disable-send.component.ts b/apps/web/src/app/admin-console/organizations/policies/disable-send.component.ts index 2acf175e3da..b323ac00d34 100644 --- a/apps/web/src/app/admin-console/organizations/policies/disable-send.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/disable-send.component.ts @@ -14,5 +14,6 @@ export class DisableSendPolicy extends BasePolicy { @Component({ selector: "policy-disable-send", templateUrl: "disable-send.component.html", + standalone: false, }) export class DisableSendPolicyComponent extends BasePolicyComponent {} diff --git a/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts b/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts index 328989df66b..54cf1be88fc 100644 --- a/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts @@ -28,6 +28,7 @@ export class MasterPasswordPolicy extends BasePolicy { @Component({ selector: "policy-master-password", templateUrl: "master-password.component.html", + standalone: false, }) export class MasterPasswordPolicyComponent extends BasePolicyComponent implements OnInit { MinPasswordLength = Utils.minimumPasswordLength; diff --git a/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts b/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts index 4439f974e55..f11b14aea38 100644 --- a/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts @@ -21,6 +21,7 @@ export class PasswordGeneratorPolicy extends BasePolicy { @Component({ selector: "policy-password-generator", templateUrl: "password-generator.component.html", + standalone: false, }) export class PasswordGeneratorPolicyComponent extends BasePolicyComponent { // these properties forward the application default settings to the UI diff --git a/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.ts b/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.ts index 36c79a61fe4..ef92ee90581 100644 --- a/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.ts @@ -14,5 +14,6 @@ export class PersonalOwnershipPolicy extends BasePolicy { @Component({ selector: "policy-personal-ownership", templateUrl: "personal-ownership.component.html", + standalone: false, }) export class PersonalOwnershipPolicyComponent extends BasePolicyComponent {} diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts index 6e3b34eaa30..73f0d99b4f9 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts @@ -31,6 +31,7 @@ import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.compo @Component({ selector: "app-org-policies", templateUrl: "policies.component.html", + standalone: false, }) export class PoliciesComponent implements OnInit { loading = true; diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts index 4d722840e23..d3d03d2aaae 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts @@ -50,6 +50,7 @@ export enum PolicyEditDialogResult { @Component({ selector: "app-policy-edit", templateUrl: "policy-edit.component.html", + standalone: false, }) export class PolicyEditComponent implements AfterViewInit { @ViewChild("policyForm", { read: ViewContainerRef, static: true }) diff --git a/apps/web/src/app/admin-console/organizations/policies/remove-unlock-with-pin.component.ts b/apps/web/src/app/admin-console/organizations/policies/remove-unlock-with-pin.component.ts index b737c803b5f..0d0f42b603e 100644 --- a/apps/web/src/app/admin-console/organizations/policies/remove-unlock-with-pin.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/remove-unlock-with-pin.component.ts @@ -14,5 +14,6 @@ export class RemoveUnlockWithPinPolicy extends BasePolicy { @Component({ selector: "remove-unlock-with-pin", templateUrl: "remove-unlock-with-pin.component.html", + standalone: false, }) export class RemoveUnlockWithPinPolicyComponent extends BasePolicyComponent {} diff --git a/apps/web/src/app/admin-console/organizations/policies/require-sso.component.ts b/apps/web/src/app/admin-console/organizations/policies/require-sso.component.ts index ea85168f986..21de143dea6 100644 --- a/apps/web/src/app/admin-console/organizations/policies/require-sso.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/require-sso.component.ts @@ -19,5 +19,6 @@ export class RequireSsoPolicy extends BasePolicy { @Component({ selector: "policy-require-sso", templateUrl: "require-sso.component.html", + standalone: false, }) export class RequireSsoPolicyComponent extends BasePolicyComponent {} diff --git a/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts index 80e4e5254ff..62fc42f6a06 100644 --- a/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts @@ -27,6 +27,7 @@ export class ResetPasswordPolicy extends BasePolicy { @Component({ selector: "policy-reset-password", templateUrl: "reset-password.component.html", + standalone: false, }) export class ResetPasswordPolicyComponent extends BasePolicyComponent implements OnInit { data = this.formBuilder.group({ diff --git a/apps/web/src/app/admin-console/organizations/policies/send-options.component.ts b/apps/web/src/app/admin-console/organizations/policies/send-options.component.ts index af6d1f38694..9a0a8871296 100644 --- a/apps/web/src/app/admin-console/organizations/policies/send-options.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/send-options.component.ts @@ -15,6 +15,7 @@ export class SendOptionsPolicy extends BasePolicy { @Component({ selector: "policy-send-options", templateUrl: "send-options.component.html", + standalone: false, }) export class SendOptionsPolicyComponent extends BasePolicyComponent { data = this.formBuilder.group({ diff --git a/apps/web/src/app/admin-console/organizations/policies/single-org.component.ts b/apps/web/src/app/admin-console/organizations/policies/single-org.component.ts index a40ec87bf6a..ad32b4218bc 100644 --- a/apps/web/src/app/admin-console/organizations/policies/single-org.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/single-org.component.ts @@ -14,6 +14,7 @@ export class SingleOrgPolicy extends BasePolicy { @Component({ selector: "policy-single-org", templateUrl: "single-org.component.html", + standalone: false, }) export class SingleOrgPolicyComponent extends BasePolicyComponent implements OnInit { async ngOnInit() { diff --git a/apps/web/src/app/admin-console/organizations/policies/two-factor-authentication.component.ts b/apps/web/src/app/admin-console/organizations/policies/two-factor-authentication.component.ts index 51151808872..691e12c72f6 100644 --- a/apps/web/src/app/admin-console/organizations/policies/two-factor-authentication.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/two-factor-authentication.component.ts @@ -14,5 +14,6 @@ export class TwoFactorAuthenticationPolicy extends BasePolicy { @Component({ selector: "policy-two-factor-authentication", templateUrl: "two-factor-authentication.component.html", + standalone: false, }) export class TwoFactorAuthenticationPolicyComponent extends BasePolicyComponent {} diff --git a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts index 9fd3170b73a..52cb24c90d1 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts @@ -17,6 +17,7 @@ import { ReportVariant, reports, ReportType, ReportEntry } from "../../../dirt/r @Component({ selector: "app-org-reports-home", templateUrl: "reports-home.component.html", + standalone: false, }) export class ReportsHomeComponent implements OnInit { reports$: Observable; diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index f3997fe669e..b376c48b39a 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -41,6 +41,7 @@ import { DeleteOrganizationDialogResult, openDeleteOrganizationDialog } from "./ @Component({ selector: "app-org-account", templateUrl: "account.component.html", + standalone: false, }) export class AccountComponent implements OnInit, OnDestroy { selfHosted = false; diff --git a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts index 014b8e3a3ef..d14e912f83e 100644 --- a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts @@ -29,6 +29,7 @@ import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor/two- @Component({ selector: "app-two-factor-setup", templateUrl: "../../../auth/settings/two-factor/two-factor-setup.component.html", + standalone: false, }) export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent implements OnInit { tabbedHeader = false; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts index 1db1fc8a06e..366df34b2b8 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts @@ -55,6 +55,7 @@ export enum PermissionMode { multi: true, }, ], + standalone: false, }) export class AccessSelectorComponent implements ControlValueAccessor, OnInit, OnDestroy { private destroy$ = new Subject(); diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/user-type.pipe.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/user-type.pipe.ts index 3d43c63efb0..673d09ec0f0 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/user-type.pipe.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/user-type.pipe.ts @@ -5,6 +5,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic @Pipe({ name: "userType", + standalone: false, }) export class UserTypePipe implements PipeTransform { constructor(private i18nService: I18nService) {} diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts index 7ceaed28f80..4df6defe8ad 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts +++ b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts @@ -19,6 +19,7 @@ import { BaseAcceptComponent } from "../../../common/base.accept.component"; @Component({ selector: "app-accept-family-sponsorship", templateUrl: "accept-family-sponsorship.component.html", + standalone: false, }) export class AcceptFamilySponsorshipComponent extends BaseAcceptComponent { protected logo = BitwardenLogo; diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index b94ce004313..cac0487d05d 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -49,6 +49,7 @@ const IdleTimeout = 60000 * 10; // 10 minutes @Component({ selector: "app-root", templateUrl: "app.component.html", + standalone: false, }) export class AppComponent implements OnDestroy, OnInit { private lastActivity: Date = null; diff --git a/apps/web/src/app/auth/guards/deep-link.guard.spec.ts b/apps/web/src/app/auth/guards/deep-link.guard.spec.ts index f9ced556e47..82ed004cf54 100644 --- a/apps/web/src/app/auth/guards/deep-link.guard.spec.ts +++ b/apps/web/src/app/auth/guards/deep-link.guard.spec.ts @@ -13,16 +13,19 @@ import { deepLinkGuard } from "./deep-link.guard"; @Component({ template: "", + standalone: false, }) export class GuardedRouteTestComponent {} @Component({ template: "", + standalone: false, }) export class LockTestComponent {} @Component({ template: "", + standalone: false, }) export class RedirectTestComponent {} diff --git a/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.ts b/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.ts index 8f3a5ca3c37..d4a381159ab 100644 --- a/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.ts +++ b/apps/web/src/app/auth/login/login-via-webauthn/login-via-webauthn.component.ts @@ -7,6 +7,7 @@ import { CreatePasskeyIcon } from "@bitwarden/angular/auth/icons/create-passkey. @Component({ selector: "app-login-via-webauthn", templateUrl: "login-via-webauthn.component.html", + standalone: false, }) export class LoginViaWebAuthnComponent extends BaseLoginViaWebAuthnComponent { protected readonly Icons = { CreatePasskeyIcon, CreatePasskeyFailedIcon }; diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts index 197b4031998..838a3029711 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts @@ -14,6 +14,7 @@ import { OrganizationInvite } from "./organization-invite"; @Component({ templateUrl: "accept-organization.component.html", + standalone: false, }) export class AcceptOrganizationComponent extends BaseAcceptComponent { orgName$ = this.acceptOrganizationInviteService.orgName$; diff --git a/apps/web/src/app/auth/recover-delete.component.ts b/apps/web/src/app/auth/recover-delete.component.ts index 6b45421911d..7381d526879 100644 --- a/apps/web/src/app/auth/recover-delete.component.ts +++ b/apps/web/src/app/auth/recover-delete.component.ts @@ -13,6 +13,7 @@ import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-recover-delete", templateUrl: "recover-delete.component.html", + standalone: false, }) export class RecoverDeleteComponent { protected recoverDeleteForm = new FormGroup({ diff --git a/apps/web/src/app/auth/recover-two-factor.component.ts b/apps/web/src/app/auth/recover-two-factor.component.ts index 106cd32a945..a69da0a66bf 100644 --- a/apps/web/src/app/auth/recover-two-factor.component.ts +++ b/apps/web/src/app/auth/recover-two-factor.component.ts @@ -16,6 +16,7 @@ import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-recover-two-factor", templateUrl: "recover-two-factor.component.html", + standalone: false, }) export class RecoverTwoFactorComponent implements OnInit { protected formGroup = new FormGroup({ diff --git a/apps/web/src/app/auth/set-password.component.ts b/apps/web/src/app/auth/set-password.component.ts index ccd329dd640..e297426f2c1 100644 --- a/apps/web/src/app/auth/set-password.component.ts +++ b/apps/web/src/app/auth/set-password.component.ts @@ -11,6 +11,7 @@ import { AcceptOrganizationInviteService } from "./organization-invite/accept-or @Component({ selector: "app-set-password", templateUrl: "set-password.component.html", + standalone: false, }) export class SetPasswordComponent extends BaseSetPasswordComponent { routerService = inject(RouterService); diff --git a/apps/web/src/app/auth/settings/account/account.component.ts b/apps/web/src/app/auth/settings/account/account.component.ts index d7ba8058b81..cfc01f17674 100644 --- a/apps/web/src/app/auth/settings/account/account.component.ts +++ b/apps/web/src/app/auth/settings/account/account.component.ts @@ -17,6 +17,7 @@ import { SetAccountVerifyDevicesDialogComponent } from "./set-account-verify-dev @Component({ selector: "app-account", templateUrl: "account.component.html", + standalone: false, }) export class AccountComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); diff --git a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts index ba2a34c7143..5d71333c0de 100644 --- a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts @@ -31,6 +31,7 @@ type ChangeAvatarDialogData = { @Component({ templateUrl: "change-avatar-dialog.component.html", encapsulation: ViewEncapsulation.None, + standalone: false, }) export class ChangeAvatarDialogComponent implements OnInit, OnDestroy { profile: ProfileResponse; diff --git a/apps/web/src/app/auth/settings/account/change-email.component.ts b/apps/web/src/app/auth/settings/account/change-email.component.ts index caf7e0933b0..c86c8c2f4f7 100644 --- a/apps/web/src/app/auth/settings/account/change-email.component.ts +++ b/apps/web/src/app/auth/settings/account/change-email.component.ts @@ -17,6 +17,7 @@ import { KdfConfigService, KeyService } from "@bitwarden/key-management"; @Component({ selector: "app-change-email", templateUrl: "change-email.component.html", + standalone: false, }) export class ChangeEmailComponent implements OnInit { tokenSent = false; diff --git a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts index a7c466d4ffc..da4d2dce9d7 100644 --- a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts +++ b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts @@ -12,6 +12,7 @@ import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "app-deauthorize-sessions", templateUrl: "deauthorize-sessions.component.html", + standalone: false, }) export class DeauthorizeSessionsComponent { deauthForm = this.formBuilder.group({ diff --git a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts index 07e6052e0a1..8a3575af5ba 100644 --- a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts @@ -11,6 +11,7 @@ import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; @Component({ templateUrl: "delete-account-dialog.component.html", + standalone: false, }) export class DeleteAccountDialogComponent { deleteForm = this.formBuilder.group({ diff --git a/apps/web/src/app/auth/settings/account/profile.component.ts b/apps/web/src/app/auth/settings/account/profile.component.ts index a87e571643d..dc3997f58bb 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.ts +++ b/apps/web/src/app/auth/settings/account/profile.component.ts @@ -19,6 +19,7 @@ import { ChangeAvatarDialogComponent } from "./change-avatar-dialog.component"; @Component({ selector: "app-profile", templateUrl: "profile.component.html", + standalone: false, }) export class ProfileComponent implements OnInit, OnDestroy { loading = true; diff --git a/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts b/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts index 7a746481563..33c307882c5 100644 --- a/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts +++ b/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts @@ -24,6 +24,7 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; > `, + standalone: false, }) export class SelectableAvatarComponent { @Input() id: string; diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index ffa5247ad08..f1ba9281f69 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -32,6 +32,7 @@ import { UserKeyRotationService } from "../../key-management/key-rotation/user-k @Component({ selector: "app-change-password", templateUrl: "change-password.component.html", + standalone: false, }) export class ChangePasswordComponent extends BaseChangePasswordComponent diff --git a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts index 95afc167374..cd7a585f3b1 100644 --- a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts @@ -26,6 +26,7 @@ type EmergencyAccessConfirmDialogData = { @Component({ selector: "emergency-access-confirm", templateUrl: "emergency-access-confirm.component.html", + standalone: false, }) export class EmergencyAccessConfirmComponent implements OnInit { loading = true; diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts index 1a6510ef011..2f3f3a20b04 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts @@ -36,6 +36,7 @@ export enum EmergencyAccessAddEditDialogResult { @Component({ selector: "emergency-access-add-edit", templateUrl: "emergency-access-add-edit.component.html", + standalone: false, }) export class EmergencyAccessAddEditComponent implements OnInit { loading = true; diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts index f55d731d7f2..23bf0c22bc7 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts @@ -42,6 +42,7 @@ import { @Component({ selector: "emergency-access", templateUrl: "emergency-access.component.html", + standalone: false, }) export class EmergencyAccessComponent implements OnInit { loaded = false; diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts index edb85dc0f1a..d683545db59 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts @@ -40,6 +40,7 @@ type EmergencyAccessTakeoverDialogData = { @Component({ selector: "emergency-access-takeover", templateUrl: "emergency-access-takeover.component.html", + standalone: false, }) export class EmergencyAccessTakeoverComponent extends ChangePasswordComponent diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts index 55ebf860cff..607e6e6a2c7 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts @@ -15,6 +15,7 @@ import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component" selector: "emergency-access-view", templateUrl: "emergency-access-view.component.html", providers: [{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }], + standalone: false, }) export class EmergencyAccessViewComponent implements OnInit { id: EmergencyAccessId | null = null; diff --git a/apps/web/src/app/auth/settings/security/api-key.component.ts b/apps/web/src/app/auth/settings/security/api-key.component.ts index 26a70476102..4f87c082881 100644 --- a/apps/web/src/app/auth/settings/security/api-key.component.ts +++ b/apps/web/src/app/auth/settings/security/api-key.component.ts @@ -23,6 +23,7 @@ export type ApiKeyDialogData = { @Component({ selector: "app-api-key", templateUrl: "api-key.component.html", + standalone: false, }) export class ApiKeyComponent { clientId: string; diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts index debe390d878..0bfc46eea96 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts @@ -16,6 +16,7 @@ import { KdfConfig, KdfType, KeyService } from "@bitwarden/key-management"; @Component({ selector: "app-change-kdf-confirmation", templateUrl: "change-kdf-confirmation.component.html", + standalone: false, }) export class ChangeKdfConfirmationComponent { kdfConfig: KdfConfig; diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts index cbbef0e016b..a059ede77b4 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts @@ -21,6 +21,7 @@ import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.compon @Component({ selector: "app-change-kdf", templateUrl: "change-kdf.component.html", + standalone: false, }) export class ChangeKdfComponent implements OnInit, OnDestroy { kdfConfig: KdfConfig = DEFAULT_KDF_CONFIG; diff --git a/apps/web/src/app/auth/settings/security/security-keys.component.ts b/apps/web/src/app/auth/settings/security/security-keys.component.ts index 70e33b242b3..98e743f57dc 100644 --- a/apps/web/src/app/auth/settings/security/security-keys.component.ts +++ b/apps/web/src/app/auth/settings/security/security-keys.component.ts @@ -13,6 +13,7 @@ import { ApiKeyComponent } from "./api-key.component"; @Component({ selector: "app-security-keys", templateUrl: "security-keys.component.html", + standalone: false, }) export class SecurityKeysComponent implements OnInit { showChangeKdf = true; diff --git a/apps/web/src/app/auth/settings/security/security.component.ts b/apps/web/src/app/auth/settings/security/security.component.ts index d643b565df2..4f70a19e378 100644 --- a/apps/web/src/app/auth/settings/security/security.component.ts +++ b/apps/web/src/app/auth/settings/security/security.component.ts @@ -6,6 +6,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co @Component({ selector: "app-security", templateUrl: "security.component.html", + standalone: false, }) export class SecurityComponent implements OnInit { showChangePassword = true; diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts index 8e7e25896ab..22c3b4376c5 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts @@ -33,6 +33,7 @@ type Step = @Component({ templateUrl: "create-credential-dialog.component.html", + standalone: false, }) export class CreateCredentialDialogComponent implements OnInit { protected readonly NameMaxCharacters = 50; diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts index 8f6bf1778e8..ea766a302ca 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts @@ -26,6 +26,7 @@ export interface DeleteCredentialDialogParams { @Component({ templateUrl: "delete-credential-dialog.component.html", + standalone: false, }) export class DeleteCredentialDialogComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts index b402e53abf2..dd1ac45a9b6 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts @@ -23,6 +23,7 @@ export interface EnableEncryptionDialogParams { @Component({ templateUrl: "enable-encryption-dialog.component.html", + standalone: false, }) export class EnableEncryptionDialogComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts index 13c7993768c..94e926ac138 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts @@ -23,6 +23,7 @@ import { openEnableCredentialDialogComponent } from "./enable-encryption-dialog/ host: { "aria-live": "polite", }, + standalone: false, }) export class WebauthnLoginSettingsComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); diff --git a/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts b/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts index 282bce4c95d..77df374f3ed 100644 --- a/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts +++ b/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts @@ -23,6 +23,7 @@ import { */ @Component({ templateUrl: "user-verification-prompt.component.html", + standalone: false, }) export class UserVerificationPromptComponent extends BaseUserVerificationPrompt { constructor( diff --git a/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.ts b/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.ts index 94d319524f7..42f4b26fb36 100644 --- a/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.ts +++ b/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.ts @@ -23,5 +23,6 @@ import { UserVerificationComponent as BaseComponent } from "@bitwarden/angular/a transition(":enter", [style({ opacity: 0 }), animate("100ms", style({ opacity: 1 }))]), ]), ], + standalone: false, }) export class UserVerificationComponent extends BaseComponent {} diff --git a/apps/web/src/app/auth/update-password.component.ts b/apps/web/src/app/auth/update-password.component.ts index da62a6812f1..c975f7c4168 100644 --- a/apps/web/src/app/auth/update-password.component.ts +++ b/apps/web/src/app/auth/update-password.component.ts @@ -9,6 +9,7 @@ import { AcceptOrganizationInviteService } from "./organization-invite/accept-or @Component({ selector: "app-update-password", templateUrl: "update-password.component.html", + standalone: false, }) export class UpdatePasswordComponent extends BaseUpdatePasswordComponent { private routerService = inject(RouterService); diff --git a/apps/web/src/app/auth/update-temp-password.component.ts b/apps/web/src/app/auth/update-temp-password.component.ts index f7d952b97f4..ead10660b92 100644 --- a/apps/web/src/app/auth/update-temp-password.component.ts +++ b/apps/web/src/app/auth/update-temp-password.component.ts @@ -5,5 +5,6 @@ import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from " @Component({ selector: "app-update-temp-password", templateUrl: "update-temp-password.component.html", + standalone: false, }) export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {} diff --git a/apps/web/src/app/auth/verify-email-token.component.ts b/apps/web/src/app/auth/verify-email-token.component.ts index 55569f44c10..9e44cc7a713 100644 --- a/apps/web/src/app/auth/verify-email-token.component.ts +++ b/apps/web/src/app/auth/verify-email-token.component.ts @@ -15,6 +15,7 @@ import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-verify-email-token", templateUrl: "verify-email-token.component.html", + standalone: false, }) export class VerifyEmailTokenComponent implements OnInit { constructor( diff --git a/apps/web/src/app/auth/verify-recover-delete.component.ts b/apps/web/src/app/auth/verify-recover-delete.component.ts index 8d95dd01b77..a475fdfd3e5 100644 --- a/apps/web/src/app/auth/verify-recover-delete.component.ts +++ b/apps/web/src/app/auth/verify-recover-delete.component.ts @@ -14,6 +14,7 @@ import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-verify-recover-delete", templateUrl: "verify-recover-delete.component.html", + standalone: false, }) export class VerifyRecoverDeleteComponent implements OnInit { email: string; diff --git a/apps/web/src/app/billing/individual/billing-history-view.component.ts b/apps/web/src/app/billing/individual/billing-history-view.component.ts index 89e6b9ca9c3..d615e01d0db 100644 --- a/apps/web/src/app/billing/individual/billing-history-view.component.ts +++ b/apps/web/src/app/billing/individual/billing-history-view.component.ts @@ -12,6 +12,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl @Component({ templateUrl: "billing-history-view.component.html", + standalone: false, }) export class BillingHistoryViewComponent implements OnInit { loading = false; diff --git a/apps/web/src/app/billing/individual/premium/premium.component.ts b/apps/web/src/app/billing/individual/premium/premium.component.ts index 2934e69f0ad..974c22455ff 100644 --- a/apps/web/src/app/billing/individual/premium/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium.component.ts @@ -25,6 +25,7 @@ import { TaxInfoComponent } from "../../shared/tax-info.component"; @Component({ templateUrl: "./premium.component.html", + standalone: false, }) export class PremiumComponent { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; diff --git a/apps/web/src/app/billing/individual/subscription.component.ts b/apps/web/src/app/billing/individual/subscription.component.ts index edd16ca81fe..2a08ec85127 100644 --- a/apps/web/src/app/billing/individual/subscription.component.ts +++ b/apps/web/src/app/billing/individual/subscription.component.ts @@ -9,6 +9,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl @Component({ templateUrl: "subscription.component.html", + standalone: false, }) export class SubscriptionComponent implements OnInit { hasPremium$: Observable; diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 38f4436fb47..4d1fa97785b 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -28,6 +28,7 @@ import { UpdateLicenseDialogResult } from "../shared/update-license-types"; @Component({ templateUrl: "user-subscription.component.html", + standalone: false, }) export class UserSubscriptionComponent implements OnInit { loading = false; diff --git a/apps/web/src/app/billing/members/free-bitwarden-families.component.ts b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts index 02e4e6a9cc1..dddc730168c 100644 --- a/apps/web/src/app/billing/members/free-bitwarden-families.component.ts +++ b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts @@ -23,6 +23,7 @@ import { AddSponsorshipDialogComponent } from "./add-sponsorship-dialog.componen @Component({ selector: "app-free-bitwarden-families", templateUrl: "free-bitwarden-families.component.html", + standalone: false, }) export class FreeBitwardenFamiliesComponent implements OnInit { loading = signal(true); diff --git a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts index 786c25c8d4c..d1086a6646b 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts @@ -19,6 +19,7 @@ import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-adjust-subscription", templateUrl: "adjust-subscription.component.html", + standalone: false, }) export class AdjustSubscription implements OnInit, OnDestroy { @Input() organizationId: string; diff --git a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts index 1eb1c7124ad..55687f00052 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts +++ b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts @@ -22,6 +22,7 @@ export interface BillingSyncApiModalData { @Component({ templateUrl: "billing-sync-api-key.component.html", + standalone: false, }) export class BillingSyncApiKeyComponent { protected organizationId: string; diff --git a/apps/web/src/app/billing/organizations/billing-sync-key.component.ts b/apps/web/src/app/billing/organizations/billing-sync-key.component.ts index f29a98b2612..37ebefc803a 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-key.component.ts +++ b/apps/web/src/app/billing/organizations/billing-sync-key.component.ts @@ -21,6 +21,7 @@ export interface BillingSyncKeyModalData { @Component({ templateUrl: "billing-sync-key.component.html", + standalone: false, }) export class BillingSyncKeyComponent { protected entityId: string; diff --git a/apps/web/src/app/billing/organizations/change-plan.component.ts b/apps/web/src/app/billing/organizations/change-plan.component.ts index 7c25413079a..31cbf4e94bf 100644 --- a/apps/web/src/app/billing/organizations/change-plan.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan.component.ts @@ -9,6 +9,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" @Component({ selector: "app-change-plan", templateUrl: "change-plan.component.html", + standalone: false, }) export class ChangePlanComponent { @Input() organizationId: string; diff --git a/apps/web/src/app/billing/organizations/download-license.component.ts b/apps/web/src/app/billing/organizations/download-license.component.ts index 66778aec50f..8ada57e8377 100644 --- a/apps/web/src/app/billing/organizations/download-license.component.ts +++ b/apps/web/src/app/billing/organizations/download-license.component.ts @@ -20,6 +20,7 @@ type DownloadLicenseDialogData = { @Component({ templateUrl: "download-license.component.html", + standalone: false, }) export class DownloadLicenceDialogComponent { licenseForm = this.formBuilder.group({ diff --git a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts index d533badabf8..ce4678ad8ef 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts +++ b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts @@ -12,6 +12,7 @@ import { @Component({ templateUrl: "organization-billing-history-view.component.html", + standalone: false, }) export class OrgBillingHistoryViewComponent implements OnInit, OnDestroy { loading = false; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 5cbaf940695..792a138c2d1 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -44,6 +44,7 @@ import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.comp @Component({ templateUrl: "organization-subscription-cloud.component.html", + standalone: false, }) export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy { static readonly QUERY_PARAM_UPGRADE: string = "upgrade"; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts index 2189bfa830f..fa4b633cb7a 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts @@ -36,6 +36,7 @@ enum LicenseOptions { @Component({ templateUrl: "organization-subscription-selfhost.component.html", + standalone: false, }) export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDestroy { subscription: SelfHostedOrganizationSubscriptionView; diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts index b4064bb1d87..c896ee6404c 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts @@ -37,6 +37,7 @@ import { FreeTrial } from "../../types/free-trial"; @Component({ templateUrl: "./organization-payment-method.component.html", + standalone: false, }) export class OrganizationPaymentMethodComponent implements OnDestroy { organizationId: string; diff --git a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts index c10c9abc9b6..33413832865 100644 --- a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts @@ -59,6 +59,7 @@ export interface SecretsManagerSubscriptionOptions { @Component({ selector: "app-sm-adjust-subscription", templateUrl: "sm-adjust-subscription.component.html", + standalone: false, }) export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDestroy { @Input() organizationId: string; diff --git a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts index 617b68abb37..6f9525e4fce 100644 --- a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts +++ b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts @@ -23,6 +23,7 @@ import { secretsManagerSubscribeFormFactory } from "../shared"; @Component({ selector: "sm-subscribe-standalone", templateUrl: "sm-subscribe-standalone.component.html", + standalone: false, }) export class SecretsManagerSubscribeStandaloneComponent { @Input() plan: PlanResponse; diff --git a/apps/web/src/app/billing/organizations/subscription-hidden.component.ts b/apps/web/src/app/billing/organizations/subscription-hidden.component.ts index 894db727b01..1455d76d67e 100644 --- a/apps/web/src/app/billing/organizations/subscription-hidden.component.ts +++ b/apps/web/src/app/billing/organizations/subscription-hidden.component.ts @@ -34,6 +34,7 @@ const SubscriptionHiddenIcon = svgIcon`

{{ "billingManagedByProvider" | i18n: providerName }}

{{ "billingContactProviderForAssistance" | i18n }}

`, + standalone: false, }) export class SubscriptionHiddenComponent { @Input() providerName: string; diff --git a/apps/web/src/app/billing/organizations/subscription-status.component.ts b/apps/web/src/app/billing/organizations/subscription-status.component.ts index a097bf674e2..0b59df3f707 100644 --- a/apps/web/src/app/billing/organizations/subscription-status.component.ts +++ b/apps/web/src/app/billing/organizations/subscription-status.component.ts @@ -26,6 +26,7 @@ type ComponentData = { @Component({ selector: "app-subscription-status", templateUrl: "subscription-status.component.html", + standalone: false, }) export class SubscriptionStatusComponent { @Input({ required: true }) organizationSubscriptionResponse: OrganizationSubscriptionResponse; diff --git a/apps/web/src/app/billing/settings/sponsored-families.component.ts b/apps/web/src/app/billing/settings/sponsored-families.component.ts index 7e493168ce2..80e66784ae8 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.ts +++ b/apps/web/src/app/billing/settings/sponsored-families.component.ts @@ -36,6 +36,7 @@ interface RequestSponsorshipForm { @Component({ selector: "app-sponsored-families", templateUrl: "sponsored-families.component.html", + standalone: false, }) export class SponsoredFamiliesComponent implements OnInit, OnDestroy { loading = false; diff --git a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts index 39a7531082a..70320e7e62e 100644 --- a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts +++ b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts @@ -18,6 +18,7 @@ import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "[sponsoring-org-row]", templateUrl: "sponsoring-org-row.component.html", + standalone: false, }) export class SponsoringOrgRowComponent implements OnInit { @Input() sponsoringOrg: Organization = null; diff --git a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts index ec6e251418b..cdf72168acf 100644 --- a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts +++ b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts @@ -35,6 +35,7 @@ export type PayPalConfig = { @Component({ templateUrl: "add-credit-dialog.component.html", + standalone: false, }) export class AddCreditDialogComponent implements OnInit { @ViewChild("ppButtonForm", { read: ElementRef, static: true }) ppButtonFormRef: ElementRef; diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts index 9d32becd1bb..94929c58656 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts @@ -39,6 +39,7 @@ export enum AdjustPaymentDialogResultType { @Component({ templateUrl: "./adjust-payment-dialog.component.html", + standalone: false, }) export class AdjustPaymentDialogComponent implements OnInit { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts index 6cd17218f02..1f9172eaf59 100644 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts @@ -31,6 +31,7 @@ export enum AdjustStorageDialogResultType { @Component({ templateUrl: "./adjust-storage-dialog.component.html", + standalone: false, }) export class AdjustStorageDialogComponent { protected formGroup = new FormGroup({ diff --git a/apps/web/src/app/billing/shared/billing-history.component.ts b/apps/web/src/app/billing/shared/billing-history.component.ts index 1994c53a375..745939f0d5e 100644 --- a/apps/web/src/app/billing/shared/billing-history.component.ts +++ b/apps/web/src/app/billing/shared/billing-history.component.ts @@ -11,6 +11,7 @@ import { @Component({ selector: "app-billing-history", templateUrl: "billing-history.component.html", + standalone: false, }) export class BillingHistoryComponent { @Input() diff --git a/apps/web/src/app/billing/shared/offboarding-survey.component.ts b/apps/web/src/app/billing/shared/offboarding-survey.component.ts index 62213c1fe94..9f21f2b8cd5 100644 --- a/apps/web/src/app/billing/shared/offboarding-survey.component.ts +++ b/apps/web/src/app/billing/shared/offboarding-survey.component.ts @@ -49,6 +49,7 @@ export const openOffboardingSurvey = ( @Component({ selector: "app-cancel-subscription-form", templateUrl: "offboarding-survey.component.html", + standalone: false, }) export class OffboardingSurveyComponent { protected ResultType = OffboardingSurveyDialogResultType; diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index f7d4480c4d4..27c9caf7186 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -35,6 +35,7 @@ import { @Component({ templateUrl: "payment-method.component.html", + standalone: false, }) export class PaymentMethodComponent implements OnInit, OnDestroy { loading = false; diff --git a/apps/web/src/app/billing/shared/self-hosting-license-uploader/individual-self-hosting-license-uploader.component.ts b/apps/web/src/app/billing/shared/self-hosting-license-uploader/individual-self-hosting-license-uploader.component.ts index 0d7698e00fd..75da10a7b09 100644 --- a/apps/web/src/app/billing/shared/self-hosting-license-uploader/individual-self-hosting-license-uploader.component.ts +++ b/apps/web/src/app/billing/shared/self-hosting-license-uploader/individual-self-hosting-license-uploader.component.ts @@ -17,6 +17,7 @@ import { AbstractSelfHostingLicenseUploaderComponent } from "../../shared/self-h @Component({ selector: "individual-self-hosting-license-uploader", templateUrl: "./self-hosting-license-uploader.component.html", + standalone: false, }) export class IndividualSelfHostingLicenseUploaderComponent extends AbstractSelfHostingLicenseUploaderComponent { /** diff --git a/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts b/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts index 222aff3fec6..1850b5e526d 100644 --- a/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts +++ b/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts @@ -24,6 +24,7 @@ import { AbstractSelfHostingLicenseUploaderComponent } from "../../shared/self-h @Component({ selector: "organization-self-hosting-license-uploader", templateUrl: "./self-hosting-license-uploader.component.html", + standalone: false, }) export class OrganizationSelfHostingLicenseUploaderComponent extends AbstractSelfHostingLicenseUploaderComponent { /** diff --git a/apps/web/src/app/billing/shared/sm-subscribe.component.ts b/apps/web/src/app/billing/shared/sm-subscribe.component.ts index 23041ccb1ae..1ecf3648bd2 100644 --- a/apps/web/src/app/billing/shared/sm-subscribe.component.ts +++ b/apps/web/src/app/billing/shared/sm-subscribe.component.ts @@ -33,6 +33,7 @@ export const secretsManagerSubscribeFormFactory = ( @Component({ selector: "sm-subscribe", templateUrl: "sm-subscribe.component.html", + standalone: false, }) export class SecretsManagerSubscribeComponent implements OnInit, OnDestroy { @Input() formGroup: FormGroup>; diff --git a/apps/web/src/app/billing/shared/update-license-dialog.component.ts b/apps/web/src/app/billing/shared/update-license-dialog.component.ts index afc62b0ca31..11b5e7fd8df 100644 --- a/apps/web/src/app/billing/shared/update-license-dialog.component.ts +++ b/apps/web/src/app/billing/shared/update-license-dialog.component.ts @@ -12,6 +12,7 @@ import { UpdateLicenseComponent } from "./update-license.component"; @Component({ templateUrl: "update-license-dialog.component.html", + standalone: false, }) export class UpdateLicenseDialogComponent extends UpdateLicenseComponent { constructor( diff --git a/apps/web/src/app/billing/shared/update-license.component.ts b/apps/web/src/app/billing/shared/update-license.component.ts index e580d420202..455b38386c6 100644 --- a/apps/web/src/app/billing/shared/update-license.component.ts +++ b/apps/web/src/app/billing/shared/update-license.component.ts @@ -15,6 +15,7 @@ import { UpdateLicenseDialogResult } from "./update-license-types"; @Component({ selector: "app-update-license", templateUrl: "update-license.component.html", + standalone: false, }) export class UpdateLicenseComponent implements OnInit { @Input() organizationId: string; diff --git a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts index 90df6e513c4..93f2bc021cd 100644 --- a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts +++ b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts @@ -47,6 +47,7 @@ export type InitiationPath = @Component({ selector: "app-complete-trial-initiation", templateUrl: "complete-trial-initiation.component.html", + standalone: false, }) export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { @ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent; diff --git a/apps/web/src/app/billing/trial-initiation/confirmation-details.component.ts b/apps/web/src/app/billing/trial-initiation/confirmation-details.component.ts index e88c49c4f57..cbb1c84284c 100644 --- a/apps/web/src/app/billing/trial-initiation/confirmation-details.component.ts +++ b/apps/web/src/app/billing/trial-initiation/confirmation-details.component.ts @@ -7,6 +7,7 @@ import { ProductType } from "@bitwarden/common/billing/enums"; @Component({ selector: "app-trial-confirmation-details", templateUrl: "confirmation-details.component.html", + standalone: false, }) export class ConfirmationDetailsComponent { @Input() email: string; diff --git a/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step-content.component.ts b/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step-content.component.ts index e43eb4e6cda..0c6e084f5c4 100644 --- a/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step-content.component.ts +++ b/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step-content.component.ts @@ -7,6 +7,7 @@ import { VerticalStep } from "./vertical-step.component"; @Component({ selector: "app-vertical-step-content", templateUrl: "vertical-step-content.component.html", + standalone: false, }) export class VerticalStepContentComponent { @Output() onSelectStep = new EventEmitter(); diff --git a/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step.component.ts b/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step.component.ts index 1ff900875df..b4b643b3889 100644 --- a/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step.component.ts +++ b/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step.component.ts @@ -5,6 +5,7 @@ import { Component, Input } from "@angular/core"; selector: "app-vertical-step", templateUrl: "vertical-step.component.html", providers: [{ provide: CdkStep, useExisting: VerticalStep }], + standalone: false, }) export class VerticalStep extends CdkStep { @Input() subLabel = ""; diff --git a/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-stepper.component.ts b/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-stepper.component.ts index 04197827813..333224aac54 100644 --- a/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-stepper.component.ts +++ b/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-stepper.component.ts @@ -9,6 +9,7 @@ import { VerticalStep } from "./vertical-step.component"; selector: "app-vertical-stepper", templateUrl: "vertical-stepper.component.html", providers: [{ provide: CdkStepper, useExisting: VerticalStepperComponent }], + standalone: false, }) export class VerticalStepperComponent extends CdkStepper { readonly steps: QueryList; diff --git a/apps/web/src/app/components/environment-selector/environment-selector.component.ts b/apps/web/src/app/components/environment-selector/environment-selector.component.ts index b86c068911f..3f71a0d719a 100644 --- a/apps/web/src/app/components/environment-selector/environment-selector.component.ts +++ b/apps/web/src/app/components/environment-selector/environment-selector.component.ts @@ -13,6 +13,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; @Component({ selector: "environment-selector", templateUrl: "environment-selector.component.html", + standalone: false, }) export class EnvironmentSelectorComponent implements OnInit { constructor( diff --git a/apps/web/src/app/dirt/reports/pages/breach-report.component.ts b/apps/web/src/app/dirt/reports/pages/breach-report.component.ts index e1da7be06f8..47bf0844468 100644 --- a/apps/web/src/app/dirt/reports/pages/breach-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/breach-report.component.ts @@ -11,6 +11,7 @@ import { BreachAccountResponse } from "@bitwarden/common/models/response/breach- @Component({ selector: "app-breach-report", templateUrl: "breach-report.component.html", + standalone: false, }) export class BreachReportComponent implements OnInit { loading = false; diff --git a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts index 900ff3703f0..5710ea1176e 100644 --- a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts @@ -20,6 +20,7 @@ type ReportResult = CipherView & { exposedXTimes: number }; @Component({ selector: "app-exposed-passwords-report", templateUrl: "exposed-passwords-report.component.html", + standalone: false, }) export class ExposedPasswordsReportComponent extends CipherReportComponent implements OnInit { disabled = true; diff --git a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts index 5265326128e..95810625dac 100644 --- a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts @@ -21,6 +21,7 @@ import { CipherReportComponent } from "./cipher-report.component"; @Component({ selector: "app-inactive-two-factor-report", templateUrl: "inactive-two-factor-report.component.html", + standalone: false, }) export class InactiveTwoFactorReportComponent extends CipherReportComponent implements OnInit { services = new Map(); diff --git a/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts index 4f0988082b4..b88987e1d25 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts @@ -36,6 +36,7 @@ import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent RoutedVaultFilterService, RoutedVaultFilterBridgeService, ], + standalone: false, }) export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent diff --git a/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts index 6dc202de0b3..1105e814245 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts @@ -35,6 +35,7 @@ import { InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponen RoutedVaultFilterService, RoutedVaultFilterBridgeService, ], + standalone: false, }) export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorReportComponent diff --git a/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts index 4e37f53ba61..7fcf3562437 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts @@ -35,6 +35,7 @@ import { ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent } RoutedVaultFilterService, RoutedVaultFilterBridgeService, ], + standalone: false, }) export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportComponent diff --git a/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts index 25e1314fceb..2e916da0294 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts @@ -35,6 +35,7 @@ import { UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponen RoutedVaultFilterService, RoutedVaultFilterBridgeService, ], + standalone: false, }) export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesReportComponent diff --git a/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts index ef9bd97008e..80be66e9ad2 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts @@ -36,6 +36,7 @@ import { WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent } from RoutedVaultFilterService, RoutedVaultFilterBridgeService, ], + standalone: false, }) export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportComponent diff --git a/apps/web/src/app/dirt/reports/pages/reports-home.component.ts b/apps/web/src/app/dirt/reports/pages/reports-home.component.ts index 604d66f6858..acc3efac58a 100644 --- a/apps/web/src/app/dirt/reports/pages/reports-home.component.ts +++ b/apps/web/src/app/dirt/reports/pages/reports-home.component.ts @@ -12,6 +12,7 @@ import { ReportEntry, ReportVariant } from "../shared"; @Component({ selector: "app-reports-home", templateUrl: "reports-home.component.html", + standalone: false, }) export class ReportsHomeComponent implements OnInit { reports: ReportEntry[]; diff --git a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts index 6d70cc23875..3e9abc779ba 100644 --- a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts @@ -19,6 +19,7 @@ import { CipherReportComponent } from "./cipher-report.component"; @Component({ selector: "app-reused-passwords-report", templateUrl: "reused-passwords-report.component.html", + standalone: false, }) export class ReusedPasswordsReportComponent extends CipherReportComponent implements OnInit { passwordUseMap: Map; diff --git a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts index 02d4117c684..d2cc792198e 100644 --- a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts @@ -18,6 +18,7 @@ import { CipherReportComponent } from "./cipher-report.component"; @Component({ selector: "app-unsecured-websites-report", templateUrl: "unsecured-websites-report.component.html", + standalone: false, }) export class UnsecuredWebsitesReportComponent extends CipherReportComponent implements OnInit { disabled = true; diff --git a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts index 4144c9ac20f..1716a98190c 100644 --- a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts @@ -28,6 +28,7 @@ type ReportResult = CipherView & { score: number; reportValue: ReportScore; scor @Component({ selector: "app-weak-passwords-report", templateUrl: "weak-passwords-report.component.html", + standalone: false, }) export class WeakPasswordsReportComponent extends CipherReportComponent implements OnInit { disabled = true; diff --git a/apps/web/src/app/dirt/reports/reports-layout.component.ts b/apps/web/src/app/dirt/reports/reports-layout.component.ts index 7bfe912c1ad..360898e6057 100644 --- a/apps/web/src/app/dirt/reports/reports-layout.component.ts +++ b/apps/web/src/app/dirt/reports/reports-layout.component.ts @@ -6,6 +6,7 @@ import { filter } from "rxjs/operators"; @Component({ selector: "app-reports-layout", templateUrl: "reports-layout.component.html", + standalone: false, }) export class ReportsLayoutComponent implements OnDestroy { homepage = true; diff --git a/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.ts b/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.ts index da42d92bf84..92e6ddb0028 100644 --- a/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.ts +++ b/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.ts @@ -9,6 +9,7 @@ import { ReportVariant } from "../models/report-variant"; @Component({ selector: "app-report-card", templateUrl: "report-card.component.html", + standalone: false, }) export class ReportCardComponent { @Input() title: string; diff --git a/apps/web/src/app/dirt/reports/shared/report-list/report-list.component.ts b/apps/web/src/app/dirt/reports/shared/report-list/report-list.component.ts index cd6b77f9c81..c81c99d50d5 100644 --- a/apps/web/src/app/dirt/reports/shared/report-list/report-list.component.ts +++ b/apps/web/src/app/dirt/reports/shared/report-list/report-list.component.ts @@ -7,6 +7,7 @@ import { ReportEntry } from "../models/report-entry"; @Component({ selector: "app-report-list", templateUrl: "report-list.component.html", + standalone: false, }) export class ReportListComponent { @Input() reports: ReportEntry[]; diff --git a/apps/web/src/app/key-management/key-connector/remove-password.component.ts b/apps/web/src/app/key-management/key-connector/remove-password.component.ts index 3ca9d3a5669..1b07f04ba8a 100644 --- a/apps/web/src/app/key-management/key-connector/remove-password.component.ts +++ b/apps/web/src/app/key-management/key-connector/remove-password.component.ts @@ -5,5 +5,6 @@ import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitward @Component({ selector: "app-remove-password", templateUrl: "remove-password.component.html", + standalone: false, }) export class RemovePasswordComponent extends BaseRemovePasswordComponent {} diff --git a/apps/web/src/app/layouts/frontend-layout.component.ts b/apps/web/src/app/layouts/frontend-layout.component.ts index 609845f22cd..9e22ab6d43e 100644 --- a/apps/web/src/app/layouts/frontend-layout.component.ts +++ b/apps/web/src/app/layouts/frontend-layout.component.ts @@ -7,6 +7,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl @Component({ selector: "app-frontend-layout", templateUrl: "frontend-layout.component.html", + standalone: false, }) export class FrontendLayoutComponent implements OnInit, OnDestroy { version: string; diff --git a/apps/web/src/app/layouts/header/web-header.component.ts b/apps/web/src/app/layouts/header/web-header.component.ts index e17d059160f..67d447723e1 100644 --- a/apps/web/src/app/layouts/header/web-header.component.ts +++ b/apps/web/src/app/layouts/header/web-header.component.ts @@ -17,6 +17,7 @@ import { UserId } from "@bitwarden/common/types/guid"; @Component({ selector: "app-header", templateUrl: "./web-header.component.html", + standalone: false, }) export class WebHeaderComponent { /** diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.ts index 737fa2d8d2e..9d4250087af 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.ts @@ -6,6 +6,7 @@ import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-s @Component({ selector: "navigation-product-switcher", templateUrl: "./navigation-switcher.component.html", + standalone: false, }) export class NavigationProductSwitcherComponent { constructor(private productSwitcherService: ProductSwitcherService) {} diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts index fca9063c8cf..b1c1a0a906a 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts @@ -24,6 +24,7 @@ import { NavigationProductSwitcherComponent } from "./navigation-switcher.compon @Directive({ selector: "[mockOrgs]", + standalone: false, }) class MockOrganizationService implements Partial { private static _orgs = new BehaviorSubject([]); @@ -40,6 +41,7 @@ class MockOrganizationService implements Partial { @Directive({ selector: "[mockProviders]", + standalone: false, }) class MockProviderService implements Partial { private static _providers = new BehaviorSubject([]); @@ -78,12 +80,14 @@ class MockPlatformUtilsService implements Partial { @Component({ selector: "story-layout", template: ``, + standalone: false, }) class StoryLayoutComponent {} @Component({ selector: "story-content", template: ``, + standalone: false, }) class StoryContentComponent {} diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts index 4a22f628570..5a6572e15be 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts @@ -9,6 +9,7 @@ import { ProductSwitcherService } from "./shared/product-switcher.service"; @Component({ selector: "product-switcher-content", templateUrl: "./product-switcher-content.component.html", + standalone: false, }) export class ProductSwitcherContentComponent { @ViewChild("menu") diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts index 834571e2cb4..5dd29815ef2 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts @@ -8,6 +8,7 @@ import { ProductSwitcherService } from "./shared/product-switcher.service"; @Component({ selector: "product-switcher", templateUrl: "./product-switcher.component.html", + standalone: false, }) export class ProductSwitcherComponent implements AfterViewInit { /** diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts index ca27aae4581..4525105e579 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts @@ -24,6 +24,7 @@ import { ProductSwitcherService } from "./shared/product-switcher.service"; @Directive({ selector: "[mockOrgs]", + standalone: false, }) class MockOrganizationService implements Partial { private static _orgs = new BehaviorSubject([]); @@ -40,6 +41,7 @@ class MockOrganizationService implements Partial { @Directive({ selector: "[mockProviders]", + standalone: false, }) class MockProviderService implements Partial { private static _providers = new BehaviorSubject([]); @@ -78,12 +80,14 @@ class MockPlatformUtilsService implements Partial { @Component({ selector: "story-layout", template: ``, + standalone: false, }) class StoryLayoutComponent {} @Component({ selector: "story-content", template: ``, + standalone: false, }) class StoryContentComponent {} diff --git a/apps/web/src/app/settings/domain-rules.component.ts b/apps/web/src/app/settings/domain-rules.component.ts index 6dd27cbf19b..2d570f4aeb4 100644 --- a/apps/web/src/app/settings/domain-rules.component.ts +++ b/apps/web/src/app/settings/domain-rules.component.ts @@ -12,6 +12,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl @Component({ selector: "app-domain-rules", templateUrl: "domain-rules.component.html", + standalone: false, }) export class DomainRulesComponent implements OnInit { loading = true; diff --git a/apps/web/src/app/settings/preferences.component.ts b/apps/web/src/app/settings/preferences.component.ts index 4d4e0c3d711..9ab23c76795 100644 --- a/apps/web/src/app/settings/preferences.component.ts +++ b/apps/web/src/app/settings/preferences.component.ts @@ -37,6 +37,7 @@ import { DialogService } from "@bitwarden/components"; @Component({ selector: "app-preferences", templateUrl: "preferences.component.html", + standalone: false, }) export class PreferencesComponent implements OnInit, OnDestroy { // For use in template diff --git a/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts b/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts index 7bb86c9f669..f9798ec7f0f 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts +++ b/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts @@ -8,6 +8,7 @@ import { Component, Input } from "@angular/core"; host: { class: "tw-max-w-max", }, + standalone: false, }) export class OnboardingTaskComponent { @Input() diff --git a/apps/web/src/app/shared/components/onboarding/onboarding.component.ts b/apps/web/src/app/shared/components/onboarding/onboarding.component.ts index 23f9015b024..5ead9fcc10b 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding.component.ts +++ b/apps/web/src/app/shared/components/onboarding/onboarding.component.ts @@ -7,6 +7,7 @@ import { OnboardingTaskComponent } from "./onboarding-task.component"; @Component({ selector: "app-onboarding", templateUrl: "./onboarding.component.html", + standalone: false, }) export class OnboardingComponent { @ContentChildren(OnboardingTaskComponent) tasks: QueryList; diff --git a/apps/web/src/app/vault/components/premium-badge.component.ts b/apps/web/src/app/vault/components/premium-badge.component.ts index 6deff43489d..ec444404aea 100644 --- a/apps/web/src/app/vault/components/premium-badge.component.ts +++ b/apps/web/src/app/vault/components/premium-badge.component.ts @@ -9,6 +9,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag {{ "premium" | i18n }} `, + standalone: false, }) export class PremiumBadgeComponent { constructor(private messagingService: MessagingService) {} diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts index 317b02356ba..ac1774cd244 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts @@ -20,6 +20,7 @@ import { RowHeightClass } from "./vault-items.component"; @Component({ selector: "tr[appVaultCipherRow]", templateUrl: "vault-cipher-row.component.html", + standalone: false, }) export class VaultCipherRowComponent implements OnInit { protected RowHeightClass = RowHeightClass; diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index d07ba46d136..06c78ea0351 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -18,6 +18,7 @@ import { RowHeightClass } from "./vault-items.component"; @Component({ selector: "tr[appVaultCollectionRow]", templateUrl: "vault-collection-row.component.html", + standalone: false, }) export class VaultCollectionRowComponent { protected RowHeightClass = RowHeightClass; diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index d27f332f13f..1c63ac85d34 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -32,8 +32,7 @@ type ItemPermission = CollectionPermission | "NoAccess"; @Component({ selector: "app-vault-items", templateUrl: "vault-items.component.html", - // TODO: Improve change detection, see: https://bitwarden.atlassian.net/browse/TDL-220 - // changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false, }) export class VaultItemsComponent { protected RowHeight = RowHeight; diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index 1650b0f371f..43a44cf5066 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -53,6 +53,7 @@ export const openBulkDeleteDialog = ( @Component({ templateUrl: "bulk-delete-dialog.component.html", + standalone: false, }) export class BulkDeleteDialogComponent { cipherIds: string[]; diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts index d287c430d49..dc262b01334 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts @@ -47,6 +47,7 @@ export const openBulkMoveDialog = ( @Component({ templateUrl: "bulk-move-dialog.component.html", + standalone: false, }) export class BulkMoveDialogComponent implements OnInit { cipherIds: string[] = []; diff --git a/apps/web/src/app/vault/individual-vault/folder-add-edit.component.ts b/apps/web/src/app/vault/individual-vault/folder-add-edit.component.ts index 6a3c5663d93..3050c00dd6c 100644 --- a/apps/web/src/app/vault/individual-vault/folder-add-edit.component.ts +++ b/apps/web/src/app/vault/individual-vault/folder-add-edit.component.ts @@ -23,6 +23,7 @@ import { KeyService } from "@bitwarden/key-management"; @Component({ selector: "app-folder-add-edit", templateUrl: "folder-add-edit.component.html", + standalone: false, }) export class FolderAddEditComponent extends BaseFolderAddEditComponent { protected override componentName = "app-folder-add-edit"; diff --git a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts index c1f935f2001..915bc00bacd 100644 --- a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts +++ b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts @@ -12,6 +12,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; @Component({ selector: "app-org-badge", templateUrl: "organization-name-badge.component.html", + standalone: false, }) export class OrganizationNameBadgeComponent implements OnChanges { @Input() organizationId?: string; diff --git a/apps/web/src/app/vault/individual-vault/pipes/get-group-name.pipe.ts b/apps/web/src/app/vault/individual-vault/pipes/get-group-name.pipe.ts index 81d3a8de749..09bce96728a 100644 --- a/apps/web/src/app/vault/individual-vault/pipes/get-group-name.pipe.ts +++ b/apps/web/src/app/vault/individual-vault/pipes/get-group-name.pipe.ts @@ -5,6 +5,7 @@ import { GroupView } from "../../../admin-console/organizations/core"; @Pipe({ name: "groupNameFromId", pure: true, + standalone: false, }) export class GetGroupNameFromIdPipe implements PipeTransform { transform(value: string, groups: GroupView[]) { diff --git a/apps/web/src/app/vault/individual-vault/pipes/get-organization-name.pipe.ts b/apps/web/src/app/vault/individual-vault/pipes/get-organization-name.pipe.ts index 4d6c0b7d8d7..bf9dc82c527 100644 --- a/apps/web/src/app/vault/individual-vault/pipes/get-organization-name.pipe.ts +++ b/apps/web/src/app/vault/individual-vault/pipes/get-organization-name.pipe.ts @@ -5,6 +5,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga @Pipe({ name: "orgNameFromId", pure: true, + standalone: false, }) export class GetOrgNameFromIdPipe implements PipeTransform { transform(value: string, organizations: Organization[]) { diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index c7d91237578..e95ea669725 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -33,6 +33,7 @@ import { OrganizationFilter } from "../shared/models/vault-filter.type"; @Component({ selector: "app-organization-options", templateUrl: "organization-options.component.html", + standalone: false, }) export class OrganizationOptionsComponent implements OnInit, OnDestroy { protected actionPromise?: Promise; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index 3f1e7755c8f..6b974296f21 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -38,6 +38,7 @@ import { OrganizationOptionsComponent } from "./organization-options.component"; @Component({ selector: "app-vault-filter", templateUrl: "vault-filter.component.html", + standalone: false, }) export class VaultFilterComponent implements OnInit, OnDestroy { filters?: VaultFilterList; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts index 41329319805..1a0a96fa19c 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts @@ -16,6 +16,7 @@ import { VaultFilter } from "../models/vault-filter.model"; @Component({ selector: "app-filter-section", templateUrl: "vault-filter-section.component.html", + standalone: false, }) export class VaultFilterSectionComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); diff --git a/apps/web/src/app/vault/settings/purge-vault.component.ts b/apps/web/src/app/vault/settings/purge-vault.component.ts index 11e75c16720..0a25122788c 100644 --- a/apps/web/src/app/vault/settings/purge-vault.component.ts +++ b/apps/web/src/app/vault/settings/purge-vault.component.ts @@ -25,6 +25,7 @@ export interface PurgeVaultDialogData { @Component({ selector: "app-purge-vault", templateUrl: "purge-vault.component.html", + standalone: false, }) export class PurgeVaultComponent { organizationId: string = null; diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts index b9b7460f7ed..970a476df22 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts @@ -24,6 +24,7 @@ export interface DomainAddEditDialogData { @Component({ templateUrl: "domain-add-edit-dialog.component.html", + standalone: false, }) export class DomainAddEditDialogComponent implements OnInit, OnDestroy { private componentDestroyed$: Subject = new Subject(); diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts index 8fc9b6bba0f..1f644f55a9f 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts @@ -35,6 +35,7 @@ import { @Component({ selector: "app-org-manage-domain-verification", templateUrl: "domain-verification.component.html", + standalone: false, }) export class DomainVerificationComponent implements OnInit, OnDestroy { private componentDestroyed$ = new Subject(); diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts index 76bcd7383f3..de870cdbdcb 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts @@ -24,6 +24,7 @@ import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "app-org-manage-scim", templateUrl: "scim.component.html", + standalone: false, }) export class ScimComponent implements OnInit { loading = true; diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.ts index c276f25663a..61e2133d059 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.ts @@ -21,5 +21,6 @@ export class ActivateAutofillPolicy extends BasePolicy { @Component({ selector: "policy-activate-autofill", templateUrl: "activate-autofill.component.html", + standalone: false, }) export class ActivateAutofillPolicyComponent extends BasePolicyComponent {} diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/automatic-app-login.component.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/automatic-app-login.component.ts index 354192ff860..1a478fb4393 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/automatic-app-login.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/automatic-app-login.component.ts @@ -19,6 +19,7 @@ export class AutomaticAppLoginPolicy extends BasePolicy { @Component({ selector: "policy-automatic-app-login", templateUrl: "automatic-app-login.component.html", + standalone: false, }) export class AutomaticAppLoginPolicyComponent extends BasePolicyComponent { data = this.formBuilder.group({ diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/disable-personal-vault-export.component.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/disable-personal-vault-export.component.ts index 848b1d173a1..c274e58ccac 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/disable-personal-vault-export.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/disable-personal-vault-export.component.ts @@ -16,5 +16,6 @@ export class DisablePersonalVaultExportPolicy extends BasePolicy { @Component({ selector: "policy-disable-personal-vault-export", templateUrl: "disable-personal-vault-export.component.html", + standalone: false, }) export class DisablePersonalVaultExportPolicyComponent extends BasePolicyComponent {} diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/maximum-vault-timeout.component.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/maximum-vault-timeout.component.ts index 9d09ead800e..a5b9ad47f6e 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/maximum-vault-timeout.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/maximum-vault-timeout.component.ts @@ -22,6 +22,7 @@ export class MaximumVaultTimeoutPolicy extends BasePolicy { @Component({ selector: "policy-maximum-timeout", templateUrl: "maximum-vault-timeout.component.html", + standalone: false, }) export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent { vaultTimeoutActionOptions: { name: string; value: string }[]; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.ts index 88738aa897c..51296c52281 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.ts @@ -19,6 +19,7 @@ interface AddOrganizationDialogData { @Component({ templateUrl: "add-organization.component.html", + standalone: false, }) export class AddOrganizationComponent implements OnInit { protected provider: Provider; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-organization.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-organization.component.ts index d22665b432f..9f3582f84bb 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-organization.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-organization.component.ts @@ -8,6 +8,7 @@ import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing"; @Component({ selector: "app-create-organization", templateUrl: "create-organization.component.html", + standalone: false, }) export class CreateOrganizationComponent implements OnInit { @ViewChild(OrganizationPlansComponent, { static: true }) diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts index a3ef074b975..7bfac8f4b32 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts @@ -14,6 +14,7 @@ import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept @Component({ selector: "app-accept-provider", templateUrl: "accept-provider.component.html", + standalone: false, }) export class AcceptProviderComponent extends BaseAcceptComponent { protected logo = BitwardenLogo; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts index 67f2382cf91..e21837f7226 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts @@ -35,6 +35,7 @@ export enum AddEditMemberDialogResultType { @Component({ templateUrl: "add-edit-member-dialog.component.html", + standalone: false, }) export class AddEditMemberDialogComponent { editing = false; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts index 8271869f1b4..8bbc299269d 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts @@ -29,6 +29,7 @@ type BulkConfirmDialogParams = { @Component({ templateUrl: "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.html", + standalone: false, }) export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { providerId: string; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts index c3f6409f296..e000d918414 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts @@ -19,6 +19,7 @@ type BulkRemoveDialogParams = { @Component({ templateUrl: "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.html", + standalone: false, }) export class BulkRemoveDialogComponent extends BaseBulkRemoveComponent { providerId: string; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts index 87f29fd91e9..2ad2ecdccbd 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts @@ -19,6 +19,7 @@ import { EventExportService } from "@bitwarden/web-vault/app/tools/event-export" @Component({ selector: "provider-events", templateUrl: "events.component.html", + standalone: false, }) export class EventsComponent extends BaseEventsComponent implements OnInit { exportFileName = "provider-events"; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts index 4a184d2dd16..9cbe8115008 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -45,6 +45,7 @@ class MembersTableDataSource extends PeopleTableDataSource { @Component({ templateUrl: "members.component.html", + standalone: false, }) export class MembersComponent extends BaseMembersComponent { accessEvents = false; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.component.ts index ac2fb333fae..f3d67abd4e1 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.component.ts @@ -10,6 +10,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; @Component({ selector: "app-providers", templateUrl: "providers.component.html", + standalone: false, }) export class ProvidersComponent implements OnInit { providers: Provider[]; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts index e72e1c7c326..12dada12aa9 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts @@ -20,6 +20,7 @@ import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "provider-account", templateUrl: "account.component.html", + standalone: false, }) export class AccountComponent implements OnDestroy, OnInit { selfHosted = false; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup-provider.component.ts index a41c7cba362..473380ff288 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup-provider.component.ts @@ -7,6 +7,7 @@ import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept @Component({ selector: "app-setup-provider", templateUrl: "setup-provider.component.html", + standalone: false, }) export class SetupProviderComponent extends BaseAcceptComponent { protected logo = BitwardenLogo; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts index 0b6483b9f48..cb47b3fe28f 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts @@ -23,6 +23,7 @@ import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/paymen @Component({ selector: "provider-setup", templateUrl: "setup.component.html", + standalone: false, }) export class SetupComponent implements OnInit, OnDestroy { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts index b27a7ddd0f4..5c0d0982fb5 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts @@ -13,6 +13,7 @@ import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-verify-recover-delete-provider", templateUrl: "verify-recover-delete-provider.component.html", + standalone: false, }) export class VerifyRecoverDeleteProviderComponent implements OnInit { name: string; diff --git a/bitwarden_license/bit-web/src/app/app.component.ts b/bitwarden_license/bit-web/src/app/app.component.ts index dd814f5c0d2..2d0dfd967a1 100644 --- a/bitwarden_license/bit-web/src/app/app.component.ts +++ b/bitwarden_license/bit-web/src/app/app.component.ts @@ -12,6 +12,7 @@ import { FreeFamiliesSponsorshipPolicy } from "./billing/policies/free-families- @Component({ selector: "app-root", templateUrl: "../../../../apps/web/src/app/app.component.html", + standalone: false, }) export class AppComponent extends BaseAppComponent implements OnInit { ngOnInit() { diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts index 03e09f8de4b..f0761757c65 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts @@ -51,6 +51,7 @@ const defaultSigningAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha2 @Component({ selector: "app-org-manage-sso", templateUrl: "sso.component.html", + standalone: false, }) export class SsoComponent implements OnInit, OnDestroy { readonly ssoType = SsoType; diff --git a/bitwarden_license/bit-web/src/app/billing/policies/free-families-sponsorship.component.ts b/bitwarden_license/bit-web/src/app/billing/policies/free-families-sponsorship.component.ts index 521f8658890..53d2b0ab66c 100644 --- a/bitwarden_license/bit-web/src/app/billing/policies/free-families-sponsorship.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/policies/free-families-sponsorship.component.ts @@ -16,5 +16,6 @@ export class FreeFamiliesSponsorshipPolicy extends BasePolicy { @Component({ selector: "policy-personal-ownership", templateUrl: "free-families-sponsorship.component.html", + standalone: false, }) export class FreeFamiliesSponsorshipPolicyComponent extends BasePolicyComponent {} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/billing-history/provider-billing-history.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/provider-billing-history.component.ts index fa3a617b45f..d1a9d43a6fc 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/billing-history/provider-billing-history.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/provider-billing-history.component.ts @@ -12,6 +12,7 @@ import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/ser @Component({ templateUrl: "./provider-billing-history.component.html", + standalone: false, }) export class ProviderBillingHistoryComponent { private providerId: string; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts index 4bb2c36ef15..f0eda893d67 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts @@ -27,6 +27,7 @@ export enum AddExistingOrganizationDialogResultType { @Component({ templateUrl: "./add-existing-organization-dialog.component.html", + standalone: false, }) export class AddExistingOrganizationDialogComponent implements OnInit { protected loading: boolean = true; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts index e87ee6a1187..c7d82c3ec09 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts @@ -99,6 +99,7 @@ export class PlanCard { @Component({ templateUrl: "./create-client-dialog.component.html", + standalone: false, }) export class CreateClientDialogComponent implements OnInit { protected discountPercentage: number | null | undefined; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-name-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-name-dialog.component.ts index 45abeab1f4a..06d1f80c101 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-name-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-name-dialog.component.ts @@ -41,6 +41,7 @@ export const openManageClientNameDialog = ( @Component({ templateUrl: "manage-client-name-dialog.component.html", + standalone: false, }) export class ManageClientNameDialogComponent { protected ResultType = ManageClientNameDialogResultType; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts index ced48bfdbea..71e549b563b 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts @@ -36,6 +36,7 @@ export const openManageClientSubscriptionDialog = ( @Component({ templateUrl: "./manage-client-subscription-dialog.component.html", + standalone: false, }) export class ManageClientSubscriptionDialogComponent implements OnInit { protected loading = true; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts index f262ba1abd0..056339b6fb7 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts @@ -19,6 +19,7 @@ import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept @Component({ templateUrl: "./setup-business-unit.component.html", + standalone: false, }) export class SetupBusinessUnitComponent extends BaseAcceptComponent { protected bitwardenLogo = BitwardenLogo; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription-status.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription-status.component.ts index c49509427b8..974dc9c460f 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription-status.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription-status.component.ts @@ -26,6 +26,7 @@ type ComponentData = { @Component({ selector: "app-provider-subscription-status", templateUrl: "provider-subscription-status.component.html", + standalone: false, }) export class ProviderSubscriptionStatusComponent { @Input({ required: true }) subscription: ProviderSubscriptionResponse; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts index 657d16d9380..74368ef7839 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts @@ -26,6 +26,7 @@ import { @Component({ selector: "app-provider-subscription", templateUrl: "./provider-subscription.component.html", + standalone: false, }) export class ProviderSubscriptionComponent implements OnInit, OnDestroy { private providerId: string; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts index 2d0a681460a..b563591f32f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts @@ -19,12 +19,14 @@ import { IntegrationsComponent } from "./integrations.component"; @Component({ selector: "app-header", template: "
", + standalone: false, }) class MockHeaderComponent {} @Component({ selector: "sm-new-menu", template: "
", + standalone: false, }) class MockNewMenuComponent {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts index cdae129de4f..01a2edb5d5d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts @@ -6,6 +6,7 @@ import { Integration } from "@bitwarden/web-vault/app/admin-console/organization @Component({ selector: "sm-integrations", templateUrl: "./integrations.component.html", + standalone: false, }) export class IntegrationsComponent { private integrationsAndSdks: Integration[] = []; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/layout.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/layout/layout.component.ts index 6a90eb2a78f..b50e586c337 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/layout.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/layout.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core"; @Component({ selector: "sm-layout", templateUrl: "./layout.component.html", + standalone: false, }) export class LayoutComponent implements OnInit { ngOnInit() { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts index 6594b71a14c..1eef528b639 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts @@ -34,6 +34,7 @@ import { CountService } from "../shared/counts/count.service"; @Component({ selector: "sm-navigation", templateUrl: "./navigation.component.html", + standalone: false, }) export class NavigationComponent implements OnInit, OnDestroy { protected readonly logo = SecretsManagerLogo; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts index 698448c2d06..a96f9a08919 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts @@ -88,6 +88,7 @@ type OrganizationTasks = { @Component({ selector: "sm-overview", templateUrl: "./overview.component.html", + standalone: false, }) export class OverviewComponent implements OnInit, OnDestroy { private destroy$: Subject = new Subject(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/section.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/section.component.ts index bd68e58ea87..6b71c81f09e 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/section.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/section.component.ts @@ -3,6 +3,7 @@ import { Component, Input } from "@angular/core"; @Component({ selector: "sm-section", templateUrl: "./section.component.html", + standalone: false, }) export class SectionComponent { @Input() open = true; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-delete-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-delete-dialog.component.ts index d6ca31399e1..8cdb1bb4d69 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-delete-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-delete-dialog.component.ts @@ -27,6 +27,7 @@ export interface ProjectDeleteOperation { @Component({ templateUrl: "./project-delete-dialog.component.html", + standalone: false, }) export class ProjectDeleteDialogComponent implements OnInit { formGroup = new FormGroup({ diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts index c96887cc9ac..ec420d653cc 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts @@ -27,6 +27,7 @@ export interface ProjectOperation { @Component({ templateUrl: "./project-dialog.component.html", + standalone: false, }) export class ProjectDialogComponent implements OnInit { protected formGroup = new FormGroup({ 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 index 159b90d5432..7523fa14a21 100644 --- 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 @@ -22,11 +22,13 @@ import { projectAccessGuard } from "./project-access.guard"; @Component({ template: "", + standalone: false, }) export class GuardedRouteTestComponent {} @Component({ template: "", + standalone: false, }) export class RedirectTestComponent {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts index 4a0c37cb4a9..13f80920558 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts @@ -27,6 +27,7 @@ import { AccessPolicyService } from "../../shared/access-policies/access-policy. @Component({ selector: "sm-project-people", templateUrl: "./project-people.component.html", + standalone: false, }) export class ProjectPeopleComponent implements OnInit, OnDestroy { private currentAccessPolicies: ApItemViewType[]; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts index f4950bf53f2..536739715ff 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts @@ -45,6 +45,7 @@ import { ProjectService } from "../project.service"; @Component({ selector: "sm-project-secrets", templateUrl: "./project-secrets.component.html", + standalone: false, }) export class ProjectSecretsComponent implements OnInit { secrets$: Observable; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts index d289f9f7b1f..fc3a489bce9 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts @@ -25,6 +25,7 @@ import { AccessPolicyService } from "../../shared/access-policies/access-policy. @Component({ selector: "sm-project-service-accounts", templateUrl: "./project-service-accounts.component.html", + standalone: false, }) export class ProjectServiceAccountsComponent implements OnInit, OnDestroy { private currentAccessPolicies: ApItemViewType[]; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts index 9e60cf0535c..2d008dd219a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts @@ -37,6 +37,7 @@ import { ProjectService } from "../project.service"; @Component({ selector: "sm-project", templateUrl: "./project.component.html", + standalone: false, }) export class ProjectComponent implements OnInit, OnDestroy { protected project$: Observable; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts index e09aaea6e2b..ea5294624af 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts @@ -40,6 +40,7 @@ import { ProjectService } from "../project.service"; @Component({ selector: "sm-projects", templateUrl: "./projects.component.html", + standalone: false, }) export class ProjectsComponent implements OnInit { protected projects$: Observable; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-delete.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-delete.component.ts index 9dfc80c9f88..6340cc42f3b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-delete.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-delete.component.ts @@ -20,6 +20,7 @@ export interface SecretDeleteOperation { @Component({ templateUrl: "./secret-delete.component.html", + standalone: false, }) export class SecretDeleteDialogComponent { constructor( diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts index 09a78e02c44..9172d44965d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts @@ -69,6 +69,7 @@ export interface SecretOperation { @Component({ templateUrl: "./secret-dialog.component.html", + standalone: false, }) export class SecretDialogComponent implements OnInit, OnDestroy { loading = true; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.ts index 7f11d69d59c..b719014a382 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.ts @@ -12,6 +12,7 @@ export interface SecretViewDialogParams { @Component({ templateUrl: "./secret-view-dialog.component.html", + standalone: false, }) export class SecretViewDialogComponent implements OnInit { protected loading = true; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts index b58173a1cc0..18ecd2e3b51 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts @@ -36,6 +36,7 @@ import { SecretService } from "./secret.service"; @Component({ selector: "sm-secrets", templateUrl: "./secrets.component.html", + standalone: false, }) export class SecretsComponent implements OnInit { protected secrets$: Observable; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.ts index caca8c92aa8..a714729d96f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.ts @@ -8,6 +8,7 @@ import { AccessTokenView } from "../models/view/access-token.view"; @Component({ selector: "sm-access-list", templateUrl: "./access-list.component.html", + standalone: false, }) export class AccessListComponent { @Input() diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.ts index a4f0c077efd..b9643ce8fd8 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.ts @@ -27,6 +27,7 @@ import { AccessTokenCreateDialogComponent } from "./dialogs/access-token-create- @Component({ selector: "sm-access-tokens", templateUrl: "./access-tokens.component.html", + standalone: false, }) export class AccessTokenComponent implements OnInit, OnDestroy { accessTokens$: Observable; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-create-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-create-dialog.component.ts index 140e357edac..dfbe0a1511d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-create-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-create-dialog.component.ts @@ -17,6 +17,7 @@ export interface AccessTokenOperation { @Component({ templateUrl: "./access-token-create-dialog.component.html", + standalone: false, }) export class AccessTokenCreateDialogComponent implements OnInit { protected formGroup = new FormGroup({ diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-dialog.component.ts index bfc120f33e2..0259b8d6e90 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-dialog.component.ts @@ -14,6 +14,7 @@ export interface AccessTokenDetails { @Component({ templateUrl: "./access-token-dialog.component.html", + standalone: false, }) export class AccessTokenDialogComponent implements OnInit { constructor( diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/expiration-options.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/expiration-options.component.ts index 2273b42897c..891501874ff 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/expiration-options.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/expiration-options.component.ts @@ -33,6 +33,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic useExisting: ExpirationOptionsComponent, }, ], + standalone: false, }) export class ExpirationOptionsComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts index 96e3b58b633..e796c4758ea 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts @@ -24,6 +24,7 @@ class ServiceAccountConfig { @Component({ selector: "sm-service-account-config", templateUrl: "./config.component.html", + standalone: false, }) export class ServiceAccountConfigComponent implements OnInit, OnDestroy { identityUrl: string; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts index 6f30aacccca..5edc57d8c74 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts @@ -27,6 +27,7 @@ export interface ServiceAccountDeleteOperation { @Component({ templateUrl: "./service-account-delete-dialog.component.html", + standalone: false, }) export class ServiceAccountDeleteDialogComponent { formGroup = new FormGroup({ diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts index 241c736fb7a..815ea1dc60c 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts @@ -26,6 +26,7 @@ export interface ServiceAccountOperation { @Component({ templateUrl: "./service-account-dialog.component.html", + standalone: false, }) export class ServiceAccountDialogComponent implements OnInit { protected formGroup = new FormGroup( diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts index 6538ae49adb..ddaa0937e6f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts @@ -18,6 +18,7 @@ import { ServiceAccountEventLogApiService } from "./service-account-event-log-ap @Component({ selector: "sm-service-accounts-events", templateUrl: "./service-accounts-events.component.html", + standalone: false, }) export class ServiceAccountEventsComponent extends BaseEventsComponent diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts index 4301b5ae81a..e0bcad8d6e9 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts @@ -22,11 +22,13 @@ import { serviceAccountAccessGuard } from "./service-account-access.guard"; @Component({ template: "", + standalone: false, }) export class GuardedRouteTestComponent {} @Component({ template: "", + standalone: false, }) export class RedirectTestComponent {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts index a57251c4f77..4449757167d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts @@ -28,6 +28,7 @@ import { AccessPolicyService } from "../../shared/access-policies/access-policy. @Component({ selector: "sm-service-account-people", templateUrl: "./service-account-people.component.html", + standalone: false, }) export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { private currentAccessPolicies: ApItemViewType[]; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts index 34a48af3055..af334b22c63 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts @@ -25,6 +25,7 @@ import { AccessPolicyService } from "../../shared/access-policies/access-policy. @Component({ selector: "sm-service-account-projects", templateUrl: "./service-account-projects.component.html", + standalone: false, }) export class ServiceAccountProjectsComponent implements OnInit, OnDestroy { private currentAccessPolicies: ApItemViewType[]; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts index 74465e8ecef..5eb074e3e99 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts @@ -18,6 +18,7 @@ import { ServiceAccountService } from "./service-account.service"; @Component({ selector: "sm-service-account", templateUrl: "./service-account.component.html", + standalone: false, }) export class ServiceAccountComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts index cbffc80aa05..47d5dd63806 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts @@ -16,6 +16,7 @@ import { @Component({ selector: "sm-service-accounts-list", templateUrl: "./service-accounts-list.component.html", + standalone: false, }) export class ServiceAccountsListComponent implements OnDestroy { protected dataSource = new TableDataSource(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts index 7deaefae823..2813ece001f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts @@ -31,6 +31,7 @@ import { ServiceAccountService } from "./service-account.service"; @Component({ selector: "sm-service-accounts", templateUrl: "./service-accounts.component.html", + standalone: false, }) export class ServiceAccountsComponent implements OnInit { protected serviceAccounts$: Observable; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/dialog/sm-import-error-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/dialog/sm-import-error-dialog.component.ts index 2576ca8e629..0bed0355a8c 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/dialog/sm-import-error-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/dialog/sm-import-error-dialog.component.ts @@ -12,6 +12,7 @@ export interface SecretsManagerImportErrorDialogOperation { @Component({ templateUrl: "./sm-import-error-dialog.component.html", + standalone: false, }) export class SecretsManagerImportErrorDialogComponent { errorLines: SecretsManagerImportErrorLine[]; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts index 266962c8268..c2b726803c5 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts @@ -29,6 +29,7 @@ type ExportFormat = { @Component({ selector: "sm-export", templateUrl: "./sm-export.component.html", + standalone: false, }) export class SecretsManagerExportComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts index 262f1a6b161..65075d12bf6 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts @@ -21,6 +21,7 @@ import { SecretsManagerPortingApiService } from "../services/sm-porting-api.serv @Component({ selector: "sm-import", templateUrl: "./sm-import.component.html", + standalone: false, }) export class SecretsManagerImportComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts index 34de4e4860b..fba3ff03ee0 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts @@ -30,6 +30,7 @@ import { ApPermissionEnum } from "./models/enums/ap-permission.enum"; multi: true, }, ], + standalone: false, }) export class AccessPolicySelectorComponent implements ControlValueAccessor, OnInit, OnDestroy { private destroy$ = new Subject(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts index 935ee1c8518..9d2a3715e16 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts @@ -25,6 +25,7 @@ export enum BulkConfirmationResult { @Component({ selector: "sm-bulk-confirmation-dialog", templateUrl: "./bulk-confirmation-dialog.component.html", + standalone: false, }) export class BulkConfirmationDialogComponent implements OnInit { constructor( diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-status-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-status-dialog.component.ts index 0a00c7f0431..fc7890f1654 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-status-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-status-dialog.component.ts @@ -20,6 +20,7 @@ export class BulkOperationStatus { @Component({ templateUrl: "./bulk-status-dialog.component.html", + standalone: false, }) export class BulkStatusDialogComponent implements OnInit { constructor( diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.ts index b0a481d077d..18823130d22 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.ts @@ -29,6 +29,7 @@ import { @Component({ selector: "sm-new-menu", templateUrl: "./new-menu.component.html", + standalone: false, }) export class NewMenuComponent implements OnInit, OnDestroy { private organizationId: string; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/org-suspended.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/org-suspended.component.ts index 2eb9d6017b7..89baf969c34 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/org-suspended.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/org-suspended.component.ts @@ -12,6 +12,7 @@ import { Icon, Icons } from "@bitwarden/components"; @Component({ templateUrl: "./org-suspended.component.html", + standalone: false, }) export class OrgSuspendedComponent { constructor( diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts index 0ca5ba09074..9774172cd4b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts @@ -13,6 +13,7 @@ import { ProjectListView } from "../models/view/project-list.view"; @Component({ selector: "sm-projects-list", templateUrl: "./projects-list.component.html", + standalone: false, }) export class ProjectsListComponent { @Input() diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts index 37b9524238f..a7ee818a01f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts @@ -15,6 +15,7 @@ import { SecretService } from "../secrets/secret.service"; @Component({ selector: "sm-secrets-list", templateUrl: "./secrets-list.component.html", + standalone: false, }) export class SecretsListComponent implements OnDestroy { protected dataSource = new TableDataSource(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-hard-delete.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-hard-delete.component.ts index 7c4fff4d3b3..29f9a85250c 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-hard-delete.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-hard-delete.component.ts @@ -15,6 +15,7 @@ export interface SecretHardDeleteOperation { @Component({ templateUrl: "./secret-hard-delete.component.html", + standalone: false, }) export class SecretHardDeleteDialogComponent { constructor( diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-restore.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-restore.component.ts index 6b5efe5e133..712757445be 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-restore.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/trash/dialog/secret-restore.component.ts @@ -15,6 +15,7 @@ export interface SecretRestoreOperation { @Component({ templateUrl: "./secret-restore.component.html", + standalone: false, }) export class SecretRestoreDialogComponent { constructor( diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts index 3a21dbe3b68..4392ae8b1bb 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts @@ -24,6 +24,7 @@ import { @Component({ selector: "sm-trash", templateUrl: "./trash.component.html", + standalone: false, }) export class TrashComponent implements OnInit { secrets$: Observable; diff --git a/libs/angular/src/auth/components/environment-selector.component.ts b/libs/angular/src/auth/components/environment-selector.component.ts index 16a249dda97..e6438b3e634 100644 --- a/libs/angular/src/auth/components/environment-selector.component.ts +++ b/libs/angular/src/auth/components/environment-selector.component.ts @@ -57,6 +57,7 @@ export interface EnvironmentSelectorRouteData { transition("* => void", animate("100ms linear", style({ opacity: 0 }))), ]), ], + standalone: false, }) export class EnvironmentSelectorComponent implements OnInit, OnDestroy { @Output() onOpenSelfHostedSettings = new EventEmitter(); diff --git a/libs/angular/src/auth/components/two-factor-icon.component.ts b/libs/angular/src/auth/components/two-factor-icon.component.ts index c75078e413a..0e811595bea 100644 --- a/libs/angular/src/auth/components/two-factor-icon.component.ts +++ b/libs/angular/src/auth/components/two-factor-icon.component.ts @@ -10,6 +10,7 @@ import { WebAuthnIcon } from "../icons/webauthn.icon"; @Component({ selector: "auth-two-factor-icon", templateUrl: "./two-factor-icon.component.html", + standalone: false, }) export class TwoFactorIconComponent { @Input() provider: any; diff --git a/libs/angular/src/auth/components/user-verification.component.ts b/libs/angular/src/auth/components/user-verification.component.ts index 408d8403b88..6f5021340c7 100644 --- a/libs/angular/src/auth/components/user-verification.component.ts +++ b/libs/angular/src/auth/components/user-verification.component.ts @@ -22,6 +22,7 @@ import { KeyService } from "@bitwarden/key-management"; */ @Directive({ selector: "app-user-verification", + standalone: false, }) export class UserVerificationComponent implements ControlValueAccessor, OnInit, OnDestroy { private _invalidSecret = false; diff --git a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts index 871895c2ede..a6f1db54241 100644 --- a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts +++ b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts @@ -48,6 +48,7 @@ type PayPalConfig = { @Component({ templateUrl: "./add-account-credit-dialog.component.html", + standalone: false, }) export class AddAccountCreditDialogComponent implements OnInit { @ViewChild("payPalForm", { read: ElementRef, static: true }) payPalForm: ElementRef; diff --git a/libs/angular/src/billing/components/invoices/invoices.component.ts b/libs/angular/src/billing/components/invoices/invoices.component.ts index 8984c1afe65..fc3352048d6 100644 --- a/libs/angular/src/billing/components/invoices/invoices.component.ts +++ b/libs/angular/src/billing/components/invoices/invoices.component.ts @@ -11,6 +11,7 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil @Component({ selector: "app-invoices", templateUrl: "./invoices.component.html", + standalone: false, }) export class InvoicesComponent implements OnInit { @Input() startWith?: InvoicesResponse; diff --git a/libs/angular/src/billing/components/invoices/no-invoices.component.ts b/libs/angular/src/billing/components/invoices/no-invoices.component.ts index fffaaf65cfe..987650d1c83 100644 --- a/libs/angular/src/billing/components/invoices/no-invoices.component.ts +++ b/libs/angular/src/billing/components/invoices/no-invoices.component.ts @@ -30,6 +30,7 @@ const partnerTrustIcon = svgIcon`

{{ "noInvoicesToList" | i18n }}

`, + standalone: false, }) export class NoInvoicesComponent { icon = partnerTrustIcon; diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts index e966b4e0a75..7088d8edfcc 100644 --- a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts +++ b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts @@ -11,6 +11,7 @@ import { CountryListItem, TaxInformation } from "@bitwarden/common/billing/model @Component({ selector: "app-manage-tax-information", templateUrl: "./manage-tax-information.component.html", + standalone: false, }) export class ManageTaxInformationComponent implements OnInit, OnDestroy { @Input() startWith: TaxInformation; diff --git a/libs/angular/src/billing/directives/not-premium.directive.ts b/libs/angular/src/billing/directives/not-premium.directive.ts index 5a1c636c009..41d62bb773e 100644 --- a/libs/angular/src/billing/directives/not-premium.directive.ts +++ b/libs/angular/src/billing/directives/not-premium.directive.ts @@ -9,6 +9,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs */ @Directive({ selector: "[appNotPremium]", + standalone: false, }) export class NotPremiumDirective implements OnInit { constructor( diff --git a/libs/angular/src/components/callout.component.ts b/libs/angular/src/components/callout.component.ts index 1e5285cc3a9..215de49f676 100644 --- a/libs/angular/src/components/callout.component.ts +++ b/libs/angular/src/components/callout.component.ts @@ -12,6 +12,7 @@ import { CalloutTypes } from "@bitwarden/components"; @Component({ selector: "app-callout", templateUrl: "callout.component.html", + standalone: false, }) export class DeprecatedCalloutComponent implements OnInit { @Input() type: CalloutTypes = "info"; diff --git a/libs/angular/src/components/modal/dynamic-modal.component.ts b/libs/angular/src/components/modal/dynamic-modal.component.ts index ccbfa19868f..77491193916 100644 --- a/libs/angular/src/components/modal/dynamic-modal.component.ts +++ b/libs/angular/src/components/modal/dynamic-modal.component.ts @@ -18,6 +18,7 @@ import { ModalRef } from "./modal.ref"; @Component({ selector: "app-modal", template: "", + standalone: false, }) export class DynamicModalComponent implements AfterViewInit, OnDestroy { componentRef: ComponentRef; diff --git a/libs/angular/src/directives/a11y-invalid.directive.ts b/libs/angular/src/directives/a11y-invalid.directive.ts index 60580fcb60e..032c08d5332 100644 --- a/libs/angular/src/directives/a11y-invalid.directive.ts +++ b/libs/angular/src/directives/a11y-invalid.directive.ts @@ -6,6 +6,7 @@ import { Subscription } from "rxjs"; @Directive({ selector: "[appA11yInvalid]", + standalone: false, }) export class A11yInvalidDirective implements OnDestroy, OnInit { private sub: Subscription; diff --git a/libs/angular/src/directives/api-action.directive.ts b/libs/angular/src/directives/api-action.directive.ts index a07ab7d0413..85ba8a7489c 100644 --- a/libs/angular/src/directives/api-action.directive.ts +++ b/libs/angular/src/directives/api-action.directive.ts @@ -15,6 +15,7 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid */ @Directive({ selector: "[appApiAction]", + standalone: false, }) export class ApiActionDirective implements OnChanges { @Input() appApiAction: Promise; diff --git a/libs/angular/src/directives/box-row.directive.ts b/libs/angular/src/directives/box-row.directive.ts index 81fb93596f2..d36dcb7ff88 100644 --- a/libs/angular/src/directives/box-row.directive.ts +++ b/libs/angular/src/directives/box-row.directive.ts @@ -4,6 +4,7 @@ import { Directive, ElementRef, HostListener, OnInit } from "@angular/core"; @Directive({ selector: "[appBoxRow]", + standalone: false, }) export class BoxRowDirective implements OnInit { el: HTMLElement = null; diff --git a/libs/angular/src/directives/copy-text.directive.ts b/libs/angular/src/directives/copy-text.directive.ts index de26973e2c9..0f9018e19ad 100644 --- a/libs/angular/src/directives/copy-text.directive.ts +++ b/libs/angular/src/directives/copy-text.directive.ts @@ -7,6 +7,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl @Directive({ selector: "[appCopyText]", + standalone: false, }) export class CopyTextDirective { constructor( diff --git a/libs/angular/src/directives/fallback-src.directive.ts b/libs/angular/src/directives/fallback-src.directive.ts index 600782f3404..f1225245912 100644 --- a/libs/angular/src/directives/fallback-src.directive.ts +++ b/libs/angular/src/directives/fallback-src.directive.ts @@ -4,6 +4,7 @@ import { Directive, ElementRef, HostListener, Input } from "@angular/core"; @Directive({ selector: "[appFallbackSrc]", + standalone: false, }) export class FallbackSrcDirective { @Input("appFallbackSrc") appFallbackSrc: string; diff --git a/libs/angular/src/directives/if-feature.directive.ts b/libs/angular/src/directives/if-feature.directive.ts index 0186592d2d0..aa10c9e8081 100644 --- a/libs/angular/src/directives/if-feature.directive.ts +++ b/libs/angular/src/directives/if-feature.directive.ts @@ -14,6 +14,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" */ @Directive({ selector: "[appIfFeature]", + standalone: false, }) export class IfFeatureDirective implements OnInit { /** diff --git a/libs/angular/src/directives/input-strip-spaces.directive.ts b/libs/angular/src/directives/input-strip-spaces.directive.ts index 3b8fee851c0..1718aaa49d6 100644 --- a/libs/angular/src/directives/input-strip-spaces.directive.ts +++ b/libs/angular/src/directives/input-strip-spaces.directive.ts @@ -5,6 +5,7 @@ import { NgControl } from "@angular/forms"; @Directive({ selector: "input[appInputStripSpaces]", + standalone: false, }) export class InputStripSpacesDirective { constructor( diff --git a/libs/angular/src/directives/input-verbatim.directive.ts b/libs/angular/src/directives/input-verbatim.directive.ts index deecae624f1..7bd18b12659 100644 --- a/libs/angular/src/directives/input-verbatim.directive.ts +++ b/libs/angular/src/directives/input-verbatim.directive.ts @@ -4,6 +4,7 @@ import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core"; @Directive({ selector: "[appInputVerbatim]", + standalone: false, }) export class InputVerbatimDirective implements OnInit { @Input() set appInputVerbatim(condition: boolean | string) { diff --git a/libs/angular/src/directives/launch-click.directive.ts b/libs/angular/src/directives/launch-click.directive.ts index e748afabf49..b270dbba5e3 100644 --- a/libs/angular/src/directives/launch-click.directive.ts +++ b/libs/angular/src/directives/launch-click.directive.ts @@ -5,6 +5,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; @Directive({ selector: "[appLaunchClick]", + standalone: false, }) export class LaunchClickDirective { constructor(private platformUtilsService: PlatformUtilsService) {} diff --git a/libs/angular/src/directives/stop-click.directive.ts b/libs/angular/src/directives/stop-click.directive.ts index 0e88dde33a4..58ae8082aa7 100644 --- a/libs/angular/src/directives/stop-click.directive.ts +++ b/libs/angular/src/directives/stop-click.directive.ts @@ -2,6 +2,7 @@ import { Directive, HostListener } from "@angular/core"; @Directive({ selector: "[appStopClick]", + standalone: false, }) export class StopClickDirective { @HostListener("click", ["$event"]) onClick($event: MouseEvent) { diff --git a/libs/angular/src/directives/stop-prop.directive.ts b/libs/angular/src/directives/stop-prop.directive.ts index 8393e799038..6ea9bc87bec 100644 --- a/libs/angular/src/directives/stop-prop.directive.ts +++ b/libs/angular/src/directives/stop-prop.directive.ts @@ -2,6 +2,7 @@ import { Directive, HostListener } from "@angular/core"; @Directive({ selector: "[appStopProp]", + standalone: false, }) export class StopPropDirective { @HostListener("click", ["$event"]) onClick($event: MouseEvent) { diff --git a/libs/angular/src/directives/true-false-value.directive.ts b/libs/angular/src/directives/true-false-value.directive.ts index dab88567a5c..5d25ac2a385 100644 --- a/libs/angular/src/directives/true-false-value.directive.ts +++ b/libs/angular/src/directives/true-false-value.directive.ts @@ -11,6 +11,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; multi: true, }, ], + standalone: false, }) export class TrueFalseValueDirective implements ControlValueAccessor { @Input() trueValue: boolean | string = true; diff --git a/libs/angular/src/pipes/color-password-count.pipe.ts b/libs/angular/src/pipes/color-password-count.pipe.ts index ed94e6dadff..61be6db2e2c 100644 --- a/libs/angular/src/pipes/color-password-count.pipe.ts +++ b/libs/angular/src/pipes/color-password-count.pipe.ts @@ -7,7 +7,10 @@ import { ColorPasswordPipe } from "./color-password.pipe"; /* An updated pipe that extends ColourPasswordPipe to include a character count */ -@Pipe({ name: "colorPasswordCount" }) +@Pipe({ + name: "colorPasswordCount", + standalone: false, +}) export class ColorPasswordCountPipe extends ColorPasswordPipe { transform(password: string) { const template = (character: string, type: string, index: number) => diff --git a/libs/angular/src/pipes/color-password.pipe.ts b/libs/angular/src/pipes/color-password.pipe.ts index 8407eddd9ca..6bb7bd4120d 100644 --- a/libs/angular/src/pipes/color-password.pipe.ts +++ b/libs/angular/src/pipes/color-password.pipe.ts @@ -6,7 +6,10 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; An updated pipe that sanitizes HTML, highlights numbers and special characters (in different colors each) and handles Unicode / Emoji characters correctly. */ -@Pipe({ name: "colorPassword" }) +@Pipe({ + name: "colorPassword", + standalone: false, +}) export class ColorPasswordPipe implements PipeTransform { transform(password: string) { const template = (character: string, type: string) => diff --git a/libs/angular/src/pipes/credit-card-number.pipe.ts b/libs/angular/src/pipes/credit-card-number.pipe.ts index 11500ef9510..80718612d35 100644 --- a/libs/angular/src/pipes/credit-card-number.pipe.ts +++ b/libs/angular/src/pipes/credit-card-number.pipe.ts @@ -28,7 +28,10 @@ const numberFormats: Record = { Other: [{ cardLength: 16, blocks: [4, 4, 4, 4] }], }; -@Pipe({ name: "creditCardNumber" }) +@Pipe({ + name: "creditCardNumber", + standalone: false, +}) export class CreditCardNumberPipe implements PipeTransform { transform(creditCardNumber: string, brand: string): string { let rules = numberFormats[brand]; diff --git a/libs/angular/src/pipes/search-ciphers.pipe.ts b/libs/angular/src/pipes/search-ciphers.pipe.ts index 04349e9842a..cbb595280af 100644 --- a/libs/angular/src/pipes/search-ciphers.pipe.ts +++ b/libs/angular/src/pipes/search-ciphers.pipe.ts @@ -4,6 +4,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @Pipe({ name: "searchCiphers", + standalone: false, }) export class SearchCiphersPipe implements PipeTransform { transform(ciphers: CipherView[], searchText: string, deleted = false): CipherView[] { diff --git a/libs/angular/src/pipes/search.pipe.ts b/libs/angular/src/pipes/search.pipe.ts index 9f640a5b48e..2c652bc6382 100644 --- a/libs/angular/src/pipes/search.pipe.ts +++ b/libs/angular/src/pipes/search.pipe.ts @@ -6,6 +6,7 @@ type PropertyValueFunction = (item: T) => { toString: () => string }; @Pipe({ name: "search", + standalone: false, }) export class SearchPipe implements PipeTransform { transform( diff --git a/libs/angular/src/pipes/user-name.pipe.ts b/libs/angular/src/pipes/user-name.pipe.ts index 29b884a3115..4c295c7ef13 100644 --- a/libs/angular/src/pipes/user-name.pipe.ts +++ b/libs/angular/src/pipes/user-name.pipe.ts @@ -9,6 +9,7 @@ export interface User { @Pipe({ name: "userName", + standalone: false, }) export class UserNamePipe implements PipeTransform { transform(user?: User): string { diff --git a/libs/angular/src/pipes/user-type.pipe.ts b/libs/angular/src/pipes/user-type.pipe.ts index 109abc3eeff..d9b5faa2ea9 100644 --- a/libs/angular/src/pipes/user-type.pipe.ts +++ b/libs/angular/src/pipes/user-type.pipe.ts @@ -5,6 +5,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic @Pipe({ name: "userType", + standalone: false, }) export class UserTypePipe implements PipeTransform { constructor(private i18nService: I18nService) {} diff --git a/libs/angular/src/platform/pipes/ellipsis.pipe.ts b/libs/angular/src/platform/pipes/ellipsis.pipe.ts index dd271f94627..a7050b2f037 100644 --- a/libs/angular/src/platform/pipes/ellipsis.pipe.ts +++ b/libs/angular/src/platform/pipes/ellipsis.pipe.ts @@ -2,6 +2,7 @@ import { Pipe, PipeTransform } from "@angular/core"; @Pipe({ name: "ellipsis", + standalone: false, }) /** * @deprecated Use the tailwind class 'tw-truncate' instead diff --git a/libs/angular/src/platform/pipes/fingerprint.pipe.ts b/libs/angular/src/platform/pipes/fingerprint.pipe.ts index 8f1a07cfd6b..90289ee212b 100644 --- a/libs/angular/src/platform/pipes/fingerprint.pipe.ts +++ b/libs/angular/src/platform/pipes/fingerprint.pipe.ts @@ -5,6 +5,7 @@ import { KeyService } from "@bitwarden/key-management"; @Pipe({ name: "fingerprint", + standalone: false, }) export class FingerprintPipe { constructor(private keyService: KeyService) {} diff --git a/libs/angular/src/platform/pipes/i18n.pipe.ts b/libs/angular/src/platform/pipes/i18n.pipe.ts index a6fdbc78255..a1c6122ba16 100644 --- a/libs/angular/src/platform/pipes/i18n.pipe.ts +++ b/libs/angular/src/platform/pipes/i18n.pipe.ts @@ -7,6 +7,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic */ @Pipe({ name: "i18n", + standalone: false, }) export class I18nPipe implements PipeTransform { constructor(private i18nService: I18nService) {} diff --git a/libs/angular/src/tools/password-strength/password-strength.component.ts b/libs/angular/src/tools/password-strength/password-strength.component.ts index d23225b7c0c..ca9892d9c6c 100644 --- a/libs/angular/src/tools/password-strength/password-strength.component.ts +++ b/libs/angular/src/tools/password-strength/password-strength.component.ts @@ -16,6 +16,7 @@ export interface PasswordColorText { @Component({ selector: "app-password-strength", templateUrl: "password-strength.component.html", + standalone: false, }) export class PasswordStrengthComponent implements OnChanges { @Input() showText = false; diff --git a/libs/angular/src/vault/components/icon.component.ts b/libs/angular/src/vault/components/icon.component.ts index 248378bf5ee..fd178db23b6 100644 --- a/libs/angular/src/vault/components/icon.component.ts +++ b/libs/angular/src/vault/components/icon.component.ts @@ -19,6 +19,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; selector: "app-vault-icon", templateUrl: "icon.component.html", changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false, }) export class IconComponent { /** diff --git a/libs/components/src/app/app.component.ts b/libs/components/src/app/app.component.ts index d4c9ddd2f41..ed8cf595d5e 100644 --- a/libs/components/src/app/app.component.ts +++ b/libs/components/src/app/app.component.ts @@ -3,6 +3,7 @@ import { Component } from "@angular/core"; @Component({ selector: "app-root", template: "", + standalone: false, }) export class AppComponent { title = "components"; diff --git a/libs/components/src/input/autofocus.directive.ts b/libs/components/src/input/autofocus.directive.ts index 46eb1b15b16..3fd06156f39 100644 --- a/libs/components/src/input/autofocus.directive.ts +++ b/libs/components/src/input/autofocus.directive.ts @@ -19,6 +19,7 @@ import { FocusableElement } from "../shared/focusable-element"; */ @Directive({ selector: "[appAutofocus], [bitAutofocus]", + standalone: false, }) export class AutofocusDirective implements AfterContentChecked { @Input() set appAutofocus(condition: boolean | string) { diff --git a/libs/tools/generator/components/src/catchall-settings.component.ts b/libs/tools/generator/components/src/catchall-settings.component.ts index 92d0909e661..3bf586c5a29 100644 --- a/libs/tools/generator/components/src/catchall-settings.component.ts +++ b/libs/tools/generator/components/src/catchall-settings.component.ts @@ -24,6 +24,7 @@ import { @Component({ selector: "tools-catchall-settings", templateUrl: "catchall-settings.component.html", + standalone: false, }) export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges { /** Instantiates the component diff --git a/libs/tools/generator/components/src/credential-generator.component.ts b/libs/tools/generator/components/src/credential-generator.component.ts index 4a83f2a9a92..0b48b4cf0f7 100644 --- a/libs/tools/generator/components/src/credential-generator.component.ts +++ b/libs/tools/generator/components/src/credential-generator.component.ts @@ -67,6 +67,7 @@ const NONE_SELECTED = "none"; @Component({ selector: "tools-credential-generator", templateUrl: "credential-generator.component.html", + standalone: false, }) export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestroy { private readonly destroyed = new Subject(); diff --git a/libs/tools/generator/components/src/forwarder-settings.component.ts b/libs/tools/generator/components/src/forwarder-settings.component.ts index 8a5311fb7f3..689cc7e258c 100644 --- a/libs/tools/generator/components/src/forwarder-settings.component.ts +++ b/libs/tools/generator/components/src/forwarder-settings.component.ts @@ -33,6 +33,7 @@ const Controls = Object.freeze({ @Component({ selector: "tools-forwarder-settings", templateUrl: "forwarder-settings.component.html", + standalone: false, }) export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy { /** Instantiates the component diff --git a/libs/tools/generator/components/src/passphrase-settings.component.ts b/libs/tools/generator/components/src/passphrase-settings.component.ts index 0509ceab60f..405914977c5 100644 --- a/libs/tools/generator/components/src/passphrase-settings.component.ts +++ b/libs/tools/generator/components/src/passphrase-settings.component.ts @@ -33,6 +33,7 @@ const Controls = Object.freeze({ @Component({ selector: "tools-passphrase-settings", templateUrl: "passphrase-settings.component.html", + standalone: false, }) export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy { /** Instantiates the component diff --git a/libs/tools/generator/components/src/password-generator.component.ts b/libs/tools/generator/components/src/password-generator.component.ts index e4e173829a6..9643c857473 100644 --- a/libs/tools/generator/components/src/password-generator.component.ts +++ b/libs/tools/generator/components/src/password-generator.component.ts @@ -53,6 +53,7 @@ import { GeneratorHistoryService } from "@bitwarden/generator-history"; @Component({ selector: "tools-password-generator", templateUrl: "password-generator.component.html", + standalone: false, }) export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy { constructor( diff --git a/libs/tools/generator/components/src/password-settings.component.ts b/libs/tools/generator/components/src/password-settings.component.ts index e4005c6fda7..346e9549cd8 100644 --- a/libs/tools/generator/components/src/password-settings.component.ts +++ b/libs/tools/generator/components/src/password-settings.component.ts @@ -37,6 +37,7 @@ const Controls = Object.freeze({ @Component({ selector: "tools-password-settings", templateUrl: "password-settings.component.html", + standalone: false, }) export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy { /** Instantiates the component diff --git a/libs/tools/generator/components/src/subaddress-settings.component.ts b/libs/tools/generator/components/src/subaddress-settings.component.ts index 8bde260693c..b09ecc86f9e 100644 --- a/libs/tools/generator/components/src/subaddress-settings.component.ts +++ b/libs/tools/generator/components/src/subaddress-settings.component.ts @@ -24,6 +24,7 @@ import { @Component({ selector: "tools-subaddress-settings", templateUrl: "subaddress-settings.component.html", + standalone: false, }) export class SubaddressSettingsComponent implements OnInit, OnChanges, OnDestroy { /** Instantiates the component diff --git a/libs/tools/generator/components/src/username-generator.component.ts b/libs/tools/generator/components/src/username-generator.component.ts index 0c06b89adb4..de48a9bd6b1 100644 --- a/libs/tools/generator/components/src/username-generator.component.ts +++ b/libs/tools/generator/components/src/username-generator.component.ts @@ -67,6 +67,7 @@ const NONE_SELECTED = "none"; @Component({ selector: "tools-username-generator", templateUrl: "username-generator.component.html", + standalone: false, }) export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy { /** Instantiates the username generator diff --git a/libs/tools/generator/components/src/username-settings.component.ts b/libs/tools/generator/components/src/username-settings.component.ts index 7e59ef9c379..ea3cfbd35fb 100644 --- a/libs/tools/generator/components/src/username-settings.component.ts +++ b/libs/tools/generator/components/src/username-settings.component.ts @@ -24,6 +24,7 @@ import { @Component({ selector: "tools-username-settings", templateUrl: "username-settings.component.html", + standalone: false, }) export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy { /** Instantiates the component From b30faeb62ba2c257f691454960b082f799618651 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Thu, 15 May 2025 10:09:51 -0700 Subject: [PATCH 055/163] [PM-21554] - Creating a new item while editing edits the item (#14770) * fix adding new cipher while editing a cipher * don't set updatedCipherView if the cached cipher has an id and the new one doesn't * fix cipher form config --- apps/desktop/src/vault/app/vault/vault-v2.component.ts | 1 + .../src/cipher-form/components/cipher-form.component.ts | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts index 66e77580d1c..6c60aaf0f02 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -538,6 +538,7 @@ export class VaultV2Component implements OnInit, OnDestroy { } this.addType = type || this.activeFilter.cipherType; this.cipher = new CipherView(); + this.cipherId = null; await this.buildFormConfig("add"); this.action = "add"; this.prefillCipherFromFilter(); diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.ts b/libs/vault/src/cipher-form/components/cipher-form.component.ts index 8b99b60bc16..eebfca65f36 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.ts @@ -275,11 +275,6 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci if (this.updatedCipherView.id === cachedCipher.id) { this.updatedCipherView = cachedCipher; } - - // `id` is null when a cipher is being added - if (this.updatedCipherView.id === null) { - this.updatedCipherView = cachedCipher; - } } constructor( From 82d0925f4eb2727b8c635d6aeef0355fb8e8c602 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Thu, 15 May 2025 13:32:05 -0400 Subject: [PATCH 056/163] PM-21620 finalize a11y UX concerns for option selection (#14777) * PM-21620 finalize a11y UX concerns for option selection * SR should announce that the button has a menu popup collapsed and expanded when they open it * support up and down keys -close menu when other menu expanded * dynamic aria label * type safety * instanceOf to replace as Node for type * default aria hidden prop that can be overridden * update mock and make message more descriptive * Update apps/browser/src/autofill/content/components/icons/collection-shared.ts Co-authored-by: Jonathan Prusik --------- Co-authored-by: Jonathan Prusik --- apps/browser/src/_locales/en/messages.json | 17 ++++++-- .../buttons/option-selection-button.ts | 3 ++ .../content/components/common-types.ts | 1 + .../content/components/icons/angle-down.ts | 9 +++- .../content/components/icons/angle-up.ts | 9 +++- .../content/components/icons/business.ts | 9 +++- .../content/components/icons/close.ts | 9 +++- .../components/icons/collection-shared.ts | 9 +++- .../components/icons/exclamation-triangle.ts | 9 +++- .../content/components/icons/external-link.ts | 9 +++- .../content/components/icons/family.ts | 9 +++- .../content/components/icons/folder.ts | 9 +++- .../content/components/icons/globe.ts | 9 +++- .../content/components/icons/pencil-square.ts | 9 +++- .../content/components/icons/shield.ts | 9 +++- .../autofill/content/components/icons/user.ts | 9 +++- .../components/lit-stories/mock-data.ts | 1 + .../option-selection/option-item.ts | 16 +++++++- .../option-selection/option-items.ts | 28 ++++++++++++- .../option-selection/option-selection.ts | 41 ++++++++++++++++--- 20 files changed, 187 insertions(+), 37 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index df35facff3c..a14d769d4cd 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1090,13 +1090,24 @@ }, "notificationLoginSaveConfirmation": { "message": "saved to Bitwarden.", - "description": "Shown to user after item is saved." }, "notificationLoginUpdatedConfirmation": { "message": "updated in Bitwarden.", "description": "Shown to user after item is updated." }, + "selectItemAriaLabel": { + "message": "Select $ITEMTYPE$, $ITEMNAME$", + "description": "Used by screen readers. $1 is the item type (like vault or folder), $2 is the selected item name.", + "placeholders": { + "itemType": { + "content": "$1" + }, + "itemName": { + "content": "$2" + } + } + }, "saveAsNewLoginAction": { "message": "Save as new login", "description": "Button text for saving login details as a new entry." @@ -3585,7 +3596,7 @@ "orgTrustWarning1": { "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":{ + "trustUser": { "message": "Trust user" }, "sendsNoItemsTitle": { @@ -5325,4 +5336,4 @@ "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } -} +} \ No newline at end of file diff --git a/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts b/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts index e3c7e0d54e6..3912c791d34 100644 --- a/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts @@ -33,6 +33,9 @@ export function OptionSelectionButton({ class=${selectionButtonStyles({ disabled, toggledOn, theme })} title=${text} type="button" + aria-haspopup="menu" + aria-expanded=${toggledOn} + aria-controls="option-menu" @click=${handleButtonClick} > ${buttonIcon ?? nothing} diff --git a/apps/browser/src/autofill/content/components/common-types.ts b/apps/browser/src/autofill/content/components/common-types.ts index 740b6963b16..5967f6205a9 100644 --- a/apps/browser/src/autofill/content/components/common-types.ts +++ b/apps/browser/src/autofill/content/components/common-types.ts @@ -11,6 +11,7 @@ export type IconProps = { color?: string; disabled?: boolean; theme: Theme; + ariaHidden?: boolean; }; export type Option = { diff --git a/apps/browser/src/autofill/content/components/icons/angle-down.ts b/apps/browser/src/autofill/content/components/icons/angle-down.ts index 27cd5ab81c5..2bd54f7dbee 100644 --- a/apps/browser/src/autofill/content/components/icons/angle-down.ts +++ b/apps/browser/src/autofill/content/components/icons/angle-down.ts @@ -4,11 +4,16 @@ import { html } from "lit"; import { IconProps } from "../common-types"; import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; -export function AngleDown({ color, disabled, theme }: IconProps) { +export function AngleDown({ ariaHidden = true, color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + + + + + + + + + +
diff --git a/apps/browser/src/autofill/content/components/option-selection/option-items.ts b/apps/browser/src/autofill/content/components/option-selection/option-items.ts index bb7f5afd0fd..854b7c05df1 100644 --- a/apps/browser/src/autofill/content/components/option-selection/option-items.ts +++ b/apps/browser/src/autofill/content/components/option-selection/option-items.ts @@ -33,17 +33,41 @@ export function OptionItems({ const isSafari = false; return html` -
+
handleMenuKeyUp(e)} + > ${label ? html`
${label}
` : nothing}
${options.map((option) => - OptionItem({ ...option, theme, handleSelection: () => handleOptionSelection(option) }), + OptionItem({ + ...option, + theme, + contextLabel: label, + handleSelection: () => handleOptionSelection(option), + }), )}
`; } +function handleMenuKeyUp(event: KeyboardEvent) { + const items = [ + ...(event.currentTarget as HTMLElement).querySelectorAll('[tabindex="0"]'), + ]; + const index = items.indexOf(document.activeElement as HTMLElement); + const direction = event.key === "ArrowDown" ? 1 : event.key === "ArrowUp" ? -1 : 0; + + if (index === -1 || direction === 0) { + return; + } + + event.preventDefault(); + items[(index + direction + items.length) % items.length]?.focus(); +} + const optionsStyles = ({ theme, topOffset }: { theme: Theme; topOffset: number }) => css` ${typography.body1} diff --git a/apps/browser/src/autofill/content/components/option-selection/option-selection.ts b/apps/browser/src/autofill/content/components/option-selection/option-selection.ts index 49b51852a39..c7dceb2b5b4 100644 --- a/apps/browser/src/autofill/content/components/option-selection/option-selection.ts +++ b/apps/browser/src/autofill/content/components/option-selection/option-selection.ts @@ -48,10 +48,18 @@ export class OptionSelection extends LitElement { @state() private selection?: Option; - private handleButtonClick = (event: Event) => { + private static currentOpenInstance: OptionSelection | null = null; + + private handleButtonClick = async (event: Event) => { if (!this.disabled) { - // Menu is about to be shown - if (!this.showMenu) { + const isOpening = !this.showMenu; + + if (isOpening) { + if (OptionSelection.currentOpenInstance && OptionSelection.currentOpenInstance !== this) { + OptionSelection.currentOpenInstance.showMenu = false; + } + OptionSelection.currentOpenInstance = this; + this.menuTopOffset = this.offsetTop; // Distance from right edge of button to left edge of the viewport @@ -71,9 +79,29 @@ export class OptionSelection extends LitElement { optionsMenuItemMaxWidth + optionItemIconWidth + 2 + 8 + 12 * 2; this.menuIsEndJustified = distanceFromViewportRightEdge < maxDifferenceThreshold; + } else { + if (OptionSelection.currentOpenInstance === this) { + OptionSelection.currentOpenInstance = null; + } } - this.showMenu = !this.showMenu; + this.showMenu = isOpening; + + if (this.showMenu) { + await this.updateComplete; + const firstItem = this.querySelector('#option-menu [tabindex="0"]') as HTMLElement; + firstItem?.focus(); + } + } + }; + + private handleFocusOut = (event: FocusEvent) => { + const relatedTarget = event.relatedTarget; + if (!(relatedTarget instanceof Node) || !this.contains(relatedTarget)) { + this.showMenu = false; + if (OptionSelection.currentOpenInstance === this) { + OptionSelection.currentOpenInstance = null; + } } }; @@ -95,7 +123,10 @@ export class OptionSelection extends LitElement { } return html` -
+
${OptionSelectionButton({ disabled: this.disabled, icon: this.selection?.icon, From ee4c3cfd9475b57118f06eb5dedc82707af92e26 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Thu, 15 May 2025 15:10:38 -0400 Subject: [PATCH 057/163] [PM-21663] nudge service name refactor (#14789) * update names of vault nudge service and their corresponding files, convert components using showNudge$ to instead target spotlight and badges directly with new observables. Core logic for dismiss remains the same --- .../popup/settings/autofill.component.ts | 20 ++--- apps/browser/src/popup/tabs-v2.component.ts | 6 +- .../popup/settings/settings-v2.component.html | 6 +- .../popup/settings/settings-v2.component.ts | 26 +++--- .../vault-v2/vault-v2.component.html | 4 +- .../components/vault-v2/vault-v2.component.ts | 18 ++-- .../settings/download-bitwarden.component.ts | 6 +- .../src/platform/state/state-definitions.ts | 2 +- .../src/cipher-form/cipher-form.stories.ts | 4 +- .../new-item-nudge.component.spec.ts | 23 +++-- .../new-item-nudge.component.ts | 28 +++--- libs/vault/src/index.ts | 2 +- .../autofill-nudge.service.ts | 6 +- .../download-bitwarden-nudge.service.ts | 4 +- .../empty-vault-nudge.service.ts | 4 +- .../has-items-nudge.service.ts | 6 +- .../new-item-nudge.service.ts | 14 +-- .../services/default-single-nudge.service.ts | 24 ++---- ...service.spec.ts => nudges.service.spec.ts} | 62 +++++++++---- ...lt-nudges.service.ts => nudges.service.ts} | 86 +++++++++++++------ 20 files changed, 199 insertions(+), 152 deletions(-) rename libs/vault/src/services/{vault-nudges.service.spec.ts => nudges.service.spec.ts} (72%) rename libs/vault/src/services/{vault-nudges.service.ts => nudges.service.ts} (56%) diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.ts b/apps/browser/src/autofill/popup/settings/autofill.component.ts index d63f9a4589d..2d29067cf0f 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill.component.ts @@ -4,14 +4,14 @@ import { CommonModule } from "@angular/common"; import { Component, DestroyRef, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { + FormBuilder, + FormControl, + FormGroup, FormsModule, ReactiveFormsModule, - FormBuilder, - FormGroup, - FormControl, } from "@angular/forms"; import { RouterModule } from "@angular/router"; -import { Observable, filter, firstValueFrom, map, switchMap } from "rxjs"; +import { filter, firstValueFrom, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -55,7 +55,7 @@ import { SelectModule, TypographyModule, } from "@bitwarden/components"; -import { SpotlightComponent, VaultNudgesService, VaultNudgeType } from "@bitwarden/vault"; +import { NudgesService, NudgeType, SpotlightComponent } from "@bitwarden/vault"; import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; @@ -108,9 +108,7 @@ export class AutofillComponent implements OnInit { protected showSpotlightNudge$: Observable = this.accountService.activeAccount$.pipe( filter((account): account is Account => account !== null), switchMap((account) => - this.vaultNudgesService - .showNudge$(VaultNudgeType.AutofillNudge, account.id) - .pipe(map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed)), + this.nudgesService.showNudgeSpotlight$(NudgeType.AutofillNudge, account.id), ), ); @@ -155,7 +153,7 @@ export class AutofillComponent implements OnInit { private configService: ConfigService, private formBuilder: FormBuilder, private destroyRef: DestroyRef, - private vaultNudgesService: VaultNudgesService, + private nudgesService: NudgesService, private accountService: AccountService, private autofillBrowserSettingsService: AutofillBrowserSettingsService, ) { @@ -343,8 +341,8 @@ export class AutofillComponent implements OnInit { } async dismissSpotlight() { - await this.vaultNudgesService.dismissNudge( - VaultNudgeType.AutofillNudge, + await this.nudgesService.dismissNudge( + NudgeType.AutofillNudge, await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)), ); } diff --git a/apps/browser/src/popup/tabs-v2.component.ts b/apps/browser/src/popup/tabs-v2.component.ts index 5df8fb85d6d..0ca763d510d 100644 --- a/apps/browser/src/popup/tabs-v2.component.ts +++ b/apps/browser/src/popup/tabs-v2.component.ts @@ -6,7 +6,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { Icons } from "@bitwarden/components"; -import { VaultNudgesService } from "@bitwarden/vault"; +import { NudgesService } from "@bitwarden/vault"; import { NavButton } from "../platform/popup/layout/popup-tab-navigation.component"; @@ -18,7 +18,7 @@ import { NavButton } from "../platform/popup/layout/popup-tab-navigation.compone export class TabsV2Component { private hasActiveBadges$ = this.accountService.activeAccount$ .pipe(getUserId) - .pipe(switchMap((userId) => this.vaultNudgesService.hasActiveBadges$(userId))); + .pipe(switchMap((userId) => this.nudgesService.hasActiveBadges$(userId))); protected navButtons$: Observable = combineLatest([ this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge), this.hasActiveBadges$, @@ -54,7 +54,7 @@ export class TabsV2Component { }), ); constructor( - private vaultNudgesService: VaultNudgesService, + private nudgesService: NudgesService, private accountService: AccountService, private readonly configService: ConfigService, ) {} diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.html b/apps/browser/src/tools/popup/settings/settings-v2.component.html index 8d31ccf8371..dc53f95a7cf 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.html +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html @@ -41,7 +41,7 @@
@@ -51,7 +51,7 @@ Will make this dynamic when more nudges are added -->

{{ "downloadBitwardenOnAllDevices" | i18n }}

= this.authenticatedAccount$.pipe( + downloadBitwardenNudgeStatus$: Observable = this.authenticatedAccount$.pipe( switchMap((account) => - this.vaultNudgesService.showNudge$(VaultNudgeType.DownloadBitwarden, account.id), + this.nudgesService.showNudgeBadge$(NudgeType.DownloadBitwarden, account.id), ), ); - showVaultBadge$: Observable = this.authenticatedAccount$.pipe( + showVaultBadge$: Observable = this.authenticatedAccount$.pipe( switchMap((account) => - this.vaultNudgesService.showNudge$(VaultNudgeType.EmptyVaultNudge, account.id), + this.nudgesService.showNudgeBadge$(NudgeType.EmptyVaultNudge, account.id), ), ); @@ -68,9 +68,9 @@ export class SettingsV2Component implements OnInit { this.authenticatedAccount$, ]).pipe( switchMap(([defaultBrowserAutofillDisabled, account]) => - this.vaultNudgesService.showNudge$(VaultNudgeType.AutofillNudge, account.id).pipe( - map((nudgeStatus) => { - return !defaultBrowserAutofillDisabled && nudgeStatus.hasBadgeDismissed === false; + this.nudgesService.showNudgeBadge$(NudgeType.AutofillNudge, account.id).pipe( + map((badgeStatus) => { + return !defaultBrowserAutofillDisabled && badgeStatus; }), ), ), @@ -81,7 +81,7 @@ export class SettingsV2Component implements OnInit { ); constructor( - private readonly vaultNudgesService: VaultNudgesService, + private readonly nudgesService: NudgesService, private readonly accountService: AccountService, private readonly autofillBrowserSettingsService: AutofillBrowserSettingsService, private readonly configService: ConfigService, @@ -94,10 +94,10 @@ export class SettingsV2Component implements OnInit { ); } - async dismissBadge(type: VaultNudgeType) { - if (!(await firstValueFrom(this.showVaultBadge$)).hasBadgeDismissed) { + async dismissBadge(type: NudgeType) { + if (await firstValueFrom(this.showVaultBadge$)) { const account = await firstValueFrom(this.authenticatedAccount$); - await this.vaultNudgesService.dismissNudge(type, account.id as UserId, true); + await this.nudgesService.dismissNudge(type, account.id as UserId, true); } } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html index 43a96fc616e..42e772be062 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html @@ -36,7 +36,7 @@ [subtitle]="'emptyVaultNudgeBody' | i18n" [buttonText]="'emptyVaultNudgeButton' | i18n" (onButtonClick)="navigateToImport()" - (onDismiss)="dismissVaultNudgeSpotlight(VaultNudgeType.EmptyVaultNudge)" + (onDismiss)="dismissVaultNudgeSpotlight(NudgeType.EmptyVaultNudge)" > @@ -44,7 +44,7 @@
- @let showFooterBorder = !isDrawer || background === "alt" || bodyHasScrolledFrom().bottom;
diff --git a/libs/components/src/dialog/dialog/dialog.component.ts b/libs/components/src/dialog/dialog/dialog.component.ts index 8cf9cd18c78..504dbd3a1ea 100644 --- a/libs/components/src/dialog/dialog/dialog.component.ts +++ b/libs/components/src/dialog/dialog/dialog.component.ts @@ -1,18 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { CdkTrapFocus } from "@angular/cdk/a11y"; import { coerceBooleanProperty } from "@angular/cdk/coercion"; -import { CdkScrollable } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; -import { Component, HostBinding, Input, inject, viewChild } from "@angular/core"; +import { Component, HostBinding, Input } from "@angular/core"; import { I18nPipe } from "@bitwarden/ui-common"; import { BitIconButtonComponent } from "../../icon-button/icon-button.component"; import { TypographyDirective } from "../../typography/typography.directive"; -import { hasScrolledFrom } from "../../utils/has-scrolled-from"; import { fadeIn } from "../animations"; -import { DialogRef } from "../dialog.service"; import { DialogCloseDirective } from "../directives/dialog-close.directive"; import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive"; @@ -21,9 +17,6 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai templateUrl: "./dialog.component.html", animations: [fadeIn], standalone: true, - host: { - "(keydown.esc)": "handleEsc($event)", - }, imports: [ CommonModule, DialogTitleContainerDirective, @@ -31,15 +24,9 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai BitIconButtonComponent, DialogCloseDirective, I18nPipe, - CdkTrapFocus, - CdkScrollable, ], }) export class DialogComponent { - protected dialogRef = inject(DialogRef, { optional: true }); - private scrollableBody = viewChild.required(CdkScrollable); - protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody); - /** Background color */ @Input() background: "default" | "alt" = "default"; @@ -77,31 +64,21 @@ export class DialogComponent { @HostBinding("class") get classes() { // `tw-max-h-[90vh]` is needed to prevent dialogs from overlapping the desktop header - return ["tw-flex", "tw-flex-col", "tw-w-screen"] - .concat( - this.width, - this.dialogRef?.isDrawer - ? ["tw-min-h-screen", "md:tw-w-[23rem]"] - : ["tw-p-4", "tw-w-screen", "tw-max-h-[90vh]"], - ) - .flat(); - } - - handleEsc(event: Event) { - this.dialogRef?.close(); - event.stopPropagation(); + return ["tw-flex", "tw-flex-col", "tw-w-screen", "tw-p-4", "tw-max-h-[90vh]"].concat( + this.width, + ); } get width() { switch (this.dialogSize) { case "small": { - return "md:tw-max-w-sm"; + return "tw-max-w-sm"; } case "large": { - return "md:tw-max-w-3xl"; + return "tw-max-w-3xl"; } default: { - return "md:tw-max-w-xl"; + return "tw-max-w-xl"; } } } diff --git a/libs/components/src/dialog/dialogs.mdx b/libs/components/src/dialog/dialogs.mdx index 3f44f31a5eb..63df0bfc131 100644 --- a/libs/components/src/dialog/dialogs.mdx +++ b/libs/components/src/dialog/dialogs.mdx @@ -22,9 +22,6 @@ For alerts or simple confirmation actions, like speedbumps, use the Dialogs's should be used sparingly as they do call extra attention to themselves and can be interruptive if overused. -For non-blocking, supplementary content, open dialogs as a -[Drawer](?path=/story/component-library-dialogs-service--drawer) (requires `bit-layout`). - ## Placement Dialogs should be centered vertically and horizontally on screen. Dialogs height should expand to diff --git a/libs/components/src/dialog/index.ts b/libs/components/src/dialog/index.ts index fb4c2721b81..0ab9a5d9e67 100644 --- a/libs/components/src/dialog/index.ts +++ b/libs/components/src/dialog/index.ts @@ -1,4 +1,4 @@ export * from "./dialog.module"; export * from "./simple-dialog/types"; export * from "./dialog.service"; -export { DIALOG_DATA } from "@angular/cdk/dialog"; +export { DialogConfig, DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; diff --git a/libs/components/src/drawer/drawer-body.component.ts b/libs/components/src/drawer/drawer-body.component.ts index 563987dd8b0..9bd2adcffbc 100644 --- a/libs/components/src/drawer/drawer-body.component.ts +++ b/libs/components/src/drawer/drawer-body.component.ts @@ -1,7 +1,7 @@ import { CdkScrollable } from "@angular/cdk/scrolling"; -import { ChangeDetectionStrategy, Component } from "@angular/core"; - -import { hasScrolledFrom } from "../utils/has-scrolled-from"; +import { ChangeDetectionStrategy, Component, Signal, inject } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { map } from "rxjs"; /** * Body container for `bit-drawer` @@ -14,7 +14,7 @@ import { hasScrolledFrom } from "../utils/has-scrolled-from"; host: { class: "tw-p-4 tw-pt-0 tw-block tw-overflow-auto tw-border-solid tw-border tw-border-transparent tw-transition-colors tw-duration-200", - "[class.tw-border-t-secondary-300]": "this.hasScrolledFrom().top", + "[class.tw-border-t-secondary-300]": "isScrolled()", }, hostDirectives: [ { @@ -24,5 +24,13 @@ import { hasScrolledFrom } from "../utils/has-scrolled-from"; template: ` `, }) export class DrawerBodyComponent { - protected hasScrolledFrom = hasScrolledFrom(); + private scrollable = inject(CdkScrollable); + + /** TODO: share this utility with browser popup header? */ + protected isScrolled: Signal = toSignal( + this.scrollable + .elementScrolled() + .pipe(map(() => this.scrollable.measureScrollOffset("top") > 0)), + { initialValue: false }, + ); } diff --git a/libs/components/src/drawer/drawer.component.ts b/libs/components/src/drawer/drawer.component.ts index 351be57e07b..ccabb6f0b6e 100644 --- a/libs/components/src/drawer/drawer.component.ts +++ b/libs/components/src/drawer/drawer.component.ts @@ -10,7 +10,7 @@ import { viewChild, } from "@angular/core"; -import { DrawerService } from "./drawer.service"; +import { DrawerHostDirective } from "./drawer-host.directive"; /** * A drawer is a panel of supplementary content that is adjacent to the page's main content. @@ -25,7 +25,7 @@ import { DrawerService } from "./drawer.service"; templateUrl: "drawer.component.html", }) export class DrawerComponent { - private drawerHost = inject(DrawerService); + private drawerHost = inject(DrawerHostDirective); private portal = viewChild.required(CdkPortal); /** diff --git a/libs/components/src/drawer/drawer.mdx b/libs/components/src/drawer/drawer.mdx index bc99fa290d6..57d618cfe95 100644 --- a/libs/components/src/drawer/drawer.mdx +++ b/libs/components/src/drawer/drawer.mdx @@ -12,8 +12,6 @@ import { DrawerComponent } from "@bitwarden/components"; # Drawer -**Note: `bit-drawer` is deprecated. Use `bit-dialog` and `DialogService.openDrawer(...)` instead.** - A drawer is a panel of supplementary content that is adjacent to the page's main content. diff --git a/libs/components/src/drawer/drawer.service.ts b/libs/components/src/drawer/drawer.service.ts deleted file mode 100644 index dd8575efee8..00000000000 --- a/libs/components/src/drawer/drawer.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Portal } from "@angular/cdk/portal"; -import { Injectable, signal } from "@angular/core"; - -@Injectable({ providedIn: "root" }) -export class DrawerService { - private _portal = signal | undefined>(undefined); - - /** The portal to display */ - portal = this._portal.asReadonly(); - - open(portal: Portal) { - this._portal.set(portal); - } - - close(portal: Portal) { - if (portal === this.portal()) { - this._portal.set(undefined); - } - } -} diff --git a/libs/components/src/layout/index.ts b/libs/components/src/layout/index.ts index a257a4dde85..6994a4f639f 100644 --- a/libs/components/src/layout/index.ts +++ b/libs/components/src/layout/index.ts @@ -1,2 +1 @@ export * from "./layout.component"; -export * from "./scroll-layout.directive"; diff --git a/libs/components/src/layout/layout.component.html b/libs/components/src/layout/layout.component.html index 19280c99756..33b8de81572 100644 --- a/libs/components/src/layout/layout.component.html +++ b/libs/components/src/layout/layout.component.html @@ -1,52 +1,43 @@ -@let mainContentId = "main-content"; -
-
- - -
- - - - @if ( - { - open: sideNavService.open$ | async, - }; - as data - ) { -
- @if (data.open) { -
- } -
- } -
-
-
- -
+ +
+
+ +
+ + + + @if ( + { + open: sideNavService.open$ | async, + }; + as data + ) { +
+ @if (data.open) { +
+ } +
+ } +
+
diff --git a/libs/components/src/layout/layout.component.ts b/libs/components/src/layout/layout.component.ts index 6ee7dd8222e..7bf8a6ad173 100644 --- a/libs/components/src/layout/layout.component.ts +++ b/libs/components/src/layout/layout.component.ts @@ -1,10 +1,9 @@ -import { A11yModule, CdkTrapFocus } from "@angular/cdk/a11y"; import { PortalModule } from "@angular/cdk/portal"; import { CommonModule } from "@angular/common"; -import { Component, ElementRef, inject, viewChild } from "@angular/core"; +import { Component, inject } from "@angular/core"; import { RouterModule } from "@angular/router"; -import { DrawerService } from "../drawer/drawer.service"; +import { DrawerHostDirective } from "../drawer/drawer-host.directive"; import { LinkModule } from "../link"; import { SideNavService } from "../navigation/side-nav.service"; import { SharedModule } from "../shared"; @@ -13,23 +12,16 @@ import { SharedModule } from "../shared"; selector: "bit-layout", templateUrl: "layout.component.html", standalone: true, - imports: [ - CommonModule, - SharedModule, - LinkModule, - RouterModule, - PortalModule, - A11yModule, - CdkTrapFocus, - ], + imports: [CommonModule, SharedModule, LinkModule, RouterModule, PortalModule], + hostDirectives: [DrawerHostDirective], }) export class LayoutComponent { + protected mainContentId = "main-content"; + protected sideNavService = inject(SideNavService); - protected drawerPortal = inject(DrawerService).portal; + protected drawerPortal = inject(DrawerHostDirective).portal; - private mainContent = viewChild.required>("main"); - - protected focusMainContent() { - this.mainContent().nativeElement.focus(); + focusMainContent() { + document.getElementById(this.mainContentId)?.focus(); } } diff --git a/libs/components/src/layout/scroll-layout.directive.ts b/libs/components/src/layout/scroll-layout.directive.ts deleted file mode 100644 index d3ea2bf557b..00000000000 --- a/libs/components/src/layout/scroll-layout.directive.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Directionality } from "@angular/cdk/bidi"; -import { CdkVirtualScrollable, ScrollDispatcher, VIRTUAL_SCROLLABLE } from "@angular/cdk/scrolling"; -import { Directive, ElementRef, NgZone, Optional } from "@angular/core"; - -@Directive({ - selector: "cdk-virtual-scroll-viewport[bitScrollLayout]", - standalone: true, - providers: [{ provide: VIRTUAL_SCROLLABLE, useExisting: ScrollLayoutDirective }], -}) -export class ScrollLayoutDirective extends CdkVirtualScrollable { - private mainRef: ElementRef; - - constructor(scrollDispatcher: ScrollDispatcher, ngZone: NgZone, @Optional() dir: Directionality) { - const mainEl = document.querySelector("main")!; - if (!mainEl) { - // eslint-disable-next-line no-console - console.error("HTML main element must be an ancestor of [bitScrollLayout]"); - } - const mainRef = new ElementRef(mainEl); - super(mainRef, scrollDispatcher, ngZone, dir); - this.mainRef = mainRef; - } - - override getElementRef(): ElementRef { - return this.mainRef; - } - - override measureBoundingClientRectWithScrollOffset( - from: "left" | "top" | "right" | "bottom", - ): number { - return ( - this.mainRef.nativeElement.getBoundingClientRect()[from] - this.measureScrollOffset(from) - ); - } -} diff --git a/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts index 904b9e11c3a..7709506f050 100644 --- a/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts @@ -3,23 +3,15 @@ import { Component, OnInit } from "@angular/core"; import { DialogModule, DialogService } from "../../../dialog"; import { IconButtonModule } from "../../../icon-button"; -import { ScrollLayoutDirective } from "../../../layout"; import { SectionComponent } from "../../../section"; import { TableDataSource, TableModule } from "../../../table"; @Component({ selector: "dialog-virtual-scroll-block", standalone: true, - imports: [ - DialogModule, - IconButtonModule, - SectionComponent, - TableModule, - ScrollingModule, - ScrollLayoutDirective, - ], + imports: [DialogModule, IconButtonModule, SectionComponent, TableModule, ScrollingModule], template: /*html*/ ` - + diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts index b62e669d369..70f56d2e28d 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts @@ -12,69 +12,8 @@ import { KitchenSinkToggleList } from "./kitchen-sink-toggle-list.component"; standalone: true, imports: [KitchenSinkSharedModule], template: ` - - -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

- - What did foo say to bar? - - -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-
+ + Dialog body text goes here. @@ -151,6 +90,72 @@ class KitchenSinkDialog {
+ + + + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+ + What did foo say to bar? + + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

+
+
`, }) export class KitchenSinkMainComponent { @@ -163,7 +168,7 @@ export class KitchenSinkMainComponent { } openDrawer() { - this.dialogService.openDrawer(KitchenSinkDialog); + this.drawerOpen.set(true); } navItems = [ diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts index d318e1b5f0e..f57a9de4e68 100644 --- a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts @@ -14,6 +14,7 @@ import { import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService } from "../../dialog"; import { LayoutComponent } from "../../layout"; import { I18nMockService } from "../../utils/i18n-mock.service"; import { positionFixedWrapperDecorator } from "../storybook-decorators"; @@ -38,20 +39,8 @@ export default { KitchenSinkTable, KitchenSinkToggleList, ], - }), - applicationConfig({ providers: [ - provideNoopAnimations(), - importProvidersFrom( - RouterModule.forRoot( - [ - { path: "", redirectTo: "bitwarden", pathMatch: "full" }, - { path: "bitwarden", component: KitchenSinkMainComponent }, - { path: "virtual-scroll", component: DialogVirtualScrollBlockComponent }, - ], - { useHash: true }, - ), - ), + DialogService, { provide: I18nService, useFactory: () => { @@ -69,6 +58,21 @@ export default { }, ], }), + applicationConfig({ + providers: [ + provideNoopAnimations(), + importProvidersFrom( + RouterModule.forRoot( + [ + { path: "", redirectTo: "bitwarden", pathMatch: "full" }, + { path: "bitwarden", component: KitchenSinkMainComponent }, + { path: "virtual-scroll", component: DialogVirtualScrollBlockComponent }, + ], + { useHash: true }, + ), + ), + ], + }), ], } as Meta; diff --git a/libs/components/src/table/table-scroll.component.html b/libs/components/src/table/table-scroll.component.html index 523912cd7ac..8f2c88ba3ad 100644 --- a/libs/components/src/table/table-scroll.component.html +++ b/libs/components/src/table/table-scroll.component.html @@ -1,5 +1,5 @@ diff --git a/libs/components/src/table/table-scroll.component.ts b/libs/components/src/table/table-scroll.component.ts index e83dbbecc60..9d81e3ffe83 100644 --- a/libs/components/src/table/table-scroll.component.ts +++ b/libs/components/src/table/table-scroll.component.ts @@ -20,8 +20,6 @@ import { TrackByFunction, } from "@angular/core"; -import { ScrollLayoutDirective } from "../layout"; - import { RowDirective } from "./row.directive"; import { TableComponent } from "./table.component"; @@ -58,7 +56,6 @@ export class BitRowDef { CdkFixedSizeVirtualScroll, CdkVirtualForOf, RowDirective, - ScrollLayoutDirective, ], }) export class TableScrollComponent diff --git a/libs/components/src/table/table.mdx b/libs/components/src/table/table.mdx index 59bf5b773a3..8d784190ed9 100644 --- a/libs/components/src/table/table.mdx +++ b/libs/components/src/table/table.mdx @@ -142,7 +142,7 @@ dataSource.filter = (data) => data.orgType === "family"; Rudimentary string filtering is supported out of the box with `TableDataSource.simpleStringFilter`. It works by converting each entry into a string of it's properties. The provided string is then -compared against the filter value using a simple `indexOf` check. For convenience, you can also just +compared against the filter value using a simple `indexOf` check. For convienence, you can also just pass a string directly. ```ts @@ -153,7 +153,7 @@ dataSource.filter = "search value"; ### Virtual Scrolling -It's heavily advised to use virtual scrolling if you expect the table to have any significant amount +It's heavily adviced to use virtual scrolling if you expect the table to have any significant amount of data. This is done by using the `bit-table-scroll` component instead of the `bit-table` component. This component behaves slightly different from the `bit-table` component. Instead of using the `*ngFor` directive to render the rows, you provide a `bitRowDef` template that will be @@ -178,14 +178,6 @@ height and align vertically. ``` -#### Deprecated approach - -Before `bit-table-scroll` was introduced, virtual scroll in tables was implemented manually via -constructs from Angular CDK. This included wrapping the table with a `cdk-virtual-scroll-viewport` -and targeting with `bit-layout`'s scroll container with the `bitScrollLayout` directive. - -This pattern is deprecated in favor of `bit-table-scroll`. - ## Accessibility - Always include a row or column header with your table; this allows assistive technology to better diff --git a/libs/components/src/table/table.stories.ts b/libs/components/src/table/table.stories.ts index d696e6077dd..e8ab24ee8b7 100644 --- a/libs/components/src/table/table.stories.ts +++ b/libs/components/src/table/table.stories.ts @@ -1,13 +1,6 @@ -import { RouterTestingModule } from "@angular/router/testing"; import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; - import { countries } from "../form/countries"; -import { LayoutComponent } from "../layout"; -import { mockLayoutI18n } from "../layout/mocks"; -import { positionFixedWrapperDecorator } from "../stories/storybook-decorators"; -import { I18nMockService } from "../utils"; import { TableDataSource } from "./table-data-source"; import { TableModule } from "./table.module"; @@ -15,17 +8,8 @@ import { TableModule } from "./table.module"; export default { title: "Component Library/Table", decorators: [ - positionFixedWrapperDecorator(), moduleMetadata({ - imports: [TableModule, LayoutComponent, RouterTestingModule], - providers: [ - { - provide: I18nService, - useFactory: () => { - return new I18nMockService(mockLayoutI18n); - }, - }, - ], + imports: [TableModule], }), ], argTypes: { @@ -132,20 +116,18 @@ export const Scrollable: Story = { trackBy: (index: number, item: any) => item.id, }, template: ` - - - - Id - Name - Other - - - {{ row.id }} - {{ row.name }} - {{ row.other }} - - - + + + Id + Name + Other + + + {{ row.id }} + {{ row.name }} + {{ row.other }} + + `, }), }; @@ -162,19 +144,17 @@ export const Filterable: Story = { sortFn: (a: any, b: any) => a.id - b.id, }, template: ` - - - - - Name - Value - - - {{ row.name }} - {{ row.value }} - - - + + + + Name + Value + + + {{ row.name }} + {{ row.value }} + + `, }), }; diff --git a/libs/components/src/utils/has-scrolled-from.ts b/libs/components/src/utils/has-scrolled-from.ts deleted file mode 100644 index 44c73465bdd..00000000000 --- a/libs/components/src/utils/has-scrolled-from.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { CdkScrollable } from "@angular/cdk/scrolling"; -import { Signal, inject, signal } from "@angular/core"; -import { toObservable, toSignal } from "@angular/core/rxjs-interop"; -import { map, startWith, switchMap } from "rxjs"; - -export type ScrollState = { - /** `true` when the scrollbar is not at the top-most position */ - top: boolean; - - /** `true` when the scrollbar is not at the bottom-most position */ - bottom: boolean; -}; - -/** - * Check if a `CdkScrollable` instance has been scrolled - * @param scrollable The instance to check, defaults to the one provided by the current injector - * @returns {Signal} - */ -export const hasScrolledFrom = (scrollable?: Signal): Signal => { - const _scrollable = scrollable ?? signal(inject(CdkScrollable)); - const scrollable$ = toObservable(_scrollable); - - const scrollState$ = scrollable$.pipe( - switchMap((_scrollable) => - _scrollable.elementScrolled().pipe( - startWith(null), - map(() => ({ - top: _scrollable.measureScrollOffset("top") > 0, - bottom: _scrollable.measureScrollOffset("bottom") > 0, - })), - ), - ), - ); - - return toSignal(scrollState$, { - initialValue: { - top: false, - bottom: false, - }, - }); -}; From b7bbf99682b58f38810b56f680ec05c26e2596e7 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Fri, 16 May 2025 17:33:33 -0400 Subject: [PATCH 088/163] Handle null or undefined fields (#14832) --- libs/common/src/vault/models/domain/fido2-credential.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/common/src/vault/models/domain/fido2-credential.ts b/libs/common/src/vault/models/domain/fido2-credential.ts index 7002a58150d..e4e874e367b 100644 --- a/libs/common/src/vault/models/domain/fido2-credential.ts +++ b/libs/common/src/vault/models/domain/fido2-credential.ts @@ -164,8 +164,8 @@ export class Fido2Credential extends Domain { keyCurve: this.keyCurve.toJSON(), keyValue: this.keyValue.toJSON(), rpId: this.rpId.toJSON(), - userHandle: this.userHandle.toJSON(), - userName: this.userName.toJSON(), + userHandle: this.userHandle?.toJSON(), + userName: this.userName?.toJSON(), counter: this.counter.toJSON(), rpName: this.rpName?.toJSON(), userDisplayName: this.userDisplayName?.toJSON(), From 0ec7ab47041bbf1406e2b50f3adc6faa876e80c3 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 19:04:06 +0000 Subject: [PATCH 089/163] Autosync the updated translations (#14834) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/az/messages.json | 6 +++--- apps/desktop/src/locales/bg/messages.json | 6 +++--- apps/desktop/src/locales/cs/messages.json | 6 +++--- apps/desktop/src/locales/de/messages.json | 6 +++--- apps/desktop/src/locales/hu/messages.json | 6 +++--- apps/desktop/src/locales/lv/messages.json | 6 +++--- apps/desktop/src/locales/ru/messages.json | 6 +++--- apps/desktop/src/locales/sk/messages.json | 6 +++--- apps/desktop/src/locales/tr/messages.json | 6 +++--- apps/desktop/src/locales/zh_CN/messages.json | 6 +++--- 10 files changed, 30 insertions(+), 30 deletions(-) diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 0f856661551..459458bdb2a 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -3707,13 +3707,13 @@ "message": "Daşı" }, "newFolder": { - "message": "New folder" + "message": "Yeni qovluq" }, "folderName": { - "message": "Folder Name" + "message": "Qovluq adı" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "Ana qovluğun adından sonra \"/\" əlavə edərək qovluğu ardıcıl yerləşdirin. Nümunə: Social/Forums" }, "newLoginNudgeTitle": { "message": "Avto-doldurma ilə vaxta qənaət edin" diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 3a6054c2b52..6300ed3391f 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -3707,13 +3707,13 @@ "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": "Можете да вложите една папка в друга като въведете името на горната папка, а след това „/“. Пример: Социални/Форуми" }, "newLoginNudgeTitle": { "message": "Спестете време с автоматично попълване" diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 5f01551a867..49cfc8823b7 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -3707,13 +3707,13 @@ "message": "Přesunout" }, "newFolder": { - "message": "New folder" + "message": "Nová složka" }, "folderName": { - "message": "Folder Name" + "message": "Název složky" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "Vnořte složku přidáním názvu nadřazené složky následovaného znakem \"/\". Příklad: Sociální/Fóra" }, "newLoginNudgeTitle": { "message": "Ušetřete čas s automatickým vyplňováním" diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 5a01344185a..da1272ef300 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -3707,13 +3707,13 @@ "message": "Verschieben" }, "newFolder": { - "message": "New folder" + "message": "Neuer Ordner" }, "folderName": { - "message": "Folder Name" + "message": "Ordnername" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "Verschachtel einen Ordner, indem du den Namen des übergeordneten Ordners hinzufügst, gefolgt von einem „/“. Beispiel: Sozial/Foren" }, "newLoginNudgeTitle": { "message": "Spare Zeit mit Auto-Ausfüllen" diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 8091cd72225..e56b62f9840 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -3707,13 +3707,13 @@ "message": "Áthelyezés" }, "newFolder": { - "message": "New folder" + "message": "Új mappa" }, "folderName": { - "message": "Folder Name" + "message": "Mappanév" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "Mappa beágyazása a szülőmappa nevének hozzáadásával, majd egy “/” karakterrel. Példa: Közösségi/Fórumok" }, "newLoginNudgeTitle": { "message": "Idő megtakarítás automatikus kitöltéssel" diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index a1e76b5a521..437aba39714 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -3707,13 +3707,13 @@ "message": "Pārvietot" }, "newFolder": { - "message": "New folder" + "message": "Jauna mape" }, "folderName": { - "message": "Folder Name" + "message": "Mapes nosaukums" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "Apakšmapes var izveidot, ja pievieno iekļaujošās mapes nosaukumu, aiz kura ir \"/\". Piemēram: Tīklošanās/Forumi" }, "newLoginNudgeTitle": { "message": "Laika ietaupīšana ar automātisko aizpildi" diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index b61c03462a3..0e58487ae88 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -3707,13 +3707,13 @@ "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": "Создайте вложенную папку, добавив название родительской папки и символ \"/\". Пример: Сообщества/Форумы" }, "newLoginNudgeTitle": { "message": "Экономьте время с помощью автозаполнения" diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 03c362d2cb3..d84b164e749 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -3707,13 +3707,13 @@ "message": "Presunúť" }, "newFolder": { - "message": "New folder" + "message": "Nový priečinok" }, "folderName": { - "message": "Folder Name" + "message": "Názov priečinka" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "Vnorte priečinok pridaním názvu nadradeného priečinka a znaku \"/\". Príklad: Sociálne siete/Fóra" }, "newLoginNudgeTitle": { "message": "Ušetrite čas s automatickým vypĺňaním" diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 35fd2be4706..c6a59afda23 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -3707,13 +3707,13 @@ "message": "Taşı" }, "newFolder": { - "message": "New folder" + "message": "Yeni klasör" }, "folderName": { - "message": "Folder Name" + "message": "Klasör adı" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "Üst klasörün adının sonuna “/” ekleyerek klasörleri iç içe koyabilirsiniz. Örnek: Sosyal/Forumlar" }, "newLoginNudgeTitle": { "message": "Otomatik doldurmayla zaman kazanın" diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 7146a8698c3..90cdb33fde5 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -3707,13 +3707,13 @@ "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": "通过在父文件夹名后面添加「/」来嵌套文件夹。示例:Social/Forums" }, "newLoginNudgeTitle": { "message": "使用自动填充节省时间" From dc776de2eb85ec5da104f87a84b85b9c3ab0ad9a Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 19:04:29 +0000 Subject: [PATCH 090/163] Autosync the updated translations (#14833) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 4 + apps/browser/src/_locales/az/messages.json | 8 +- apps/browser/src/_locales/be/messages.json | 4 + apps/browser/src/_locales/bg/messages.json | 8 +- apps/browser/src/_locales/bn/messages.json | 4 + apps/browser/src/_locales/bs/messages.json | 4 + apps/browser/src/_locales/ca/messages.json | 4 + apps/browser/src/_locales/cs/messages.json | 6 +- apps/browser/src/_locales/cy/messages.json | 4 + apps/browser/src/_locales/da/messages.json | 4 + apps/browser/src/_locales/de/messages.json | 18 +- apps/browser/src/_locales/el/messages.json | 4 + apps/browser/src/_locales/en_GB/messages.json | 4 + apps/browser/src/_locales/en_IN/messages.json | 4 + apps/browser/src/_locales/es/messages.json | 4 + apps/browser/src/_locales/et/messages.json | 4 + apps/browser/src/_locales/eu/messages.json | 4 + apps/browser/src/_locales/fa/messages.json | 180 +++++++++--------- apps/browser/src/_locales/fi/messages.json | 4 + apps/browser/src/_locales/fil/messages.json | 4 + apps/browser/src/_locales/fr/messages.json | 4 + apps/browser/src/_locales/gl/messages.json | 4 + apps/browser/src/_locales/he/messages.json | 4 + apps/browser/src/_locales/hi/messages.json | 4 + apps/browser/src/_locales/hr/messages.json | 4 + apps/browser/src/_locales/hu/messages.json | 6 +- apps/browser/src/_locales/id/messages.json | 4 + apps/browser/src/_locales/it/messages.json | 4 + apps/browser/src/_locales/ja/messages.json | 4 + apps/browser/src/_locales/ka/messages.json | 4 + apps/browser/src/_locales/km/messages.json | 4 + apps/browser/src/_locales/kn/messages.json | 4 + apps/browser/src/_locales/ko/messages.json | 4 + apps/browser/src/_locales/lt/messages.json | 4 + apps/browser/src/_locales/lv/messages.json | 6 +- apps/browser/src/_locales/ml/messages.json | 4 + apps/browser/src/_locales/mr/messages.json | 4 + apps/browser/src/_locales/my/messages.json | 4 + apps/browser/src/_locales/nb/messages.json | 4 + apps/browser/src/_locales/ne/messages.json | 4 + apps/browser/src/_locales/nl/messages.json | 4 + apps/browser/src/_locales/nn/messages.json | 4 + apps/browser/src/_locales/or/messages.json | 4 + apps/browser/src/_locales/pl/messages.json | 6 +- apps/browser/src/_locales/pt_BR/messages.json | 4 + apps/browser/src/_locales/pt_PT/messages.json | 4 + apps/browser/src/_locales/ro/messages.json | 4 + apps/browser/src/_locales/ru/messages.json | 6 +- apps/browser/src/_locales/si/messages.json | 4 + apps/browser/src/_locales/sk/messages.json | 6 +- apps/browser/src/_locales/sl/messages.json | 4 + apps/browser/src/_locales/sr/messages.json | 4 + apps/browser/src/_locales/sv/messages.json | 4 + apps/browser/src/_locales/te/messages.json | 4 + apps/browser/src/_locales/th/messages.json | 4 + apps/browser/src/_locales/tr/messages.json | 6 +- apps/browser/src/_locales/uk/messages.json | 4 + apps/browser/src/_locales/vi/messages.json | 4 + apps/browser/src/_locales/zh_CN/messages.json | 6 +- apps/browser/src/_locales/zh_TW/messages.json | 4 + apps/browser/store/locales/fa/copy.resx | 60 +++--- 61 files changed, 377 insertions(+), 137 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 3502f0fff4e..c96f4a5803b 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 81cbdfd8040..51eb1345466 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "Yeni element, yeni bir pəncərədə açılır", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Saxlamazdan əvvəl düzəliş et", "description": "Tooltip and Aria label for edit button on cipher item" @@ -1097,7 +1101,7 @@ "description": "Shown to user after item is updated." }, "selectItemAriaLabel": { - "message": "Select $ITEMTYPE$, $ITEMNAME$", + "message": "$ITEMTYPE$, $ITEMNAME$ seç", "description": "Used by screen readers. $1 is the item type (like vault or folder), $2 is the selected item name.", "placeholders": { "itemType": { @@ -1117,7 +1121,7 @@ "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "Bu girişi saxlamaq üçün kilidi açın", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 1c7fb4ff57c..a51b96547da 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index e61e8c8567e..09e5260f9b3 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "Нов елемент, отваря се в нов прозорец", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Редактиране преди запазване", "description": "Tooltip and Aria label for edit button on cipher item" @@ -1097,7 +1101,7 @@ "description": "Shown to user after item is updated." }, "selectItemAriaLabel": { - "message": "Select $ITEMTYPE$, $ITEMNAME$", + "message": "Избиране на $ITEMTYPE$, $ITEMNAME$", "description": "Used by screen readers. $1 is the item type (like vault or folder), $2 is the selected item name.", "placeholders": { "itemType": { @@ -1117,7 +1121,7 @@ "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "Отключете, за да запазите тези данни за вписване", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index ab9b880109e..5e19936e975 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 940c886fab5..f48037814e5 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index e8d276e67e7..151412c02ed 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index aa22beab63a..effc4c950c6 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "Nová položka, otevře se v novém okně", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Upravit před uložením", "description": "Tooltip and Aria label for edit button on cipher item" @@ -1117,7 +1121,7 @@ "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "Odemknout pro uložení tohoto přihlášení", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 3e037ae58d7..047b45dd9b8 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index edba6e56f4e..e6233b1f8db 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index a739a568479..1eac22c919c 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "Neuer Eintrag, öffnet sich in neuem Fenster", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Vor dem Speichern bearbeiten", "description": "Tooltip and Aria label for edit button on cipher item" @@ -1097,7 +1101,7 @@ "description": "Shown to user after item is updated." }, "selectItemAriaLabel": { - "message": "Select $ITEMTYPE$, $ITEMNAME$", + "message": "$ITEMTYPE$, $ITEMNAME$ auswählen", "description": "Used by screen readers. $1 is the item type (like vault or folder), $2 is the selected item name.", "placeholders": { "itemType": { @@ -1117,7 +1121,7 @@ "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "Entsperren, um diese Zugangsdaten zu speichern", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { @@ -5265,7 +5269,7 @@ "message": "Speicher eine unbegrenzte Anzahl von Passwörtern auf unbegrenzt vielen Geräten mit Bitwarden-Apps für Smartphones, Browser und Desktop." }, "nudgeBadgeAria": { - "message": "1 notification" + "message": "1 Benachrichtigung" }, "emptyVaultNudgeTitle": { "message": "Vorhandene Passwörter importieren" @@ -5280,13 +5284,13 @@ "message": "Willkommen in deinem Tresor!" }, "hasItemsVaultNudgeBodyOne": { - "message": "Autofill items for the current page" + "message": "Einträge für die aktuelle Seite automatisch ausfüllen" }, "hasItemsVaultNudgeBodyTwo": { - "message": "Favorite items for easy access" + "message": "Favoriten-Einträge für einfachen Zugriff" }, "hasItemsVaultNudgeBodyThree": { - "message": "Search your vault for something else" + "message": "Deinen Tresor nach etwas anderem durchsuchen" }, "newLoginNudgeTitle": { "message": "Spare Zeit mit Auto-Ausfüllen" @@ -5338,6 +5342,6 @@ "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "noPermissionsViewPage": { - "message": "You do not have permissions to view this page. Try logging in with a different account." + "message": "Du hast keine Berechtigung, diese Seite anzuzeigen. Versuche dich mit einem anderen Konto anzumelden." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 723631f4d53..faad1a90a07 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index ac95545676c..d131d3ec33d 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 008c93e4397..aa1c076b2e9 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 1ab96b99154..3018cd36ed4 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 15e07b5c508..5bce2142219 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 0c9af65128b..54b7b9b234c 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 2e2ce5203aa..f132b61fc4e 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -3,7 +3,7 @@ "message": "Bitwarden" }, "appLogoLabel": { - "message": "Bitwarden logo" + "message": "لوگو Bitwarden" }, "extName": { "message": "مدیریت رمز عبور Bitwarden", @@ -17,22 +17,22 @@ "message": "وارد شوید یا یک حساب کاربری بسازید تا به گاوصندوق امن‌تان دسترسی یابید." }, "inviteAccepted": { - "message": "Invitation accepted" + "message": "دعوتنامه پذیرفته شد" }, "createAccount": { "message": "ایجاد حساب کاربری" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "در Bitwarden تازه وارد هستید؟" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "با کلید عبور وارد شوید" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "استفاده از ورود تک مرحله‌ای" }, "welcomeBack": { - "message": "Welcome back" + "message": "خوش آمدید" }, "setAStrongPassword": { "message": "تنظیم رمز عبور قوی" @@ -65,7 +65,7 @@ "message": "یادآور کلمه عبور اصلی کمک می‌کند در صورت فراموشی آن را به یاد بیارید." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "اگر کلمه عبور خود را فراموش کنید، یادآور کلمه عبور می‌تواند به ایمیل شما ارسال شود. حداکثر $CURRENT$/$MAXIMUM$ کاراکتر.", "placeholders": { "current": { "content": "$1", @@ -84,7 +84,7 @@ "message": "یادآور کلمه عبور اصلی (اختیاری)" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "امتیاز قدرت کلمه عبور $SCORE$", "placeholders": { "score": { "content": "$1", @@ -93,10 +93,10 @@ } }, "joinOrganization": { - "message": "Join organization" + "message": "به سازمان بپیوندید" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "به $ORGANIZATIONNAME$ بپیوندید", "placeholders": { "organizationName": { "content": "$1", @@ -105,7 +105,7 @@ } }, "finishJoiningThisOrganizationBySettingAMasterPassword": { - "message": "Finish joining this organization by setting a master password." + "message": "با تعیین یک کلمه عبور اصلی، عضویت خود در این سازمان را کامل کنید." }, "tab": { "message": "زبانه" @@ -132,7 +132,7 @@ "message": "کپی کلمه عبور" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "کپی عبارت عبور" }, "copyNote": { "message": "کپی یادداشت" @@ -150,31 +150,31 @@ "message": "کپی کد امنیتی" }, "copyName": { - "message": "Copy name" + "message": "کپی نام" }, "copyCompany": { - "message": "Copy company" + "message": "کپی شرکت" }, "copySSN": { - "message": "Copy Social Security number" + "message": "کپی شماره کد ملی" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "کپی شماره گذرنامه" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "کپی شماره گواهینامه" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "کپی کلید خصوصی" }, "copyPublicKey": { - "message": "Copy public key" + "message": "کپی کلید عمومی" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "کپی اثر انگشت" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "کپی $FIELD$", "placeholders": { "field": { "content": "$1", @@ -183,17 +183,17 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "کپی وب‌سایت" }, "copyNotes": { - "message": "Copy notes" + "message": "کپی یادداشت‌ها" }, "copy": { - "message": "Copy", + "message": "کپی", "description": "Copy to clipboard" }, "fill": { - "message": "Fill", + "message": "پر کردن", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -209,10 +209,10 @@ "message": "پر کردن خودکار هویت" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "پر کردن کد تأیید" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "پر کردن کد تأیید", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -255,16 +255,16 @@ "message": "افزودن مورد" }, "accountEmail": { - "message": "Account email" + "message": "حساب ایمیل" }, "requestHint": { - "message": "Request hint" + "message": "درخواست راهنمایی" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "درخواست یادآور کلمه عبور" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "نشانی ایمیل حساب کاربری خود را وارد کنید تا راهنمای کلمه عبور برای شما ارسال شود" }, "getMasterPasswordHint": { "message": "دریافت یادآور کلمه عبور اصلی" @@ -291,13 +291,13 @@ "message": "تغییر کلمه عبور اصلی" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "با برنامه وب ادامه می‌دهید؟" }, "continueToWebAppDesc": { "message": "ویژگی‌های بیشتر حساب Bitwarden خود را در برنامه وب کاوش کنید." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "به مرکز راهنمایی ادامه می‌دهید؟" }, "continueToHelpCenterDesc": { "message": "درباره استفاده از Bitwarden در مرکز راهنما بیشتر بیاموزید." @@ -309,7 +309,7 @@ "message": "به دیگران کمک کنید تا بفهمند آیا Bitwarden برایشان مناسب است یا نه. به فروشگاه افزونه مرورگر خود بروید و نظر خود را به اشتراک بگذارید." }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "می‌توانید کلمه عبور اصلی خود را در برنامه وب Bitwarden تغییر دهید." }, "fingerprintPhrase": { "message": "عبارت اثر انگشت", @@ -332,10 +332,10 @@ "message": "درباره" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "موارد بیشتر از Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "به bitwarden.com ادامه می‌دهید؟" }, "bitwardenForBusiness": { "message": "Bitwarden برای کسب و کارها" @@ -344,25 +344,25 @@ "message": "تاییدکننده هویت Bitwarden" }, "continueToAuthenticatorPageDesc": { - "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website" + "message": "احراز هویت کننده Bitwarden به شما امکان می‌دهد کلیدهای احراز هویت را ذخیره کرده و کدهای TOTP را برای فرآیندهای تأیید دومرحله‌ای تولید کنید. برای اطلاعات بیشتر به وب‌سایت bitwarden.com مراجعه کنید" }, "bitwardenSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "مدیر اسرار Bitwarden" }, "continueToSecretsManagerPageDesc": { - "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." + "message": "اسرار توسعه دهنده را با مدیر اسرا Bitwarden به‌صورت ایمن ذخیره، مدیریت و به اشتراک بگذارید. برای اطلاعات بیشتر به وب‌سایت bitwarden.com مراجعه کنید." }, "passwordlessDotDev": { "message": "Passwordless.dev" }, "continueToPasswordlessDotDevPageDesc": { - "message": "Create smooth and secure login experiences free from traditional passwords with Passwordless.dev. Learn more on the bitwarden.com website." + "message": "با استفاده از Passwordless.dev تجربه‌های ورود روان و ایمنی را بدون نیاز به کلمات عبور سنتی ایجاد کنید. برای اطلاعات بیشتر به وب‌سایت bitwarden.com مراجعه کنید." }, "freeBitwardenFamilies": { "message": "خانواده‌های رایگان Bitwarden" }, "freeBitwardenFamiliesPageDesc": { - "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." + "message": "شما واجد شرایط استفاده رایگان از Bitwarden Families هستید. این پیشنهاد را امروز در نسخه وب دریافت کنید." }, "version": { "message": "نسخه" @@ -383,7 +383,7 @@ "message": "ويرايش پوشه" }, "editFolderWithName": { - "message": "Edit folder: $FOLDERNAME$", + "message": "ویرایش پوشه: $FOLDERNAME$", "placeholders": { "foldername": { "content": "$1", @@ -392,22 +392,22 @@ } }, "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": "برای تو در تو کردن یک پوشه، نام پوشه والد را وارد کرده و سپس یک “/” اضافه کنید. مثال: Social/Forums" }, "noFoldersAdded": { - "message": "No folders added" + "message": "هیچ پوشه‌ای اضافه نشد" }, "createFoldersToOrganize": { - "message": "Create folders to organize your vault items" + "message": "برای سامان‌دهی موردهای گاوصندوق خود پوشه ایجاد کنید" }, "deleteFolderPermanently": { - "message": "Are you sure you want to permanently delete this folder?" + "message": "مطمئنید می‌خواهید این پوشه را برای همیشه پاک کنید؟" }, "deleteFolder": { "message": "حذف پوشه" @@ -450,7 +450,7 @@ "message": "به طور خودکار کلمه‌های عبور قوی و منحصر به فرد برای ورود به سیستم خود ایجاد کنید." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "برنامه وب Bitwarden" }, "importItems": { "message": "درون ریزی موارد" @@ -462,19 +462,19 @@ "message": "تولید کلمه عبور" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "تولید عبارت عبور" }, "passwordGenerated": { - "message": "Password generated" + "message": "کلمه عبور تولید شد" }, "passphraseGenerated": { - "message": "Passphrase generated" + "message": "عبارت عبور تولید شد" }, "usernameGenerated": { - "message": "Username generated" + "message": "نام کاربری تولید شد" }, "emailGenerated": { - "message": "Email generated" + "message": "ایمیل تولید شد" }, "regeneratePassword": { "message": "تولید مجدد کلمه عبور" @@ -486,11 +486,11 @@ "message": "طول" }, "include": { - "message": "Include", + "message": "شامل", "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "شامل حروف بزرگ باشد", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -498,7 +498,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "شامل حروف کوچک باشد", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -506,7 +506,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "شامل اعداد", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -514,7 +514,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "افزودن کاراکترهای خاص", "description": "Full description for the password generator special characters checkbox" }, "numWords": { @@ -537,11 +537,11 @@ "message": "حداقل حرف خاص" }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "از کاراکترهای مبهم خودداری کن", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { - "message": "Enterprise policy requirements have been applied to your generator options.", + "message": "نیازمندی‌های سیاست سازمانی بر گزینه‌های تولید کننده شما اعمال شده‌اند.", "description": "Indicates that a policy limits the credential generator screen." }, "searchVault": { @@ -566,7 +566,7 @@ "message": "کلمه عبور" }, "totp": { - "message": "Authenticator secret" + "message": "کلید مخفی احراز کننده هویت‌" }, "passphrase": { "message": "عبارت عبور" @@ -587,7 +587,7 @@ "message": "یادداشت‌ها" }, "privateNote": { - "message": "Private note" + "message": "یادداشت خصوصی" }, "note": { "message": "یادداشت" @@ -608,10 +608,10 @@ "message": "راه اندازی" }, "launchWebsite": { - "message": "Launch website" + "message": "باز کردن وب‌سایت" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "باز کردن وب‌سایت $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -632,19 +632,19 @@ "message": "ساير" }, "unlockMethods": { - "message": "Unlock options" + "message": "باز کردن امکانات" }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "یک روش بازگشایی برای پایان زمان مجاز تنظیم کنید." }, "unlockMethodNeeded": { - "message": "Set up an unlock method in Settings" + "message": "یک روش باز کردن قفل را در تنظیمات راه‌اندازی کنید" }, "sessionTimeoutHeader": { - "message": "Session timeout" + "message": "پایان زمان نشست" }, "vaultTimeoutHeader": { - "message": "Vault timeout" + "message": "متوقف شدن گاو‌صندوق" }, "otherOptions": { "message": "سایر گزینه‌ها" @@ -656,25 +656,25 @@ "message": "مرورگر شما از کپی کلیپ بورد آسان پشتیبانی نمی‌کند. به جای آن به صورت دستی کپی کنید." }, "verifyYourIdentity": { - "message": "Verify your identity" + "message": "هویت خود را تأیید کنید" }, "weDontRecognizeThisDevice": { - "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + "message": "ما این دستگاه را نمی‌شناسیم. برای تأیید هویت خود، کدی را که به ایمیلتان ارسال شده وارد کنید." }, "continueLoggingIn": { - "message": "Continue logging in" + "message": "ادامه ورود" }, "yourVaultIsLocked": { "message": "گاوصندوق شما قفل شده است. برای ادامه هویت خود را تأیید کنید." }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "گاوصندوق‌تان قفل شد" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "حساب شما قفل شده است" }, "or": { - "message": "or" + "message": "یا" }, "unlock": { "message": "باز کردن قفل" @@ -699,13 +699,13 @@ "message": "متوقف شدن گاو‌صندوق" }, "vaultTimeout1": { - "message": "Timeout" + "message": "پایان زمان" }, "lockNow": { "message": "الان قفل شود" }, "lockAll": { - "message": "Lock all" + "message": "قفل کردن همه" }, "immediately": { "message": "بلافاصله" @@ -753,16 +753,16 @@ "message": "امنیت" }, "confirmMasterPassword": { - "message": "Confirm master password" + "message": "تأیید کلمه عبور اصلی" }, "masterPassword": { - "message": "Master password" + "message": "کلمه عبور اصلی" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "کلمه‌های عبور اصلی در صورت فراموشی قابل بازیابی نیستند!" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "یادآور کلمه عبور اصلی" }, "errorOccurred": { "message": "خطایی رخ داده است" @@ -796,10 +796,10 @@ "message": "حساب کاربری جدید شما ساخته شد! حالا می‌توانید وارد شوید." }, "newAccountCreated2": { - "message": "Your new account has been created!" + "message": "حساب جدید شما ایجاد شده است!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "شما با موفقیت وارد شدید!" }, "youSuccessfullyLoggedIn": { "message": "شما با موفقیت وارد شدید" @@ -814,7 +814,7 @@ "message": "کد تأیید مورد نیاز است." }, "webauthnCancelOrTimeout": { - "message": "The authentication was cancelled or took too long. Please try again." + "message": "احراز هویت لغو شد یا بیش از حد طول کشید. لطفاً دوباره تلاش کنید." }, "invalidVerificationCode": { "message": "کد تأیید نامعتبر است" @@ -833,7 +833,7 @@ "message": "ناتوان در پر کردن خودکار مورد انتخاب شده در این صفحه. اطلاعات را کپی و جای‌گذاری کنید." }, "totpCaptureError": { - "message": "Unable to scan QR code from the current webpage" + "message": "امکان اسکن کد QR از صفحه وب فعلی وجود ندارد" }, "totpCaptureSuccess": { "message": "کلید احراز هویت اضافه شد" @@ -890,10 +890,10 @@ "message": "Follow the steps below to finish logging in with your security key." }, "restartRegistration": { - "message": "Restart registration" + "message": "ثبت‌نام را دوباره آغاز کنید" }, "expiredLink": { - "message": "Expired link" + "message": "پیوند منقضی شد" }, "pleaseRestartRegistrationOrTryLoggingIn": { "message": "Please restart registration or try logging in." @@ -911,7 +911,7 @@ "message": "خیر" }, "location": { - "message": "Location" + "message": "موقعیت" }, "unexpectedError": { "message": "یک خطای غیر منتظره رخ داده است." @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index f28d82c201e..ead59384ff8 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Muokkaa ennen tallentamista", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index b15713a07a4..a7734899843 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 125cb19ea25..8a825082b5f 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index f65a26d2d1d..91a5121db16 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index d25d7c9f2dd..2e42a819b65 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 77a0b90c6be..8af1aa70cc3 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 8bf191cf6ec..557e465ea19 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index eabc67cde49..b47093bc2ee 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "Új elem, megnyitás új ablakban", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Szerkesztés mentés előtt", "description": "Tooltip and Aria label for edit button on cipher item" @@ -1117,7 +1121,7 @@ "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "Feloldás a bejelentkezés mentéséhez", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 8d71d7a6ba2..03e47fca575 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Sunting sebelum menyimpan", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 16b77251cf4..d3d0dc9ec43 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 29455d9515b..e9c22c1286f 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index a888e84a154..d8397db5da8 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 61a6d73e52c..f436c45ab75 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 737d91430b5..0459e007126 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index fd957ef5061..8add57d61ec 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 46b8eaddee1..06ff7cda8fa 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 24887061e71..e9044f4c041 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "Jauns vienums, atvērsies jaunā logā", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Labot pirms saglabāšanas", "description": "Tooltip and Aria label for edit button on cipher item" @@ -1117,7 +1121,7 @@ "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "Jāatslēdz, lai saglabātu šo pieteikšanās vienumu", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 02c6e5ee14c..e6de809dfbd 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 4b418eff676..dd347d822c7 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 61a6d73e52c..f436c45ab75 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index c4319d140da..62b55eb6501 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Rediger før du lagrer", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 61a6d73e52c..f436c45ab75 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index d16080d962a..af637b2f4cd 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "Nieuw Item, opent in nieuw venster", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Bewerken voor opslaan", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 61a6d73e52c..f436c45ab75 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 61a6d73e52c..f436c45ab75 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index bf6b4d8b70c..16696e5f828 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "Nowy element, otwiera się w nowym oknie", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edytuj przed zapisaniem", "description": "Tooltip and Aria label for edit button on cipher item" @@ -1117,7 +1121,7 @@ "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "Odblokuj, aby zapisać ten login", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 293e72b6dc0..e5ca45500a5 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index d29f5848153..a4ed095c4db 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "Novo item, abre numa nova janela", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Editar antes de guardar", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 08b0c93b2db..71990435ad2 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index f4e53e8c0d6..2d23c5352e5 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "Новый элемент, откроется в новом окне", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Изменить перед сохранением", "description": "Tooltip and Aria label for edit button on cipher item" @@ -1117,7 +1121,7 @@ "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "Разблокировать, чтобы сохранить этот логин", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index da35b2cc13f..308404946b3 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index a71aed7a8a2..e8d2993a8df 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "Nová položka, otvorí sa v novom okne", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Upraviť pred uložením", "description": "Tooltip and Aria label for edit button on cipher item" @@ -1117,7 +1121,7 @@ "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "Odomknúť na uloženie prihlasovacích údajov", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 8ef5f1aef09..65ee31c888c 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index a60766ca944..255b8dfb924 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Уреди пре сачувавања", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index c6280ce5a44..3a063f8f066 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 61a6d73e52c..f436c45ab75 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index e528897676d..19430463090 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 0899f4bb196..700e9b73881 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "Yeni kayıt (yeni pencerede açılır)", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Kaydetmeden önce düzenle", "description": "Tooltip and Aria label for edit button on cipher item" @@ -1117,7 +1121,7 @@ "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "Bu hesabı kaydetmek için kilidi açın", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 6b7e6a18aaf..b0c78a17106 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Редагувати перед збереженням", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index afd801839a8..14649828aaf 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 73c7b9a4b0d..e0bc34bd47d 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "新增项目,在新窗口中打开", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "保存前编辑", "description": "Tooltip and Aria label for edit button on cipher item" @@ -1117,7 +1121,7 @@ "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "解锁以保存此登录", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 4d58c479838..e9eab0a726d 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -1071,6 +1071,10 @@ }, "description": "Aria label for the view button in notification bar confirmation message" }, + "notificationNewItemAria": { + "message": "New Item, opens in new window", + "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" + }, "notificationEditTooltip": { "message": "Edit before saving", "description": "Tooltip and Aria label for edit button on cipher item" diff --git a/apps/browser/store/locales/fa/copy.resx b/apps/browser/store/locales/fa/copy.resx index 69816ce3590..2d45e114719 100644 --- a/apps/browser/store/locales/fa/copy.resx +++ b/apps/browser/store/locales/fa/copy.resx @@ -118,58 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - مدیریت رمز عبور Bitwarden + مدیر کلمه عبور Bitwarden - در خانه، محل کار، یا در حال حرکت، Bitwarden به سادگی تمامی رمزهای عبور، passkeyها، و اطلاعات حساس شما را امن نگاه می‌دارد. + در خانه، محل کار، یا در حال حرکت، Bitwarden به سادگی تمامی کلمات عبور، کلیدها، و اطلاعات حساس شما را امن نگاه می‌دارد. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + شناخته‌شده به‌عنوان بهترین مدیر کلمه عبور توسط PCMag، WIRED، The Verge، CNET، G2 و دیگر منابع! -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. +همه باید ابزارهای ایمنی آنلاین را در اختیار داشته باشند +از Bitwarden به‌صورت رایگان استفاده کنید، بدون تبلیغات یا فروش داده‌ها. Bitwarden باور دارد که همه باید بتوانند در فضای آنلاین ایمن بمانند. طرح‌های پریمیوم، ویژگی‌های پیشرفته‌تری ارائه می‌دهند. -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 +طرح‌های ویژه تیم‌ها و سازمان‌ها شامل ویژگی‌های حرفه‌ای برای کسب‌وکار هستند، مانند: ادغام SSO، میزبانی شخصی، ادغام با دایرکتوری و فراهم‌سازی SCIM، سیاست‌های جهانی، دسترسی API، گزارش رویدادها و موارد دیگر. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +از Bitwarden برای ایمن‌سازی محیط کاری و اشتراک‌گذاری اطلاعات حساس با همکاران استفاده کنید. +دلایل بیشتر برای انتخاب 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 hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +ممیزی‌های امنیتی توسط اشخاص ثالث + Bitwarden به‌طور منظم توسط شرکت‌های معتبر امنیتی مورد ارزیابی و تست نفوذ قرار می‌گیرد. این بررسی‌ها شامل بررسی کد منبع و تست‌های امنیتی روی IPها، سرورها و اپلیکیشن‌های وب 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. - -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +احراز هویت دومرحله‌ای پیشرفته (2FA) + ورود امن با استفاده از احراز هویت‌کننده‌های شخص ثالث، کدهای ایمیلی، یا گواهی‌نامه‌های FIDO2 WebAuthn مانند کلید امنیتی سخت‌افزاری یا کلید عبور. 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. +مولد داخلی + تولید کلمات عبور بلند، پیچیده و منحصربه‌فرد و همچنین نام‌های کاربری برای هر وب‌سایتی که بازدید می‌کنید. امکان ادغام با ارائه‌دهندگان ایمیل مستعار برای حفظ بیشتر حریم خصوصی. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +ترجمه‌های جهانی + Bitwarden به بیش از ۶۰ زبان در دسترس است که توسط جامعه جهانی از طریق Crowdin ترجمه شده‌اند. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +اپلیکیشن‌های چند سکویی + ایمن‌سازی و اشتراک‌گذاری داده‌ها از طریق هر مرورگر، دستگاه موبایل یا سیستم‌عامل دسکتاپ و موارد دیگر. -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 فقط کلمات عبور را ایمن نمی‌کند +راهکارهای مدیریت اعتبارات با رمزگذاری سرتاسری از Bitwarden به سازمان‌ها کمک می‌کند تا همه چیز را ایمن کنند، از جمله اسرار توسعه‌دهنده و تجربه‌های بدون کلمات عبور. +برای اطلاعات بیشتر درباره Bitwarden Secrets Manager و Bitwarden Passwordless.dev به وب‌سایت Bitwarden.com مراجعه کنید! - در خانه، محل کار، یا در حال حرکت، Bitwarden به سادگی تمامی رمزهای عبور، passkeyها، و اطلاعات حساس شما را امن نگاه می‌دارد. + در خانه، محل کار، یا در حال حرکت، Bitwarden به سادگی تمامی کلمات عبور، کلیدها، و اطلاعات حساس شما را امن نگاه می‌دارد. همگام‌سازی و دسترسی به گاوصندوق خود از دستگاه های مختلف From 4079607a3e07cadd5ce5289225c4b5faa30bac47 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 21:38:05 +0200 Subject: [PATCH 091/163] Autosync the updated translations (#14835) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 7 +- apps/web/src/locales/ar/messages.json | 7 +- apps/web/src/locales/az/messages.json | 7 +- apps/web/src/locales/be/messages.json | 7 +- apps/web/src/locales/bg/messages.json | 7 +- apps/web/src/locales/bn/messages.json | 7 +- apps/web/src/locales/bs/messages.json | 7 +- apps/web/src/locales/ca/messages.json | 7 +- apps/web/src/locales/cs/messages.json | 7 +- apps/web/src/locales/cy/messages.json | 7 +- apps/web/src/locales/da/messages.json | 7 +- apps/web/src/locales/de/messages.json | 15 +- apps/web/src/locales/el/messages.json | 7 +- apps/web/src/locales/en_GB/messages.json | 7 +- apps/web/src/locales/en_IN/messages.json | 7 +- apps/web/src/locales/eo/messages.json | 59 +++---- apps/web/src/locales/es/messages.json | 7 +- apps/web/src/locales/et/messages.json | 7 +- apps/web/src/locales/eu/messages.json | 7 +- apps/web/src/locales/fa/messages.json | 7 +- apps/web/src/locales/fi/messages.json | 7 +- apps/web/src/locales/fil/messages.json | 7 +- apps/web/src/locales/fr/messages.json | 7 +- apps/web/src/locales/gl/messages.json | 7 +- apps/web/src/locales/he/messages.json | 7 +- apps/web/src/locales/hi/messages.json | 7 +- apps/web/src/locales/hr/messages.json | 7 +- apps/web/src/locales/hu/messages.json | 7 +- apps/web/src/locales/id/messages.json | 7 +- apps/web/src/locales/it/messages.json | 7 +- apps/web/src/locales/ja/messages.json | 7 +- apps/web/src/locales/ka/messages.json | 7 +- apps/web/src/locales/km/messages.json | 7 +- apps/web/src/locales/kn/messages.json | 7 +- apps/web/src/locales/ko/messages.json | 7 +- apps/web/src/locales/lv/messages.json | 7 +- apps/web/src/locales/ml/messages.json | 7 +- apps/web/src/locales/mr/messages.json | 7 +- apps/web/src/locales/my/messages.json | 7 +- apps/web/src/locales/nb/messages.json | 7 +- apps/web/src/locales/ne/messages.json | 7 +- apps/web/src/locales/nl/messages.json | 7 +- apps/web/src/locales/nn/messages.json | 7 +- apps/web/src/locales/or/messages.json | 7 +- apps/web/src/locales/pl/messages.json | 7 +- apps/web/src/locales/pt_BR/messages.json | 7 +- apps/web/src/locales/pt_PT/messages.json | 7 +- apps/web/src/locales/ro/messages.json | 189 ++++++++++++----------- apps/web/src/locales/ru/messages.json | 7 +- apps/web/src/locales/si/messages.json | 7 +- apps/web/src/locales/sk/messages.json | 9 +- apps/web/src/locales/sl/messages.json | 7 +- apps/web/src/locales/sr/messages.json | 7 +- apps/web/src/locales/sr_CS/messages.json | 7 +- apps/web/src/locales/sv/messages.json | 7 +- apps/web/src/locales/te/messages.json | 7 +- apps/web/src/locales/th/messages.json | 7 +- apps/web/src/locales/tr/messages.json | 7 +- apps/web/src/locales/uk/messages.json | 7 +- apps/web/src/locales/vi/messages.json | 7 +- apps/web/src/locales/zh_CN/messages.json | 9 +- apps/web/src/locales/zh_TW/messages.json | 7 +- 62 files changed, 433 insertions(+), 247 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index fe109d6c2f0..152eb51d7d1 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Enige geënkripteerde uitsture wat u bewaar het word ook ongeldig." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Intekening" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index b12ff40f7a6..c84858fce08 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index fa35fc3e6ed..3518898335e 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Şifrələmə açarını güncəllədikdən sonra, hazırda istifadə etdiyiniz (mobil tətbiq və ya brauzer uzantıları kimi) bütün Bitwarden tətbiqlərində çıxış edib yenidən giriş etməlisiniz. Çıxış edib təkrar giriş etməmək (yeni şifrələmə açarının endirilməsi prosesi) dataların zədələnməsi ilə nəticələnə bilər. Avtomatik olaraq çıxış etməyə çalışacağıq, bu gecikə bilər." }, - "updateEncryptionKeyExportWarning": { - "message": "Saxladığınız bütün şifrələmə ixracları da yararsız olacaq." + "updateEncryptionKeyAccountExportWarning": { + "message": "Məhdudiyyətli hesablara aid saxladığınız xaricə köçürmələr yararsız olacaq." }, "subscription": { "message": "Abunəlik" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Bank hesabı ilə ödəniş, yalnız Amerika Birləşmiş Ştatlarındakı müştərilər üçün əlçatandır. Bank hesabınızı doğrulamağınız tələb olunacaq. Növbəti 1-2 iş günü ərzində mikro depozit qoyacağıq. Bank hesabını doğrulamaq üçün bu depozitdəki çıxarış deskriptor kodunu təşkilatın abunəlik səhifəsində daxil edin. Bank hesabı doğrulanmadıqda ödəniş buraxılacaq və abunəliyiniz dayandırılacaq." + }, + "clickPayWithPayPal": { + "message": "Ödəniş üsulunuzu əlavə etmək üçün lütfən Paypal ilə ödəniş et düyməsinə klikləyin." } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 3e78155f7ba..789f292aeaf 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Пасля абнаўлення вашага ключа шыфравання вам неабходна выйсці з сістэмы, а потым выканаць паўторны ўваход ва ўсе праграмы Bitwarden, якія вы зараз выкарыстоўваеце (напрыклад, мабільныя праграмы або пашырэнні для браўзераў). Збой пры выхадзе і паўторным уваходзе (пры гэтым спампоўваецца ваш новы ключ шыфравання) можа стаць прычынай пашкоджання даных. Мы паспрабуем аўтаматычна ажыццявіць завяршэнне ўсіх вашых сеансаў, але гэта можа адбывацца з затрымкай." }, - "updateEncryptionKeyExportWarning": { - "message": "Любыя зашыфраваныя экспартаванні, якія вы захавалі, таксама стануць памылковымі." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Падпіска" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index ee858d0a23f..b26d6a21578 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "След смяната на ключа за шифриране ще трябва да се отпишете и след това да се впишете в регистрацията си във всички приложения на Битуорден, които ползвате (като мобилното приложение и разширенията за браузъри). Ако не се отпишете и впишете повторно (за да получите достъп до новия ключ), рискувате да повредите записите си. Сега ще се пробва да бъдете отписани автоматично, това обаче може да се забави." }, - "updateEncryptionKeyExportWarning": { - "message": "Всички шифрирани вече изнесени данни ще станат безполезни." + "updateEncryptionKeyAccountExportWarning": { + "message": "Всички изнасяния ограничени от акаунта, които сте запазили, ще станат невалидни." }, "subscription": { "message": "Абонамент" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Плащането чрез банкова сметка е налично само за клиенти от САЩ. Ще трябва да потвърдите банковата си сметка. В следващите 1-2 работни дни ще направим малък депозит. Въведете кода от описанието на трансакцията в страницата за абонаменти на доставчика, за да потвърдите банковата сметка. Ако не потвърдите банковата сметка, може да пропуснете плащането и абонаментът Ви да бъде спрян." + }, + "clickPayWithPayPal": { + "message": "Моля, натиснете бутона за плащане с PayPal, за да добавите платежния си метод." } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index d09919e0f3c..bb71e44f09d 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 35d8401a8d2..a05e84245e8 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index d74d707735b..f24353d69b3 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Després d'actualitzar la vostra clau de xifratge, heu de tancar la sessió i tornar a entrar a totes les aplicacions de Bitwarden que esteu utilitzant actualment (com ara l'aplicació mòbil o les extensions del navegador). Si no es tanca i torna a iniciar la sessió (la qual descarrega la vostra nova clau de xifratge) pot provocar corrupció en les dades. Intentarem registrar-vos automàticament, però, es pot retardar." }, - "updateEncryptionKeyExportWarning": { - "message": "Totes les exportacions xifrades que hàgeu desat també seran no vàlides." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscripció" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 9f10b3c6404..4ba5a9c3fea 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Po aktualizace šifrovacího klíče dojde k odhlášení a budete se muset opětovně přihlásit do všech aplikací Bitwardenu, které aktuálně používáte (např. mobilní aplikace či rozšíření pro prohlížeč). Nezdaří-li se odhlášení a opětovné přihlášení (během něhož bude stažen nový šifrovací klíč), může dojít k poškození dat. Pokusíme se Vás automaticky odhlásit, nicméně, může to chvíli trvat." }, - "updateEncryptionKeyExportWarning": { - "message": "Jakékoli šifrované exporty, které jste uložili, budou také neplatné." + "updateEncryptionKeyAccountExportWarning": { + "message": "Všechny uložené exporty s omezením účtu se stanou neplatnými." }, "subscription": { "message": "Předplatné" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Platba bankovním účtem je dostupná jen pro zákazníky ve Spojených státech. Budete požádáni o ověření svého bankovního účtu. Mikrovklad provedeme během následujících 1-2 pracovních dnů. Pro ověření bankovního účtu zadejte kód popisu výpisu z této zálohy na stránce předplatného poskytovatele. Pokud bankovní účet neověříte, bude to mít za následek zmeškání platby a pozastavení předplatného." + }, + "clickPayWithPayPal": { + "message": "Pro přidání způsobu platby klepněte na tlačítko \"Pay with PayPal\"." } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 39dba259cc2..f91dc727a84 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 40c1283cb24..f40daec488f 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Efter opdatering af din krypteringsnøgle skal du logge ud og ind igen i alle Bitwarden-programmer, du bruger i øjeblikket (f.eks. mobilapp eller browserudvidelser). Hvis du ikke logger ud og ind (som downloader din nye krypteringsnøgle), kan det resultere i data korruption. Vi vil forsøge at logge dig ud automatisk, men det kan blive forsinket." }, - "updateEncryptionKeyExportWarning": { - "message": "Enhver krypteret eksport du har gemt, vil også blive utilgængelig." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Abonnement" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 7cdc0adc7d8..f39bb85a082 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -99,7 +99,7 @@ "message": "Anwendung als kritisch markieren" }, "applicationsMarkedAsCriticalSuccess": { - "message": "Applications marked as critical" + "message": "Als kritisch markierte Anwendungen" }, "application": { "message": "Anwendung" @@ -141,13 +141,13 @@ "message": "Diese Mitglieder melden sich bei Anwendungen mit schwachen, kompromittierten oder wiederverwendeten Passwörtern an." }, "atRiskMembersDescriptionNone": { - "message": "These are no members logging into applications with weak, exposed, or reused passwords." + "message": "Dies sind keine Mitglieder, die sich in Anwendungen mit schwachen, kompromittierten oder wiederverwendeten Passwörtern anmelden." }, "atRiskApplicationsDescription": { "message": "Diese Anwendungen haben schwache, kompromittierte oder wiederverwendete Passwörter." }, "atRiskApplicationsDescriptionNone": { - "message": "These are no applications with weak, exposed, or reused passwords." + "message": "Dies sind keine Anwendungen mit schwachen, kompromittierten oder wiederverwendeten Passwörtern." }, "atRiskMembersDescriptionWithApp": { "message": "Diese Mitglieder melden sich bei $APPNAME$ mit schwachen, kompromittierten oder wiederverwendeten Passwörtern an.", @@ -159,7 +159,7 @@ } }, "atRiskMembersDescriptionWithAppNone": { - "message": "There are no at risk members for $APPNAME$.", + "message": "Es gibt keine gefährdeten Mitglieder für $APPNAME$.", "placeholders": { "appname": { "content": "$1", @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Nach der Aktualisierung Ihres Verschlüsselungsschlüssels musst du dich bei allen Bitwarden-Anwendungen, die du momentan benutzt, erneut anmelden (wie z.B. die mobile App oder die Browser-Erweiterungen). Fehler bei Ab- und Anmeldung (die deinen neuen Verschlüsselungsschlüssel herunterlädt) könnte zu einer Beschädigung der Daten führen. Wir werden versuchen dich automatisch abzumelden, was jedoch verzögert geschehen kann." }, - "updateEncryptionKeyExportWarning": { - "message": "Alle verschlüsselten Exporte, die du gespeichert hast, werden ebenfalls ungültig." + "updateEncryptionKeyAccountExportWarning": { + "message": "Alle von dir gespeicherten Exporte mit Konto-Beschränkungen werden ungültig." }, "subscription": { "message": "Abo" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Die Zahlung mit einem Bankkonto ist nur für Kunden in den Vereinigten Staaten möglich. Du musst dein Bankkonto verifizieren. Wir werden innerhalb der nächsten 1-2 Werktage eine Mikro-Einzahlung vornehmen. Gib den Code aus der Beschreibung dieser Einzahlung auf der Abonnementseite des Anbieters ein, um das Bankkonto zu verifizieren. Schlägt die Verifizierung des Bankkontos fehl, wird dies als versäumte Zahlung gewertet und dein Abonnement gesperrt." + }, + "clickPayWithPayPal": { + "message": "Bitte klicke auf den Mit PayPal bezahlen Button, um deine Zahlungsmethode hinzuzufügen." } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index a7c423c91e0..24de28d39be 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Μετά την ενημέρωση του κλειδιού κρυπτογράφησης, πρέπει να αποσυνδεθείτε και να επιστρέψετε σε όλες τις εφαρμογές Bitwarden που χρησιμοποιείτε αυτήν τη στιγμή (όπως η εφαρμογή για κινητά ή οι επεκτάσεις του προγράμματος περιήγησης). Η αποτυχία αποσύνδεσης και επαναφοράς (στην οποία γίνεται λήψη του νέου κλειδιού κρυπτογράφησης) ενδέχεται να προκαλέσει καταστροφή δεδομένων. Θα προσπαθήσουμε να αποσυνδεθείτε αυτόματα, ωστόσο αυτό μπορεί να καθυστερήσει." }, - "updateEncryptionKeyExportWarning": { - "message": "Οποιεσδήποτε κρυπτογραφημένες εξαγωγές που έχετε αποθηκεύσει θα είναι επίσης άκυρες." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Συνδρομή" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 460501615e9..d6a1b01f1d5 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, although this may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 3a3b4391a56..a0e9f4de149 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, although this may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 0e3985b4d40..5b5e1f21582 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -925,10 +925,10 @@ "message": "Maksimuma dosiergrandeco estas 500 MB." }, "addedItem": { - "message": "La ero aldoniĝis" + "message": "Ero aldoniĝis" }, "editedItem": { - "message": "La ero redaktiĝis" + "message": "Ero redaktiĝis" }, "movedItemToOrg": { "message": "$ITEMNAME$ moviĝis al $ORGNAME$", @@ -944,7 +944,7 @@ } }, "itemsMovedToOrg": { - "message": "La eroj moviĝis al $ORGNAME$", + "message": "Eroj moviĝis al $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -953,7 +953,7 @@ } }, "itemMovedToOrg": { - "message": "La ero moviĝis al $ORGNAME$", + "message": "Ero moviĝis al $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -986,7 +986,7 @@ "message": "Ĉu vi certe volas anstataŭigi la nunan pasvorton?" }, "editedFolder": { - "message": "La dosierujo konserviĝis" + "message": "`Dosierujo konserviĝis" }, "addedFolder": { "message": "Dosierujo aldoniĝis" @@ -2866,7 +2866,7 @@ "message": "Ĉu vi certas, ke vi volas nuligi? Vi perdos aliron al ĉiuj funkcioj de ĉi tiu abono fine de ĉi tiu faktura ciklo." }, "canceledSubscription": { - "message": "La abono estis nuligita." + "message": "Abono nuliĝis" }, "neverExpires": { "message": "Neniam eksvalidiĝis" @@ -4248,7 +4248,7 @@ "message": "La organizo kaj ĉiuj rilataj datumoj estis forigitaj." }, "organizationUpdated": { - "message": "La organizo ĝisdatiĝis" + "message": "Organizo konserviĝis" }, "taxInformation": { "message": "Impostaj Informoj" @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Post ĝisdatigi vian ĉifradan ŝlosilon, vi devas elsaluti kaj reeniri al ĉiuj Bitwarden-aplikaĵoj, kiujn vi nun uzas (kiel la poŝtelefona programo aŭ retumila etendaĵoj). Malsukceso elsaluti kaj reeniri (kiu elŝutas via nova ĉifra ŝlosilo) povas rezultigi korupton de datumoj. Ni provos elsaluti vin aŭtomate, tamen ĝi eble prokrastos. " }, - "updateEncryptionKeyExportWarning": { - "message": "Ĉiuj ĉifritaj eksportaĵoj, kiujn vi konservis, ankaŭ malvalidiĝos." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Abono" @@ -4878,7 +4878,7 @@ "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { - "message": "La prefero de uzanto" + "message": "Prefero de la uzanto" }, "vaultTimeoutAction": { "message": "Volta Tempolima Ago" @@ -4949,7 +4949,7 @@ "message": "Eroj rehaviĝis" }, "restoredItemId": { - "message": "La ero $ID$ rehaviĝis", + "message": "Restariĝis ero $ID$.", "placeholders": { "id": { "content": "$1", @@ -8694,7 +8694,7 @@ } }, "collectionManagement": { - "message": "Collection management" + "message": "Administrado de kolekto" }, "collectionManagementDesc": { "message": "Manage the collection behavior for the organization" @@ -8770,13 +8770,13 @@ "message": "At least one member or group must have can manage permission." }, "typePasskey": { - "message": "Passkey" + "message": "Pasŝlosilo" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "Pasŝlosilo ne estos kopiita" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "La pasŝlosilo ne estos kopiita al la klonaĵo. Ĉu vi volas daŭrigi la klonadon de ĉi tiu ero?" }, "modifiedCollectionManagement": { "message": "Modified collection management setting $ID$.", @@ -8792,13 +8792,13 @@ "description": "This is followed a by a hyperlink to the help website." }, "installBrowserExtension": { - "message": "Install browser extension" + "message": "Instali la retumilan etendaĵon" }, "installBrowserExtensionDetails": { - "message": "Use the extension to quickly save logins and auto-fill forms without opening the web app." + "message": "Uzu la etendaĵon por rapide konservi salutilojn kaj aŭtomate plenigi la kampojn sen malfermo de la reteja apo." }, "projectAccessUpdated": { - "message": "Project access updated" + "message": "Projekto-alirebleco ĝisdatiĝis" }, "unexpectedErrorSend": { "message": "An unexpected error has occurred while loading this Send. Try again later." @@ -8813,7 +8813,7 @@ "message": "Seat limit has been reached. Contact your provider to purchase additional seats." }, "collectionAccessRestricted": { - "message": "Collection access is restricted" + "message": "La aliro al kolekto estas limigita" }, "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." @@ -9057,15 +9057,15 @@ "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." }, "machineAccount": { - "message": "Machine account", + "message": "Maŝina konto", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { - "message": "Machine accounts", + "message": "Maŝinaj kontoj", "description": "The title for the section that deals with machine accounts." }, "newMachineAccount": { - "message": "New machine account", + "message": "Nova maŝina konto", "description": "Title for creating a new machine account." }, "machineAccountsNoItemsMessage": { @@ -9073,23 +9073,23 @@ "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { - "message": "Nothing to show yet", + "message": "Ankoraŭ neniu por montri", "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { - "message": "Delete machine accounts", + "message": "Forĵeti maŝinan konton", "description": "Title for the action to delete one or multiple machine accounts." }, "deleteMachineAccount": { - "message": "Delete machine account", + "message": "Forĵeti maŝinajn kontojn", "description": "Title for the action to delete a single machine account." }, "viewMachineAccount": { - "message": "View machine account", + "message": "Vidi maŝinan konton", "description": "Action to view the details of a machine account." }, "deleteMachineAccountDialogMessage": { - "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "message": "Forĵeti la maŝinan konton $MACHINE_ACCOUNT$ estas poreterne kaj neinverseble.", "placeholders": { "machine_account": { "content": "$1", @@ -10102,7 +10102,7 @@ "message": "Non-compliant members will be revoked. Administrators can restore members once they leave all other organizations." }, "deleteOrganizationUser": { - "message": "Delete $NAME$", + "message": "Fo", "placeholders": { "name": { "content": "$1", @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index c53a464402e..a535b45e721 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Una vez actualices tu clave de cifrado, será necesario que cierres sesión y vuelvas a identificarte en todas las aplicaciones de Bitwarden que estés utilizando (como la aplicación móvil o la extensión de navegador). Si la reautenticación falla (la cual descargaría la nueva clave de cifrad) puede producirse corrupción de datos. Intentaremos cerrar tu sesión automáticamente, pero puede tardar un tiempo." }, - "updateEncryptionKeyExportWarning": { - "message": "Cualquier exportación cifrada que hayas guardado también será inválida." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Suscripción" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 3d71fbdf9e9..9ed4db4b0e3 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Pärast krüpteerimisvõtme uuendamist pead kõikides seadmetes, kus Bitwardeni rakendust kasutad, oma kontosse uuesti sisse logima (nt nutitelefonis ja brauseris). Välja- ja sisselogimise (mis ühtlasi laadib ka uue krüpteerimisvõtme) nurjumine võib tingida andmete riknemise. Üritame sinu seadmetest ise välja logida, aga see võib võtta natukene aega." }, - "updateEncryptionKeyExportWarning": { - "message": "Mistahes krüpteeritud, eksporditud andmed muutuvad samuti vigaseks." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Tellimus" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 7feb54a6adb..29f132852eb 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Zifratze-gakoa eguneratu ondoren, saioa hasi eta erabiltzen ari zaren Bitwarden aplikazio guztietara itzuli behar duzu (adibidez, aplikazio mugikorra edo nabigatzailearen gehigarriak). Saioa berriro hasteak huts egiten badu (zifratze-gako berria deskargatzea dakar) datuen korrupzioa ekar lezake. Automatikoki saioa ixten saiatuko gara, baina atzeratu egin daiteke." }, - "updateEncryptionKeyExportWarning": { - "message": "Gorde duzun edozein esportazio zifratu ez da baliozkoa izango." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Harpidetza" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index e2e939d1e40..2fbacc8dca2 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "پس از به‌روزرسانی کلید رمزگذاری، باید از سیستم خارج شوید و دوباره به همه برنامه‌های Bitwarden که در حال حاضر استفاده می‌کنید (مانند برنامه تلفن همراه یا برنامه‌های افزودنی مرورگر) وارد شوید. عدم خروج و ورود مجدد (که کلید رمزگذاری جدید شما را دانلود می‌کند) ممکن است منجر به خراب شدن داده‌ها شود. ما سعی خواهیم کرد شما را به طور خودکار از سیستم خارج کنیم، اما ممکن است با تأخیر انجام شود." }, - "updateEncryptionKeyExportWarning": { - "message": "هر برون ریزی رمزگذاری شده ای که ذخیره کرده اید نیز نامعتبر خواهد بود." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "اشتراک" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 041b54601e0..83efcc197fc 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Salausavaimesi päivityksen jälkeen, sinun tulee kirjautua ulos ja sitten takaisin sisään kaikissa Bitwarden-sovelluksissa, jotka ovat käytössäsi (esim. mobiilisovellus ja selainlaajennukset). Uudelleenkirjautumisen (joka lataa uuden salausavaimen) suorittamatta jättäminen saattaa johtaa tietojen vaurioitumiseen. Yritämme kirjata sinut ulos autmaattisesti, mutta tämä voi tapahtua vasta jonkin ajan kuluttua." }, - "updateEncryptionKeyExportWarning": { - "message": "Tallentamistasi salatuista vienneistä tulee käyttökelvottomia." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Tilaus" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 3cf1f3bcf7b..15e0793d6ce 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Matapos i update ang iyong key sa pag encrypt, kinakailangan kang mag log out at bumalik sa lahat ng mga application ng Bitwarden na kasalukuyang ginagamit mo (tulad ng mobile app o mga extension ng browser). Ang kabiguan na mag log out at bumalik sa (na nag download ng iyong bagong key ng pag encrypt) ay maaaring magresulta sa pagkasira ng data. Susubukan naming awtomatikong mag log out sa iyo, gayunpaman, maaari itong maantala." }, - "updateEncryptionKeyExportWarning": { - "message": "Ang anumang naka encrypt na pag export na iyong nai save ay magiging hindi wasto din." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subskripsyon" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 3374312e8cd..39fe5b3c1cf 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Après avoir mis à jour votre clé de chiffrement, vous devez vous déconnecter et vous reconnecter à toutes les applications Bitwarden que vous utilisez actuellement (comme l'application mobile ou les extensions de navigateur). Si vous ne vous déconnectez pas et ne vous reconnectez pas (ce qui télécharge votre nouvelle clé de chiffrement), cela peut entraîner une corruption des données. Nous tenterons de vous déconnecter automatiquement, mais cela peut être retardé." }, - "updateEncryptionKeyExportWarning": { - "message": "Tous les exports chiffrés que vous avez enregistrés deviendront également invalides." + "updateEncryptionKeyAccountExportWarning": { + "message": "Toutes les exportations restreintes du compte que vous avez enregistrées seront invalides." }, "subscription": { "message": "Abonnement" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Le paiement avec un compte bancaire est seulement disponible pour les clients aux États-Unis. Vous devrez procéder à la vérification de votre compte bancaire. Nous effectuerons un micro-dépôt dans les 1-2 prochains jours ouvrables. Entrez le code de la transaction de ce dépôt sur la page d'abonnement de votre fournisseur pour compléter la vérification du compte bancaire. Si vous ne complétez pas la vérification de votre compte bancaire résultera en un paiement manqué et votre abonnement sera suspendu." + }, + "clickPayWithPayPal": { + "message": "Veuillez cliquer sur le bouton Payer avec PayPal pour ajouter votre mode de paiement." } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index bfbd2983950..eae36865876 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index aa16586a707..286276f96e5 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "לאחר עדכון מפתחות ההצפנה שלך, תתבקש לצאת ולהכנס שוב בכל אפליקציות Bitwarden שאתה משתמש בהן (האפליקציה לפלאפון או ההרחבה לדפדפן). אם לא תצא ותכנס שוב (פעולת הכניסה מורידה את המפתח החדש), יתכן שתתקל במידע שגוי. אנו ננסה לגרום ליציאה אוטומטית, אך יתכן שהדבר לא יקרה מיידית." }, - "updateEncryptionKeyExportWarning": { - "message": "כל הייצואים המוצפנים ששמרת יהפכו גם הם ללא תקפים." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "מנוי" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index dbf2b839ad9..dd2073a924f 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 36ba847012b..4f1c4c981b2 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Nakon ažuriranja svojeg ključa za šifriranje, obavezno se trebaš odjaviti i ponovno prijaviti u sve Bitwarden aplikacije koje trenutno koristiš (npr. mobilna aplikacija, proširenje preglednika, ...). Ako se ne odjaviš i ponovno prijaviš (čime se preuzima tvoj novi ključ za šifriranje) može doći do oštećenja spremljenih podataka. Pokušati ćemo te automatski odjaviti, no, to bi možda moglo potrajati." }, - "updateEncryptionKeyExportWarning": { - "message": "Svi spremljeni šifrirani izvozi također će postati nevažeći." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Pretplata" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index e83e65e2ded..12fd95bbbaa 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "A titkosítási kulcs frissítése után ki kell jelentkezni és vissza kell jelentkezni az összes jelenleg használt Bitwarden alkalmazásba (például a mobilalkalmazás vagy a böngésző bővítmények). A kijelentkezés és a bejelentkezés elmulasztása (amely letölti az új titkosítási kulcsot) adatvesztést okozhat. Megkíséreljük az automatikusan kijelentkeztetést, azonban ez késhet." }, - "updateEncryptionKeyExportWarning": { - "message": "Az el nem mentett titkosított exportok szintén érvénytelenné válnak." + "updateEncryptionKeyAccountExportWarning": { + "message": "A fiókhoz korlátozottan mentett exportálások érvénytelenek lesznek." }, "subscription": { "message": "Előfizetés" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "A bankszámlával történő fizetés csak az Egyesült Államokban élő ügyfelek számára érhető el. A bankszámlát igazolni kell. A fejlesztők a következő 1-2 munkanapon belül mikrobetétet teljesítenek. A bankszámla ellenőrzéséhez írjuk be az utalás leíró kódját a szervezet számlázási oldalán. Ha elmulasztjuk a bankszámla igazolását, az a fizetés elmaradását és az előfizetés felfüggesztését vonja maga után." + }, + "clickPayWithPayPal": { + "message": "Kattintás a Pay with PayPal gombra a fizetési mód hozzáadásához." } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index f29ef18ed3b..3706fb73f63 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Setelah memperbarui kunci enkripsi Anda, Anda diminta untuk keluar dan masuk kembali ke semua aplikasi Bitwarden yang saat ini Anda gunakan (seperti aplikasi seluler atau ekstensi browser). Kegagalan untuk keluar dan masuk kembali (yang mengunduh kunci enkripsi baru Anda) dapat menyebabkan kerusakan data. Kami akan mencoba mengeluarkan Anda secara otomatis, namun, hal itu mungkin tertunda." }, - "updateEncryptionKeyExportWarning": { - "message": "Ekspor terenkripsi apa pun yang telah Anda simpan juga akan menjadi tidak valid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Langganan" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index b336a29df48..a30c1f81f93 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Dopo aver aggiornato la tua chiave di crittografia, devi uscire ed entrare di nuovo in tutte le app Bitwarden che stai usando (come l'app mobile o l'estensione del browser). Se non si esce e rientra (per scaricare la nuova chiave di crittografia) i dati della tua cassaforte potrebbero essere danneggiati. Cercheremo di farti uscire automaticamente, ma potrebbe esserci un ritardo." }, - "updateEncryptionKeyExportWarning": { - "message": "Anche le esportazioni crittografate che hai salvato non saranno più valide." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Abbonamento" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 3ec7d6a3ffe..1bb8bf364b0 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "暗号化キーの更新後は、モバイルアプリやブラウザ拡張機能など現在利用中のすべてのBitwardenアプリで再ログインが必要となります。再ログインしないと(新しい暗号化キーをダウンロードすると)データが破損する可能性があります。自動的にログアウトを試みますが、遅延することがあります。" }, - "updateEncryptionKeyExportWarning": { - "message": "保存済みの暗号化されたエクスポートも無効になります。" + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "契約" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 6ebe9759a29..72e9066806c 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 7a206aca2e3..38740c78cba 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index d4847a51bad..2758b022b64 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "ನಿಮ್ಮ ಎನ್‌ಕ್ರಿಪ್ಶನ್ ಕೀಲಿಯನ್ನು ನವೀಕರಿಸಿದ ನಂತರ, ನೀವು ಪ್ರಸ್ತುತ ಬಳಸುತ್ತಿರುವ (ಮೊಬೈಲ್ ಅಪ್ಲಿಕೇಶನ್ ಅಥವಾ ಬ್ರೌಸರ್ ವಿಸ್ತರಣೆಗಳಂತಹ) ಎಲ್ಲಾ ಬಿಟ್‌ವಾರ್ಡೆನ್ ಅಪ್ಲಿಕೇಶನ್‌ಗಳಿಗೆ ನೀವು ಲಾಗ್ ಔಟ್ ಮತ್ತು ಬ್ಯಾಕ್ ಇನ್ ಮಾಡಬೇಕಾಗುತ್ತದೆ. ಲಾಗ್ and ಟ್ ಮಾಡಲು ಮತ್ತು ಹಿಂತಿರುಗಲು ವಿಫಲವಾದರೆ (ಅದು ನಿಮ್ಮ ಹೊಸ ಎನ್‌ಕ್ರಿಪ್ಶನ್ ಕೀಲಿಯನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡುತ್ತದೆ) ಡೇಟಾ ಭ್ರಷ್ಟಾಚಾರಕ್ಕೆ ಕಾರಣವಾಗಬಹುದು. ನಾವು ನಿಮ್ಮನ್ನು ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಲಾಗ್ ಔಟ್ ಮಾಡಲು ಪ್ರಯತ್ನಿಸುತ್ತೇವೆ, ಆದಾಗ್ಯೂ, ಇದು ವಿಳಂಬವಾಗಬಹುದು." }, - "updateEncryptionKeyExportWarning": { - "message": "ನೀವು ಉಳಿಸಿದ ಯಾವುದೇ ಎನ್‌ಕ್ರಿಪ್ಟ್ ಮಾಡಿದ ರಫ್ತು ಸಹ ಅಮಾನ್ಯವಾಗುತ್ತದೆ." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "ಚಂದಾದಾರಿಕೆ" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 24fc6dad11a..ee089584adf 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "암호화 키를 업데이트하고난 후 현재 사용 중인 모든 Bitwarden 애플리케이션(예. 모바일 앱 혹은 브라우저 확장 기능)에서 로그아웃 후 다시 로그인해야 합니다. 재로그인하지 않으면 (새 암호화 키를 다운로드받는 경우) 데이터 손실이 발생할 수 있습니다. 자동으로 로그아웃을 시도하지만 지연될 수 있습니다." }, - "updateEncryptionKeyExportWarning": { - "message": "이전에 암호화 상태로 내보내기하여 저장한 데이터도 무효화됩니다." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "구독" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 0e561cec5c6..61ff8651c72 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Pēc šifrēšanas atslēgas atjaunināšanas ir nepieciešams atteikties un tad pieteikties visās Bitwarden lietotnēs, kas pašreiz tiek izmantotas (piemēram, tālruņa lietotnē vai pārlūku paplašinājumā). Ja tas netiks darīts (tā tiek lejupielādēta jaunā šifrēšanas atslēga), dati var tikt bojāti. Tiks veikts automātisks atteikšanās mēģinājums, tomēr tas var notikt ar aizkavi." }, - "updateEncryptionKeyExportWarning": { - "message": "Arī katra šifrētā izguve, kas ir saglabāta, kļūs nederīga." + "updateEncryptionKeyAccountExportWarning": { + "message": "Jebkuras konta ierobežotās izguves, kas ir saglabātas, kļūs nederīgas." }, "subscription": { "message": "Abonements" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Apmaksa ar bankas kontu ir pieejama tikai klientiem Savienotajās Valstīs. Būs nepieciešams apliecināt savu bankas kontu. Mēs veiksim sīkiemaksu nākamās darba dienas vai divu laikā. Pēc tam šīs iemaksas uzdevuma aprakstā esošais kods būs jāievada pakalpojuma sniedzēja abonēšanas lapā, lai apliecinātu bankas kontu. Bankas konta apliecināšanas neveikšana beigsies ar nokavētu maksājumu un apturētu abonementu." + }, + "clickPayWithPayPal": { + "message": "Lūgums klikšķināt pogu \"Apmaksāt ar PayPal, lai pievienotu savu maksājumu veidu." } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index a447cc360b3..dd4ca0b94cf 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "സബ്സ്ക്രിപ്ഷൻ" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 7a206aca2e3..38740c78cba 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 7a206aca2e3..38740c78cba 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 8f019081d67..93df01f3440 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Etter å ha oppdatert krypteringsnøkkelen din, er du påkrevd å logge av og på på alle Bitwarden-appene og -programmene som du bruker for øyeblikket (deriblant mobilappen og nettleserutvidelsene). Å ikke logge av og på igjen (noe som vil laste ned din nye krypteringsnøkkel) kan føre til datakorrumpering. Vi vil forsøke å logge deg av automatisk, men det kan kanskje bli forsinket." }, - "updateEncryptionKeyExportWarning": { - "message": "Eventuelle krypterte eksporter som du har lagret blir også ugyldig." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Abonnement" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 579022bfb93..215ebaf1849 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 7939d444d32..9d192639377 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Na het bijwerken van je encryptiesleutel moet je je afmelden en weer aanmelden bij alle Bitwarden-applicaties die je gebruikt (zoals de mobiele app of browserextensies). Als je niet opnieuw inlogt (wat je nieuwe encryptiesleutel downloadt), kan dit gegevensbeschadiging tot gevolg hebben. We proberen je automatisch uit te loggen, maar het kan zijn dat dit met enige vertraging gebeurt." }, - "updateEncryptionKeyExportWarning": { - "message": "Elke versleutelde export die je hebt bewaard wordt onbruikbaar." + "updateEncryptionKeyAccountExportWarning": { + "message": "Alle account-beperkte bewaarde exports die je hebt opgeslagen worden ongeldig." }, "subscription": { "message": "Abonnement" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Betaling met een bankrekening is alleen beschikbaar voor klanten in de Verenigde Staten. Je moet je bankrekening verifiëren. We zullen binnen 1-2 werkdagen een microbetaling uitvoeren. Voer de code van het bankafschrift uit deze storting in op de factuurpagina van de provider om de bankrekening te verifiëren. Als de bankrekening niet wordt geverifieerd, wordt er een betaling gemist en wordt je abonnement opgeschort." + }, + "clickPayWithPayPal": { + "message": "Klik op \"Pay with PayPal\" voor het toevoegen van je betaalmethode." } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 8de39adc8c4..3c03b2e7cb2 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 7a206aca2e3..38740c78cba 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 5e8ac7f9fe3..ab471e892ef 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Po zaktualizowaniu klucza szyfrowania, musisz ponownie zalogować się do wszystkich aplikacji Bitwarden, z których obecnie korzystasz (na przykład aplikacje mobilne lub rozszerzenia przeglądarki). Niepowodzenie logowania (podczas którego pobierany jest nowy klucz szyfrowania) może spowodować uszkodzenie danych. Postaramy się wylogować Ciebie automatycznie, jednak może to chwilę potrwać." }, - "updateEncryptionKeyExportWarning": { - "message": "Wszystkie zaszyfrowane pliki eksportu, które wcześniej zapisałeś, staną się nieprawidłowe." + "updateEncryptionKeyAccountExportWarning": { + "message": "Wszystkie zapisane eksporty objęte ograniczeniami konta zostaną unieważnione." }, "subscription": { "message": "Subskrypcja" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Płatność za pomocą konta bankowego jest dostępna tylko dla klientów w Stanach Zjednoczonych. Będziesz musiał/musiała zweryfikować swoje konto bankowe. W ciągu najbliższych 1-2 dni roboczych dokonamy mikrowpłaty. Wprowadź kod deskryptora z tej wpłaty na stronie dostawcy subskrypcji, aby zweryfikować konto bankowe. Niezweryfikowanie konta bankowego spowoduje brak płatności i zawieszenie Twojej subskrypcji." + }, + "clickPayWithPayPal": { + "message": "Kliknij przycisk Zapłać za pomocą PayPal, aby dodać metodę płatności." } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index a3f5038d178..b3f6dd37a8c 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Depois de atualizar sua chave de criptografia, é necessário encerrar e iniciar a sessão em todos os aplicativos do Bitwarden que você está usando atualmente (como o aplicativo móvel ou as extensões do navegador). Não encerrar e iniciar sessão (que baixa sua nova chave de criptografia) pode resultar em corrupção de dados. Nós tentaremos desconectá-lo automaticamente, mas isso pode demorar um pouco." }, - "updateEncryptionKeyExportWarning": { - "message": "Quaisquer exportações criptografadas que você tenha salvo também se tornarão inválidas." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Assinatura" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 2a72ced6496..5f1e02c82e3 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Depois de atualizar a sua chave de encriptação, é necessário terminar sessão e voltar a iniciar em todas as aplicações Bitwarden que está a utilizar atualmente (como a aplicação móvel ou as extensões do navegador). A falha em terminar sessão e voltar a iniciar (que descarrega a sua nova chave de encriptação) pode resultar em corrupção de dados. Tentaremos terminar a sua sessão automaticamente, no entanto, pode demorar." }, - "updateEncryptionKeyExportWarning": { - "message": "Quaisquer exportações encriptadas que tenha guardado também se tornarão inválidas." + "updateEncryptionKeyAccountExportWarning": { + "message": "Todas as exportações com restrições de conta que tenha guardado tornar-se-ão inválidas." }, "subscription": { "message": "Subscrição" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "O pagamento com uma conta bancária só está disponível para clientes nos Estados Unidos. Ser-lhe-á pedido que verifique a sua conta bancária. Efetuaremos um micro-depósito nos próximos 1-2 dias úteis. Introduza o código descritor do extrato deste depósito na página de subscrição do fornecedor para verificar a conta bancária. A não verificação da conta bancária resultará na falta de pagamento e na suspensão da sua subscrição." + }, + "clickPayWithPayPal": { + "message": "Por favor, clique no botão Pagar com PayPal para adicionar o seu método de pagamento." } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 2c825332283..64eb05dad22 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -9,7 +9,7 @@ "message": "Aplicațiile critice" }, "noCriticalAppsAtRisk": { - "message": "No critical applications at risk" + "message": "Nicio aplicație critică în pericol" }, "accessIntelligence": { "message": "Access Intelligence" @@ -18,7 +18,7 @@ "message": "Risk Insights" }, "passwordRisk": { - "message": "Password Risk" + "message": "Risc Parola" }, "reviewAtRiskPasswords": { "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." @@ -33,19 +33,19 @@ } }, "notifiedMembers": { - "message": "Notified members" + "message": "Membri notificați" }, "revokeMembers": { - "message": "Revoke members" + "message": "Revocare utilizatori" }, "restoreMembers": { - "message": "Restore members" + "message": "Restabilire utilizatori" }, "cannotRestoreAccessError": { - "message": "Cannot restore organization access" + "message": "Nu se poate restaura accesul organizației" }, "allApplicationsWithCount": { - "message": "All applications ($COUNT$)", + "message": "Toate aplicațiile ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -87,31 +87,31 @@ "message": "As users save logins, applications appear here, showing any at-risk passwords. Mark critical apps and notify users to update passwords." }, "noCriticalAppsTitle": { - "message": "You haven't marked any applications as a Critical" + "message": "Nu ai marcat nicio aplicație drept critică" }, "noCriticalAppsDescription": { "message": "Select your most critical applications to discover at-risk passwords, and notify users to change those passwords." }, "markCriticalApps": { - "message": "Mark critical apps" + "message": "Marchează aplicațiile critice" }, "markAppAsCritical": { - "message": "Mark app as critical" + "message": "Marchează aplicația drept critică" }, "applicationsMarkedAsCriticalSuccess": { - "message": "Applications marked as critical" + "message": "Aplicații marcate drept critice" }, "application": { - "message": "Application" + "message": "Aplicație" }, "atRiskPasswords": { "message": "At-risk passwords" }, "requestPasswordChange": { - "message": "Request password change" + "message": "Solicită modificarea parolei" }, "totalPasswords": { - "message": "Total passwords" + "message": "Total parole" }, "searchApps": { "message": "Search applications" @@ -168,13 +168,13 @@ } }, "totalMembers": { - "message": "Total members" + "message": "Total membri" }, "atRiskApplications": { - "message": "At-risk applications" + "message": "Aplicații cu risc" }, "totalApplications": { - "message": "Total applications" + "message": "Toate aplicațiile" }, "unmarkAsCriticalApp": { "message": "Unmark as critical app" @@ -220,10 +220,10 @@ "message": "Note" }, "privateNote": { - "message": "Private note" + "message": "Notă privată" }, "note": { - "message": "Note" + "message": "Notă" }, "customFields": { "message": "Câmpuri particularizate" @@ -232,22 +232,22 @@ "message": "Numele titularului cardului" }, "loginCredentials": { - "message": "Login credentials" + "message": "Date de logare" }, "personalDetails": { - "message": "Personal details" + "message": "Detalii personale" }, "identification": { - "message": "Identification" + "message": "Identificare" }, "contactInfo": { - "message": "Contact info" + "message": "Informații contact" }, "cardDetails": { - "message": "Card details" + "message": "Detalii card" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "Detalii $BRAND$", "placeholders": { "brand": { "content": "$1", @@ -256,16 +256,16 @@ } }, "itemHistory": { - "message": "Item history" + "message": "Istoricul articolului" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "Cheie de autentificare" }, "autofillOptions": { - "message": "Autofill options" + "message": "Opțiuni de completare automată" }, "websiteUri": { - "message": "Website (URI)" + "message": "Site web (URI)" }, "websiteUriCount": { "message": "Website (URI) $COUNT$", @@ -278,13 +278,13 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "Site web adăugat" }, "addWebsite": { - "message": "Add website" + "message": "Adăugare site" }, "deleteWebsite": { - "message": "Delete website" + "message": "Ștergere site" }, "defaultLabel": { "message": "Default ($VALUE$)", @@ -315,7 +315,7 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Completare automată la încărcarea paginii?" }, "number": { "message": "Număr card" @@ -330,7 +330,7 @@ "message": "Cod de securitate (CVV)" }, "securityCodeSlashCVV": { - "message": "Security code / CVV" + "message": "Cod de securitate (CVV)" }, "identityName": { "message": "Numele identității" @@ -408,10 +408,10 @@ "message": "Dr." }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Card expirat" }, "cardExpiredMessage": { - "message": "If you've renewed it, update the card's information" + "message": "Dacă l-ați reînnoit, actualizați informațiile cardului" }, "expirationMonth": { "message": "Luna expirării" @@ -457,10 +457,10 @@ "description": "This describes a field that is 'linked' (related) to another field." }, "fieldType": { - "message": "Field type" + "message": "Tipul câmpului" }, "fieldLabel": { - "message": "Field label" + "message": "Eticheta câmp" }, "remove": { "message": "Ștergere" @@ -473,7 +473,7 @@ "description": "This is the folder for uncategorized items" }, "selfOwnershipLabel": { - "message": "You", + "message": "Tu", "description": "Used as a label to indicate that the user is the owner of an item." }, "addFolder": { @@ -496,10 +496,10 @@ } }, "newFolder": { - "message": "New folder" + "message": "Folder nou" }, "folderName": { - "message": "Folder name" + "message": "Numele folderului" }, "folderHintText": { "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" @@ -551,7 +551,7 @@ "message": "Generare parolă" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Generare parolă" }, "checkPassword": { "message": "Verificați dacă parola a fost dezvăluită." @@ -606,11 +606,11 @@ "description": "Search Login type" }, "searchCard": { - "message": "Search cards", + "message": "Căutare carduri", "description": "Search Card type" }, "searchIdentity": { - "message": "Search identities", + "message": "Caută identități", "description": "Search Identity type" }, "searchSecureNote": { @@ -624,7 +624,7 @@ "message": "Caută în seiful meu" }, "searchOrganization": { - "message": "Cauta organizatie" + "message": "Caută organizație" }, "searchMembers": { "message": "Caută membri" @@ -654,7 +654,7 @@ "message": "Notă securizată" }, "typeSshKey": { - "message": "SSH key" + "message": "Cheie SSH" }, "typeLoginPlural": { "message": "Conectări" @@ -687,7 +687,7 @@ "message": "Numele complet" }, "address": { - "message": "Address" + "message": "Adresă" }, "address1": { "message": "Adresă 1" @@ -759,7 +759,7 @@ } }, "new": { - "message": "New", + "message": "Nou", "description": "for adding new items" }, "item": { @@ -850,22 +850,22 @@ "message": "Copy phone" }, "copyEmail": { - "message": "Copy email" + "message": "Copiere e-mail" }, "copyCompany": { - "message": "Copy company" + "message": "Copiază firma" }, "copySSN": { "message": "Copy Social Security number" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Copiați numărul pașaportului" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "Copiați numărul de licență" }, "copyName": { - "message": "Copy name" + "message": "Copiați numele" }, "me": { "message": "Eu" @@ -886,7 +886,7 @@ "message": "Articolele seifului" }, "filter": { - "message": "Filter" + "message": "Filtru" }, "deleteSelected": { "message": "Ștergere selecție" @@ -998,37 +998,37 @@ "message": "Dosar șters" }, "editInfo": { - "message": "Edit info" + "message": "Editează info" }, "access": { - "message": "Access" + "message": "Acces" }, "accessLevel": { - "message": "Access level" + "message": "Nivel de acces" }, "accessing": { - "message": "Accessing" + "message": "Accesare" }, "loggedOut": { "message": "Deconectat" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Ai fost deconectat din contul tău." }, "loginExpired": { "message": "Sesiunea de autentificare a expirat." }, "restartRegistration": { - "message": "Restart registration" + "message": "Reporniți înregistrarea" }, "expiredLink": { - "message": "Expired link" + "message": "Link expirat" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "Vă rugăm să reporniți înregistrarea sau să încercați să vă conectați." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "Este posibil să aveți deja un cont" }, "logOutConfirmation": { "message": "Sigur doriți să vă deconectați?" @@ -1046,7 +1046,7 @@ "message": "Nu" }, "location": { - "message": "Location" + "message": "Locație" }, "loginOrCreateNewAccount": { "message": "Autentificați-vă sau creați un cont nou pentru a accesa seiful dvs. securizat." @@ -1058,31 +1058,31 @@ "message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Selectezi o altă opțiune?" }, "loginWithMasterPassword": { "message": "Autentificați-vă cu parola principală" }, "readingPasskeyLoading": { - "message": "Reading passkey..." + "message": "Citire parolă..." }, "readingPasskeyLoadingInfo": { - "message": "Keep this window open and follow prompts from your browser." + "message": "Păstrează această fereastră deschisă și urmează instrucțiunile din browser-ul tău." }, "useADifferentLogInMethod": { - "message": "Use a different log in method" + "message": "Folosiți o metodă diferită de autentificare" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Autentificare cu parolă" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Autentificare unică" }, "welcomeBack": { - "message": "Welcome back" + "message": "Bine ați revenit!" }, "invalidPasskeyPleaseTryAgain": { - "message": "Invalid Passkey. Please try again." + "message": "Parolă nevalidă. Vă rugăm să încercați din nou." }, "twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn": { "message": "2FA for passkeys is not supported. Update the app to log in." @@ -1091,7 +1091,7 @@ "message": "Use a generated passkey that will automatically log you in without a password. Biometrics, like facial recognition or fingerprint, or another FIDO2 security method will verify your identity." }, "newPasskey": { - "message": "New passkey" + "message": "Cheie parolă nouă" }, "learnMoreAboutPasswordless": { "message": "Learn more about passwordless" @@ -1106,31 +1106,31 @@ "message": "Error creating passkey" }, "errorCreatingPasskeyInfo": { - "message": "There was a problem creating your passkey." + "message": "A apărut o eroare la crearea parolei." }, "passkeySuccessfullyCreated": { - "message": "Passkey successfully created!" + "message": "Parola a fost creata!" }, "customPasskeyNameInfo": { - "message": "Name your passkey to help you identify it." + "message": "Numiți cheia de acces pentru a vă ajuta să o identificați." }, "useForVaultEncryption": { - "message": "Use for vault encryption" + "message": "Utilizat pentru criptarea seifului" }, "useForVaultEncryptionInfo": { "message": "Log in and unlock on supported devices without your master password. Follow the prompts from your browser to finalize setup." }, "useForVaultEncryptionErrorReadingPasskey": { - "message": "Error reading passkey. Try again or uncheck this option." + "message": "Eroare la citirea parolei. Încercați din nou sau debifați această opțiune." }, "encryptionNotSupported": { - "message": "Encryption not supported" + "message": "Criptare neacceptată" }, "enablePasskeyEncryption": { - "message": "Set up encryption" + "message": "Configurare criptare" }, "usedForEncryption": { - "message": "Used for encryption" + "message": "Folosit pentru criptare" }, "loginWithPasskeyEnabled": { "message": "Log in with passkey turned on" @@ -1145,10 +1145,10 @@ } }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Parolă eliminată" }, "removePasskey": { - "message": "Remove passkey" + "message": "Înlăturare parolă" }, "removePasskeyInfo": { "message": "If all passkeys are removed, you will be unable to log into new devices without your master password." @@ -1157,16 +1157,16 @@ "message": "Passkey limit reached. Remove a passkey to add another." }, "tryAgain": { - "message": "Try again" + "message": "Încercați din nou" }, "createAccount": { "message": "Creare cont" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Nou pe Bitwarden?" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Setați o parolă puternică" }, "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" @@ -1184,10 +1184,10 @@ "message": "Log in to Bitwarden" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "Introdu codul trimis la emailul tău" }, "enterTheCodeFromYourAuthenticatorApp": { - "message": "Enter the code from your authenticator app" + "message": "Introdu codul din aplicația de autentificare" }, "pressYourYubiKeyToAuthenticate": { "message": "Press your YubiKey to authenticate" @@ -1199,13 +1199,13 @@ "message": "The authentication session timed out. Please restart the login process." }, "verifyYourIdentity": { - "message": "Verify your Identity" + "message": "Verificați identitatea" }, "weDontRecognizeThisDevice": { - "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + "message": "Dispozitiv nerecunoscut. Introdu codul trimis pe email pentru a verifica identitatea." }, "continueLoggingIn": { - "message": "Continue logging in" + "message": "Continuă autentificarea" }, "whatIsADevice": { "message": "What is a device?" @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "După actualizarea cheii de criptare, trebuie să vă reconectați în toate aplicațiile Bitwarden pe care le utilizați în prezent (cum ar fi aplicația mobilă sau extensiile browserului). Faptul de a nu vă deconecta și reconecta (care descarcă noua cheie de criptare) poate duce la corupția datelor. Vom încerca să vă deconectăm automat, însă ar putea fi întârziat." }, - "updateEncryptionKeyExportWarning": { - "message": "Orice export criptat pe care l-ați salvat va deveni, de asemenea, nevalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Abonament" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index bf35c9a916a..aaccc3b2145 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "После обновления ключа шифрования необходимо выйти из всех приложений Bitwarden, которые вы используете (например, из мобильного приложения или расширения браузера). Если этого не сделать, могут повредиться данные (так как при выходе и последующем входе загружается ваш новый ключ шифрования). Мы попытаемся автоматически осуществить завершение ваших сессий, однако это может произойти с задержкой." }, - "updateEncryptionKeyExportWarning": { - "message": "Любые зашифрованные экспортированные данные, которые вы сохранили, также станут недействительными." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Подписка" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Оплата с помощью банковского счета доступна только для клиентов в США. Вам потребуется подтвердить свой банковский счет. Мы сделаем микродепозит в течение следующих 1-2 рабочих дней. Введите код дескриптора выписки из этого депозита на странице подписки провайдера для подтверждения банковского счета. Неподтверждение банковского счета приведет к пропуску платежа и приостановке подписки." + }, + "clickPayWithPayPal": { + "message": "Пожалуйста, нажмите кнопку \"Оплатить с помощью PayPal\", чтобы добавить свой способ оплаты." } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index a8ddb31d50a..2d270a54731 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 3192cdd9644..6bbf765ba78 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -99,7 +99,7 @@ "message": "Označiť aplikáciu ako kritickú" }, "applicationsMarkedAsCriticalSuccess": { - "message": "Applications marked as critical" + "message": "Aplikácie označené ako kritické" }, "application": { "message": "Aplikácia" @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Po aktualizácii šifrovacieho kľúča budete požiadaní o opätovné prihlásenie do všetkých Bitwarden aplikácii ktoré momentálne používate (napríklad mobilné aplikácie, alebo rozšírenia v prehliadači). Ak sa opätovne neprihlásite (touto operáciou sa stiahnu nové šifrovacie kľúče), mohlo by to viesť k poškodeniu uložených dát. Pokúsime sa odhlásiť vás automaticky, ale môže to chvíľu trvať." }, - "updateEncryptionKeyExportWarning": { - "message": "Všetky uložené šifrované exporty sa tiež stanú neplatnými." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Predplatné" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Platba prostredníctvom bankového účtu je dostupná len pre zákazníkov v Spojených Štátoch. Budete musieť overiť svoj bankový účet. V priebehu nasledujúcich 1-2 pracovných dní vykonáme mikro vklad. Na overenie bankového účtu zadajte kód popisu výpisu z tohto vkladu na fakturačnej stránke poskytovateľa. Neoverenie bankového účtu bude mať za následok neuskutočnenie platby a pozastavenie vášho predplatného." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index a3605be29ef..06c5339ad98 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Naročnina" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index e1099d2c5f8..2ca7e72e227 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Након ажурирања кључа за шифровање, мораћете да се одјавите и вратите у све Bitwarden апликације које тренутно користите (као што су мобилна апликација или додаци прегледача). Ако се не одјавите и поново пријавите (чиме се преузима ваш нови кључ за шифровање), може доћи до оштећења података. Покушаћемо аутоматски да се одјавимо, али може доћи до одлагања." }, - "updateEncryptionKeyExportWarning": { - "message": "Сваки шифровани извоз који сте сачували такође ће постати неважећи." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Претплата" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Плаћање са банковним рачуном доступно је само купцима у Сједињеним Државама. Од вас ће бити потребно да проверите свој банковни рачун. Направит ћемо микро депозит у наредних 1-2 радна дана. Унесите кôд за дескриптор изјаве са овог депозита на претплатничкој страници провајдера да бисте потврдили банковни рачун. Неуспех у верификацији банковног рачуна резултираће пропуштеним плаћањем и претплатом је суспендовано." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 13a974031cc..6b10fd30018 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 634a709762b..1ca059f95ec 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Efter att ha uppdaterat din krypteringsnyckel, måste du logga ut och in igen i alla Bitwarden-program som du använder (t.ex. mobilappen och webbläsartillägget). Att inte logga ut och in igen (vilket hämtar din nya krypteringsnyckel) kan resultera i datakorruption. Vi kommer försöka logga ut dig automatiskt, men det kan vara fördröjt." }, - "updateEncryptionKeyExportWarning": { - "message": "Alla krypterade exporter som du har sparat kommer också bli ogiltiga." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Prenumeration" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 7a206aca2e3..38740c78cba 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 4fff9ab837e..393442e42e5 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Subscription" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 3f82a6d6c15..c59f33bba45 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Şifreleme anahtarınızı güncelledikten sonra, şu anda kullanmakta olduğunuz tüm Bitwarden uygulamalarında (mobil uygulama veya tarayıcı uzantıları gibi) oturumunuzu kapatıp tekrar açmanız gerekir. Yeni şifreleme anahtarınızı indirme için oturumu kapatıp tekrar açmamamız verilerin bozulmasına neden olabilir. Oturumunuzu otomatik olarak kapatmaya çalışacağız, ancak bu gecikebilir." }, - "updateEncryptionKeyExportWarning": { - "message": "Şifrelenmiş dışa aktarmalarınız da geçersiz olacaktır." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Abonelik" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index cdeaacf5fb4..288e8e840a0 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "Після оновлення вашого ключа шифрування вам необхідно вийти з системи і потім виконати повторний вхід у всіх програмах Bitwarden, які ви використовуєте. Збій при виході та повторному вході може призвести до пошкодження даних. Ми спробуємо завершити ваші сеанси автоматично, однак, цей процес може відбутися із затримкою." }, - "updateEncryptionKeyExportWarning": { - "message": "Будь-які зашифровані експортування, які ви зберегли, також стануть недійсними." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Передплата" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Оплата з банківським рахунком доступна тільки для клієнтів у США. Вам необхідно буде підтвердити свій банківський рахунок. Ми здійснимо мікродепозит протягом наступних 1–2 робочих днів. Введіть код дескриптора з цього депозиту на сторінці передплати провайдера, щоб підтвердити банківський рахунок. Неможливість засвідчення банківського рахунку призведе до втрати платежу та припинення вашої передплати." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 7204365cb21..cee1bf8ebbd 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed." }, - "updateEncryptionKeyExportWarning": { - "message": "Any encrypted exports that you have saved will also become invalid." + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "Gói" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index af785810c7f..9805eaac5fd 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -2836,7 +2836,7 @@ "message": "支付卡" }, "paypalClickSubmit": { - "message": "点击 PayPal 按钮登录您的 PayPal 账户,然后点击下面的提交按钮继续。" + "message": "选择 PayPal 按钮登录您的 PayPal 账户,然后点击下面的「提交」按钮继续。" }, "cancelSubscription": { "message": "取消订阅" @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "更新加密密钥后,您需要注销所有当前使用的 Bitwarden 应用程序(例如移动 App 或浏览器扩展)然后重新登录。注销或者重新登录(这将下载新的加密密钥)失败可能会导致数据损坏。我们会尝试自动为您注销,但可能会有所延迟。" }, - "updateEncryptionKeyExportWarning": { - "message": "您保存的任何已加密导出也将变为无效。" + "updateEncryptionKeyAccountExportWarning": { + "message": "您已保存的所有账户限制的导出文件将失效。" }, "subscription": { "message": "订阅" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "使用银行账户付款仅对美国用户开放。您将被要求验证您的银行账户。我们将在 1-2 个工作日内进行一笔小额转账,请在提供商的订阅页面输入该转账的对账单描述符代码以验证银行账户。验证银行账户失败将会错过支付,您的订阅将失效。" + }, + "clickPayWithPayPal": { + "message": "请点击「使用 PayPal 付款」按钮以添加您的付款方式。" } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 59e77927862..57dd4c8631f 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -4548,8 +4548,8 @@ "updateEncryptionKeyWarning": { "message": "更新加密金鑰後,您需要登出並重新登入目前使用的所有 Bitwarden 應用程式(如行動應用程式或瀏覽器擴充套件)。登出和重新登入(這會下載新的加密金鑰)失敗可能會導致資料損毀。我們將嘗試自動登出,但可能會有所延遲。" }, - "updateEncryptionKeyExportWarning": { - "message": "您儲存的任何已加密匯出的檔案也將變成無效。" + "updateEncryptionKeyAccountExportWarning": { + "message": "Any account restricted exports you have saved will become invalid." }, "subscription": { "message": "訂閱" @@ -10610,5 +10610,8 @@ }, "verifyProviderBankAccountWithStatementDescriptorWarning": { "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the provider's subscription page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended." + }, + "clickPayWithPayPal": { + "message": "Please click the Pay with PayPal button to add your payment method." } } From a02c230e4d9f16f93c295ae857e6618349338f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Sat, 17 May 2025 22:17:36 +0200 Subject: [PATCH 092/163] Pin rust toolchain (#14817) * Pin rust toolchain * Always install targets in build script * Delete installed toolchains --- .github/renovate.json5 | 6 +++++ .github/workflows/build-desktop.yml | 26 ++----------------- apps/desktop/desktop_native/Cargo.lock | 16 ++++++------ apps/desktop/desktop_native/build.js | 6 +++++ .../desktop_native/rust-toolchain.toml | 4 +++ 5 files changed, 26 insertions(+), 32 deletions(-) create mode 100644 apps/desktop/desktop_native/rust-toolchain.toml diff --git a/.github/renovate.json5 b/.github/renovate.json5 index d0066ddd7ba..12ae415c6a1 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -51,6 +51,12 @@ commitMessagePrefix: "[deps] BRE:", addLabels: ["hold"], }, + { + // Enable support for Rust toolchain updates. + matchManagers: ["custom.regex"], + matchDepNames: ["rust"], + commitMessageTopic: "Rust", + }, { // By default, we send patch updates to the Dependency Dashboard and do not generate a PR. // We want to generate PRs for a select number of dependencies to ensure we stay up to date on these. diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 86dc74f7351..fab0df693cb 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -428,12 +428,6 @@ jobs: - name: Set up environmentF run: choco install checksum --no-progress - - name: Rust - shell: pwsh - run: | - rustup target install i686-pc-windows-msvc - rustup target install aarch64-pc-windows-msvc - - name: Print environment run: | node --version @@ -681,10 +675,6 @@ jobs: - name: Set up Node-gyp run: python3 -m pip install setuptools - - name: Rust - shell: pwsh - run: rustup target install aarch64-apple-darwin - - name: Print environment run: | node --version @@ -890,10 +880,6 @@ jobs: - name: Set up Node-gyp run: python3 -m pip install setuptools - - name: Rust - shell: pwsh - run: rustup target install aarch64-apple-darwin - - name: Print environment run: | node --version @@ -1040,9 +1026,7 @@ jobs: - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' working-directory: apps/desktop/desktop_native - run: | - rustup target add aarch64-apple-darwin - node build.js cross-platform + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' @@ -1139,10 +1123,6 @@ jobs: - name: Set up Node-gyp run: python3 -m pip install setuptools - - name: Rust - shell: pwsh - run: rustup target install aarch64-apple-darwin - - name: Print environment run: | node --version @@ -1296,9 +1276,7 @@ jobs: - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' working-directory: apps/desktop/desktop_native - run: | - rustup target add aarch64-apple-darwin - node build.js cross-platform + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 115947f1a81..f3db03f8639 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -889,7 +889,7 @@ dependencies = [ "ssh-encoding", "ssh-key", "sysinfo", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", "tokio-stream", "tokio-util", @@ -931,7 +931,7 @@ dependencies = [ "cc", "core-foundation", "glob", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", ] @@ -2480,7 +2480,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -2971,11 +2971,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -2991,9 +2991,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", diff --git a/apps/desktop/desktop_native/build.js b/apps/desktop/desktop_native/build.js index da61da15f9d..2edd0e89616 100644 --- a/apps/desktop/desktop_native/build.js +++ b/apps/desktop/desktop_native/build.js @@ -45,6 +45,10 @@ function buildProxyBin(target, release = true) { } } +function installTarget(target) { + child_process.execSync(`rustup target add ${target}`, { stdio: 'inherit', cwd: __dirname }); +} + if (!crossPlatform && !target) { console.log(`Building native modules in ${mode} mode for the native architecture`); buildNapiModule(false, mode === "release"); @@ -54,6 +58,7 @@ if (!crossPlatform && !target) { if (target) { console.log(`Building for target: ${target} in ${mode} mode`); + installTarget(target); buildNapiModule(target, mode === "release"); buildProxyBin(target, mode === "release"); return; @@ -70,6 +75,7 @@ if (process.platform === "linux") { } platformTargets.forEach(([target, _]) => { + installTarget(target); buildNapiModule(target); buildProxyBin(target); }); diff --git a/apps/desktop/desktop_native/rust-toolchain.toml b/apps/desktop/desktop_native/rust-toolchain.toml new file mode 100644 index 00000000000..898a61f3f4b --- /dev/null +++ b/apps/desktop/desktop_native/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.85.0" +components = [ "rustfmt", "clippy" ] +profile = "minimal" From e73f902aee23f2576e2b0409df2bd8a04ac76423 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Mon, 19 May 2025 08:51:46 +0200 Subject: [PATCH 093/163] [PM-18576] Fix missing user id on remove password (#13777) * Passed in userId on RemovePasswordComponent. * Added userId on other references to KeyConnectorService methods * remove password component refactor, test coverage, enabled strict * explicit user id provided to key connector service * redirect to / instead when user not logged in or not managing organization * key connector service explicit user id * key connector service no longer requires account service * key connector service missing null type * cli convert to key connector unit tests * remove unnecessary SyncService * error toast not showing on ErrorResponse * bad import due to merge conflict * bad import due to merge conflict * missing loading in remove password component for browser extension * error handling in remove password component * organization observable race condition in key-connector * usesKeyConnector always returns boolean * unit test coverage * key connector reactive * reactive key connector service * introducing convertAccountRequired$ * cli build fix * moving message sending side effect to sync * key connector service unit tests * fix unit tests * unit tests in wrong place after KM code ownership move * infinite page reload * failing unit tests * failing unit tests --------- Co-authored-by: Todd Martin --- .../remove-password.component.html | 16 +- apps/cli/src/auth/commands/unlock.command.ts | 5 +- apps/cli/src/base-program.ts | 1 - .../convert-to-key-connector.command.spec.ts | 140 ++++++++ .../convert-to-key-connector.command.ts | 12 +- .../src/models/response/message.response.ts | 4 +- apps/cli/src/oss-serve-configurator.ts | 1 - apps/cli/src/program.ts | 1 - .../remove-password.component.html | 14 +- .../src/auth/guards/auth.guard.spec.ts | 6 +- libs/angular/src/auth/guards/auth.guard.ts | 2 +- .../abstractions/key-connector.service.ts | 29 +- .../services/key-connector.service.spec.ts | 338 +++++++++++++----- .../services/key-connector.service.ts | 104 +++--- .../src/platform/sync/default-sync.service.ts | 7 +- .../remove-password.component.spec.ts | 252 +++++++++++++ .../remove-password.component.ts | 95 +++-- 17 files changed, 782 insertions(+), 245 deletions(-) create mode 100644 apps/cli/src/key-management/convert-to-key-connector.command.spec.ts create mode 100644 libs/key-management-ui/src/key-connector/remove-password.component.spec.ts diff --git a/apps/browser/src/key-management/key-connector/remove-password.component.html b/apps/browser/src/key-management/key-connector/remove-password.component.html index 793bcff3e09..272f727a7bb 100644 --- a/apps/browser/src/key-management/key-connector/remove-password.component.html +++ b/apps/browser/src/key-management/key-connector/remove-password.component.html @@ -8,17 +8,17 @@
-
+
+
+ +
+
+

{{ "convertOrganizationEncryptionDesc" | i18n: organization.name }}

- - diff --git a/libs/angular/src/auth/guards/auth.guard.spec.ts b/libs/angular/src/auth/guards/auth.guard.spec.ts index 4ed72baf284..db08553749a 100644 --- a/libs/angular/src/auth/guards/auth.guard.spec.ts +++ b/libs/angular/src/auth/guards/auth.guard.spec.ts @@ -2,7 +2,7 @@ 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 { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec"; import { @@ -30,9 +30,7 @@ describe("AuthGuard", () => { authService.getAuthStatus.mockResolvedValue(authStatus); const messagingService: MockProxy = mock(); const keyConnectorService: MockProxy = mock(); - keyConnectorService.getConvertAccountRequired.mockResolvedValue( - keyConnectorServiceRequiresAccountConversion, - ); + keyConnectorService.convertAccountRequired$ = of(keyConnectorServiceRequiresAccountConversion); const accountService: MockProxy = mock(); const activeAccountSubject = new BehaviorSubject(null); accountService.activeAccount$ = activeAccountSubject; diff --git a/libs/angular/src/auth/guards/auth.guard.ts b/libs/angular/src/auth/guards/auth.guard.ts index 329e365e542..690db37d090 100644 --- a/libs/angular/src/auth/guards/auth.guard.ts +++ b/libs/angular/src/auth/guards/auth.guard.ts @@ -47,7 +47,7 @@ export const authGuard: CanActivateFn = async ( if ( !routerState.url.includes("remove-password") && - (await keyConnectorService.getConvertAccountRequired()) + (await firstValueFrom(keyConnectorService.convertAccountRequired$)) ) { return router.createUrlTree(["/remove-password"]); } diff --git a/libs/common/src/key-management/key-connector/abstractions/key-connector.service.ts b/libs/common/src/key-management/key-connector/abstractions/key-connector.service.ts index b88f7bc7cd6..131b941f274 100644 --- a/libs/common/src/key-management/key-connector/abstractions/key-connector.service.ts +++ b/libs/common/src/key-management/key-connector/abstractions/key-connector.service.ts @@ -1,22 +1,25 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore +import { Observable } from "rxjs"; + import { Organization } from "../../../admin-console/models/domain/organization"; import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response"; import { UserId } from "../../../types/guid"; export abstract class KeyConnectorService { - setMasterKeyFromUrl: (url: string, userId: UserId) => Promise; - getManagingOrganization: (userId?: UserId) => Promise; - getUsesKeyConnector: (userId: UserId) => Promise; - migrateUser: (userId?: UserId) => Promise; - userNeedsMigration: (userId: UserId) => Promise; - convertNewSsoUserToKeyConnector: ( + abstract setMasterKeyFromUrl(url: string, userId: UserId): Promise; + + abstract getManagingOrganization(userId: UserId): Promise; + + abstract getUsesKeyConnector(userId: UserId): Promise; + + abstract migrateUser(userId: UserId): Promise; + + abstract convertNewSsoUserToKeyConnector( tokenResponse: IdentityTokenResponse, orgId: string, userId: UserId, - ) => Promise; - setUsesKeyConnector: (enabled: boolean, userId: UserId) => Promise; - setConvertAccountRequired: (status: boolean, userId?: UserId) => Promise; - getConvertAccountRequired: () => Promise; - removeConvertAccountRequired: (userId?: UserId) => Promise; + ): Promise; + + abstract setUsesKeyConnector(enabled: boolean, userId: UserId): Promise; + + abstract convertAccountRequired$: Observable; } diff --git a/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts b/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts index eff3fc7f47c..a2e994ad9e4 100644 --- a/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts +++ b/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts @@ -1,7 +1,8 @@ import { mock } from "jest-mock-extended"; -import { of } from "rxjs"; +import { firstValueFrom, of, timeout, TimeoutError } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { KeyService } from "@bitwarden/key-management"; import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; @@ -20,11 +21,7 @@ import { MasterKey } from "../../../types/key"; import { FakeMasterPasswordService } from "../../master-password/services/fake-master-password.service"; import { KeyConnectorUserKeyRequest } from "../models/key-connector-user-key.request"; -import { - USES_KEY_CONNECTOR, - CONVERT_ACCOUNT_TO_KEY_CONNECTOR, - KeyConnectorService, -} from "./key-connector.service"; +import { USES_KEY_CONNECTOR, KeyConnectorService } from "./key-connector.service"; describe("KeyConnectorService", () => { let keyConnectorService: KeyConnectorService; @@ -73,32 +70,87 @@ describe("KeyConnectorService", () => { expect(keyConnectorService).not.toBeFalsy(); }); - describe("setUsesKeyConnector()", () => { - it("should update the usesKeyConnectorState with the provided value", async () => { - const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); + describe("setUsesKeyConnector", () => { + it("should update the usesKeyConnectorState with the provided false value", async () => { + const state = stateProvider.singleUser.getFake(mockUserId, USES_KEY_CONNECTOR); state.nextState(false); - const newValue = true; + await keyConnectorService.setUsesKeyConnector(true, mockUserId); - await keyConnectorService.setUsesKeyConnector(newValue, mockUserId); + expect(await firstValueFrom(state.state$)).toBe(true); + }); - expect(await keyConnectorService.getUsesKeyConnector(mockUserId)).toBe(newValue); + it("should update the usesKeyConnectorState with the provided true value", async () => { + const state = stateProvider.singleUser.getFake(mockUserId, USES_KEY_CONNECTOR); + state.nextState(true); + + await keyConnectorService.setUsesKeyConnector(false, mockUserId); + + expect(await firstValueFrom(state.state$)).toBe(false); }); }); - describe("getManagingOrganization()", () => { + describe("getUsesKeyConnector", () => { + it("should return false when uses key connector state is not set", async () => { + const state = stateProvider.singleUser.getFake(mockUserId, USES_KEY_CONNECTOR); + state.nextState(null); + + const usesKeyConnector = await keyConnectorService.getUsesKeyConnector(mockUserId); + + expect(usesKeyConnector).toEqual(false); + }); + + it("should return false when uses key connector state is set to false", async () => { + stateProvider.getUserState$(USES_KEY_CONNECTOR, mockUserId); + const state = stateProvider.singleUser.getFake(mockUserId, USES_KEY_CONNECTOR); + state.nextState(false); + + const usesKeyConnector = await keyConnectorService.getUsesKeyConnector(mockUserId); + + expect(usesKeyConnector).toEqual(false); + }); + + it("should return true when uses key connector state is set to true", async () => { + const state = stateProvider.singleUser.getFake(mockUserId, USES_KEY_CONNECTOR); + state.nextState(true); + + const usesKeyConnector = await keyConnectorService.getUsesKeyConnector(mockUserId); + + expect(usesKeyConnector).toEqual(true); + }); + }); + + describe("getManagingOrganization", () => { it("should return the managing organization with key connector enabled", async () => { // Arrange const orgs = [ - organizationData(true, true, "https://key-connector-url.com", 2, false), - organizationData(false, true, "https://key-connector-url.com", 2, false), - organizationData(true, false, "https://key-connector-url.com", 2, false), - organizationData(true, true, "https://other-url.com", 2, false), + organizationData( + true, + true, + "https://key-connector-url.com", + OrganizationUserType.User, + false, + ), + organizationData( + false, + true, + "https://key-connector-url.com", + OrganizationUserType.User, + false, + ), + organizationData( + true, + false, + "https://key-connector-url.com", + OrganizationUserType.User, + false, + ), + organizationData(true, true, "https://other-url.com", OrganizationUserType.User, false), ]; organizationService.organizations$.mockReturnValue(of(orgs)); // Act - const result = await keyConnectorService.getManagingOrganization(); + const result = await keyConnectorService.getManagingOrganization(mockUserId); // Assert expect(result).toEqual(orgs[0]); @@ -107,13 +159,25 @@ describe("KeyConnectorService", () => { it("should return undefined if no managing organization with key connector enabled is found", async () => { // Arrange const orgs = [ - organizationData(true, false, "https://key-connector-url.com", 2, false), - organizationData(false, false, "https://key-connector-url.com", 2, false), + organizationData( + true, + false, + "https://key-connector-url.com", + OrganizationUserType.User, + false, + ), + organizationData( + false, + false, + "https://key-connector-url.com", + OrganizationUserType.User, + false, + ), ]; organizationService.organizations$.mockReturnValue(of(orgs)); // Act - const result = await keyConnectorService.getManagingOrganization(); + const result = await keyConnectorService.getManagingOrganization(mockUserId); // Assert expect(result).toBeUndefined(); @@ -128,7 +192,7 @@ describe("KeyConnectorService", () => { organizationService.organizations$.mockReturnValue(of(orgs)); // Act - const result = await keyConnectorService.getManagingOrganization(); + const result = await keyConnectorService.getManagingOrganization(mockUserId); // Assert expect(result).toBeUndefined(); @@ -137,74 +201,31 @@ describe("KeyConnectorService", () => { it("should return undefined if user is a Provider", async () => { // Arrange const orgs = [ - organizationData(true, true, "https://key-connector-url.com", 2, true), - organizationData(false, true, "https://key-connector-url.com", 2, true), + organizationData( + true, + true, + "https://key-connector-url.com", + OrganizationUserType.User, + true, + ), + organizationData( + false, + true, + "https://key-connector-url.com", + OrganizationUserType.User, + true, + ), ]; organizationService.organizations$.mockReturnValue(of(orgs)); // Act - const result = await keyConnectorService.getManagingOrganization(); + const result = await keyConnectorService.getManagingOrganization(mockUserId); // Assert expect(result).toBeUndefined(); }); }); - describe("setConvertAccountRequired()", () => { - it("should update the convertAccountToKeyConnectorState with the provided value", async () => { - const state = stateProvider.activeUser.getFake(CONVERT_ACCOUNT_TO_KEY_CONNECTOR); - state.nextState(false); - - const newValue = true; - - await keyConnectorService.setConvertAccountRequired(newValue); - - expect(await keyConnectorService.getConvertAccountRequired()).toBe(newValue); - }); - - it("should remove the convertAccountToKeyConnectorState", async () => { - const state = stateProvider.activeUser.getFake(CONVERT_ACCOUNT_TO_KEY_CONNECTOR); - state.nextState(false); - - const newValue: boolean = null; - - await keyConnectorService.setConvertAccountRequired(newValue); - - expect(await keyConnectorService.getConvertAccountRequired()).toBe(newValue); - }); - }); - - describe("userNeedsMigration()", () => { - it("should return true if the user needs migration", async () => { - // token - tokenService.getIsExternal.mockResolvedValue(true); - - // create organization object - const data = organizationData(true, true, "https://key-connector-url.com", 2, false); - organizationService.organizations$.mockReturnValue(of([data])); - - // uses KeyConnector - const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); - state.nextState(false); - - const result = await keyConnectorService.userNeedsMigration(mockUserId); - - expect(result).toBe(true); - }); - - it("should return false if the user does not need migration", async () => { - tokenService.getIsExternal.mockResolvedValue(false); - const data = organizationData(false, false, "https://key-connector-url.com", 2, false); - organizationService.organizations$.mockReturnValue(of([data])); - - const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); - state.nextState(true); - const result = await keyConnectorService.userNeedsMigration(mockUserId); - - expect(result).toBe(false); - }); - }); - describe("setMasterKeyFromUrl", () => { it("should set the master key from the provided URL", async () => { // Arrange @@ -221,10 +242,7 @@ describe("KeyConnectorService", () => { // Assert expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url); - expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( - masterKey, - expect.any(String), - ); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(masterKey, mockUserId); }); it("should handle errors thrown during the process", async () => { @@ -246,10 +264,16 @@ describe("KeyConnectorService", () => { }); }); - describe("migrateUser()", () => { + describe("migrateUser", () => { it("should migrate the user to the key connector", async () => { // Arrange - const organization = organizationData(true, true, "https://key-connector-url.com", 2, false); + const organization = organizationData( + true, + true, + "https://key-connector-url.com", + OrganizationUserType.User, + false, + ); const masterKey = getMockMasterKey(); masterPasswordService.masterKeySubject.next(masterKey); const keyConnectorRequest = new KeyConnectorUserKeyRequest( @@ -260,7 +284,7 @@ describe("KeyConnectorService", () => { jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue(); // Act - await keyConnectorService.migrateUser(); + await keyConnectorService.migrateUser(mockUserId); // Assert expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); @@ -273,7 +297,13 @@ describe("KeyConnectorService", () => { it("should handle errors thrown during migration", async () => { // Arrange - const organization = organizationData(true, true, "https://key-connector-url.com", 2, false); + const organization = organizationData( + true, + true, + "https://key-connector-url.com", + OrganizationUserType.User, + false, + ); const masterKey = getMockMasterKey(); const keyConnectorRequest = new KeyConnectorUserKeyRequest( Utils.fromBufferToB64(masterKey.inner().encryptionKey), @@ -288,7 +318,7 @@ describe("KeyConnectorService", () => { try { // Act - await keyConnectorService.migrateUser(); + await keyConnectorService.migrateUser(mockUserId); } catch { // Assert expect(logService.error).toHaveBeenCalledWith(error); @@ -301,6 +331,136 @@ describe("KeyConnectorService", () => { }); }); + describe("convertAccountRequired$", () => { + beforeEach(async () => { + const organization = organizationData( + true, + true, + "https://key-connector-url.com", + OrganizationUserType.User, + false, + ); + organizationService.organizations$.mockReturnValue(of([organization])); + await stateProvider.getUser(mockUserId, USES_KEY_CONNECTOR).update(() => false); + tokenService.getIsExternal.mockResolvedValue(true); + tokenService.hasAccessToken$.mockReturnValue(of(true)); + }); + + it("should return true when user logged in with sso, belong to organization using key connector and user does not use key connector", async () => { + await expect( + firstValueFrom(keyConnectorService.convertAccountRequired$.pipe(timeout(100))), + ).resolves.toEqual(true); + }); + + it("should return false when user logged in with password", async () => { + tokenService.getIsExternal.mockResolvedValue(false); + + await expect( + firstValueFrom(keyConnectorService.convertAccountRequired$.pipe(timeout(100))), + ).resolves.toEqual(false); + }); + + it("should return false when organization's key connector disabled", async () => { + const organization = organizationData( + true, + false, + "https://key-connector-url.com", + OrganizationUserType.User, + false, + ); + organizationService.organizations$.mockReturnValue(of([organization])); + + await expect( + firstValueFrom(keyConnectorService.convertAccountRequired$.pipe(timeout(100))), + ).resolves.toEqual(false); + }); + + it("should return false when user is admin of the organization", async () => { + const organization = organizationData( + true, + true, + "https://key-connector-url.com", + OrganizationUserType.Admin, + false, + ); + organizationService.organizations$.mockReturnValue(of([organization])); + + await expect( + firstValueFrom(keyConnectorService.convertAccountRequired$.pipe(timeout(100))), + ).resolves.toEqual(false); + }); + + it("should return false when user is owner of the organization", async () => { + const organization = organizationData( + true, + true, + "https://key-connector-url.com", + OrganizationUserType.Owner, + false, + ); + organizationService.organizations$.mockReturnValue(of([organization])); + + await expect( + firstValueFrom(keyConnectorService.convertAccountRequired$.pipe(timeout(100))), + ).resolves.toEqual(false); + }); + + it("should return false when user is provider user of the organization", async () => { + const organization = organizationData( + true, + true, + "https://key-connector-url.com", + OrganizationUserType.User, + true, + ); + organizationService.organizations$.mockReturnValue(of([organization])); + + await expect( + firstValueFrom(keyConnectorService.convertAccountRequired$.pipe(timeout(100))), + ).resolves.toEqual(false); + }); + + it("should return false when user already uses key connector", async () => { + await stateProvider.getUser(mockUserId, USES_KEY_CONNECTOR).update(() => true); + + await expect( + firstValueFrom(keyConnectorService.convertAccountRequired$.pipe(timeout(100))), + ).resolves.toEqual(false); + }); + + it("should not return any value when user not logged in", async () => { + await accountService.switchAccount(null as unknown as UserId); + + await expect( + firstValueFrom(keyConnectorService.convertAccountRequired$.pipe(timeout(100))), + ).rejects.toBeInstanceOf(TimeoutError); + }); + + it("should not return any value when organization state is empty", async () => { + organizationService.organizations$.mockReturnValue(of(null as unknown as Organization[])); + + await expect( + firstValueFrom(keyConnectorService.convertAccountRequired$.pipe(timeout(100))), + ).rejects.toBeInstanceOf(TimeoutError); + }); + + it("should not return any value when user is not using key connector", async () => { + await stateProvider.getUser(mockUserId, USES_KEY_CONNECTOR).update(() => null); + + await expect( + firstValueFrom(keyConnectorService.convertAccountRequired$.pipe(timeout(100))), + ).rejects.toBeInstanceOf(TimeoutError); + }); + + it("should not return any value when user does not have access token", async () => { + tokenService.hasAccessToken$.mockReturnValue(of(false)); + + await expect( + firstValueFrom(keyConnectorService.convertAccountRequired$.pipe(timeout(100))), + ).rejects.toBeInstanceOf(TimeoutError); + }); + }); + function organizationData( usesKeyConnector: boolean, keyConnectorEnabled: boolean, diff --git a/libs/common/src/key-management/key-connector/services/key-connector.service.ts b/libs/common/src/key-management/key-connector/services/key-connector.service.ts index 9799f06f64a..744b7d6608c 100644 --- a/libs/common/src/key-management/key-connector/services/key-connector.service.ts +++ b/libs/common/src/key-management/key-connector/services/key-connector.service.ts @@ -1,8 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { combineLatest, filter, firstValueFrom, Observable, of, switchMap } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { Argon2KdfConfig, KdfConfig, @@ -15,7 +16,6 @@ import { ApiService } from "../../../abstractions/api.service"; import { OrganizationService } from "../../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "../../../admin-console/enums"; import { Organization } from "../../../admin-console/models/domain/organization"; -import { AccountService } from "../../../auth/abstractions/account.service"; import { TokenService } from "../../../auth/abstractions/token.service"; import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response"; import { KeysRequest } from "../../../models/request/keys.request"; @@ -23,12 +23,7 @@ import { KeyGenerationService } from "../../../platform/abstractions/key-generat import { LogService } from "../../../platform/abstractions/log.service"; import { Utils } from "../../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; -import { - ActiveUserState, - KEY_CONNECTOR_DISK, - StateProvider, - UserKeyDefinition, -} from "../../../platform/state"; +import { KEY_CONNECTOR_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { MasterKey } from "../../../types/key"; import { InternalMasterPasswordServiceAbstraction } from "../../master-password/abstractions/master-password.service.abstraction"; @@ -42,23 +37,15 @@ export const USES_KEY_CONNECTOR = new UserKeyDefinition( { deserializer: (usesKeyConnector) => usesKeyConnector, clearOn: ["logout"], - }, -); - -export const CONVERT_ACCOUNT_TO_KEY_CONNECTOR = new UserKeyDefinition( - KEY_CONNECTOR_DISK, - "convertAccountToKeyConnector", - { - deserializer: (convertAccountToKeyConnector) => convertAccountToKeyConnector, - clearOn: ["logout"], + cleanupDelayMs: 0, }, ); export class KeyConnectorService implements KeyConnectorServiceAbstraction { - private usesKeyConnectorState: ActiveUserState; - private convertAccountToKeyConnectorState: ActiveUserState; + readonly convertAccountRequired$: Observable; + constructor( - private accountService: AccountService, + accountService: AccountService, private masterPasswordService: InternalMasterPasswordServiceAbstraction, private keyService: KeyService, private apiService: ApiService, @@ -69,9 +56,27 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { private logoutCallback: (logoutReason: LogoutReason, userId?: string) => Promise, private stateProvider: StateProvider, ) { - this.usesKeyConnectorState = this.stateProvider.getActive(USES_KEY_CONNECTOR); - this.convertAccountToKeyConnectorState = this.stateProvider.getActive( - CONVERT_ACCOUNT_TO_KEY_CONNECTOR, + this.convertAccountRequired$ = accountService.activeAccount$.pipe( + filter((account) => account != null), + switchMap((account) => + combineLatest([ + of(account.id), + this.organizationService + .organizations$(account.id) + .pipe(filter((organizations) => organizations != null)), + this.stateProvider + .getUserState$(USES_KEY_CONNECTOR, account.id) + .pipe(filter((usesKeyConnector) => usesKeyConnector != null)), + tokenService.hasAccessToken$(account.id).pipe(filter((hasToken) => hasToken)), + ]), + ), + switchMap(async ([userId, organizations, usesKeyConnector]) => { + const loggedInUsingSso = await this.tokenService.getIsExternal(userId); + const requiredByOrganization = this.findManagingOrganization(organizations) != null; + const userIsNotUsingKeyConnector = !usesKeyConnector; + + return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector; + }), ); } @@ -79,20 +84,13 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { await this.stateProvider.getUser(userId, USES_KEY_CONNECTOR).update(() => usesKeyConnector); } - getUsesKeyConnector(userId: UserId): Promise { - return firstValueFrom(this.stateProvider.getUserState$(USES_KEY_CONNECTOR, userId)); + async getUsesKeyConnector(userId: UserId): Promise { + return ( + (await firstValueFrom(this.stateProvider.getUserState$(USES_KEY_CONNECTOR, userId))) ?? false + ); } - async userNeedsMigration(userId: UserId) { - const loggedInUsingSso = await this.tokenService.getIsExternal(userId); - const requiredByOrganization = (await this.getManagingOrganization(userId)) != null; - const userIsNotUsingKeyConnector = !(await this.getUsesKeyConnector(userId)); - - return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector; - } - - async migrateUser(userId?: UserId) { - userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; + async migrateUser(userId: UserId) { const organization = await this.getManagingOrganization(userId); const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); const keyConnectorRequest = new KeyConnectorUserKeyRequest( @@ -109,6 +107,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } await this.apiService.postConvertToKeyConnector(); + + await this.setUsesKeyConnector(true, userId); } // TODO: UserKey should be renamed to MasterKey and typed accordingly @@ -123,15 +123,9 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } } - async getManagingOrganization(userId?: UserId): Promise { - const orgs = await firstValueFrom(this.organizationService.organizations$(userId)); - return orgs.find( - (o) => - o.keyConnectorEnabled && - o.type !== OrganizationUserType.Admin && - o.type !== OrganizationUserType.Owner && - !o.isProviderUser, - ); + async getManagingOrganization(userId: UserId): Promise { + const organizations = await firstValueFrom(this.organizationService.organizations$(userId)); + return this.findManagingOrganization(organizations); } async convertNewSsoUserToKeyConnector( @@ -188,18 +182,6 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { await this.apiService.postSetKeyConnectorKey(setPasswordRequest); } - async setConvertAccountRequired(status: boolean, userId?: UserId) { - await this.stateProvider.setUserState(CONVERT_ACCOUNT_TO_KEY_CONNECTOR, status, userId); - } - - getConvertAccountRequired(): Promise { - return firstValueFrom(this.convertAccountToKeyConnectorState.state$); - } - - async removeConvertAccountRequired(userId?: UserId) { - await this.setConvertAccountRequired(null, userId); - } - private handleKeyConnectorError(e: any) { this.logService.error(e); if (this.logoutCallback != null) { @@ -209,4 +191,14 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } throw new Error("Key Connector error"); } + + private findManagingOrganization(organizations: Organization[]) { + return organizations.find( + (o) => + o.keyConnectorEnabled && + o.type !== OrganizationUserType.Admin && + o.type !== OrganizationUserType.Owner && + !o.isProviderUser, + ); + } } diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index faf54f11912..e9f6c60af64 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -224,13 +224,8 @@ export class DefaultSyncService extends CoreSyncService { await this.syncProfileOrganizations(response, response.id); - if (await this.keyConnectorService.userNeedsMigration(response.id)) { - await this.keyConnectorService.setConvertAccountRequired(true, response.id); + if (await firstValueFrom(this.keyConnectorService.convertAccountRequired$)) { this.messageSender.send("convertAccountToKeyConnector"); - } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.keyConnectorService.removeConvertAccountRequired(response.id); } } diff --git a/libs/key-management-ui/src/key-connector/remove-password.component.spec.ts b/libs/key-management-ui/src/key-connector/remove-password.component.spec.ts new file mode 100644 index 00000000000..a3ab70a6184 --- /dev/null +++ b/libs/key-management-ui/src/key-connector/remove-password.component.spec.ts @@ -0,0 +1,252 @@ +import { Router } from "@angular/router"; +import { mock } from "jest-mock-extended"; + +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogService, ToastService } from "@bitwarden/components"; + +import { mockAccountServiceWith } from "../../../common/spec"; + +import { RemovePasswordComponent } from "./remove-password.component"; + +describe("RemovePasswordComponent", () => { + let component: RemovePasswordComponent; + + const userId = "test-user-id" as UserId; + const organization = { + id: "test-organization-id", + name: "test-organization-name", + } as Organization; + + const accountService = mockAccountServiceWith(userId); + + const mockRouter = mock(); + const mockSyncService = mock(); + const mockI18nService = mock(); + const mockKeyConnectorService = mock(); + const mockOrganizationApiService = mock(); + const mockDialogService = mock(); + const mockToastService = mock(); + + beforeEach(async () => { + jest.clearAllMocks(); + + await accountService.switchAccount(userId); + + component = new RemovePasswordComponent( + mock(), + mockRouter, + accountService, + mockSyncService, + mockI18nService, + mockKeyConnectorService, + mockOrganizationApiService, + mockDialogService, + mockToastService, + ); + }); + + describe("ngOnInit", () => { + it("should set activeUserId and organization", async () => { + mockKeyConnectorService.getManagingOrganization.mockResolvedValue(organization); + + await component.ngOnInit(); + + expect(component["activeUserId"]).toBe("test-user-id"); + expect(component.organization).toEqual(organization); + expect(component.loading).toEqual(false); + + expect(mockKeyConnectorService.getManagingOrganization).toHaveBeenCalledWith(userId); + expect(mockSyncService.fullSync).toHaveBeenCalledWith(false); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + + it("should redirect to login when no active account is found", async () => { + await accountService.switchAccount(null as unknown as UserId); + + await component.ngOnInit(); + + expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]); + }); + + it("should redirect to login when no organization is found", async () => { + mockKeyConnectorService.getManagingOrganization.mockResolvedValue( + null as unknown as Organization, + ); + + await component.ngOnInit(); + + expect(mockKeyConnectorService.getManagingOrganization).toHaveBeenCalledWith(userId); + expect(mockSyncService.fullSync).toHaveBeenCalledWith(false); + expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]); + }); + }); + + describe("get action", () => { + it.each([ + [true, false], + [false, true], + [true, true], + ])( + "should return true when continuing is $continuing and leaving is $leaving", + (continuing, leaving) => { + component.continuing = continuing; + component.leaving = leaving; + + expect(component.action).toBe(true); + }, + ); + + it("should return false when continuing and leaving are both false", () => { + component.continuing = false; + component.leaving = false; + + expect(component.action).toBe(false); + }); + }); + + describe("convert", () => { + beforeEach(async () => { + mockKeyConnectorService.getManagingOrganization.mockResolvedValue(organization); + + await component.ngOnInit(); + }); + + it("should call migrateUser and show success toast", async () => { + mockI18nService.t.mockReturnValue("removed master password"); + + await component.convert(); + + expect(component.continuing).toBe(true); + expect(mockKeyConnectorService.migrateUser).toHaveBeenCalledWith(userId); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "success", + message: "removed master password", + }); + expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]); + }); + + it("should handle errors and show error toast", async () => { + const errorMessage = "Can't migrate user error"; + mockKeyConnectorService.migrateUser.mockRejectedValue(new Error(errorMessage)); + mockI18nService.t.mockReturnValue("error occurred"); + + await component.convert(); + + expect(component.continuing).toBe(false); + expect(mockKeyConnectorService.migrateUser).toHaveBeenCalledWith(userId); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "error occurred", + message: errorMessage, + }); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + + it("should handle error response and show error toast", async () => { + const errorMessage = "Can't migrate user error"; + mockKeyConnectorService.migrateUser.mockRejectedValue( + new ErrorResponse( + { + message: errorMessage, + }, + 404, + ), + ); + mockI18nService.t.mockReturnValue("error occurred"); + + await component.convert(); + + expect(component.continuing).toBe(false); + expect(mockKeyConnectorService.migrateUser).toHaveBeenCalledWith(userId); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "error occurred", + message: errorMessage, + }); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + }); + + describe("leave", () => { + beforeEach(async () => { + mockKeyConnectorService.getManagingOrganization.mockResolvedValue(organization); + + await component.ngOnInit(); + }); + + it("should call leave and show success toast", async () => { + mockDialogService.openSimpleDialog.mockResolvedValue(true); + mockI18nService.t.mockReturnValue("left organization"); + + await component.leave(); + + expect(component.leaving).toBe(true); + expect(mockOrganizationApiService.leave).toHaveBeenCalledWith(organization.id); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "success", + message: "left organization", + }); + expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]); + }); + + it("should handle error response and show error toast", async () => { + const errorMessage = "Can't leave organization error"; + mockDialogService.openSimpleDialog.mockResolvedValue(true); + mockOrganizationApiService.leave.mockRejectedValue(new Error(errorMessage)); + mockI18nService.t.mockReturnValue("error occurred"); + + await component.leave(); + + expect(component.leaving).toBe(false); + expect(mockOrganizationApiService.leave).toHaveBeenCalledWith(organization.id); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "error occurred", + message: errorMessage, + }); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + + it("should handle error response and show error toast", async () => { + const errorMessage = "Can't leave organization error"; + mockDialogService.openSimpleDialog.mockResolvedValue(true); + mockOrganizationApiService.leave.mockRejectedValue( + new ErrorResponse( + { + message: errorMessage, + }, + 404, + ), + ); + mockI18nService.t.mockReturnValue("error occurred"); + + await component.leave(); + + expect(component.leaving).toBe(false); + expect(mockOrganizationApiService.leave).toHaveBeenCalledWith(organization.id); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "error occurred", + message: errorMessage, + }); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + + it("should not call leave when dialog is canceled", async () => { + mockDialogService.openSimpleDialog.mockResolvedValue(false); + + await component.leave(); + + expect(component.leaving).toBe(false); + expect(mockOrganizationApiService.leave).not.toHaveBeenCalled(); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/key-management-ui/src/key-connector/remove-password.component.ts b/libs/key-management-ui/src/key-connector/remove-password.component.ts index d68644f588e..967e34223d1 100644 --- a/libs/key-management-ui/src/key-connector/remove-password.component.ts +++ b/libs/key-management-ui/src/key-connector/remove-password.component.ts @@ -1,33 +1,32 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Directive, OnInit } from "@angular/core"; import { Router } from "@angular/router"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; @Directive() export class RemovePasswordComponent implements OnInit { - actionPromise: Promise; continuing = false; leaving = false; loading = true; - organization: Organization; - email: string; + organization!: Organization; + private activeUserId!: UserId; constructor( + private logService: LogService, private router: Router, private accountService: AccountService, private syncService: SyncService, - private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private keyConnectorService: KeyConnectorService, private organizationApiService: OrganizationApiServiceAbstraction, @@ -36,35 +35,49 @@ export class RemovePasswordComponent implements OnInit { ) {} async ngOnInit() { - this.organization = await this.keyConnectorService.getManagingOrganization(); - this.email = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.email)), - ); + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + if (activeAccount == null) { + this.logService.info( + "[Key Connector remove password] No active account found, redirecting to login.", + ); + await this.router.navigate(["/"]); + return; + } + this.activeUserId = activeAccount.id; + await this.syncService.fullSync(false); + + this.organization = await this.keyConnectorService.getManagingOrganization(this.activeUserId); + if (this.organization == null) { + this.logService.info( + "[Key Connector remove password] No organization found, redirecting to login.", + ); + await this.router.navigate(["/"]); + return; + } this.loading = false; } + get action() { + return this.continuing || this.leaving; + } + convert = async () => { this.continuing = true; - this.actionPromise = this.keyConnectorService.migrateUser(); try { - await this.actionPromise; + await this.keyConnectorService.migrateUser(this.activeUserId); + this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t("removedMasterPassword"), }); - await this.keyConnectorService.removeConvertAccountRequired(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([""]); + + await this.router.navigate(["/"]); } catch (e) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: e.message, - }); + this.continuing = false; + + this.handleActionError(e); } }; @@ -79,25 +92,33 @@ export class RemovePasswordComponent implements OnInit { return false; } + this.leaving = true; try { - this.leaving = true; - this.actionPromise = this.organizationApiService.leave(this.organization.id); - await this.actionPromise; + await this.organizationApiService.leave(this.organization.id); + this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t("leftOrganization"), }); - await this.keyConnectorService.removeConvertAccountRequired(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([""]); + + await this.router.navigate(["/"]); } catch (e) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: e, - }); + this.leaving = false; + + this.handleActionError(e); } }; + + handleActionError(e: unknown) { + let message = ""; + if (e instanceof ErrorResponse || e instanceof Error) { + message = e.message; + } + + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: message, + }); + } } From a82eb8c22bcc4045b7337b761fd3eecf8613c3e6 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 19 May 2025 10:40:05 +0000 Subject: [PATCH 094/163] Bumped client version(s) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- apps/cli/package.json | 2 +- apps/web/package.json | 2 +- package-lock.json | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index 6237d91d4db..c44743add7c 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2025.5.0", + "version": "2025.5.1", "scripts": { "build": "npm run build:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 97b94e06fec..1c89916c6f7 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2025.5.0", + "version": "2025.5.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 0f44b4af26d..834ac8d8b7d 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2025.5.0", + "version": "2025.5.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/cli/package.json b/apps/cli/package.json index b01c96b23d1..18147cf4d2d 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2025.4.0", + "version": "2025.5.0", "keywords": [ "bitwarden", "password", diff --git a/apps/web/package.json b/apps/web/package.json index 3884ed37e0d..6df2974129e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2025.5.0", + "version": "2025.5.1", "scripts": { "build:oss": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", "build:bit": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index 96cd9c0444a..c9b33a0df45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -190,11 +190,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2025.5.0" + "version": "2025.5.1" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2025.4.0", + "version": "2025.5.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.1.0", @@ -245,7 +245,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2025.5.0" + "version": "2025.5.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From ef592bf23a5a68cb39d42c23c7cf562f516d2a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Mon, 19 May 2025 13:17:12 +0100 Subject: [PATCH 095/163] [PM-20050] Readonly members page for users with Account Recovery permission only (#14647) * Add SeparateCustomRolePermissions feature flag * Allow 'Manage Account Recovery' to be configured separately from 'Manage Users' * Add showUserManagementControls$ observable to show/hide user management controls based on user permissions * Update user management controls visibility to be dependant on user permissions. --- .../members/members.component.html | 387 +++++++++++------- .../members/members.component.ts | 15 + 2 files changed, 251 insertions(+), 151 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index 2162e33081f..610821cfd1b 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -5,7 +5,14 @@ [placeholder]="'searchMembers' | i18n" > - @@ -16,6 +23,7 @@ [selected]="status" (selectedChange)="statusToggle.next($event)" [attr.aria-label]="'memberStatusFilter' | i18n" + *ngIf="showUserManagementControls$ | async" > {{ "all" | i18n }} @@ -71,7 +79,7 @@ - + @@ -174,74 +183,143 @@ alignContent="middle" [ngClass]="rowHeightClass" > - + - -
- -
-
- - - {{ "invited" | i18n }} - - - {{ "needsConfirmation" | i18n }} - - - {{ "revoked" | i18n }} - -
-
- {{ u.email }} + + +
+ +
+
+ + + {{ "invited" | i18n }} + + + {{ "needsConfirmation" | i18n }} + + + {{ "revoked" | i18n }} + +
+
+ {{ u.email }} +
-
- + + + + +
+ +
+
+ {{ u.name ?? u.email }} + + {{ "invited" | i18n }} + + + {{ "needsConfirmation" | i18n }} + + + {{ "revoked" | i18n }} + +
+
+ {{ u.email }} +
+
+
+ +
- - - + + + + + + + + + + - - {{ u.type | userType }} - + + + {{ u.type | userType }} + + + + + {{ u.type | userType }} + + @@ -271,54 +349,58 @@ > - - - - - - - - + + + + + + + + + + + + - - - - + + + + + + + diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 834aa2c7111..e5a94bc4b4f 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -46,7 +46,9 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -102,6 +104,7 @@ export class MembersComponent extends BaseMembersComponent orgIsOnSecretsManagerStandalone = false; protected canUseSecretsManager$: Observable; + protected showUserManagementControls$: Observable; // Fixed sizes used for cdkVirtualScroll protected rowHeight = 69; @@ -135,6 +138,7 @@ export class MembersComponent extends BaseMembersComponent private collectionService: CollectionService, private billingApiService: BillingApiServiceAbstraction, protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService, + private configService: ConfigService, ) { super( apiService, @@ -229,6 +233,17 @@ export class MembersComponent extends BaseMembersComponent takeUntilDestroyed(), ) .subscribe(); + + // Setup feature flag-dependent observables + const separateCustomRolePermissionsEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.SeparateCustomRolePermissions, + ); + this.showUserManagementControls$ = separateCustomRolePermissionsEnabled$.pipe( + map( + (separateCustomRolePermissionsEnabled) => + !separateCustomRolePermissionsEnabled || this.organization.canManageUsers, + ), + ); } async getUsers(): Promise { From 239556b55f666ff887c7b2bc1d418e852b5434f7 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Mon, 19 May 2025 14:58:51 +0200 Subject: [PATCH 096/163] [PM-18017] Show key connector domain in remove password page (#14695) * Passed in userId on RemovePasswordComponent. * Added userId on other references to KeyConnectorService methods * remove password component refactor, test coverage, enabled strict * explicit user id provided to key connector service * redirect to / instead when user not logged in or not managing organization * key connector service explicit user id * key connector service no longer requires account service * key connector service missing null type * cli convert to key connector unit tests * remove unnecessary SyncService * error toast not showing on ErrorResponse * bad import due to merge conflict * bad import due to merge conflict * missing loading in remove password component for browser extension * error handling in remove password component * organization observable race condition in key-connector * usesKeyConnector always returns boolean * unit test coverage * key connector reactive * reactive key connector service * introducing convertAccountRequired$ * cli build fix * moving message sending side effect to sync * key connector service unit tests * fix unit tests * move key connector components to KM team ownership * new unit tests in wrong place * key connector domain shown in remove password component * type safety improvements * convert to key connector command localization * key connector domain in convert to key connector command * convert to key connector command unit tests with prompt assert * organization name placement change in the remove password component * unit test update * key connector url required to be provided when migrating user * unit tests in wrong place after KM code ownership move * infinite page reload * failing unit tests * failing unit tests --------- Co-authored-by: Todd Martin --- apps/browser/src/_locales/en/messages.json | 16 +-- .../remove-password.component.html | 6 +- apps/cli/src/auth/commands/unlock.command.ts | 3 + apps/cli/src/base-program.ts | 1 + .../convert-to-key-connector.command.spec.ts | 76 ++++++++++++- .../convert-to-key-connector.command.ts | 23 ++-- apps/cli/src/locales/en/messages.json | 28 +++++ apps/cli/src/oss-serve-configurator.ts | 1 + apps/cli/src/program.ts | 1 + .../remove-password.component.html | 6 +- apps/desktop/src/locales/en/messages.json | 16 +-- .../remove-password.component.html | 6 +- apps/web/src/locales/en/messages.json | 13 +-- .../abstractions/key-connector.service.ts | 4 +- .../services/key-connector.service.spec.ts | 106 ++++-------------- .../services/key-connector.service.ts | 14 +-- .../remove-password.component.spec.ts | 16 ++- .../remove-password.component.ts | 5 +- 18 files changed, 201 insertions(+), 140 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index f436c45ab75..3a5583f5468 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/browser/src/key-management/key-connector/remove-password.component.html b/apps/browser/src/key-management/key-connector/remove-password.component.html index 272f727a7bb..56baf0de2a0 100644 --- a/apps/browser/src/key-management/key-connector/remove-password.component.html +++ b/apps/browser/src/key-management/key-connector/remove-password.component.html @@ -15,7 +15,11 @@
-

{{ "convertOrganizationEncryptionDesc" | i18n: organization.name }}

+

{{ "removeMasterPasswordForOrganizationUserKeyConnector" | i18n }}

+

{{ "organizationName" | i18n }}:

+

{{ organization.name }}

+

{{ "keyConnectorDomain" | i18n }}:

+

{{ organization.keyConnectorUrl }}

-

{{ "convertOrganizationEncryptionDesc" | i18n: organization.name }}

+

{{ "removeMasterPasswordForOrganizationUserKeyConnector" | i18n }}

+

{{ "organizationName" | i18n }}:

+

{{ organization.name }}

+

{{ "keyConnectorDomain" | i18n }}:

+

{{ organization.keyConnectorUrl }}

- + diff --git a/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.ts b/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.ts index 7ac0f8a6726..e0ec1358ee1 100644 --- a/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.ts +++ b/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.ts @@ -6,13 +6,14 @@ import { Component, Input } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view"; import { - CardComponent, SectionHeaderComponent, TypographyModule, FormFieldModule, IconButtonModule, } from "@bitwarden/components"; +import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component"; + @Component({ selector: "app-sshkey-view", templateUrl: "sshkey-view.component.html", @@ -20,8 +21,8 @@ import { imports: [ CommonModule, JslibModule, - CardComponent, SectionHeaderComponent, + ReadOnlyCipherCardComponent, TypographyModule, FormFieldModule, IconButtonModule, From 7641dab0f06df63aeb655c4bd805b127ff0b31e6 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 20 May 2025 12:19:34 -0700 Subject: [PATCH 109/163] [PM-18801] - account security nudge (#14771) * account security nudge * fix messages.json * fix tests * fix logic for account security item * fix tests * adjust account security nudge work to updated nudge service * fix account security nudge * remove unused code. do not show account security badge * include ff and safari in link html * fix import * Revert "include ff and safari in link html" This reverts commit cd12a36274a082c62caae010812d742ec11e4696. --- apps/browser/src/_locales/en/messages.json | 9 ++++ .../settings/account-security.component.html | 7 +++ .../account-security.component.spec.ts | 14 ++++++ .../settings/account-security.component.ts | 26 ++++++++++ .../popup/settings/settings-v2.component.html | 2 +- .../popup/settings/settings-v2.component.ts | 2 +- .../account-security-nudge.service.ts | 49 +++++++++++++++++++ .../services/custom-nudges-services/index.ts | 1 + .../src/vault/services/nudges.service.spec.ts | 10 ++++ .../src/vault/services/nudges.service.ts | 3 ++ 10 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 3a5583f5468..99ca31bafd5 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -5019,6 +5019,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/auth/popup/settings/account-security.component.html b/apps/browser/src/auth/popup/settings/account-security.component.html index ebf79af644c..d835497d9be 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.html +++ b/apps/browser/src/auth/popup/settings/account-security.component.html @@ -5,6 +5,13 @@ +
diff --git a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts index abe642970bb..56b18068778 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts @@ -4,7 +4,10 @@ import { By } from "@angular/platform-browser"; import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; +import { CollectionService } from "@bitwarden/admin-console/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; @@ -16,14 +19,18 @@ import { VaultTimeoutStringType, VaultTimeoutAction, } from "@bitwarden/common/key-management/vault-timeout"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { MessageSender } from "@bitwarden/common/platform/messaging"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { StateProvider } from "@bitwarden/common/platform/state"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { BiometricStateService, BiometricsService, KeyService } from "@bitwarden/key-management"; @@ -71,6 +78,13 @@ describe("AccountSecurityComponent", () => { { provide: UserVerificationService, useValue: mock() }, { provide: VaultTimeoutService, useValue: mock() }, { provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService }, + { provide: StateProvider, useValue: mock() }, + { provide: CipherService, useValue: mock() }, + { provide: ApiService, useValue: mock() }, + { provide: LogService, useValue: mock() }, + { provide: OrganizationService, useValue: mock() }, + { provide: CollectionService, useValue: mock() }, + { provide: ConfigService, useValue: mock() }, ], }) .overrideComponent(AccountSecurityComponent, { diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index ede044b21de..b2380a1a47e 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -22,6 +22,7 @@ import { } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; import { FingerprintDialogComponent, VaultTimeoutInputComponent } from "@bitwarden/auth/angular"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -64,6 +65,7 @@ import { BiometricStateService, BiometricsStatus, } from "@bitwarden/key-management"; +import { SpotlightComponent } from "@bitwarden/vault"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; import { BrowserApi } from "../../../platform/browser/browser-api"; @@ -96,6 +98,7 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component"; SectionComponent, SectionHeaderComponent, SelectModule, + SpotlightComponent, TypographyModule, VaultTimeoutInputComponent, ], @@ -120,6 +123,14 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { enableAutoBiometricsPrompt: true, }); + protected showAccountSecurityNudge$: Observable = + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.vaultNudgesService.showNudgeSpotlight$(NudgeType.AccountSecurity, userId), + ), + ); + private refreshTimeoutSettings$ = new BehaviorSubject(undefined); private destroy$ = new Subject(); @@ -142,6 +153,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { private biometricStateService: BiometricStateService, private toastService: ToastService, private biometricsService: BiometricsService, + private vaultNudgesService: NudgesService, ) {} async ngOnInit() { @@ -402,6 +414,14 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { } } + protected async dismissAccountSecurityNudge() { + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + if (!activeAccount) { + return; + } + await this.vaultNudgesService.dismissNudge(NudgeType.AccountSecurity, activeAccount.id); + } + async saveVaultTimeoutAction(value: VaultTimeoutAction) { if (value === VaultTimeoutAction.LogOut) { const confirmed = await this.dialogService.openSimpleDialog({ @@ -453,6 +473,12 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { this.form.controls.pin.setValue(userHasPinSet, { emitEvent: false }); const requireReprompt = (await this.pinService.getPinLockType(userId)) == "EPHEMERAL"; this.form.controls.pinLockWithMasterPassword.setValue(requireReprompt, { emitEvent: false }); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("unlockPinSet"), + }); + await this.vaultNudgesService.dismissNudge(NudgeType.AccountSecurity, userId); } else { await this.vaultTimeoutSettingsService.clear(); } diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.html b/apps/browser/src/tools/popup/settings/settings-v2.component.html index dc53f95a7cf..0b2e84712a4 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.html +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html @@ -82,7 +82,7 @@

{{ "downloadBitwardenOnAllDevices" | i18n }}

= this.authenticatedAccount$.pipe( + showDownloadBitwardenNudge$: Observable = this.authenticatedAccount$.pipe( switchMap((account) => this.nudgesService.showNudgeBadge$(NudgeType.DownloadBitwarden, account.id), ), diff --git a/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts b/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts new file mode 100644 index 00000000000..862beb333d4 --- /dev/null +++ b/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts @@ -0,0 +1,49 @@ +import { Injectable, inject } from "@angular/core"; +import { Observable, combineLatest, from, of } from "rxjs"; +import { catchError, map } from "rxjs/operators"; + +import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; +import { PinServiceAbstraction } from "@bitwarden/auth/common"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { DefaultSingleNudgeService } from "../default-single-nudge.service"; +import { NudgeStatus, NudgeType } from "../nudges.service"; + +const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; + +@Injectable({ providedIn: "root" }) +export class AccountSecurityNudgeService extends DefaultSingleNudgeService { + private vaultProfileService = inject(VaultProfileService); + private logService = inject(LogService); + private pinService = inject(PinServiceAbstraction); + private vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService); + + nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable { + const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe( + catchError(() => { + this.logService.error("Failed to load profile date:"); + // Default to today to ensure the nudge is shown in case of an error + return of(new Date()); + }), + ); + + return combineLatest([ + profileDate$, + this.getNudgeStatus$(nudgeType, userId), + of(Date.now() - THIRTY_DAYS_MS), + from(this.pinService.isPinSet(userId)), + from(this.vaultTimeoutSettingsService.isBiometricLockSet(userId)), + ]).pipe( + map(([profileCreationDate, status, profileCutoff, isPinSet, isBiometricLockSet]) => { + const profileOlderThanCutoff = profileCreationDate.getTime() < profileCutoff; + const hideNudge = profileOlderThanCutoff || isPinSet || isBiometricLockSet; + return { + hasBadgeDismissed: status.hasBadgeDismissed || hideNudge, + hasSpotlightDismissed: status.hasSpotlightDismissed || hideNudge, + }; + }), + ); + } +} diff --git a/libs/angular/src/vault/services/custom-nudges-services/index.ts b/libs/angular/src/vault/services/custom-nudges-services/index.ts index 2e9ade985cc..e94b8bf71e5 100644 --- a/libs/angular/src/vault/services/custom-nudges-services/index.ts +++ b/libs/angular/src/vault/services/custom-nudges-services/index.ts @@ -1,4 +1,5 @@ export * from "./autofill-nudge.service"; +export * from "./account-security-nudge.service"; export * from "./has-items-nudge.service"; export * from "./download-bitwarden-nudge.service"; export * from "./empty-vault-nudge.service"; diff --git a/libs/angular/src/vault/services/nudges.service.spec.ts b/libs/angular/src/vault/services/nudges.service.spec.ts index 4eee349cf10..4edd57f5428 100644 --- a/libs/angular/src/vault/services/nudges.service.spec.ts +++ b/libs/angular/src/vault/services/nudges.service.spec.ts @@ -2,8 +2,10 @@ import { TestBed } from "@angular/core/testing"; import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; +import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateProvider } from "@bitwarden/common/platform/state"; @@ -74,6 +76,14 @@ describe("Vault Nudges Service", () => { provide: LogService, useValue: mock(), }, + { + provide: PinServiceAbstraction, + useValue: mock(), + }, + { + provide: VaultTimeoutSettingsService, + useValue: mock(), + }, ], }); }); diff --git a/libs/angular/src/vault/services/nudges.service.ts b/libs/angular/src/vault/services/nudges.service.ts index 6784e9c7b4e..55e6009a8e0 100644 --- a/libs/angular/src/vault/services/nudges.service.ts +++ b/libs/angular/src/vault/services/nudges.service.ts @@ -12,6 +12,7 @@ import { AutofillNudgeService, DownloadBitwardenNudgeService, NewItemNudgeService, + AccountSecurityNudgeService, } from "./custom-nudges-services"; import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service"; @@ -32,6 +33,7 @@ export enum NudgeType { EmptyVaultNudge = "empty-vault-nudge", HasVaultItems = "has-vault-items", AutofillNudge = "autofill-nudge", + AccountSecurity = "account-security", DownloadBitwarden = "download-bitwarden", NewLoginItemStatus = "new-login-item-status", NewCardItemStatus = "new-card-item-status", @@ -61,6 +63,7 @@ export class NudgesService { private customNudgeServices: Partial> = { [NudgeType.HasVaultItems]: inject(HasItemsNudgeService), [NudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService), + [NudgeType.AccountSecurity]: inject(AccountSecurityNudgeService), [NudgeType.AutofillNudge]: inject(AutofillNudgeService), [NudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService), [NudgeType.NewLoginItemStatus]: this.newItemNudgeService, From d7c936e1ea07338aafe15b6e055c28f5589cf383 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 20 May 2025 21:25:14 +0200 Subject: [PATCH 110/163] [PM-17900] Add cose / xchacha20poly1305 migration on userkey rotation (#14539) * Add new encrypt service functions * Undo changes * Cleanup * Fix build * Fix comments * Switch encrypt service to use SDK functions * Add cose migration on userkey rotation * Update sdk * Set featureflag to default disabled * Add tests * Update sdk to build 168 * Make changes according to feedback --- .../user-key-rotation.service.spec.ts | 78 +++++++++++++++---- .../key-rotation/user-key-rotation.service.ts | 25 +++++- libs/common/src/enums/feature-flag.enum.ts | 2 + .../platform/enums/encryption-type.enum.ts | 8 ++ .../models/domain/symmetric-crypto-key.ts | 18 ++++- libs/key-management/src/models/kdf-config.ts | 19 +++++ package-lock.json | 8 +- package.json | 2 +- 8 files changed, 133 insertions(+), 27 deletions(-) diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts index dac5afa191a..c65c4ac3ea4 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts @@ -11,7 +11,6 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; @@ -30,6 +29,7 @@ import { EmergencyAccessTrustComponent, KeyRotationTrustInfoComponent, } from "@bitwarden/key-management-ui"; +import { PureCrypto } from "@bitwarden/sdk-internal"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { WebauthnLoginAdminService } from "../../auth"; @@ -96,6 +96,11 @@ describe("KeyRotationService", () => { const mockTrustedPublicKeys = [Utils.fromUtf8ToArray("test-public-key")]; beforeAll(() => { + jest.spyOn(PureCrypto, "make_user_key_aes256_cbc_hmac").mockReturnValue(new Uint8Array(64)); + jest.spyOn(PureCrypto, "make_user_key_xchacha20_poly1305").mockReturnValue(new Uint8Array(70)); + jest + .spyOn(PureCrypto, "encrypt_user_key_with_master_password") + .mockReturnValue("mockNewUserKey"); mockUserVerificationService = mock(); mockApiService = mock(); mockCipherService = mock(); @@ -158,6 +163,7 @@ describe("KeyRotationService", () => { mockToastService, mockI18nService, mockDialogService, + mockConfigService, ); }); @@ -181,7 +187,7 @@ describe("KeyRotationService", () => { } as any, ]); mockKeyService.hashMasterKey.mockResolvedValue("mockMasterPasswordHash"); - mockConfigService.getFeatureFlag.mockResolvedValue(true); + mockConfigService.getFeatureFlag.mockResolvedValue(false); mockEncryptService.wrapSymmetricKey.mockResolvedValue({ encryptedString: "mockEncryptedData", @@ -286,6 +292,59 @@ describe("KeyRotationService", () => { expect(arg.accountUnlockData.emergencyAccessUnlockData.length).toBe(1); expect(arg.accountUnlockData.organizationAccountRecoveryUnlockData.length).toBe(1); expect(arg.accountUnlockData.passkeyUnlockData.length).toBe(2); + expect(PureCrypto.make_user_key_aes256_cbc_hmac).toHaveBeenCalled(); + expect(PureCrypto.encrypt_user_key_with_master_password).toHaveBeenCalledWith( + new Uint8Array(64), + "newMasterPassword", + mockUser.email, + DEFAULT_KDF_CONFIG.toSdkConfig(), + ); + expect(PureCrypto.make_user_key_xchacha20_poly1305).not.toHaveBeenCalled(); + }); + + it("rotates the userkey to xchacha20poly1305 and encrypted data and changes master password when featureflag is active", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + await keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData( + "mockMasterPassword", + "newMasterPassword", + mockUser, + ); + + expect(mockApiService.postUserKeyUpdateV2).toHaveBeenCalled(); + const arg = mockApiService.postUserKeyUpdateV2.mock.calls[0][0]; + expect(arg.accountUnlockData.masterPasswordUnlockData.masterKeyEncryptedUserKey).toBe( + "mockNewUserKey", + ); + expect(arg.oldMasterKeyAuthenticationHash).toBe("mockMasterPasswordHash"); + expect(arg.accountUnlockData.masterPasswordUnlockData.email).toBe("mockEmail"); + expect(arg.accountUnlockData.masterPasswordUnlockData.kdfType).toBe( + DEFAULT_KDF_CONFIG.kdfType, + ); + expect(arg.accountUnlockData.masterPasswordUnlockData.kdfIterations).toBe( + DEFAULT_KDF_CONFIG.iterations, + ); + + expect(arg.accountKeys.accountPublicKey).toBe(Utils.fromUtf8ToB64("mockPublicKey")); + expect(arg.accountKeys.userKeyEncryptedAccountPrivateKey).toBe("mockEncryptedData"); + + expect(arg.accountData.ciphers.length).toBe(2); + expect(arg.accountData.folders.length).toBe(2); + expect(arg.accountData.sends.length).toBe(2); + expect(arg.accountUnlockData.emergencyAccessUnlockData.length).toBe(1); + expect(arg.accountUnlockData.organizationAccountRecoveryUnlockData.length).toBe(1); + expect(arg.accountUnlockData.passkeyUnlockData.length).toBe(2); + expect(PureCrypto.make_user_key_aes256_cbc_hmac).not.toHaveBeenCalled(); + expect(PureCrypto.encrypt_user_key_with_master_password).toHaveBeenCalledWith( + new Uint8Array(70), + "newMasterPassword", + mockUser.email, + DEFAULT_KDF_CONFIG.toSdkConfig(), + ); + expect(PureCrypto.make_user_key_xchacha20_poly1305).toHaveBeenCalled(); }); it("returns early when first trust warning dialog is declined", async () => { @@ -344,21 +403,6 @@ describe("KeyRotationService", () => { ).rejects.toThrow(); }); - it("throws if user key creation fails", async () => { - mockKeyService.makeUserKey.mockResolvedValueOnce([ - null as unknown as UserKey, - null as unknown as EncString, - ]); - - await expect( - keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData( - "mockMasterPassword", - "mockMasterPassword1", - mockUser, - ), - ).rejects.toThrow(); - }); - it("legacy throws if no private key is found", async () => { privateKey.next(null); diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index ec38f49baeb..129d643f677 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -5,14 +5,17 @@ import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { HashPurpose } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; @@ -26,6 +29,7 @@ import { EmergencyAccessTrustComponent, KeyRotationTrustInfoComponent, } from "@bitwarden/key-management-ui"; +import { PureCrypto } from "@bitwarden/sdk-internal"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { WebauthnLoginAdminService } from "../../auth/core"; @@ -59,6 +63,7 @@ export class UserKeyRotationService { private toastService: ToastService, private i18nService: I18nService, private dialogService: DialogService, + private configService: ConfigService, ) {} /** @@ -116,8 +121,22 @@ export class UserKeyRotationService { const newMasterKey = await this.keyService.makeMasterKey(newMasterPassword, email, kdfConfig); - const [newUnencryptedUserKey, newMasterKeyEncryptedUserKey] = - await this.keyService.makeUserKey(newMasterKey); + let userKeyBytes: Uint8Array; + if (await this.configService.getFeatureFlag(FeatureFlag.EnrollAeadOnKeyRotation)) { + userKeyBytes = PureCrypto.make_user_key_xchacha20_poly1305(); + } else { + userKeyBytes = PureCrypto.make_user_key_aes256_cbc_hmac(); + } + + const newMasterKeyEncryptedUserKey = new EncString( + PureCrypto.encrypt_user_key_with_master_password( + userKeyBytes, + newMasterPassword, + email, + kdfConfig.toSdkConfig(), + ), + ); + const newUnencryptedUserKey = new SymmetricCryptoKey(userKeyBytes) as UserKey; if (!newUnencryptedUserKey || !newMasterKeyEncryptedUserKey) { this.logService.info("[Userkey rotation] User key could not be created. Aborting!"); diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index c71215a4a09..f9e68efe4be 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -48,6 +48,7 @@ export enum FeatureFlag { PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", UseSDKForDecryption = "use-sdk-for-decryption", PM17987_BlockType0 = "pm-17987-block-type-0", + EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation", /* Tools */ ItemShare = "item-share", @@ -131,6 +132,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, [FeatureFlag.UseSDKForDecryption]: FALSE, [FeatureFlag.PM17987_BlockType0]: FALSE, + [FeatureFlag.EnrollAeadOnKeyRotation]: FALSE, /* Platform */ [FeatureFlag.IpcChannelFramework]: FALSE, diff --git a/libs/common/src/platform/enums/encryption-type.enum.ts b/libs/common/src/platform/enums/encryption-type.enum.ts index 7f4b5048a45..426b1a23134 100644 --- a/libs/common/src/platform/enums/encryption-type.enum.ts +++ b/libs/common/src/platform/enums/encryption-type.enum.ts @@ -1,9 +1,16 @@ // FIXME: update to use a const object instead of a typescript enum // eslint-disable-next-line @bitwarden/platform/no-enums export enum EncryptionType { + // Symmetric encryption types AesCbc256_B64 = 0, // Type 1 was the unused and removed AesCbc128_HmacSha256_B64 AesCbc256_HmacSha256_B64 = 2, + // Cose is the encoding for the key used, but contained can be: + // - XChaCha20Poly1305 + CoseEncrypt0 = 7, + + // Asymmetric encryption types. These never occur in the same places that the symmetric ones would + // and can be split out into a separate enum. Rsa2048_OaepSha256_B64 = 3, Rsa2048_OaepSha1_B64 = 4, Rsa2048_OaepSha256_HmacSha256_B64 = 5, @@ -38,4 +45,5 @@ export const EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE = { [EncryptionType.Rsa2048_OaepSha1_B64]: 1, [EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64]: 2, [EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64]: 2, + [EncryptionType.CoseEncrypt0]: 1, }; diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts index ad16ddd06f6..1fdca04aceb 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts @@ -16,13 +16,19 @@ export type Aes256CbcKey = { encryptionKey: Uint8Array; }; +export type CoseKey = { + type: EncryptionType.CoseEncrypt0; + // Encryption key here refers to the cose-encoded and padded key. This MAY later be refactored to contain the actual key bytes, as is the case in the SDK + encryptionKey: Uint8Array; +}; + /** * A symmetric crypto key represents a symmetric key usable for symmetric encryption and decryption operations. * The specific algorithm used is private to the key, and should only be exposed to encrypt service implementations. * This can be done via `inner()`. */ export class SymmetricCryptoKey { - private innerKey: Aes256CbcHmacKey | Aes256CbcKey; + private innerKey: Aes256CbcHmacKey | Aes256CbcKey | CoseKey; keyB64: string; @@ -47,6 +53,12 @@ export class SymmetricCryptoKey { authenticationKey: key.slice(32), }; this.keyB64 = this.toBase64(); + } else if (key.byteLength > 64) { + this.innerKey = { + type: EncryptionType.CoseEncrypt0, + encryptionKey: key, + }; + this.keyB64 = this.toBase64(); } else { throw new Error(`Unsupported encType/key length ${key.byteLength}`); } @@ -63,7 +75,7 @@ export class SymmetricCryptoKey { * * @returns The inner key instance that can be directly used for encryption primitives */ - inner(): Aes256CbcHmacKey | Aes256CbcKey { + inner(): Aes256CbcHmacKey | Aes256CbcKey | CoseKey { return this.innerKey; } @@ -90,6 +102,8 @@ export class SymmetricCryptoKey { encodedKey.set(this.innerKey.encryptionKey, 0); encodedKey.set(this.innerKey.authenticationKey, 32); return encodedKey; + } else if (this.innerKey.type === EncryptionType.CoseEncrypt0) { + return this.innerKey.encryptionKey; } else { throw new Error("Unsupported encryption type."); } diff --git a/libs/key-management/src/models/kdf-config.ts b/libs/key-management/src/models/kdf-config.ts index 689da77c163..a2ed8a22505 100644 --- a/libs/key-management/src/models/kdf-config.ts +++ b/libs/key-management/src/models/kdf-config.ts @@ -1,6 +1,7 @@ import { Jsonify } from "type-fest"; import { RangeWithDefault } from "@bitwarden/common/platform/misc/range-with-default"; +import { Kdf } from "@bitwarden/sdk-internal"; import { KdfType } from "../enums/kdf-type.enum"; @@ -49,6 +50,14 @@ export class PBKDF2KdfConfig { static fromJSON(json: Jsonify): PBKDF2KdfConfig { return new PBKDF2KdfConfig(json.iterations); } + + toSdkConfig(): Kdf { + return { + pBKDF2: { + iterations: this.iterations, + }, + }; + } } /** @@ -124,6 +133,16 @@ export class Argon2KdfConfig { static fromJSON(json: Jsonify): Argon2KdfConfig { return new Argon2KdfConfig(json.iterations, json.memory, json.parallelism); } + + toSdkConfig(): Kdf { + return { + argon2id: { + iterations: this.iterations, + memory: this.memory, + parallelism: this.parallelism, + }, + }; + } } export const DEFAULT_KDF_CONFIG = new PBKDF2KdfConfig(PBKDF2KdfConfig.ITERATIONS.defaultValue); diff --git a/package-lock.json b/package-lock.json index 9707005d892..bd2cabe1cfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.159", + "@bitwarden/sdk-internal": "0.2.0-main.168", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.1.0", @@ -4830,9 +4830,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.159", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.159.tgz", - "integrity": "sha512-vliX5w/A6fuKWZJpDZTCPV4EU5CFrrs6zAv0aQaUQXF9LqL1YVh113D1NhOMuG2ILLWs2kDcTKiprvWFSTu1dg==", + "version": "0.2.0-main.168", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.168.tgz", + "integrity": "sha512-NU10oqw+GI9oHrh8/i/IC8/7oaYmswqC2E/0Zc56xC3jY7uNgFZgpae7JhyMU6UxzrAjiEqdmGnm+AGWFiPG8w==", "license": "GPL-3.0" }, "node_modules/@bitwarden/send-ui": { diff --git a/package.json b/package.json index fc948fc5ee6..9831417de59 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.159", + "@bitwarden/sdk-internal": "0.2.0-main.168", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.1.0", From 94815085ed0e463c325a7f7018f7c626b60b0361 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 20 May 2025 13:36:10 -0700 Subject: [PATCH 111/163] set default values for navButtons$ observable (#14859) --- apps/browser/src/popup/tabs-v2.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/popup/tabs-v2.component.ts b/apps/browser/src/popup/tabs-v2.component.ts index 54aa4a5544d..3d93f5d4e04 100644 --- a/apps/browser/src/popup/tabs-v2.component.ts +++ b/apps/browser/src/popup/tabs-v2.component.ts @@ -1,5 +1,5 @@ import { Component } from "@angular/core"; -import { combineLatest, map, Observable, switchMap } from "rxjs"; +import { combineLatest, map, Observable, startWith, switchMap } from "rxjs"; import { NudgesService } from "@bitwarden/angular/vault"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -23,6 +23,7 @@ export class TabsV2Component { this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge), this.hasActiveBadges$, ]).pipe( + startWith([false, false]), map(([onboardingFeatureEnabled, hasBadges]) => { return [ { From bc56bc8e37b8d58683b146b8c95769d2d5ea351c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 09:29:14 +0200 Subject: [PATCH 112/163] [deps]: Lock file maintenance (#13866) * [deps]: Lock file maintenance * add override for parse5 version to 7.2.1 (7.3.0 has breaking ts changes) * manual rebuild of package-lock * fix test event listeners persistence Co-authored-by: Oscar Hinton --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jonathan Prusik Co-authored-by: Oscar Hinton --- .../list/autofill-inline-menu-list.spec.ts | 11 +- apps/desktop/desktop_native/Cargo.lock | 481 ++--- .../package-lock.json | 6 +- package-lock.json | 1859 ++++++++--------- package.json | 1 + 5 files changed, 1160 insertions(+), 1198 deletions(-) diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts index ed28375e4fe..b4e480797da 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts @@ -21,10 +21,16 @@ describe("AutofillInlineMenuList", () => { disconnect: jest.fn(), })); - let autofillInlineMenuList: AutofillInlineMenuList; + let autofillInlineMenuList: AutofillInlineMenuList | null; const portKey: string = "inlineMenuListPortKey"; + const events: { eventName: any; callback: any }[] = []; beforeEach(() => { + const oldEv = globalThis.addEventListener; + globalThis.addEventListener = (eventName: any, callback: any) => { + events.push({ eventName, callback }); + oldEv.call(globalThis, eventName, callback); + }; document.body.innerHTML = ``; autofillInlineMenuList = document.querySelector("autofill-inline-menu-list"); jest.spyOn(globalThis.document, "createElement"); @@ -33,6 +39,9 @@ describe("AutofillInlineMenuList", () => { afterEach(() => { jest.clearAllMocks(); + events.forEach(({ eventName, callback }) => { + globalThis.removeEventListener(eventName, callback); + }); }); describe("initAutofillInlineMenuList", () => { diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index f3db03f8639..242c1e1b9f2 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -232,14 +232,15 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", + "pin-project-lite", "slab", ] @@ -267,7 +268,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix", + "rustix 0.38.44", "slab", "tracing", "windows-sys 0.59.0", @@ -299,7 +300,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix", + "rustix 0.38.44", "tracing", ] @@ -326,7 +327,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 0.38.44", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -340,9 +341,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.86" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", @@ -363,9 +364,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -384,15 +385,15 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "basic-toml" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" dependencies = [ "serde", ] @@ -419,9 +420,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "bitwarden-russh" @@ -497,9 +498,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "camino" @@ -587,9 +588,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.31" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" +checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" dependencies = [ "clap_builder", "clap_derive", @@ -597,9 +598,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.31" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" +checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" dependencies = [ "anstream", "anstyle", @@ -609,9 +610,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.28" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck", "proc-macro2", @@ -636,10 +637,11 @@ dependencies = [ [[package]] name = "codespan-reporting" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ + "serde", "termcolor", "unicode-width", ] @@ -763,9 +765,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.141" +version = "1.0.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bc580dceb395cae0efdde0a88f034cfd8a276897e40c693a7b87bed17971d33" +checksum = "a71ea7f29c73f7ffa64c50b83c9fe4d3a6d4be89a86b009eb80d5a6d3429d741" dependencies = [ "cc", "cxxbridge-cmd", @@ -777,9 +779,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.141" +version = "1.0.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49d8c1baedad72a7efda12ad8d7ad687b3e7221dfb304a12443fd69e9de8bb30" +checksum = "36a8232661d66dcf713394726157d3cfe0a89bfc85f52d6e9f9bbc2306797fe7" dependencies = [ "cc", "codespan-reporting", @@ -791,9 +793,9 @@ dependencies = [ [[package]] name = "cxxbridge-cmd" -version = "1.0.141" +version = "1.0.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e43afb0e3b2ef293492a31ecd796af902112460d53e5f923f7804f348a769f9c" +checksum = "4f44296c8693e9ea226a48f6a122727f77aa9e9e338380cb021accaeeb7ee279" dependencies = [ "clap", "codespan-reporting", @@ -804,15 +806,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.141" +version = "1.0.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0257ad2096a2474fe877e9e055ab69603851c3d6b394efcc7e0443899c2492ce" +checksum = "c42f69c181c176981ae44ba9876e2ea41ce8e574c296b38d06925ce9214fb8e4" [[package]] name = "cxxbridge-macro" -version = "1.0.141" +version = "1.0.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b46cbd7358a46b760609f1cb5093683328e58ca50e594a308716f5403fdc03e5" +checksum = "8faff5d4467e0709448187df29ccbf3b0982cc426ee444a193f87b11afb565a8" dependencies = [ "proc-macro2", "quote", @@ -835,9 +837,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468", @@ -846,9 +848,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] @@ -1078,9 +1080,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.59.0", @@ -1088,9 +1090,9 @@ dependencies = [ [[package]] name = "error-code" -version = "3.3.1" +version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "event-listener" @@ -1105,9 +1107,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener", "pin-project-lite", @@ -1139,9 +1141,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" @@ -1285,9 +1287,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -1296,14 +1298,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -1347,9 +1349,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" [[package]] name = "heck" @@ -1401,21 +1403,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -1424,31 +1427,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -1456,67 +1439,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "idna" version = "1.0.3" @@ -1530,9 +1500,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1540,12 +1510,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.3", ] [[package]] @@ -1581,9 +1551,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "keytar" @@ -1623,19 +1593,19 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.0", ] [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" @@ -1649,9 +1619,9 @@ dependencies = [ [[package]] name = "link-cplusplus" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" +checksum = "4a6f6da007f968f9def0d65a05b187e2960183de70c160204ecfccf0ee330212" dependencies = [ "cc", ] @@ -1663,10 +1633,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] -name = "litemap" -version = "0.7.5" +name = "linux-raw-sys" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" @@ -1748,9 +1724,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] @@ -2057,9 +2033,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oo7" @@ -2074,7 +2050,7 @@ dependencies = [ "digest", "endi", "futures-util", - "getrandom 0.3.1", + "getrandom 0.3.3", "hkdf", "hmac", "md-5", @@ -2299,9 +2275,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plain" @@ -2319,7 +2295,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix", + "rustix 0.38.44", "tracing", "windows-sys 0.59.0", ] @@ -2347,6 +2323,15 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2355,49 +2340,55 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" -version = "0.37.2" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.8.5" @@ -2445,7 +2436,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -2454,7 +2445,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.3", ] [[package]] @@ -2465,9 +2456,9 @@ checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" [[package]] name = "redox_syscall" -version = "0.5.9" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ "bitflags", ] @@ -2478,7 +2469,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", "thiserror 2.0.12", ] @@ -2567,21 +2558,34 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa20" @@ -2600,9 +2604,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scratch" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" +checksum = "9f6280af86e5f559536da57a45ebc84948833b3bee313a7dd25232e09c878a52" [[package]] name = "scroll" @@ -2615,9 +2619,9 @@ dependencies = [ [[package]] name = "scroll_derive" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", @@ -2660,9 +2664,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ "serde", ] @@ -2701,9 +2705,9 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", @@ -2740,9 +2744,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -2785,9 +2789,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "smawk" @@ -2797,9 +2801,9 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2894,9 +2898,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.98" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -2916,9 +2920,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.35.0" +version = "0.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b897c8ea620e181c7955369a31be5f48d9a9121cb59fd33ecef9ff2a34323422" +checksum = "79251336d17c72d9762b8b54be4befe38d2db56fbbc0241396d70f173c39d47a" dependencies = [ "libc", "memchr", @@ -2930,15 +2934,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.17.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "cfg-if", "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.3", "once_cell", - "rustix", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -2953,9 +2956,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", ] @@ -3002,9 +3005,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -3019,15 +3022,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -3035,9 +3038,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -3107,15 +3110,15 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", "toml_datetime", @@ -3191,9 +3194,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-segmentation" @@ -3203,9 +3206,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "uniffi" @@ -3359,12 +3362,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -3391,43 +3388,43 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wayland-backend" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" +checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" dependencies = [ "cc", "downcast-rs", - "rustix", + "rustix 0.38.44", "smallvec", "wayland-sys", ] [[package]] name = "wayland-client" -version = "0.31.8" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" +checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" dependencies = [ "bitflags", - "rustix", + "rustix 0.38.44", "wayland-backend", "wayland-scanner", ] [[package]] name = "wayland-protocols" -version = "0.32.6" +version = "0.32.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" +checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" dependencies = [ "bitflags", "wayland-backend", @@ -3437,9 +3434,9 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" +checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" dependencies = [ "bitflags", "wayland-backend", @@ -3829,18 +3826,18 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.3" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen-rt" -version = "0.33.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] @@ -3854,7 +3851,7 @@ dependencies = [ "libc", "log", "os_pipe", - "rustix", + "rustix 0.38.44", "tempfile", "thiserror 1.0.69", "tree_magic_mini", @@ -3864,17 +3861,11 @@ dependencies = [ "wayland-protocols-wlr", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "x11rb" @@ -3883,7 +3874,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "gethostname", - "rustix", + "rustix 0.38.44", "x11rb-protocol", ] @@ -3905,9 +3896,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -3917,9 +3908,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", @@ -4059,19 +4050,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", @@ -4126,10 +4116,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebff5e6b81c1c7dca2d0bd333b2006da48cb37dbcae5a8da888f31fcb3c19934" [[package]] -name = "zerovec" -version = "0.10.4" +name = "zerotrie" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -4138,9 +4139,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 8b39fd9805e..37b8cf96ff3 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -110,9 +110,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "license": "MIT", "bin": { "acorn": "bin/acorn" diff --git a/package-lock.json b/package-lock.json index bd2cabe1cfe..579ff539de3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -357,9 +357,9 @@ "license": "GPL-3.0" }, "node_modules/@adobe/css-tools": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz", - "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", + "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", "dev": true, "license": "MIT" }, @@ -398,14 +398,14 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1901.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1901.8.tgz", - "integrity": "sha512-DzvlL1Zg+zOnVmMN3CjE5KzjZAltRZwOwwcso72iWenBPvl/trKzPDlA6ySmpRonm+AR9i9JrdLEUlwczW6/bQ==", + "version": "0.1902.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.12.tgz", + "integrity": "sha512-LfUc7k84YL290hAxsG+FvjQpXugQXyw5aDzrQQB4iTYhBgaABu2aaNOU4eu3JH+F8NeXd2EBF/YMr2LDSkYlMw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@angular-devkit/core": "19.1.8", + "@angular-devkit/core": "19.2.12", "rxjs": "7.8.1" }, "engines": { @@ -626,19 +626,6 @@ "semver": "bin/semver.js" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", @@ -750,9 +737,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "version": "4.17.22", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz", + "integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==", "dev": true, "license": "MIT", "dependencies": { @@ -841,20 +828,6 @@ "webpack": ">=5" } }, - "node_modules/@angular-devkit/build-angular/node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", - "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3", - "core-js-compat": "^3.40.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/browserslist": { "version": "4.24.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", @@ -913,6 +886,19 @@ "fsevents": "~2.3.2" } }, + "node_modules/@angular-devkit/build-angular/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -945,19 +931,6 @@ "webpack": "^5.1.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -1016,19 +989,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@angular-devkit/build-angular/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/https-proxy-agent": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", @@ -1618,9 +1578,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "19.1.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.8.tgz", - "integrity": "sha512-j1zHKvOsGwu5YwAZGuzi835R9vcW7PkfxmSRIJeVl+vawgk31K3zFb4UPH8AY/NPWYqXIAnwpka3HC1+JrWLWA==", + "version": "19.2.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.12.tgz", + "integrity": "sha512-v5pdfZHZ8MTZozfpkhKoPFBpXQW+2GFbTfdyis8FBtevJWCbIsCR3xhodgI4jwzkSEAraN4oVtWvSytdNyBC6A==", "dev": true, "license": "MIT", "peer": true, @@ -1781,13 +1741,13 @@ } }, "node_modules/@angular-eslint/builder/node_modules/@angular-devkit/architect": { - "version": "0.1802.14", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.14.tgz", - "integrity": "sha512-eplaGCXSlPwf1f4XwyzsYTd8/lJ0/Adm6XsODsBxvkZlIpLcps80/h2lH5MVJpoDREzIFu1BweDpYCoNK5yYZg==", + "version": "0.1802.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.19.tgz", + "integrity": "sha512-M4B1tzxGX1nWCZr9GMM8OO0yBJO2HFSdK8M8P74vEFQfKIeq3y16IQ5zlEveJrkCOFVtmlIy2C9foMCdNyBRMA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.14", + "@angular-devkit/core": "18.2.19", "rxjs": "7.8.1" }, "engines": { @@ -1797,9 +1757,9 @@ } }, "node_modules/@angular-eslint/builder/node_modules/@angular-devkit/core": { - "version": "18.2.14", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.14.tgz", - "integrity": "sha512-UGIGOjXuOyCW+5S4tINu7e6LOu738CmTw3h7Ui1I8OzdTIYJcYJrei8sgrwDwOYADRal+p0MeMlnykH3TM5XBA==", + "version": "18.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.19.tgz", + "integrity": "sha512-Ptf92Zomc6FCr7GWmHKdgOUbA1GpctZwH/hRcpYpU3tM56MG2t5FOFpufnE595GgolOCktabkFEoODMG8PBVDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1955,9 +1915,9 @@ } }, "node_modules/@angular-eslint/schematics/node_modules/@angular-devkit/core": { - "version": "18.2.14", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.14.tgz", - "integrity": "sha512-UGIGOjXuOyCW+5S4tINu7e6LOu738CmTw3h7Ui1I8OzdTIYJcYJrei8sgrwDwOYADRal+p0MeMlnykH3TM5XBA==", + "version": "18.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.19.tgz", + "integrity": "sha512-Ptf92Zomc6FCr7GWmHKdgOUbA1GpctZwH/hRcpYpU3tM56MG2t5FOFpufnE595GgolOCktabkFEoODMG8PBVDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2253,6 +2213,19 @@ "semver": "bin/semver.js" } }, + "node_modules/@angular/build/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@angular/build/node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -2746,13 +2719,13 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.8.3.tgz", - "integrity": "sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "license": "MIT", "dependencies": { - "@csstools/css-calc": "^2.1.1", - "@csstools/css-color-parser": "^3.0.7", + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" @@ -2779,9 +2752,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", "dev": true, "license": "MIT", "engines": { @@ -2854,27 +2827,27 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", - "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.24.7" + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -2884,9 +2857,9 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, "funding": [ { @@ -2904,10 +2877,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -2927,18 +2900,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", - "integrity": "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.26.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.26.9", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", "semver": "^6.3.1" }, "engines": { @@ -2949,13 +2922,13 @@ } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3013,9 +2986,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", - "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", + "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", "dev": true, "license": "MIT", "dependencies": { @@ -3030,42 +3003,42 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3075,13 +3048,13 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3098,15 +3071,15 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", - "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-wrap-function": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3116,28 +3089,28 @@ } }, "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", - "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.26.5" + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3147,14 +3120,14 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3192,9 +3165,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", "engines": { @@ -3202,15 +3175,15 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", - "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", + "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3246,14 +3219,14 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", - "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3263,13 +3236,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", - "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3279,13 +3252,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", - "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3295,15 +3268,15 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3313,14 +3286,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", - "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", + "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3424,13 +3397,13 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", - "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3482,13 +3455,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3608,13 +3581,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3641,13 +3614,13 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", - "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3693,13 +3666,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", - "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3709,13 +3682,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", - "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz", + "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3725,14 +3698,14 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", - "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3742,14 +3715,14 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", - "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", + "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3759,17 +3732,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", - "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", + "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/traverse": "^7.25.9", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.27.1", "globals": "^11.1.0" }, "engines": { @@ -3780,27 +3753,27 @@ } }, "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", - "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/template": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3810,13 +3783,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", - "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz", + "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3826,14 +3799,14 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", - "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3843,13 +3816,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", - "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3859,14 +3832,14 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3876,13 +3849,13 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", - "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3892,13 +3865,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", - "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3908,13 +3881,13 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", - "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3924,14 +3897,14 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz", - "integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3941,15 +3914,15 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", - "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3959,13 +3932,13 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", - "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3975,13 +3948,13 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", - "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3991,13 +3964,13 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", - "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4007,13 +3980,13 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", - "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4023,14 +3996,14 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", - "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4040,14 +4013,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", - "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4057,16 +4030,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", - "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4076,14 +4049,14 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", - "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4093,14 +4066,14 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4110,13 +4083,13 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", - "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4126,13 +4099,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.26.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", - "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4142,13 +4115,13 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", - "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4158,15 +4131,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", - "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.2.tgz", + "integrity": "sha512-AIUHD7xJ1mCrj3uPozvtngY3s0xpv7Nu7DoUSnzNY6Xam1Cy4rUznR//pvMHOhQ4AvbCexhbqXCtpxGHOGOO6g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9" + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4176,14 +4150,14 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", - "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4193,13 +4167,13 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", - "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4209,14 +4183,14 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4226,13 +4200,13 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", - "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", + "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4242,14 +4216,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", - "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4259,15 +4233,15 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", - "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4277,26 +4251,26 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", - "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4306,14 +4280,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", - "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz", + "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "regenerator-transform": "^0.15.2" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4340,13 +4313,13 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", - "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4376,20 +4349,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-runtime/node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", - "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3", - "core-js-compat": "^3.40.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -4401,13 +4360,13 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", - "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4417,14 +4376,14 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", - "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4434,13 +4393,13 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", - "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4450,13 +4409,13 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz", - "integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4466,13 +4425,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz", - "integrity": "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4482,13 +4441,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", - "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4498,14 +4457,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", - "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4515,14 +4474,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", - "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4532,14 +4491,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", - "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4644,6 +4603,20 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5082,13 +5055,13 @@ } }, "node_modules/@compodoc/compodoc/node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -5207,6 +5180,20 @@ "semver": "bin/semver.js" } }, + "node_modules/@compodoc/compodoc/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@compodoc/compodoc/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -5431,9 +5418,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.2.tgz", - "integrity": "sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.3.tgz", + "integrity": "sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==", "funding": [ { "type": "github", @@ -5454,9 +5441,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.8.tgz", - "integrity": "sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.9.tgz", + "integrity": "sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==", "funding": [ { "type": "github", @@ -5470,7 +5457,7 @@ "license": "MIT", "dependencies": { "@csstools/color-helpers": "^5.0.2", - "@csstools/css-calc": "^2.1.2" + "@csstools/css-calc": "^2.1.3" }, "engines": { "node": ">=18" @@ -5584,9 +5571,9 @@ } }, "node_modules/@electron/asar": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.3.1.tgz", - "integrity": "sha512-WtpC/+34p0skWZiarRjLAyqaAX78DofhDxnREy/V5XHfu1XEXbFCSSMcDQ6hNCPJFaPy8/NnUgYuf9uiCkvKPg==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", "dev": true, "license": "MIT", "dependencies": { @@ -6020,29 +6007,32 @@ } }, "node_modules/@emnapi/core": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.1.tgz", - "integrity": "sha512-4JFstCTaToCFrPqrGzgkF8N2NHjtsaY4uRh6brZQ5L9e4wbMieX8oDT8N7qfVFTQecHFEtkj4ve49VIZ3mKVqw==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", + "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", "dev": true, + "license": "MIT", "dependencies": { - "@emnapi/wasi-threads": "1.0.1", + "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.1.tgz", - "integrity": "sha512-LMshMVP0ZhACNjQNYXiU1iZJ6QCcv0lUdPDPugqGvCGXt5xtRVBPdtA0qU12pEXZzpWAhWlZYptfdAFq10DOVQ==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", "dev": true, + "license": "MIT", "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz", - "integrity": "sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", + "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", "dev": true, + "license": "MIT", "dependencies": { "tslib": "^2.4.0" } @@ -6559,9 +6549,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -6763,9 +6753,9 @@ } }, "node_modules/@figspec/react": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@figspec/react/-/react-1.0.3.tgz", - "integrity": "sha512-r683qOko+5CbT48Ox280fMx2MNAtaFPgCNJvldOqN3YtmAzlcTT+YSxd3OahA+kjXGGrnzDbUgeTOX1cPLII+g==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@figspec/react/-/react-1.0.4.tgz", + "integrity": "sha512-jaPvkIef4d6NjsRiw91OZabrfdPH9FtoPGYcY5mpXjYEcdUqIq1aHtLq3SkMVyVysEapTEJ6yS8amy93MyXBEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6773,7 +6763,7 @@ "@lit-labs/react": "^1.0.2" }, "peerDependencies": { - "react": "^16.14.0 || ^17.0.0 || ^18.0.0" + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@gar/promisify": { @@ -6967,9 +6957,9 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.10.tgz", - "integrity": "sha512-Ey6176gZmeqZuY/W/nZiUyvmb1/qInjcpiZjXWi6nON+nxJpD1bxtSoBxNliGISae32n6OwbY+TSXPZ1CfS4bw==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", + "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", "dev": true, "license": "MIT", "engines": { @@ -7347,16 +7337,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/console/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/core": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", @@ -7440,16 +7420,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@jest/core/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/create-cache-key-function": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", @@ -7630,16 +7600,6 @@ "node": "*" } }, - "node_modules/@jest/reporters/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -7700,16 +7660,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/test-sequencer/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/transform": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", @@ -7744,16 +7694,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@jest/transform/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/types": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", @@ -7849,9 +7789,9 @@ } }, "node_modules/@jsonjoy.com/json-pack": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.1.tgz", - "integrity": "sha512-osjeBqMJ2lb/j/M8NCPjs1ylqWIcTRTycIhVB5pt6LgzgeRSb0YRZ7j9RfA8wIUrsr/medIuhVyonXRZWLyfdw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz", + "integrity": "sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -7872,9 +7812,9 @@ } }, "node_modules/@jsonjoy.com/util": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", - "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.6.0.tgz", + "integrity": "sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A==", "dev": true, "license": "Apache-2.0", "engines": { @@ -7962,9 +7902,9 @@ "license": "BSD-3-Clause" }, "node_modules/@lit/reactive-element": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", - "integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.0.tgz", + "integrity": "sha512-L2qyoZSQClcBmq0qajBVbhYEcG6iK0XfLn66ifLe/RfC0/ihpc+pl0Wdn8bJ8o+hj38cG0fGXRgSS20MuXn7qA==", "license": "BSD-3-Clause", "dependencies": { "@lit-labs/ssr-dom-shim": "^1.2.0" @@ -8301,6 +8241,7 @@ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz", "integrity": "sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==", "dev": true, + "license": "MIT", "dependencies": { "@emnapi/core": "^1.1.0", "@emnapi/runtime": "^1.1.0", @@ -8840,6 +8781,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -8856,6 +8798,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -8872,6 +8815,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -8888,6 +8832,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -8904,6 +8849,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -8920,6 +8866,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -8936,6 +8883,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -8952,6 +8900,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -8968,6 +8917,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -8984,6 +8934,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -10699,9 +10650,9 @@ } }, "node_modules/@storybook/csf": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.12.tgz", - "integrity": "sha512-9/exVhabisyIVL0VxTCxo01Tdm8wefIXKXfltAPTSr8cbLn5JAxGQ6QV3mjdecLGEOucfoVhAKtJfVHxEK1iqw==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.13.tgz", + "integrity": "sha512-7xOOwCLGB3ebM87eemep89MYRFTko+D8qE7EdAAq74lgdqRR5cOUtYWJLjO2dLtP94nqoOdHJo6MdLLKzg412Q==", "dev": true, "license": "MIT", "dependencies": { @@ -10733,9 +10684,9 @@ "license": "MIT" }, "node_modules/@storybook/icons": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.3.2.tgz", - "integrity": "sha512-t3xcbCKkPvqyef8urBM0j/nP6sKtnlRkVgC+8JTbTAZQjaTmOjes3byEgzs89p4B/K6cJsg9wLW2k3SknLtYJw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.4.0.tgz", + "integrity": "sha512-Td73IeJxOyalzvjQL+JXx72jlIYHgs+REaHiREOqfpo3A2AYYG71AUbcv+lg7mEDIweKVCxsMQ0UKo634c8XeA==", "dev": true, "license": "MIT", "engines": { @@ -11361,6 +11312,7 @@ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", "dev": true, + "license": "MIT", "dependencies": { "tslib": "^2.4.0" } @@ -11404,9 +11356,9 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, "license": "MIT", "dependencies": { @@ -11425,9 +11377,9 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", "dev": true, "license": "MIT", "dependencies": { @@ -11560,15 +11512,14 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", - "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.2.tgz", + "integrity": "sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/qs": "*", "@types/serve-static": "*" } }, @@ -11876,9 +11827,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", + "version": "4.17.16", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", + "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", "dev": true, "license": "MIT" }, @@ -12033,9 +11984,9 @@ } }, "node_modules/@types/qs": { - "version": "6.9.18", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "dev": true, "license": "MIT" }, @@ -12058,9 +12009,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.3.5", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", - "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -12085,9 +12036,9 @@ "license": "MIT" }, "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", "dev": true, "license": "MIT" }, @@ -12211,9 +12162,9 @@ "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", - "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, "license": "MIT", "dependencies": { @@ -12475,16 +12426,6 @@ "node": ">= 4" } }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@typescript-eslint/parser": { "version": "8.31.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.0.tgz", @@ -12631,9 +12572,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", - "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", "dev": true, "license": "MIT", "engines": { @@ -12975,15 +12916,15 @@ } }, "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz", - "integrity": "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.10.tgz", + "integrity": "sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.0", - "@emnapi/runtime": "^1.4.0", + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" } }, @@ -13495,6 +13436,7 @@ "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.2.tgz", "integrity": "sha512-/HcYgtUSiJiot/XWGLOlGxPYUG65+/31V8oqk17vZLW1xlCoR4PampyePljOxY2n8/3jz9+tIFzICsyGujJZoA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "js-yaml": "^3.10.0", "tslib": "^2.4.0" @@ -13508,6 +13450,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, + "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } @@ -13517,6 +13460,7 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -13529,13 +13473,15 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@zkochan/js-yaml": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.7.tgz", "integrity": "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -13591,9 +13537,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", "bin": { @@ -13788,9 +13734,9 @@ } }, "node_modules/angular-eslint/node_modules/@angular-devkit/core": { - "version": "18.2.14", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.14.tgz", - "integrity": "sha512-UGIGOjXuOyCW+5S4tINu7e6LOu738CmTw3h7Ui1I8OzdTIYJcYJrei8sgrwDwOYADRal+p0MeMlnykH3TM5XBA==", + "version": "18.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.19.tgz", + "integrity": "sha512-Ptf92Zomc6FCr7GWmHKdgOUbA1GpctZwH/hRcpYpU3tM56MG2t5FOFpufnE595GgolOCktabkFEoODMG8PBVDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14390,18 +14336,19 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -14655,9 +14602,9 @@ } }, "node_modules/axe-core": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", - "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", "dev": true, "license": "MPL-2.0", "engines": { @@ -14698,10 +14645,11 @@ } }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "dev": true, + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -14740,16 +14688,6 @@ "@babel/core": "^7.8.0" } }, - "node_modules/babel-jest/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/babel-loader": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", @@ -14878,14 +14816,14 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", - "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", + "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.3", + "@babel/helper-define-polyfill-provider": "^0.6.4", "semver": "^6.3.1" }, "peerDependencies": { @@ -14903,27 +14841,27 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", - "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2", - "core-js-compat": "^3.38.0" + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", - "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", + "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3" + "@babel/helper-define-polyfill-provider": "^0.6.4" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -15898,13 +15836,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -15963,9 +15901,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001717", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", - "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", "dev": true, "funding": [ { @@ -16271,9 +16209,9 @@ } }, "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", "license": "MIT", "engines": { "node": ">=6" @@ -16985,13 +16923,13 @@ } }, "node_modules/core-js-compat": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", - "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz", + "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.3" + "browserslist": "^4.24.4" }, "funding": { "type": "opencollective", @@ -16999,9 +16937,9 @@ } }, "node_modules/core-js-compat/node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, "funding": [ { @@ -17019,10 +16957,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -17322,12 +17260,12 @@ "license": "MIT" }, "node_modules/cssstyle": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz", - "integrity": "sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.1.tgz", + "integrity": "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==", "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^2.8.2", + "@asamuzakjp/css-color": "^3.1.2", "rrweb-cssom": "^0.8.0" }, "engines": { @@ -17438,9 +17376,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -17484,9 +17422,9 @@ "license": "MIT" }, "node_modules/decode-named-character-reference": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", - "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", + "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", "dev": true, "license": "MIT", "dependencies": { @@ -17527,9 +17465,9 @@ } }, "node_modules/dedent": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", - "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -17754,9 +17692,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -18207,21 +18145,33 @@ } }, "node_modules/dotenv": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", - "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/dotenv-expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", "dev": true, - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } }, "node_modules/dunder-proto": { "version": "1.0.1", @@ -18494,9 +18444,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.151", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.151.tgz", - "integrity": "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==", + "version": "1.5.155", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", + "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", "dev": true, "license": "ISC" }, @@ -18547,9 +18497,9 @@ } }, "node_modules/electron/node_modules/@types/node": { - "version": "20.17.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz", - "integrity": "sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==", + "version": "20.17.48", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.48.tgz", + "integrity": "sha512-KpSfKOHPsiSC4IkZeu2LsusFwExAIVGkhG1KkbaBMLwau0uMhj0fCrvyg9ddM2sAvd+gtiBJLir4LAw1MNMIaw==", "dev": true, "license": "MIT", "dependencies": { @@ -18649,6 +18599,7 @@ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-colors": "^4.1.1" }, @@ -18832,9 +18783,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, @@ -19375,9 +19326,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -20107,9 +20058,9 @@ } }, "node_modules/fastq": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", - "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "license": "ISC", "dependencies": { @@ -20910,6 +20861,7 @@ "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz", "integrity": "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==", "dev": true, + "license": "MIT", "dependencies": { "js-yaml": "^3.13.1" } @@ -20919,6 +20871,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, + "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } @@ -20928,6 +20881,7 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -20940,7 +20894,8 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/fs-constants": { "version": "1.0.0", @@ -21210,9 +21165,9 @@ "license": "MIT" }, "node_modules/glob": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", - "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", + "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", "dev": true, "license": "ISC", "dependencies": { @@ -21416,15 +21371,28 @@ } }, "node_modules/globby/node_modules/ignore": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", - "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", "dev": true, "license": "MIT", "engines": { "node": ">= 4" } }, + "node_modules/globby/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -21763,9 +21731,9 @@ } }, "node_modules/html-entities": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", - "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", "dev": true, "funding": [ { @@ -22006,9 +21974,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "dev": true, "license": "BSD-2-Clause" }, @@ -22045,9 +22013,9 @@ } }, "node_modules/http-parser-js": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", - "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", "dev": true, "license": "MIT" }, @@ -22320,9 +22288,9 @@ "license": "MIT" }, "node_modules/immutable": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", - "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz", + "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==", "dev": true, "license": "MIT" }, @@ -22725,9 +22693,9 @@ } }, "node_modules/is-bun-module/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -23668,16 +23636,6 @@ "dev": true, "license": "MIT" }, - "node_modules/jest-circus/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/jest-cli": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", @@ -23839,16 +23797,6 @@ "dev": true, "license": "MIT" }, - "node_modules/jest-config/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/jest-diff": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", @@ -24422,16 +24370,6 @@ "dev": true, "license": "MIT" }, - "node_modules/jest-message-util/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/jest-mock": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", @@ -24728,16 +24666,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-resolve/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/jest-runner": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", @@ -24872,16 +24800,6 @@ "node": "*" } }, - "node_modules/jest-runtime/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/jest-serializer-html": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/jest-serializer-html/-/jest-serializer-html-7.1.0.tgz", @@ -25127,6 +25045,19 @@ "node": ">=12.20" } }, + "node_modules/jest-watch-typeahead/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-watch-typeahead/node_modules/string-length": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", @@ -25414,13 +25345,13 @@ "license": "BSD-2-Clause" }, "node_modules/json-stable-stringify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.2.1.tgz", - "integrity": "sha512-Lp6HbbBgosLmJbjx0pBLbgvx68FaFU1sdkmBuckmhhJ88kL13OA51CDtR2yJB50eCNMH9wRqtQNNiAqQH4YXnA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" @@ -26029,10 +25960,14 @@ } }, "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", + "integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } }, "node_modules/lint-staged": { "version": "15.5.1", @@ -26216,9 +26151,9 @@ } }, "node_modules/lint-staged/node_modules/listr2": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.2.tgz", - "integrity": "sha512-vsBzcU4oE+v0lj4FhVLzr9dBTv4/fHIa57l+GCwovP8MoFNZJTOhGU8PXd4v2VJCbECAaijBiHntiekFMLvo0g==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", "dev": true, "license": "MIT", "dependencies": { @@ -26542,20 +26477,20 @@ } }, "node_modules/lit-element": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.1.tgz", - "integrity": "sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.0.tgz", + "integrity": "sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==", "license": "BSD-3-Clause", "dependencies": { "@lit-labs/ssr-dom-shim": "^1.2.0", - "@lit/reactive-element": "^2.0.4", - "lit-html": "^3.2.0" + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" } }, "node_modules/lit-html": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.1.tgz", - "integrity": "sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.0.tgz", + "integrity": "sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw==", "license": "BSD-3-Clause", "dependencies": { "@types/trusted-types": "^2.0.2" @@ -27524,9 +27459,9 @@ } }, "node_modules/micromark": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.1.tgz", - "integrity": "sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", "dev": true, "funding": [ { @@ -27560,9 +27495,9 @@ } }, "node_modules/micromark-core-commonmark": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.2.tgz", - "integrity": "sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", "dev": true, "funding": [ { @@ -28058,9 +27993,9 @@ } }, "node_modules/micromark-util-subtokenize": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.4.tgz", - "integrity": "sha512-N6hXjrin2GTJDe3MVjf5FuXpm12PGm80BrUAeub9XFXca8JZbP+oIwY4LJSVwFUCL1IPm/WwSVUN7goFHmSGGQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", "dev": true, "funding": [ { @@ -28098,9 +28033,9 @@ "license": "MIT" }, "node_modules/micromark-util-types": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz", - "integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "dev": true, "funding": [ { @@ -28564,9 +28499,9 @@ "license": "MIT" }, "node_modules/msgpackr": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", - "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.4.tgz", + "integrity": "sha512-uaff7RG9VIC4jacFW9xzL3jc0iM32DNHe4jYVycBcjUePT/Klnfj7pqtWJt9khvDFizmjN2TlYniYmSS2LIaZg==", "dev": true, "license": "MIT", "optionalDependencies": { @@ -28698,9 +28633,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -28724,9 +28659,9 @@ "license": "MIT" }, "node_modules/napi-postinstall": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.2.tgz", - "integrity": "sha512-Wy1VI/hpKHwy1MsnFxHCJxqFwmmxD0RA/EKPL7e6mfbsY01phM2SZyJnRdU0bLvhu0Quby1DCcAZti3ghdl4/A==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.4.tgz", + "integrity": "sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg==", "dev": true, "license": "MIT", "bin": { @@ -28840,9 +28775,9 @@ } }, "node_modules/node-abi": { - "version": "3.74.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", - "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", "dev": true, "license": "MIT", "dependencies": { @@ -28869,9 +28804,9 @@ } }, "node_modules/node-api-version": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.0.tgz", - "integrity": "sha512-fthTTsi8CxaBXMaBAD7ST2uylwvsnYxh2PfaScwpMhos6KlSFajXQPcM4ogNE1q2s3Lbz9GCGqeIHC+C6OZnKg==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -29258,7 +29193,8 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/node-preload": { "version": "0.2.1", @@ -29737,9 +29673,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz", - "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==", + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", "license": "MIT" }, "node_modules/nx": { @@ -29748,6 +29684,7 @@ "integrity": "sha512-+BN5B5DFBB5WswD8flDDTnr4/bf1VTySXOv60aUAllHqR+KS6deT0p70TTMZF4/A2n/L2UCWDaDro37MGaYozA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", "@yarnpkg/lockfile": "^1.1.0", @@ -29813,50 +29750,12 @@ } } }, - "node_modules/nx/node_modules/cli-spinners": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", - "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nx/node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/nx/node_modules/dotenv-expand": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", - "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", - "dev": true, - "dependencies": { - "dotenv": "^16.4.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/nx/node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -29865,22 +29764,15 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, - "node_modules/nx/node_modules/lines-and-columns": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", - "integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==", "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } + "license": "MIT" }, "node_modules/nx/node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -29896,6 +29788,7 @@ "resolved": "https://registry.npmjs.org/ora/-/ora-5.3.0.tgz", "integrity": "sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==", "dev": true, + "license": "MIT", "dependencies": { "bl": "^4.0.3", "chalk": "^4.1.0", @@ -29918,6 +29811,7 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -29927,6 +29821,7 @@ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.14" } @@ -29936,6 +29831,7 @@ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, + "license": "MIT", "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", @@ -31012,6 +30908,12 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, + "node_modules/parse-json/node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -31291,9 +31193,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", - "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", "dev": true, "license": "ISC", "engines": { @@ -31391,9 +31293,9 @@ } }, "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, "license": "MIT", "engines": { @@ -31502,9 +31404,9 @@ } }, "node_modules/pkg-dir/node_modules/yocto-queue": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", - "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", "dev": true, "license": "MIT", "engines": { @@ -32128,9 +32030,9 @@ } }, "node_modules/prismjs": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", - "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", "dev": true, "license": "MIT", "engines": { @@ -32587,6 +32489,23 @@ "node": ">=12.0.0" } }, + "node_modules/read-config-file/node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/read-config-file/node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -32641,9 +32560,9 @@ } }, "node_modules/recast": { - "version": "0.23.9", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.9.tgz", - "integrity": "sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==", + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", "dev": true, "license": "MIT", "dependencies": { @@ -32750,16 +32669,6 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, "node_modules/regex-parser": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", @@ -33253,9 +33162,9 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -33732,20 +33641,19 @@ "optional": true }, "node_modules/send": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", - "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.5", - "destroy": "^1.2.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", - "fresh": "^0.5.2", + "fresh": "^2.0.0", "http-errors": "^2.0.0", - "mime-types": "^2.1.35", + "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", @@ -33765,6 +33673,39 @@ "node": ">= 0.8" } }, + "node_modules/send/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/send/node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -34317,16 +34258,13 @@ "license": "MIT" }, "node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/slice-ansi": { @@ -35236,6 +35174,13 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/sucrase/node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/sucrase/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -35327,11 +35272,15 @@ "license": "MIT" }, "node_modules/tablesort": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/tablesort/-/tablesort-5.3.0.tgz", - "integrity": "sha512-WkfcZBHsp47gVH9CBHG0ZXopriG01IA87arGrchvIe868d4RiXVvoYPS1zMq9IdW05kBs5iGsqxTABqLyWonbg==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/tablesort/-/tablesort-5.6.0.tgz", + "integrity": "sha512-cZZXK3G089PbpxH8N7vN7Z21SEKqXAaCiSVOmZdR/v7z8TFCsF/OFr0rzjhQuFlQQHy9uQtW9P2oQFJzJFGVrg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 16", + "npm": ">= 8" + } }, "node_modules/tailwindcss": { "version": "3.4.17", @@ -35450,9 +35399,9 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", "dev": true, "license": "MIT", "engines": { @@ -35587,9 +35536,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", - "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "dev": true, "license": "MIT", "dependencies": { @@ -35840,9 +35789,9 @@ } }, "node_modules/tldts-core": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.4.tgz", - "integrity": "sha512-9/IRbnIvUENGD6rg7m6Q9h/jH5ZL28hwjAhxrJx0AmcBue1FSsc84XZFaV748EsDVflid86aGDR11eSz6sbQjA==", + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.7.tgz", + "integrity": "sha512-ECqb8imSroX1UmUuhRBNPkkmtZ8mHEenieim80UVxG0M5wXVjY2Fp2tYXCPvk+nLy1geOhFpeD5YQhM/gF63Jg==", "license": "MIT" }, "node_modules/tmp": { @@ -35930,9 +35879,9 @@ } }, "node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -35942,9 +35891,9 @@ } }, "node_modules/tree-dump": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", - "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.3.tgz", + "integrity": "sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -35990,9 +35939,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { @@ -36647,6 +36596,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -38173,9 +38123,9 @@ } }, "node_modules/webpack-dev-middleware/node_modules/memfs": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.0.tgz", - "integrity": "sha512-4eirfZ7thblFmqFjywlTmuWVSvccHAJbn1r8qQLzmTO11qcqpohOjmY2mFce6x7x7WtskzRqApPD0hv+Oa74jg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz", + "integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -38263,9 +38213,9 @@ } }, "node_modules/webpack-dev-server/node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "version": "4.17.22", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz", + "integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==", "dev": true, "license": "MIT", "dependencies": { @@ -38340,9 +38290,9 @@ } }, "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", - "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -38394,9 +38344,9 @@ } }, "node_modules/webpack-dev-server/node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", "dev": true, "license": "MIT", "dependencies": { @@ -38522,9 +38472,9 @@ "license": "MIT" }, "node_modules/webpack/node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, "funding": [ { @@ -38542,10 +38492,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -38632,12 +38582,12 @@ } }, "node_modules/whatwg-url": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", - "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "license": "MIT", "dependencies": { - "tr46": "^5.0.0", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { @@ -38734,16 +38684,17 @@ "license": "ISC" }, "node_modules/which-typed-array": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", - "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "for-each": "^0.3.3", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, @@ -38896,9 +38847,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -38975,15 +38926,15 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index 9831417de59..b1e91d92771 100644 --- a/package.json +++ b/package.json @@ -211,6 +211,7 @@ "@storybook/angular": { "zone.js": "$zone.js" }, + "parse5": "7.2.1", "react": "18.3.1", "react-dom": "18.3.1", "@types/react": "18.3.20", From 2e4b310137ac4b24128c185420de42a7d93b1d76 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 11:04:35 +0200 Subject: [PATCH 113/163] [deps] Platform: Pin dependencies (#14446) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/desktop/desktop_native/Cargo.lock | 10 +++++----- apps/desktop/desktop_native/Cargo.toml | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 242c1e1b9f2..e5d90446ddc 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -498,9 +498,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "camino" @@ -1598,7 +1598,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" dependencies = [ "cfg-if", - "windows-targets 0.53.0", + "windows-targets 0.52.6", ] [[package]] @@ -2920,9 +2920,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.35.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79251336d17c72d9762b8b54be4befe38d2db56fbbc0241396d70f173c39d47a" +checksum = "b897c8ea620e181c7955369a31be5f48d9a9121cb59fd33ecef9ff2a34323422" dependencies = [ "libc", "memchr", diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 81149164253..71da53c867d 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -14,10 +14,10 @@ anyhow = "=1.0.94" arboard = { version = "=3.5.0", default-features = false } argon2 = "=0.5.3" base64 = "=0.22.1" -bindgen = "0.71.1" +bindgen = "=0.71.1" bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "3d48f140fd506412d186203238993163a8c4e536" } byteorder = "=1.5.0" -bytes = "1.9.0" +bytes = "=1.9.0" cbc = "=0.1.2" core-foundation = "=0.10.0" dirs = "=6.0.0" @@ -49,7 +49,7 @@ sha2 = "=0.10.8" simplelog = "=0.12.2" ssh-encoding = "=0.2.0" ssh-key = {version = "=0.6.7", default-features = false } -sysinfo = "0.35.0" +sysinfo = "=0.35.0" thiserror = "=2.0.12" tokio = "=1.45.0" tokio-stream = "=0.1.15" From ae35cb4e65af6e068cd0504272f0828cba763d1b Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Wed, 21 May 2025 08:24:17 -0400 Subject: [PATCH 114/163] [PM-20540] Deep-link refactor to fix SSO deep links (#14587) * PM-20540 - TwoFactorAuthComponent - Refactor determineDefaultSuccessRoute to rely on user's auth status as the loginStrategyService's state is cleared after successful AuthN * PM-20540 - DeepLinkGuard - Refactor to exempt login-initiated so that TDE + unlock with MP + deep link works. * doc: Add documentation and change folder structure. * test: add test for new excluded route. --------- Co-authored-by: Jared Snider --- .../organization-routing.module.ts | 2 +- .../src/app/auth/guards/deep-link.guard.ts | 58 ----------- .../{ => deep-link}/deep-link.guard.spec.ts | 14 ++- .../auth/guards/deep-link/deep-link.guard.ts | 99 +++++++++++++++++++ .../src/app/auth/guards/deep-link/readme.md | 23 +++++ apps/web/src/app/oss-routing.module.ts | 2 +- .../organizations-routing.module.ts | 2 +- .../bit-web/src/app/app-routing.module.ts | 2 +- .../two-factor-auth.component.spec.ts | 9 +- .../two-factor-auth.component.ts | 8 +- libs/common/src/platform/misc/utils.ts | 2 +- 11 files changed, 153 insertions(+), 68 deletions(-) delete mode 100644 apps/web/src/app/auth/guards/deep-link.guard.ts rename apps/web/src/app/auth/guards/{ => deep-link}/deep-link.guard.spec.ts (92%) create mode 100644 apps/web/src/app/auth/guards/deep-link/deep-link.guard.ts create mode 100644 apps/web/src/app/auth/guards/deep-link/readme.md diff --git a/apps/web/src/app/admin-console/organizations/organization-routing.module.ts b/apps/web/src/app/admin-console/organizations/organization-routing.module.ts index e5c68b73546..4d8971f74fd 100644 --- a/apps/web/src/app/admin-console/organizations/organization-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/organization-routing.module.ts @@ -14,7 +14,7 @@ import { } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { deepLinkGuard } from "../../auth/guards/deep-link.guard"; +import { deepLinkGuard } from "../../auth/guards/deep-link/deep-link.guard"; import { VaultModule } from "./collections/vault.module"; import { isEnterpriseOrgGuard } from "./guards/is-enterprise-org.guard"; diff --git a/apps/web/src/app/auth/guards/deep-link.guard.ts b/apps/web/src/app/auth/guards/deep-link.guard.ts deleted file mode 100644 index 387e7b17e88..00000000000 --- a/apps/web/src/app/auth/guards/deep-link.guard.ts +++ /dev/null @@ -1,58 +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 { CanActivateFn, Router } from "@angular/router"; - -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; - -import { RouterService } from "../../core/router.service"; - -/** - * Guard to persist and apply deep links to handle users who are not unlocked. - * @returns returns true. If user is not Unlocked will store URL to state for redirect once - * user is unlocked/Authenticated. - */ -export function deepLinkGuard(): CanActivateFn { - return async (route, routerState) => { - // Inject Services - const authService = inject(AuthService); - const router = inject(Router); - const routerService = inject(RouterService); - - // Fetch State - const currentUrl = routerState.url; - const transientPreviousUrl = routerService.getPreviousUrl(); - const authStatus = await authService.getAuthStatus(); - - // Evaluate State - /** before anything else, check if the user is already unlocked. */ - if (authStatus === AuthenticationStatus.Unlocked) { - const persistedPreLoginUrl = await routerService.getAndClearLoginRedirectUrl(); - if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) { - return router.navigateByUrl(persistedPreLoginUrl); - } - return true; - } - /** - * At this point the user is either `locked` or `loggedOut`, it doesn't matter. - * We opt to persist the currentUrl over the transient previousUrl. This supports - * the case where a user is locked out of their vault and they deep link from - * the "lock" page. - * - * When the user is locked out of their vault the currentUrl contains "lock" so it will - * not be persisted, the previousUrl will be persisted instead. - */ - if (isValidUrl(currentUrl)) { - await routerService.persistLoginRedirectUrl(currentUrl); - } else if (isValidUrl(transientPreviousUrl)) { - await routerService.persistLoginRedirectUrl(transientPreviousUrl); - } - return true; - }; - - function isValidUrl(url: string | null | undefined): boolean { - return !Utils.isNullOrEmpty(url) && !url?.toLocaleLowerCase().includes("/lock"); - } -} diff --git a/apps/web/src/app/auth/guards/deep-link.guard.spec.ts b/apps/web/src/app/auth/guards/deep-link/deep-link.guard.spec.ts similarity index 92% rename from apps/web/src/app/auth/guards/deep-link.guard.spec.ts rename to apps/web/src/app/auth/guards/deep-link/deep-link.guard.spec.ts index 82ed004cf54..dba4dbd8357 100644 --- a/apps/web/src/app/auth/guards/deep-link.guard.spec.ts +++ b/apps/web/src/app/auth/guards/deep-link/deep-link.guard.spec.ts @@ -7,7 +7,7 @@ import { MockProxy, mock } from "jest-mock-extended"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { RouterService } from "../../core/router.service"; +import { RouterService } from "../../../core/router.service"; import { deepLinkGuard } from "./deep-link.guard"; @@ -99,6 +99,18 @@ describe("Deep Link Guard", () => { expect(routerService.persistLoginRedirectUrl).not.toHaveBeenCalled(); }); + it('should not persist routerService.previousUrl when routerService.previousUrl contains "login-initiated"', async () => { + // Arrange + authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Locked); + routerService.getPreviousUrl.mockReturnValue("/login-initiated"); + + // Act + await routerHarness.navigateByUrl("/lock-route"); + + // Assert + expect(routerService.persistLoginRedirectUrl).not.toHaveBeenCalled(); + }); + // Story: User's vault times out and previousUrl is undefined it("should not persist routerService.previousUrl when routerService.previousUrl is undefined", async () => { // Arrange diff --git a/apps/web/src/app/auth/guards/deep-link/deep-link.guard.ts b/apps/web/src/app/auth/guards/deep-link/deep-link.guard.ts new file mode 100644 index 00000000000..003f0580969 --- /dev/null +++ b/apps/web/src/app/auth/guards/deep-link/deep-link.guard.ts @@ -0,0 +1,99 @@ +import { inject } from "@angular/core"; +import { CanActivateFn, Router } from "@angular/router"; + +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; + +import { RouterService } from "../../../core/router.service"; + +/** + * Guard to persist and apply deep links to handle users who are not unlocked. + * @returns returns true. If user is not Unlocked will store URL to state for redirect once + * user is unlocked/Authenticated. + */ +export function deepLinkGuard(): CanActivateFn { + return async (route, routerState) => { + // Inject Services + const authService = inject(AuthService); + const router = inject(Router); + const routerService = inject(RouterService); + + // Fetch State + const currentUrl = routerState.url; + const transientPreviousUrl = routerService.getPreviousUrl(); + const authStatus = await authService.getAuthStatus(); + + // Evaluate State + /** before anything else, check if the user is already unlocked. */ + if (authStatus === AuthenticationStatus.Unlocked) { + const persistedPreLoginUrl: string | undefined = + await routerService.getAndClearLoginRedirectUrl(); + if (persistedPreLoginUrl === undefined) { + // Url us undefined, so there is nothing to navigate to. + return true; + } + // Check if the url is empty or null + if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) { + // const urlTree: string | UrlTree = persistedPreLoginUrl; + return router.navigateByUrl(persistedPreLoginUrl); + } + return true; + } + /** + * At this point the user is either `locked` or `loggedOut`, it doesn't matter. + * We opt to persist the currentUrl over the transient previousUrl. This supports + * the case where a user is locked out of their vault and they deep link from + * the "lock" page. + * + * When the user is locked out of their vault the currentUrl contains "lock" so it will + * not be persisted, the previousUrl will be persisted instead. + */ + if (isValidUrl(currentUrl)) { + await routerService.persistLoginRedirectUrl(currentUrl); + } else if (isValidUrl(transientPreviousUrl) && transientPreviousUrl !== undefined) { + await routerService.persistLoginRedirectUrl(transientPreviousUrl); + } + return true; + }; + + /** + * Check if the URL is valid for deep linking. A valid url is described as not including + * "lock" or "login-initiated". Valid urls are only urls that are not part of login or + * decryption flows. + * We ignore the "lock" url because standard SSO flows will send users to the lock component. + * We ignore "login-initiated" because TDE users decrypting with master passwords are + * sent to the lock component. + * @param url The URL to check. + * @returns True if the URL is valid, false otherwise. + */ + function isValidUrl(url: string | null | undefined): boolean { + if (url === undefined || url === null) { + return false; + } + + if (Utils.isNullOrEmpty(url)) { + return false; + } + const lowerCaseUrl: string = url.toLocaleLowerCase(); + + /** + * "Login-initiated" ignored because it is used for TDE users decrypting from a new device. A TDE user + * can opt to decrypt using their password. Decrypting with a password will send the user to the lock component, + * which is protected by the deep link guard. We don't persist the `login-initiated` url because it is not a + * valid deep-link. We don't want users to be sent to the login-initiated url when they are unlocked. + * If we did navigate to the login-initiated url, the user would get caught by the TDE Guard and be sent + * to the vault and not the intended deep link. + * + * "Lock" is ignored because users cannot deep-link to the lock component if they are already unlocked. + * Users logging in with SSO will be sent to the lock component after they are authenticated with their IdP. + * SSO users would be navigated to the "lock" component loop if we persisted the "lock" url. + */ + + if (lowerCaseUrl.includes("/login-initiated") || lowerCaseUrl.includes("/lock")) { + return false; + } + + return true; + } +} diff --git a/apps/web/src/app/auth/guards/deep-link/readme.md b/apps/web/src/app/auth/guards/deep-link/readme.md new file mode 100644 index 00000000000..82aebf95476 --- /dev/null +++ b/apps/web/src/app/auth/guards/deep-link/readme.md @@ -0,0 +1,23 @@ +# Deep-link Guard + +The `deep-link.guard.ts` supports users who are trying to access a protected route from an unauthenticated or locked state. + +This guard will persist the protected URL to session state when a user is either unauthenticated or in an encrypted/locked state. This allows users to have multiple tabs of the application running simultaneously without interfering with 'previousUrl` functionality. + +Writing to session state allows users who are authenticating through SSO to be routed to their identity provider and back without losing the protected route they were trying to access in the first place. + +The deep link guard will not persist Urls that are in the middle of authentication or decryption. SSO users will sometimes have to decrypt their vault after a successful authentication. This is why we do not persist the `/lock` route. + +## General operation + +The `deep-link.guard.ts` will always return true. The `deep-link.guard.ts` will only persist a URL if the user is in an unauthenticated or locked state. The URL cannot contain `/lock` or `/login-initiated`. The persisted URL is cleared from state when it is read. + +## Routes to protect + +The deep link guards should be used on routes where a user will be navigated to a protected route but may not be authenticated, decrypted, or have an account. + +A use cases is the `emergency-access` route which is a link that is sent to the user's email address, and in order for them to accept the request, they must first authenticate and decrypt. + +## TDE Users decrypting/unlocking with password + +For TDE users opting to decrypt with a password they will be routed from the `login-initiated` to the `lock` route. We ignore the `login-initiated` route for this reason allowing TDE users who decrypt/unlock with a password to still be navigated to the initial request. diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 37da9a35f69..0d6ffb88ad6 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -48,7 +48,7 @@ import { VerifyRecoverDeleteOrgComponent } from "./admin-console/organizations/m import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/sponsorships/accept-family-sponsorship.component"; import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component"; import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component"; -import { deepLinkGuard } from "./auth/guards/deep-link.guard"; +import { deepLinkGuard } from "./auth/guards/deep-link/deep-link.guard"; import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component"; import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component"; import { RecoverDeleteComponent } from "./auth/recover-delete.component"; diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts index a995b0cb1f4..f63140a8b23 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts @@ -6,7 +6,7 @@ import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractio import { isEnterpriseOrgGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/is-enterprise-org.guard"; import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard"; import { OrganizationLayoutComponent } from "@bitwarden/web-vault/app/admin-console/organizations/layouts/organization-layout.component"; -import { deepLinkGuard } from "@bitwarden/web-vault/app/auth/guards/deep-link.guard"; +import { deepLinkGuard } from "@bitwarden/web-vault/app/auth/guards/deep-link/deep-link.guard"; import { SsoComponent } from "../../auth/sso/sso.component"; diff --git a/bitwarden_license/bit-web/src/app/app-routing.module.ts b/bitwarden_license/bit-web/src/app/app-routing.module.ts index 6aed12511c1..3f2803695eb 100644 --- a/bitwarden_license/bit-web/src/app/app-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/app-routing.module.ts @@ -3,7 +3,7 @@ import { RouterModule, Routes } from "@angular/router"; import { unauthGuardFn } from "@bitwarden/angular/auth/guards"; import { AnonLayoutWrapperComponent } from "@bitwarden/auth/angular"; -import { deepLinkGuard } from "@bitwarden/web-vault/app/auth/guards/deep-link.guard"; +import { deepLinkGuard } from "@bitwarden/web-vault/app/auth/guards/deep-link/deep-link.guard"; import { RouteDataProperties } from "@bitwarden/web-vault/app/core"; import { ProvidersModule } from "./admin-console/providers/providers.module"; diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts index de3fa7a3321..e52f08941d4 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts @@ -16,8 +16,10 @@ import { } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -72,6 +74,7 @@ describe("TwoFactorAuthComponent", () => { let mockEnvService: MockProxy; let mockLoginSuccessHandlerService: MockProxy; let mockTwoFactorAuthCompCacheService: MockProxy; + let mockAuthService: MockProxy; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -106,6 +109,7 @@ describe("TwoFactorAuthComponent", () => { mockDialogService = mock(); mockToastService = mock(); mockTwoFactorAuthCompService = mock(); + mockAuthService = mock(); mockEnvService = mock(); mockLoginSuccessHandlerService = mock(); @@ -204,6 +208,7 @@ describe("TwoFactorAuthComponent", () => { provide: TwoFactorAuthComponentCacheService, useValue: mockTwoFactorAuthCompCacheService, }, + { provide: AuthService, useValue: mockAuthService }, ], }); @@ -295,6 +300,7 @@ describe("TwoFactorAuthComponent", () => { it("navigates to the component's defined success route (vault is default) when the login is successful", async () => { mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult()); + mockAuthService.activeAccountStatus$ = new BehaviorSubject(AuthenticationStatus.Unlocked); // Act await component.submit("testToken"); @@ -316,13 +322,14 @@ describe("TwoFactorAuthComponent", () => { async (authType, expectedRoute) => { mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult()); currentAuthTypeSubject.next(authType); + mockAuthService.activeAccountStatus$ = new BehaviorSubject(AuthenticationStatus.Locked); // Act await component.submit("testToken"); // Assert expect(mockRouter.navigate).toHaveBeenCalledTimes(1); - expect(mockRouter.navigate).toHaveBeenCalledWith(["lock"], { + expect(mockRouter.navigate).toHaveBeenCalledWith([expectedRoute], { queryParams: { identifier: component.orgSsoIdentifier, }, diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index 91901fa3544..b14a368e066 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -24,9 +24,10 @@ import { LoginSuccessHandlerService, } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; -import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -167,6 +168,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { private environmentService: EnvironmentService, private loginSuccessHandlerService: LoginSuccessHandlerService, private twoFactorAuthComponentCacheService: TwoFactorAuthComponentCacheService, + private authService: AuthService, ) {} async ngOnInit() { @@ -507,8 +509,8 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { } private async determineDefaultSuccessRoute(): Promise { - const authType = await firstValueFrom(this.loginStrategyService.currentAuthType$); - if (authType == AuthenticationType.Sso || authType == AuthenticationType.UserApiKey) { + const activeAccountStatus = await firstValueFrom(this.authService.activeAccountStatus$); + if (activeAccountStatus === AuthenticationStatus.Locked) { return "lock"; } diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index 203a04851c5..f9c5ab4a843 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -391,7 +391,7 @@ export class Utils { return str == null || typeof str !== "string" || str.trim() === ""; } - static isNullOrEmpty(str: string): boolean { + static isNullOrEmpty(str: string | null): boolean { return str == null || typeof str !== "string" || str == ""; } From 1c4d851046eca62a39e4c82c58f898eebd0b0946 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 21 May 2025 08:00:49 -0500 Subject: [PATCH 115/163] [PM-21005] Clear Add/Edit form cache when browser loses focus (#14634) --- .../view-cache/popup-view-cache.service.ts | 2 + .../popup-view-cache-background.service.ts | 40 ++++++++++++++++++- .../add-edit/add-edit-v2.component.ts | 22 ++++++++-- .../new-item-dropdown-v2.component.spec.ts | 3 +- .../new-item-dropdown-v2.component.ts | 14 +++---- .../platform/view-cache/view-cache.service.ts | 6 +++ .../default-cipher-form-cache.service.ts | 1 + 7 files changed, 73 insertions(+), 15 deletions(-) diff --git a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts index 2a946982990..83d6edbc141 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts @@ -81,6 +81,7 @@ export class PopupViewCacheService implements ViewCacheService { injector = inject(Injector), initialValue, persistNavigation, + clearOnTabChange, } = options; const cachedValue = this.cache[key]?.value ? deserializer(JSON.parse(this.cache[key].value)) @@ -89,6 +90,7 @@ export class PopupViewCacheService implements ViewCacheService { const viewCacheOptions = { ...(persistNavigation && { persistNavigation }), + ...(clearOnTabChange && { clearOnTabChange }), }; effect( diff --git a/apps/browser/src/platform/services/popup-view-cache-background.service.ts b/apps/browser/src/platform/services/popup-view-cache-background.service.ts index 79c04e90aad..49eae15fbbd 100644 --- a/apps/browser/src/platform/services/popup-view-cache-background.service.ts +++ b/apps/browser/src/platform/services/popup-view-cache-background.service.ts @@ -1,4 +1,4 @@ -import { switchMap, delay, filter, concatMap } from "rxjs"; +import { switchMap, delay, filter, concatMap, map, first, of } from "rxjs"; import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging"; import { @@ -12,6 +12,7 @@ import { GlobalStateProvider, } from "@bitwarden/common/platform/state"; +import { BrowserApi } from "../browser/browser-api"; import { fromChromeEvent } from "../browser/from-chrome-event"; const popupClosedPortName = "new_popup"; @@ -21,6 +22,12 @@ export type ViewCacheOptions = { * Optional flag to persist the cached value between navigation events. */ persistNavigation?: boolean; + + /** + * When set, the cached value will be cleared when the user changes tabs. + * @optional + */ + clearOnTabChange?: true; }; export type ViewCacheState = { @@ -129,6 +136,37 @@ export class PopupViewCacheBackgroundService { ), ) .subscribe(); + + // On tab changed, excluding extension tabs + fromChromeEvent(chrome.tabs.onActivated) + .pipe( + switchMap((tabs) => BrowserApi.getTab(tabs[0].tabId)!), + switchMap((tab) => { + // FireFox sets the `url` to "about:blank" and won't populate the `url` until the `onUpdated` event + if (tab.url !== "about:blank") { + return of(tab); + } + + return fromChromeEvent(chrome.tabs.onUpdated).pipe( + first(), + switchMap(([tabId]) => BrowserApi.getTab(tabId)!), + ); + }), + map((tab) => tab.url || tab.pendingUrl), + filter((url) => !url?.startsWith(chrome.runtime.getURL(""))), + switchMap(() => + this.popupViewCacheState.update((state) => { + if (!state) { + return null; + } + // Only remove keys that are marked with `clearOnTabChange` + return Object.fromEntries( + Object.entries(state).filter(([, { options }]) => !options?.clearOnTabChange), + ); + }), + ), + ) + .subscribe(); } async clearState() { diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index 30fd57a6bc6..e47f4637199 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -14,6 +14,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -40,6 +41,7 @@ import { } from "@bitwarden/vault"; import { BrowserFido2UserInterfaceSession } from "../../../../../autofill/fido2/services/browser-fido2-user-interface.service"; +import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component"; @@ -70,6 +72,7 @@ class QueryParams { this.uri = params.uri; this.username = params.username; this.name = params.name; + this.prefillNameAndURIFromTab = params.prefillNameAndURIFromTab; } /** @@ -116,6 +119,12 @@ class QueryParams { * Optional name to pre-fill for the cipher. */ name?: string; + + /** + * Optional flag to pre-fill the name and URI from the current tab. + * NOTE: This will override the `uri` and `name` query parameters if set to true. + */ + prefillNameAndURIFromTab?: true; } export type AddEditQueryParams = Partial>; @@ -281,8 +290,7 @@ export class AddEditV2Component implements OnInit { if (config.mode === "edit" && !config.originalCipher.edit) { config.mode = "partial-edit"; } - - config.initialValues = this.setInitialValuesFromParams(params); + config.initialValues = await this.setInitialValuesFromParams(params); const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(getUserId), @@ -326,7 +334,7 @@ export class AddEditV2Component implements OnInit { }); } - setInitialValuesFromParams(params: QueryParams) { + async setInitialValuesFromParams(params: QueryParams) { const initialValues = {} as OptionalInitialValues; if (params.folderId) { initialValues.folderId = params.folderId; @@ -346,6 +354,14 @@ export class AddEditV2Component implements OnInit { if (params.name) { initialValues.name = params.name; } + + if (params.prefillNameAndURIFromTab) { + const tab = await BrowserApi.getTabFromCurrentWindow(); + + initialValues.loginUri = tab.url; + initialValues.name = Utils.getHostname(tab.url); + } + return initialValues; } diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts index a9b92274c9e..54c6ba2f788 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts @@ -94,8 +94,7 @@ describe("NewItemDropdownV2Component", () => { collectionId: "777-888-999", organizationId: "444-555-666", folderId: "222-333-444", - uri: "https://example.com", - name: "example.com", + prefillNameAndURIFromTab: "true", }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts index bb452b89c7b..1bcc4297b71 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts @@ -2,10 +2,9 @@ // @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, Input, OnInit } from "@angular/core"; -import { Router, RouterLink } from "@angular/router"; +import { RouterLink } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components"; @@ -35,10 +34,8 @@ export class NewItemDropdownV2Component implements OnInit { */ @Input() initialValues: NewItemInitialValues; - constructor( - private router: Router, - private dialogService: DialogService, - ) {} + + constructor(private dialogService: DialogService) {} async ngOnInit() { this.tab = await BrowserApi.getTabFromCurrentWindow(); @@ -47,13 +44,12 @@ export class NewItemDropdownV2Component implements OnInit { buildQueryParams(type: CipherType): AddEditQueryParams { const poppedOut = BrowserPopupUtils.inPopout(window); - const loginDetails: { uri?: string; name?: string } = {}; + const loginDetails: { prefillNameAndURIFromTab?: string } = {}; // When a Login Cipher is created and the extension is not popped out, // pass along the uri and name if (!poppedOut && type === CipherType.Login && this.tab) { - loginDetails.uri = this.tab.url; - loginDetails.name = Utils.getHostname(this.tab.url); + loginDetails.prefillNameAndURIFromTab = "true"; } return { diff --git a/libs/angular/src/platform/view-cache/view-cache.service.ts b/libs/angular/src/platform/view-cache/view-cache.service.ts index 386ddc7bdd1..ed74ac0ba57 100644 --- a/libs/angular/src/platform/view-cache/view-cache.service.ts +++ b/libs/angular/src/platform/view-cache/view-cache.service.ts @@ -23,6 +23,12 @@ type BaseCacheOptions = { * Optional flag to persist the cached value between navigation events. */ persistNavigation?: boolean; + + /** + * When set, the cached value will be cleared when the user changes tabs. + * @optional + */ + clearOnTabChange?: true; } & (T extends JsonValue ? Deserializer : Required>); export type SignalCacheOptions = BaseCacheOptions & { diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts index b4a8138e025..521cc09f140 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts @@ -27,6 +27,7 @@ export class CipherFormCacheService { key: CIPHER_FORM_CACHE_KEY, initialValue: null, deserializer: CipherView.fromJSON, + clearOnTabChange: true, }); constructor() { From cf7da2ebdcf4535d7be90cefa47208442e814c8a Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Wed, 21 May 2025 11:28:22 -0400 Subject: [PATCH 116/163] Added data validation where it was missing to upgrade dialog (#14866) --- .../change-plan-dialog.component.ts | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index 49c5bb775b1..9b6694a3bbe 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -627,6 +627,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } get passwordManagerSubtotal() { + if (!this.selectedPlan || !this.selectedPlan.PasswordManager) { + return 0; + } + let subTotal = this.selectedPlan.PasswordManager.basePrice; if (this.selectedPlan.PasswordManager.hasAdditionalSeatsOption) { subTotal += this.passwordManagerSeatTotal(this.selectedPlan); @@ -638,10 +642,12 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } secretsManagerSubtotal() { - this.secretsManagerTotal = 0; - const plan = this.selectedSecretsManagerPlan; + const plan = this.selectedPlan; + if (!plan || !plan.SecretsManager) { + return this.secretsManagerTotal || 0; + } - if (!this.organization.useSecretsManager) { + if (this.secretsManagerTotal) { return this.secretsManagerTotal; } @@ -653,6 +659,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } get passwordManagerSeats() { + if (!this.selectedPlan) { + return 0; + } + if (this.selectedPlan.productTier === ProductTierType.Families) { return this.selectedPlan.PasswordManager.baseSeats; } @@ -660,7 +670,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } get total() { - if (this.organization && this.organization.useSecretsManager) { + if (!this.organization || !this.selectedPlan) { + return 0; + } + + if (this.organization.useSecretsManager) { return ( this.passwordManagerSubtotal + this.additionalStorageTotal(this.selectedPlan) + @@ -680,6 +694,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } get additionalServiceAccount() { + if (!this.currentPlan || !this.currentPlan.SecretsManager) { + return 0; + } + const baseServiceAccount = this.currentPlan.SecretsManager?.baseServiceAccount || 0; const usedServiceAccounts = this.sub?.smServiceAccounts || 0; @@ -1096,8 +1114,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { get submitButtonLabel(): string { if ( + this.organization && + this.sub && this.organization.productTierType !== ProductTierType.Free && - this.sub.subscription.status === "canceled" + this.sub.subscription?.status === "canceled" ) { return this.i18nService.t("restart"); } else { From fd10a26df9903e5418dc782dd2981c5bfdf59ad5 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 21 May 2025 13:46:02 -0400 Subject: [PATCH 117/163] [PM-18804] generator nudges (#14705) * added generator spotlight to credential generator component * moved generator spotlight to browser component and add as slot in libs. update copy for send * added an aria label for the generator nudge body content * new copy and styles for browser send will be behind feature flag * update featureflag call to observable in send-v2 * changed how nudge text is made in credential generator * added new observable to vault nudges to return specific boolean. Update naming of vault types. update observable calls in credential-generator and send-v2 * update send-v2 and credential generator to use new renamed nudges * update to create nudge generator spotlight component. using this inside the credential generator for nudge spotlight * fix imports for Nudge related code * add libs/angular to storybook --------- Co-authored-by: Nick Krantz --- .storybook/main.ts | 1 + apps/browser/src/_locales/en/messages.json | 25 ++++++++++++ .../popup/settings/autofill.component.ts | 2 +- .../popup/send-v2/send-v2.component.html | 32 +++++++++++----- .../popup/send-v2/send-v2.component.spec.ts | 2 + .../tools/popup/send-v2/send-v2.component.ts | 24 +++++++----- .../components/vault-v2/vault-v2.component.ts | 5 +-- .../spotlight/spotlight.component.html | 0 .../spotlight/spotlight.component.ts | 0 .../components/spotlight/spotlight.stories.ts | 0 .../src/vault/services/nudges.service.ts | 2 + .../src/credential-generator.component.html | 2 + .../components/src/generator.module.ts | 2 + .../nudge-generator-spotlight.component.html | 15 ++++++++ .../nudge-generator-spotlight.component.ts | 38 +++++++++++++++++++ libs/tools/generator/components/tsconfig.json | 3 +- .../new-send-dropdown.component.html | 8 +++- .../new-send-dropdown.component.ts | 3 +- .../new-item-nudge.component.ts | 3 +- libs/vault/src/index.ts | 2 - 20 files changed, 139 insertions(+), 30 deletions(-) rename libs/{vault/src => angular/src/vault}/components/spotlight/spotlight.component.html (100%) rename libs/{vault/src => angular/src/vault}/components/spotlight/spotlight.component.ts (100%) rename libs/{vault/src => angular/src/vault}/components/spotlight/spotlight.stories.ts (100%) create mode 100644 libs/tools/generator/components/src/nudge-generator-spotlight.component.html create mode 100644 libs/tools/generator/components/src/nudge-generator-spotlight.component.ts diff --git a/.storybook/main.ts b/.storybook/main.ts index 9583d1fc6f2..d5d116e99be 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -22,6 +22,7 @@ const config: StorybookConfig = { "../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)", "../libs/tools/card/src/**/*.mdx", "../libs/tools/card/src/**/*.stories.@(js|jsx|ts|tsx)", + "../libs/angular/src/**/*.stories.@(js|jsx|ts|tsx)", ], addons: [ getAbsolutePath("@storybook/addon-links"), diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 99ca31bafd5..4775d1f7af0 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5350,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.ts b/apps/browser/src/autofill/popup/settings/autofill.component.ts index 0605f5a03a6..e1a5a2fc218 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill.component.ts @@ -15,6 +15,7 @@ import { filter, firstValueFrom, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; +import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { @@ -56,7 +57,6 @@ import { SelectModule, TypographyModule, } from "@bitwarden/components"; -import { SpotlightComponent } from "@bitwarden/vault"; import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.html b/apps/browser/src/tools/popup/send-v2/send-v2.component.html index d51bda45b55..d271f67fa3b 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.html @@ -20,15 +20,29 @@ *ngIf="listState === sendState.Empty" class="tw-flex tw-flex-col tw-h-full tw-justify-center" > - - {{ "sendsNoItemsTitle" | i18n }} - {{ "sendsNoItemsMessage" | i18n }} - - + + + {{ "sendsNoItemsTitle" | i18n }} + {{ "sendsNoItemsMessage" | i18n }} + + + + + + {{ "sendsTitleNoItems" | i18n }} + {{ "sendsBodyNoItems" | i18n }} + + +
diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts index 6fc4793f5c0..c1f8e9fb263 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts @@ -6,6 +6,7 @@ import { MockProxy, mock } from "jest-mock-extended"; import { of, BehaviorSubject } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { NudgesService } from "@bitwarden/angular/vault"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -121,6 +122,7 @@ describe("SendV2Component", () => { { provide: SendListFiltersService, useValue: sendListFiltersService }, { provide: PopupRouterCacheService, useValue: mock() }, { provide: PolicyService, useValue: policyService }, + { provide: NudgesService, useValue: mock() }, ], }).compileComponents(); diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts index def425a51a5..9fc19e98b34 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts @@ -1,10 +1,10 @@ import { CommonModule } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component, OnDestroy } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { RouterLink } from "@angular/router"; -import { combineLatest, switchMap } from "rxjs"; +import { combineLatest, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -12,13 +12,13 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { ButtonModule, CalloutModule, Icons, NoItemsModule } from "@bitwarden/components"; import { - NoSendsIcon, NewSendDropdownComponent, - SendListItemsContainerComponent, + NoSendsIcon, SendItemsService, - SendSearchComponent, SendListFiltersComponent, SendListFiltersService, + SendListItemsContainerComponent, + SendSearchComponent, } from "@bitwarden/send-ui"; import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component"; @@ -46,14 +46,13 @@ export enum SendState { JslibModule, CommonModule, ButtonModule, - RouterLink, NewSendDropdownComponent, SendListItemsContainerComponent, SendListFiltersComponent, SendSearchComponent, ], }) -export class SendV2Component implements OnInit, OnDestroy { +export class SendV2Component implements OnDestroy { sendType = SendType; sendState = SendState; @@ -63,6 +62,12 @@ export class SendV2Component implements OnInit, OnDestroy { protected title: string = "allSends"; protected noItemIcon = NoSendsIcon; protected noResultsIcon = Icons.NoResults; + private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); + protected showSendSpotlight$: Observable = this.activeUserId$.pipe( + switchMap((userId) => + this.nudgesService.showNudgeSpotlight$(NudgeType.SendNudgeStatus, userId), + ), + ); protected sendsDisabled = false; @@ -71,6 +76,7 @@ export class SendV2Component implements OnInit, OnDestroy { protected sendListFiltersService: SendListFiltersService, private policyService: PolicyService, private accountService: AccountService, + private nudgesService: NudgesService, ) { combineLatest([ this.sendItemsService.emptyList$, @@ -111,7 +117,5 @@ export class SendV2Component implements OnInit, OnDestroy { }); } - ngOnInit(): void {} - ngOnDestroy(): void {} } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index 474524cf883..db853d45940 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -17,10 +17,10 @@ import { import { JslibModule } from "@bitwarden/angular/jslib.module"; import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; +import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -31,7 +31,7 @@ import { NoItemsModule, TypographyModule, } from "@bitwarden/components"; -import { DecryptionFailureDialogComponent, SpotlightComponent, VaultIcons } from "@bitwarden/vault"; +import { DecryptionFailureDialogComponent, VaultIcons } from "@bitwarden/vault"; import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component"; import { BrowserApi } from "../../../../platform/browser/browser-api"; @@ -154,7 +154,6 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { private introCarouselService: IntroCarouselService, private nudgesService: NudgesService, private router: Router, - private i18nService: I18nService, ) { combineLatest([ this.vaultPopupItemsService.emptyVault$, diff --git a/libs/vault/src/components/spotlight/spotlight.component.html b/libs/angular/src/vault/components/spotlight/spotlight.component.html similarity index 100% rename from libs/vault/src/components/spotlight/spotlight.component.html rename to libs/angular/src/vault/components/spotlight/spotlight.component.html diff --git a/libs/vault/src/components/spotlight/spotlight.component.ts b/libs/angular/src/vault/components/spotlight/spotlight.component.ts similarity index 100% rename from libs/vault/src/components/spotlight/spotlight.component.ts rename to libs/angular/src/vault/components/spotlight/spotlight.component.ts diff --git a/libs/vault/src/components/spotlight/spotlight.stories.ts b/libs/angular/src/vault/components/spotlight/spotlight.stories.ts similarity index 100% rename from libs/vault/src/components/spotlight/spotlight.stories.ts rename to libs/angular/src/vault/components/spotlight/spotlight.stories.ts diff --git a/libs/angular/src/vault/services/nudges.service.ts b/libs/angular/src/vault/services/nudges.service.ts index 55e6009a8e0..006f568f33e 100644 --- a/libs/angular/src/vault/services/nudges.service.ts +++ b/libs/angular/src/vault/services/nudges.service.ts @@ -40,6 +40,8 @@ export enum NudgeType { NewIdentityItemStatus = "new-identity-item-status", NewNoteItemStatus = "new-note-item-status", NewSshItemStatus = "new-ssh-item-status", + GeneratorNudgeStatus = "generator-nudge-status", + SendNudgeStatus = "send-nudge-status", } export const NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition< diff --git a/libs/tools/generator/components/src/credential-generator.component.html b/libs/tools/generator/components/src/credential-generator.component.html index e95df388f39..24146968456 100644 --- a/libs/tools/generator/components/src/credential-generator.component.html +++ b/libs/tools/generator/components/src/credential-generator.component.html @@ -11,6 +11,8 @@ + +
diff --git a/libs/tools/generator/components/src/generator.module.ts b/libs/tools/generator/components/src/generator.module.ts index f0d09b53ebb..d710f368106 100644 --- a/libs/tools/generator/components/src/generator.module.ts +++ b/libs/tools/generator/components/src/generator.module.ts @@ -22,6 +22,7 @@ import { CatchallSettingsComponent } from "./catchall-settings.component"; import { CredentialGeneratorComponent } from "./credential-generator.component"; import { ForwarderSettingsComponent } from "./forwarder-settings.component"; import { GeneratorServicesModule } from "./generator-services.module"; +import { NudgeGeneratorSpotlightComponent } from "./nudge-generator-spotlight.component"; import { PassphraseSettingsComponent } from "./passphrase-settings.component"; import { PasswordGeneratorComponent } from "./password-generator.component"; import { PasswordSettingsComponent } from "./password-settings.component"; @@ -48,6 +49,7 @@ import { UsernameSettingsComponent } from "./username-settings.component"; SelectModule, ToggleGroupModule, TypographyModule, + NudgeGeneratorSpotlightComponent, ], declarations: [ CatchallSettingsComponent, diff --git a/libs/tools/generator/components/src/nudge-generator-spotlight.component.html b/libs/tools/generator/components/src/nudge-generator-spotlight.component.html new file mode 100644 index 00000000000..9c65a1cea96 --- /dev/null +++ b/libs/tools/generator/components/src/nudge-generator-spotlight.component.html @@ -0,0 +1,15 @@ +
+ +

+ {{ "generatorNudgeBodyOne" | i18n }} + {{ "generatorNudgeBodyTwo" | i18n }} +

+
+
diff --git a/libs/tools/generator/components/src/nudge-generator-spotlight.component.ts b/libs/tools/generator/components/src/nudge-generator-spotlight.component.ts new file mode 100644 index 00000000000..a0008bac782 --- /dev/null +++ b/libs/tools/generator/components/src/nudge-generator-spotlight.component.ts @@ -0,0 +1,38 @@ +import { AsyncPipe, CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { firstValueFrom, Observable, switchMap } from "rxjs"; + +import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; +import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { TypographyModule } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; + +@Component({ + standalone: true, + selector: "nudge-generator-spotlight", + templateUrl: "nudge-generator-spotlight.component.html", + imports: [I18nPipe, SpotlightComponent, AsyncPipe, CommonModule, TypographyModule], +}) +export class NudgeGeneratorSpotlightComponent { + protected readonly NudgeType = NudgeType; + private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); + protected showGeneratorSpotlight$: Observable = this.activeUserId$.pipe( + switchMap((userId) => + this.nudgesService.showNudgeSpotlight$(NudgeType.GeneratorNudgeStatus, userId), + ), + ); + + constructor( + private nudgesService: NudgesService, + private accountService: AccountService, + ) {} + + async dismissGeneratorSpotlight(type: NudgeType) { + const activeUserId = await firstValueFrom(this.activeUserId$); + + await this.nudgesService.dismissNudge(type, activeUserId as UserId); + } +} diff --git a/libs/tools/generator/components/tsconfig.json b/libs/tools/generator/components/tsconfig.json index e0e4da268da..9a3a08b40fc 100644 --- a/libs/tools/generator/components/tsconfig.json +++ b/libs/tools/generator/components/tsconfig.json @@ -11,7 +11,8 @@ "@bitwarden/generator-history": ["../../../tools/generator/extensions/history/src"], "@bitwarden/key-management": ["../../../key-management/src"], "@bitwarden/platform": ["../../../platform/src"], - "@bitwarden/ui-common": ["../../../ui/common/src"] + "@bitwarden/ui-common": ["../../../ui/common/src"], + "@bitwarden/vault": ["../../../vault/src"] } }, "include": ["src"], diff --git a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html index ef8b28aba33..c62e646540b 100644 --- a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html +++ b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html @@ -1,4 +1,10 @@ - diff --git a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts index 19f9d3a174a..6563789f1c7 100644 --- a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts +++ b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts @@ -7,7 +7,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; -import { BadgeModule, ButtonModule, MenuModule } from "@bitwarden/components"; +import { BadgeModule, ButtonModule, ButtonType, MenuModule } from "@bitwarden/components"; @Component({ selector: "tools-new-send-dropdown", @@ -17,6 +17,7 @@ import { BadgeModule, ButtonModule, MenuModule } from "@bitwarden/components"; }) export class NewSendDropdownComponent implements OnInit { @Input() hideIcon: boolean = false; + @Input() buttonType: ButtonType = "primary"; sendType = SendType; diff --git a/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.ts b/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.ts index 6781cafcbb2..705b98f241a 100644 --- a/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.ts +++ b/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.ts @@ -3,14 +3,13 @@ import { Component, Input, OnInit } from "@angular/core"; import { firstValueFrom } from "rxjs"; import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; +import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/sdk-internal"; -import { SpotlightComponent } from "../../../components/spotlight/spotlight.component"; - @Component({ selector: "vault-new-item-nudge", templateUrl: "./new-item-nudge.component.html", diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index 9d19e4c239d..b39bb85ab30 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -27,5 +27,3 @@ export { SshImportPromptService } from "./services/ssh-import-prompt.service"; export * from "./abstractions/change-login-password.service"; export * from "./services/default-change-login-password.service"; - -export { SpotlightComponent } from "./components/spotlight/spotlight.component"; From 105ec701b9e7b1cc4d36169c5b615da5dc47b04f Mon Sep 17 00:00:00 2001 From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com> Date: Wed, 21 May 2025 13:03:18 -0500 Subject: [PATCH 118/163] chore(tailwind): [PM-20609] Migrate duo-redirect.html * Update duo-redirect to use Tailwind classes similar to sso.html * Update dynamic classes to tailwind * fix: updating styling * Update button and logo styles to match previous * Update button styles * Update button styles --------- Co-authored-by: Ike Kottlowski Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> --- apps/web/src/connectors/duo-redirect.html | 31 ++++++++++------------- apps/web/src/connectors/duo-redirect.ts | 9 ++++--- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/apps/web/src/connectors/duo-redirect.html b/apps/web/src/connectors/duo-redirect.html index bfbbe8d216e..bcd1c7fccd3 100644 --- a/apps/web/src/connectors/duo-redirect.html +++ b/apps/web/src/connectors/duo-redirect.html @@ -10,23 +10,20 @@ -
-
- Bitwarden -
-

- -

-
+
+ Bitwarden +
+

+ +

diff --git a/apps/web/src/connectors/duo-redirect.ts b/apps/web/src/connectors/duo-redirect.ts index 5389b31f6af..ae8f84715db 100644 --- a/apps/web/src/connectors/duo-redirect.ts +++ b/apps/web/src/connectors/duo-redirect.ts @@ -108,7 +108,7 @@ function displayHandoffMessage(client: string) { if (!content) { throw new Error("content element not found"); } - content.className = "text-center"; + content.className = "tw-text-center"; content.innerHTML = ""; const h1 = document.createElement("h1"); @@ -123,8 +123,8 @@ function displayHandoffMessage(client: string) { ? localeService.t("thisWindowWillCloseIn5Seconds") : localeService.t("youMayCloseThisWindow"); - h1.className = "font-weight-semibold"; - p.className = "mb-4"; + h1.className = "tw-font-semibold"; + p.className = "tw-mb-4"; content.appendChild(h1); content.appendChild(p); @@ -133,7 +133,8 @@ function displayHandoffMessage(client: string) { if (client == "web") { const button = document.createElement("button"); button.textContent = localeService.t("close"); - button.className = "bg-primary text-white border-0 rounded py-2 px-3"; + button.className = + "tw-bg-primary-600 hover:tw-bg-primary-700 tw-text-contrast tw-px-4 tw-py-2 tw-rounded-md tw-transition tw-border-transparent tw-text-center focus:tw-outline-none"; button.addEventListener("click", () => { window.close(); From 27884d9ffe4400314ca19387b62ef7e6a0703c42 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 21 May 2025 13:43:10 -0500 Subject: [PATCH 119/163] fix `SpotlightComponent` import (#14868) --- .../src/auth/popup/settings/account-security.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index b2380a1a47e..d7b8aa573d4 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -23,6 +23,7 @@ import { import { JslibModule } from "@bitwarden/angular/jslib.module"; import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; +import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component"; import { FingerprintDialogComponent, VaultTimeoutInputComponent } from "@bitwarden/auth/angular"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -65,7 +66,6 @@ import { BiometricStateService, BiometricsStatus, } from "@bitwarden/key-management"; -import { SpotlightComponent } from "@bitwarden/vault"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; import { BrowserApi } from "../../../platform/browser/browser-api"; From f0b255117e7b2c7bee1d70c8e200da48d38e4b67 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 16:35:30 -0400 Subject: [PATCH 120/163] [deps] Autofill: Update lit to v3.3.0 (#14479) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 579ff539de3..2e2623f89cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "koa": "2.16.1", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", - "lit": "3.2.1", + "lit": "3.3.0", "lowdb": "1.0.0", "lunr": "2.3.9", "multer": "1.4.5-lts.2", @@ -26466,14 +26466,14 @@ } }, "node_modules/lit": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz", - "integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", + "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", "license": "BSD-3-Clause", "dependencies": { - "@lit/reactive-element": "^2.0.4", - "lit-element": "^4.1.0", - "lit-html": "^3.2.0" + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" } }, "node_modules/lit-element": { diff --git a/package.json b/package.json index b1e91d92771..ce6adf3009d 100644 --- a/package.json +++ b/package.json @@ -185,7 +185,7 @@ "koa": "2.16.1", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", - "lit": "3.2.1", + "lit": "3.3.0", "lowdb": "1.0.0", "lunr": "2.3.9", "multer": "1.4.5-lts.2", From 121ff93ea18282160453436f7039e4b258b34d55 Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Wed, 21 May 2025 16:57:32 -0400 Subject: [PATCH 121/163] fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement (#14870) - Fixed up code that was not properly using the policy api. (#14870) --- .../app/auth/core/services/login/web-login-component.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/auth/core/services/login/web-login-component.service.ts b/apps/web/src/app/auth/core/services/login/web-login-component.service.ts index c644f26dd90..36e7143ccd0 100644 --- a/apps/web/src/app/auth/core/services/login/web-login-component.service.ts +++ b/apps/web/src/app/auth/core/services/login/web-login-component.service.ts @@ -98,7 +98,7 @@ export class WebLoginComponentService const enforcedPasswordPolicyOptions = await firstValueFrom( this.accountService.activeAccount$.pipe( getUserId, - switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)), + switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId, policies)), ), ); From 0555d827c635913ab679a4abb7881d458fa2dcae Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Wed, 21 May 2025 18:06:06 -0400 Subject: [PATCH 122/163] exclude fido2crednetials when creating login item from template via CLI (#14766) --- libs/common/src/models/export/login.export.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/common/src/models/export/login.export.ts b/libs/common/src/models/export/login.export.ts index d24c084aa48..dd0cfa7d32b 100644 --- a/libs/common/src/models/export/login.export.ts +++ b/libs/common/src/models/export/login.export.ts @@ -15,7 +15,7 @@ export class LoginExport { req.username = "jdoe"; req.password = "myp@ssword123"; req.totp = "JBSWY3DPEHPK3PXP"; - req.fido2Credentials = [Fido2CredentialExport.template()]; + req.fido2Credentials = []; return req; } @@ -48,7 +48,7 @@ export class LoginExport { username: string; password: string; totp: string; - fido2Credentials: Fido2CredentialExport[] = []; + fido2Credentials: Fido2CredentialExport[]; constructor(o?: LoginView | LoginDomain) { if (o == null) { From 068c63e891f2d6e396c86bef1f2668e77ac1485f Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 22 May 2025 15:05:28 +0200 Subject: [PATCH 123/163] Fix send rotation broken due to incorrect types (#14874) --- libs/common/src/tools/send/services/send.service.spec.ts | 6 ++---- libs/common/src/tools/send/services/send.service.ts | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index 65fd53edd75..611cc9c7b76 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -477,11 +477,9 @@ describe("SendService", () => { let encryptedKey: EncString; beforeEach(() => { - encryptService.unwrapSymmetricKey.mockResolvedValue( - new SymmetricCryptoKey(new Uint8Array(32)), - ); + encryptService.decryptBytes.mockResolvedValue(new Uint8Array(16)); encryptedKey = new EncString("Re-encrypted Send Key"); - encryptService.wrapSymmetricKey.mockResolvedValue(encryptedKey); + encryptService.encryptBytes.mockResolvedValue(encryptedKey); }); it("returns re-encrypted user sends", async () => { diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index db3834789c8..3a5bcbe997b 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -292,8 +292,9 @@ export class SendService implements InternalSendServiceAbstraction { ) { const requests = await Promise.all( sends.map(async (send) => { - const sendKey = await this.encryptService.unwrapSymmetricKey(send.key, originalUserKey); - send.key = await this.encryptService.wrapSymmetricKey(sendKey, rotateUserKey); + // Send key is not a key but a 16 byte seed used to derive the key + const sendKey = await this.encryptService.decryptBytes(send.key, originalUserKey); + send.key = await this.encryptService.encryptBytes(sendKey, rotateUserKey); return new SendWithIdRequest(send); }), ); From 46a0b709fe5b17027b571affbcbc82eb58f61745 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Thu, 22 May 2025 09:15:30 -0400 Subject: [PATCH 124/163] [PM-21644] Cannot retrieve attachment from bw serve (#14806) * Modified saveAttachmenttofIle to implement callback attachment content decryption * renamed parameter --- apps/cli/src/commands/download.command.ts | 9 +++---- apps/cli/src/commands/get.command.ts | 26 ++++++++++++------- .../tools/send/commands/receive.command.ts | 9 ++++++- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/apps/cli/src/commands/download.command.ts b/apps/cli/src/commands/download.command.ts index 472b084f5d7..2c75617ca5b 100644 --- a/apps/cli/src/commands/download.command.ts +++ b/apps/cli/src/commands/download.command.ts @@ -2,8 +2,6 @@ // @ts-strict-ignore import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { Response } from "../models/response"; import { FileResponse } from "../models/response/file.response"; @@ -25,15 +23,15 @@ export abstract class DownloadCommand { /** * Fetches an attachment via the url, decrypts it's content and saves it to a file * @param url - url used to retrieve the attachment - * @param key - SymmetricCryptoKey to decrypt the file contents * @param fileName - filename used when written to disk + * @param decrypt - Function used to decrypt the response * @param output - If output is empty or `--raw` was passed to the initial command the content is output onto stdout * @returns Promise */ protected async saveAttachmentToFile( url: string, - key: SymmetricCryptoKey, fileName: string, + decrypt: (resp: globalThis.Response) => Promise, output?: string, ) { const response = await this.apiService.nativeFetch( @@ -46,8 +44,7 @@ export abstract class DownloadCommand { } try { - const encBuf = await EncArrayBuffer.fromResponse(response); - const decBuf = await this.encryptService.decryptFileData(encBuf, key); + const decBuf = await decrypt(response); if (process.env.BW_SERVE === "true") { const res = new FileResponse(Buffer.from(decBuf), fileName); return Response.success(res); diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index 60be0a8d2cb..8554f8e2ae1 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -27,7 +27,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response" import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; -import { OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; @@ -345,12 +345,11 @@ export class GetCommand extends DownloadCommand { return Response.multipleResults(attachments.map((a) => a.id)); } - const account = await firstValueFrom(this.accountService.activeAccount$); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const canAccessPremium = await firstValueFrom( - this.accountProfileService.hasPremiumFromAnySource$(account.id), + this.accountProfileService.hasPremiumFromAnySource$(activeUserId), ); if (!canAccessPremium) { - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const originalCipher = await this.cipherService.get(cipher.id, activeUserId); if (originalCipher == null || originalCipher.organizationId == null) { return Response.error("Premium status is required to use this feature."); @@ -374,11 +373,20 @@ export class GetCommand extends DownloadCommand { } } - const key = - attachments[0].key != null - ? attachments[0].key - : await this.keyService.getOrgKey(cipher.organizationId); - return await this.saveAttachmentToFile(url, key, attachments[0].fileName, options.output); + const decryptBufferFn = (resp: globalThis.Response) => + this.cipherService.getDecryptedAttachmentBuffer( + cipher.id as CipherId, + attachments[0], + resp, + activeUserId, + ); + + return await this.saveAttachmentToFile( + url, + attachments[0].fileName, + decryptBufferFn, + options.output, + ); } private async getFolder(id: string) { diff --git a/apps/cli/src/tools/send/commands/receive.command.ts b/apps/cli/src/tools/send/commands/receive.command.ts index c67b4213d97..a412f7c1667 100644 --- a/apps/cli/src/tools/send/commands/receive.command.ts +++ b/apps/cli/src/tools/send/commands/receive.command.ts @@ -11,6 +11,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response" import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendAccess } from "@bitwarden/common/tools/send/models/domain/send-access"; @@ -98,10 +99,16 @@ export class SendReceiveCommand extends DownloadCommand { this.sendAccessRequest, apiUrl, ); + + const decryptBufferFn = async (resp: globalThis.Response) => { + const encBuf = await EncArrayBuffer.fromResponse(resp); + return this.encryptService.decryptFileData(encBuf, this.decKey); + }; + return await this.saveAttachmentToFile( downloadData.url, - this.decKey, response?.file?.fileName, + decryptBufferFn, options.output, ); } From 9417d8a94328ce636c55dcbe987ed3e7b1846938 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 22 May 2025 09:35:39 -0400 Subject: [PATCH 125/163] [PM-18633] Remove feature flagged logic (#14856) * remove feature flagged logic * clean up --- .../manage/group-add-edit.component.html | 2 +- .../manage/group-add-edit.component.ts | 9 +++--- .../member-dialog.component.html | 4 +-- .../member-dialog/member-dialog.component.ts | 24 +++++--------- .../collection-dialog.component.html | 2 +- .../collection-dialog.component.ts | 31 +++---------------- libs/common/src/enums/feature-flag.enum.ts | 2 -- 7 files changed, 21 insertions(+), 53 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html index 5c8c0c07f88..101512dea04 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html @@ -23,7 +23,7 @@ {{ "characterMaximum" | i18n: 100 }} - + {{ "externalId" | i18n }} {{ "externalIdDesc" | i18n }} diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index 53a6a3cf196..ca7d07220b2 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -28,7 +28,6 @@ import { } 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -147,6 +146,10 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { return this.params.organizationId; } + protected get isExternalIdVisible(): boolean { + return !!this.groupForm.get("externalId")?.value; + } + protected get editMode(): boolean { return this.groupId != null; } @@ -227,10 +230,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { this.groupDetails$, ]).pipe(map(([allowAdminAccess, groupDetails]) => !allowAdminAccess && groupDetails != null)); - protected isExternalIdVisible$ = this.configService - .getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility) - .pipe(map((isEnabled) => !isEnabled || !!this.groupForm.get("externalId")?.value)); - constructor( @Inject(DIALOG_DATA) private params: GroupAddEditDialogParams, private dialogRef: DialogRef, diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html index 9564952f511..fa0a7bd85a3 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html @@ -177,13 +177,13 @@ - + {{ "externalId" | i18n }} {{ "externalIdDesc" | i18n }} - + {{ "ssoExternalId" | i18n }} {{ "ssoExternalIdDesc" | i18n }} diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index 10bbc5cfe52..9adfb1db3f2 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -157,28 +157,20 @@ export class MemberDialogComponent implements OnDestroy { manageResetPassword: false, }); - protected isExternalIdVisible$ = this.configService - .getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility) - .pipe( - map((isEnabled) => { - return !isEnabled || !!this.formGroup.get("externalId")?.value; - }), - ); + get isExternalIdVisible(): boolean { + return !!this.formGroup.get("externalId")?.value; + } - protected isSsoExternalIdVisible$ = this.configService - .getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility) - .pipe( - map((isEnabled) => { - return isEnabled && !!this.formGroup.get("ssoExternalId")?.value; - }), - ); - - private destroy$ = new Subject(); + get isSsoExternalIdVisible(): boolean { + return !!this.formGroup.get("ssoExternalId")?.value; + } get customUserTypeSelected(): boolean { return this.formGroup.value.type === OrganizationUserType.Custom; } + private destroy$ = new Subject(); + isEditDialogParams( params: EditMemberDialogParams | AddMemberDialogParams, ): params is EditMemberDialogParams { diff --git a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.html b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.html index 12d7a920a2d..4a91fcc2a41 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.html @@ -35,7 +35,7 @@ - + {{ "externalId" | i18n }} {{ "externalIdDesc" | i18n }} 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 07bff3aba64..70b26041df6 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 @@ -38,7 +38,6 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DIALOG_DATA, @@ -135,7 +134,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { protected showOrgSelector = false; protected formGroup = this.formBuilder.group({ name: ["", [Validators.required, BitValidators.forbiddenCharacters(["/"])]], - externalId: "", + externalId: { value: "", disabled: true }, parent: undefined as string | undefined, access: [[] as AccessItemValue[]], selectedOrg: "", @@ -145,16 +144,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { protected showAddAccessWarning = false; protected collections: Collection[]; protected buttonDisplayName: ButtonType = ButtonType.Save; - protected isExternalIdVisible$ = this.configService - .getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility) - .pipe( - map((isEnabled) => { - return ( - !isEnabled || - (!!this.params.isAdminConsoleActive && !!this.formGroup.get("externalId")?.value) - ); - }), - ); private orgExceedingCollectionLimit!: Organization; constructor( @@ -165,7 +154,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { private groupService: GroupApiService, private collectionAdminService: CollectionAdminService, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private organizationUserApiService: OrganizationUserApiService, private dialogService: DialogService, private changeDetectorRef: ChangeDetectorRef, @@ -354,6 +342,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { return this.formGroup.controls.selectedOrg; } + protected get isExternalIdVisible(): boolean { + return this.params.isAdminConsoleActive && !!this.formGroup.get("externalId")?.value; + } + protected get collectionId() { return this.params.collectionId; } @@ -490,23 +482,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { private handleFormGroupReadonly(readonly: boolean) { if (readonly) { this.formGroup.controls.name.disable(); - this.formGroup.controls.externalId.disable(); this.formGroup.controls.parent.disable(); this.formGroup.controls.access.disable(); } else { this.formGroup.controls.name.enable(); - - this.configService - .getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility) - .pipe(takeUntil(this.destroy$)) - .subscribe((isEnabled) => { - if (isEnabled) { - this.formGroup.controls.externalId.disable(); - } else { - this.formGroup.controls.externalId.enable(); - } - }); - this.formGroup.controls.parent.enable(); this.formGroup.controls.access.enable(); } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index f9e68efe4be..dcd214c37d7 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -12,7 +12,6 @@ import { ServerConfig } from "../platform/abstractions/config/server-config"; export enum FeatureFlag { /* Admin Console Team */ LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission", - SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility", AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner", SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions", @@ -83,7 +82,6 @@ const FALSE = false as boolean; export const DefaultFeatureFlagValue = { /* Admin Console Team */ [FeatureFlag.LimitItemDeletion]: FALSE, - [FeatureFlag.SsoExternalIdVisibility]: FALSE, [FeatureFlag.AccountDeprovisioningBanner]: FALSE, [FeatureFlag.SeparateCustomRolePermissions]: FALSE, From f52e4e27a030cb5ba9cc743f088c61bbebcbcc72 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 22 May 2025 11:09:33 -0500 Subject: [PATCH 126/163] [PM-12770] Assign to Collections Hint (#14529) * allow use of common spec in lib/vault tests * pass readonly collections to the assign collection component - The assign to collections component filters them out already. -They're also needed to display copy within the component * add hint to assign to collections component when there are read only collections assigned to a cipher already * add readonly hint to desktop * only show collection hint for collections that are assigned to the provided ciphers * consider admin/owner edit everything permission when assigning to collections * fix icon in test --- .../assign-collections.component.ts | 2 +- apps/desktop/src/locales/en/messages.json | 9 ++ .../collections/vault.component.ts | 3 +- .../vault/individual-vault/vault.component.ts | 2 +- libs/vault/jest.config.js | 10 +- .../assign-collections.component.html | 5 +- .../assign-collections.component.spec.ts | 113 ++++++++++++++++++ .../assign-collections.component.ts | 29 +++++ 8 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 libs/vault/src/components/assign-collections.component.spec.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts index 7052be5ea62..a11a7d806bd 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts @@ -74,7 +74,7 @@ export class AssignCollections { combineLatest([cipher$, this.collectionService.decryptedCollections$]) .pipe(takeUntilDestroyed(), first()) .subscribe(([cipherView, collections]) => { - let availableCollections = collections.filter((c) => !c.readOnly); + let availableCollections = collections; const organizationId = (cipherView?.organizationId as OrganizationId) ?? null; // If the cipher is already a part of an organization, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 35433e8e2e1..50b9ed4336c 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index 96c00faceb2..a3b62838d6a 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -362,8 +362,7 @@ export class VaultComponent implements OnInit, OnDestroy { if (this.organization.canEditAllCiphers) { return collections; } - // The user is only allowed to add/edit items to assigned collections that are not readonly - return collections.filter((c) => c.assigned && !c.readOnly); + return collections.filter((c) => c.assigned); }), shareReplay({ refCount: true, bufferSize: 1 }), ); diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 55bbd0c0651..6e751f600dc 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -933,7 +933,7 @@ export class VaultComponent implements OnInit, OnDestroy { if (orgId && orgId !== "MyVault") { const organization = this.allOrganizations.find((o) => o.id === orgId); availableCollections = this.allCollections.filter( - (c) => c.organizationId === organization.id && !c.readOnly, + (c) => c.organizationId === organization.id, ); } diff --git a/libs/vault/jest.config.js b/libs/vault/jest.config.js index e33c115e8dd..16db37527ac 100644 --- a/libs/vault/jest.config.js +++ b/libs/vault/jest.config.js @@ -10,7 +10,11 @@ module.exports = { displayName: "libs/vault tests", preset: "jest-preset-angular", setupFilesAfterEnv: ["/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), + moduleNameMapper: pathsToModuleNameMapper( + // lets us use @bitwarden/common/spec in tests + { "@bitwarden/common/spec": ["../common/spec"], ...(compilerOptions?.paths ?? {}) }, + { + prefix: "/", + }, + ), }; diff --git a/libs/vault/src/components/assign-collections.component.html b/libs/vault/src/components/assign-collections.component.html index d68799eec6d..a82f2cb29a1 100644 --- a/libs/vault/src/components/assign-collections.component.html +++ b/libs/vault/src/components/assign-collections.component.html @@ -37,13 +37,16 @@
- + {{ "selectCollectionsToAssign" | i18n }} + + {{ "cannotRemoveViewOnlyCollections" | i18n: readOnlyCollectionNames.join(", ") }} +
diff --git a/libs/vault/src/components/assign-collections.component.spec.ts b/libs/vault/src/components/assign-collections.component.spec.ts new file mode 100644 index 00000000000..d6707cd1064 --- /dev/null +++ b/libs/vault/src/components/assign-collections.component.spec.ts @@ -0,0 +1,113 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +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 { ProductTierType } from "@bitwarden/common/billing/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { ToastService } from "@bitwarden/components"; + +import { + AssignCollectionsComponent, + CollectionAssignmentParams, +} from "./assign-collections.component"; + +describe("AssignCollectionsComponent", () => { + let component: AssignCollectionsComponent; + let fixture: ComponentFixture; + + const mockUserId = "mock-user-id" as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); + + const editCollection = new CollectionView(); + editCollection.id = "collection-id" as CollectionId; + editCollection.organizationId = "org-id" as OrganizationId; + editCollection.name = "Editable Collection"; + editCollection.readOnly = false; + editCollection.manage = true; + + const readOnlyCollection1 = new CollectionView(); + readOnlyCollection1.id = "read-only-collection-id" as CollectionId; + readOnlyCollection1.organizationId = "org-id" as OrganizationId; + readOnlyCollection1.name = "Read Only Collection"; + readOnlyCollection1.readOnly = true; + + const readOnlyCollection2 = new CollectionView(); + readOnlyCollection2.id = "read-only-collection-id-2" as CollectionId; + readOnlyCollection2.organizationId = "org-id" as OrganizationId; + readOnlyCollection2.name = "Read Only Collection 2"; + readOnlyCollection2.readOnly = true; + + const params = { + organizationId: "org-id" as OrganizationId, + ciphers: [ + { + id: "cipher-id", + name: "Cipher Name", + collectionIds: [readOnlyCollection1.id], + edit: true, + } as unknown as CipherView, + ], + availableCollections: [editCollection, readOnlyCollection1, readOnlyCollection2], + } as CollectionAssignmentParams; + + const org = { + id: "org-id", + name: "Test Org", + productTierType: ProductTierType.Enterprise, + } as Organization; + + const organizations$ = jest.fn().mockReturnValue(of([org])); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [ + { provide: CipherService, useValue: mock() }, + { provide: OrganizationService, useValue: mock({ organizations$ }) }, + { provide: CollectionService, useValue: mock() }, + { provide: ToastService, useValue: mock() }, + { provide: AccountService, useValue: accountService }, + { provide: I18nService, useValue: { t: (...keys: string[]) => keys.join(" ") } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AssignCollectionsComponent); + component = fixture.componentInstance; + component.params = params; + fixture.detectChanges(); + }); + + describe("read only collections", () => { + beforeEach(async () => { + await component.ngOnInit(); + fixture.detectChanges(); + }); + + it("shows read-only hint for assigned collections", () => { + const hint = fixture.debugElement.query(By.css('[data-testid="view-only-hint"]')); + + expect(hint.nativeElement.textContent.trim()).toBe( + "cannotRemoveViewOnlyCollections Read Only Collection", + ); + }); + + it("does not show read only collections in the list", () => { + expect(component["availableCollections"]).toEqual([ + { + icon: "bwi-collection-shared", + id: editCollection.id, + labelName: editCollection.name, + listName: editCollection.name, + }, + ]); + }); + }); +}); diff --git a/libs/vault/src/components/assign-collections.component.ts b/libs/vault/src/components/assign-collections.component.ts index 6a0c45cfbe3..db62f096faa 100644 --- a/libs/vault/src/components/assign-collections.component.ts +++ b/libs/vault/src/components/assign-collections.component.ts @@ -126,6 +126,12 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI collections: [[], [Validators.required]], }); + /** + * Collections that are already assigned to the cipher and are read-only. These cannot be removed. + * @protected + */ + protected readOnlyCollectionNames: string[] = []; + protected totalItemCount: number; protected editableItemCount: number; protected readonlyItemCount: number; @@ -301,6 +307,8 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI this.organizationService.organizations$(userId).pipe(getOrganizationById(organizationId)), ); + await this.setReadOnlyCollectionNames(); + this.availableCollections = this.params.availableCollections .filter((collection) => { return collection.canEditItems(org); @@ -503,4 +511,25 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI await this.cipherService.saveCollectionsWithServer(cipher, userId); } } + + /** + * Only display collections that are read-only and are assigned to the ciphers. + */ + private async setReadOnlyCollectionNames() { + const { availableCollections, ciphers } = this.params; + + const organization = await firstValueFrom( + this.organizations$.pipe(map((orgs) => orgs.find((o) => o.id === this.selectedOrgId))), + ); + + this.readOnlyCollectionNames = availableCollections + .filter((c) => { + return ( + c.readOnly && + ciphers.some((cipher) => cipher.collectionIds.includes(c.id)) && + !c.canEditItems(organization) + ); + }) + .map((c) => c.name); + } } From 753e7af380b53ec111bad58e13b442a713c6db4c Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Thu, 22 May 2025 12:29:15 -0400 Subject: [PATCH 127/163] [PM-21882] Lit Components Cleanup (#14872) * rename item row component to cipher item row * move CipherItem into CipherItemRow instead of passing a generic children prop * remove redundant embedded stories * add mock data to dropdown button story --- .../lit-stories/.lit-docs/cipher-action.mdx | 83 ----------------- .../lit-stories/.lit-docs/cipher-icon.mdx | 90 ------------------- .../.lit-docs/cipher-indicator-icon.mdx | 81 ----------------- .../ciphers/cipher-action.lit-stories.ts | 31 ------- .../ciphers/cipher-icon.lit-stories.ts | 33 ------- .../cipher-indicator-icons.lit-stories.ts | 28 ------ .../ciphers/cipher-info.lit-stories.ts | 23 ----- .../rows/button-row.lit-stories.ts | 51 +++++++++++ .../cipher-item-row.lit-stories.ts} | 20 ++--- .../lit-stories/rows/item-row.lit-stories.ts | 22 ----- .../content/components/notification/body.ts | 16 ++-- .../components/rows/cipher-item-row.ts | 78 ++++++++++++++++ .../content/components/rows/item-row.ts | 55 ------------ 13 files changed, 145 insertions(+), 466 deletions(-) delete mode 100644 apps/browser/src/autofill/content/components/lit-stories/.lit-docs/cipher-action.mdx delete mode 100644 apps/browser/src/autofill/content/components/lit-stories/.lit-docs/cipher-icon.mdx delete mode 100644 apps/browser/src/autofill/content/components/lit-stories/.lit-docs/cipher-indicator-icon.mdx delete mode 100644 apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts delete mode 100644 apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-icon.lit-stories.ts delete mode 100644 apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icons.lit-stories.ts delete mode 100644 apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-info.lit-stories.ts rename apps/browser/src/autofill/content/components/lit-stories/{ciphers/cipher-item.lit-stories.ts => rows/cipher-item-row.lit-stories.ts} (62%) delete mode 100644 apps/browser/src/autofill/content/components/lit-stories/rows/item-row.lit-stories.ts create mode 100644 apps/browser/src/autofill/content/components/rows/cipher-item-row.ts delete mode 100644 apps/browser/src/autofill/content/components/rows/item-row.ts diff --git a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/cipher-action.mdx b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/cipher-action.mdx deleted file mode 100644 index 3b5dcd8797a..00000000000 --- a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/cipher-action.mdx +++ /dev/null @@ -1,83 +0,0 @@ -import { Meta, Controls, Primary } from "@storybook/addon-docs"; - -import * as stories from "./cipher-action.lit-stories"; - - - -## Cipher Action - -The `CipherAction` component is a functional UI element that handles actions related to ciphers in a -secure environment. Built with the `lit` library and styled for consistency across themes, it -provides flexibility and accessibility while supporting various notification types. - - - - -## Props - -| **Prop** | **Type** | **Required** | **Description** | -| ------------------ | --------------------------------------------------- | ------------ | -------------------------------------------------------------- | -| `handleAction` | `(e: Event) => void` | No | Function to execute when an action is triggered. | -| `notificationType` | `NotificationTypes.Change \| NotificationTypes.Add` | Yes | Specifies the type of notification associated with the action. | -| `theme` | `Theme` | Yes | The theme to style the component. Must match the `Theme` enum. | - -## Installation and Setup - -1. Ensure the necessary dependencies are installed: - - - `lit`: Used to render the component. - -2. Pass the required props when rendering the component: - - `handleAction`: Optional function to handle the triggered action. - - `notificationType`: Mandatory type from `NotificationTypes` to define the action context. - - `theme`: The styling theme (must be a valid `Theme` enum value). - -## Accessibility (WCAG) Compliance - -The `CipherAction` component is designed to be accessible, ensuring usability across diverse user -bases. Below are the key considerations for accessibility: - -### Keyboard Accessibility - -- Fully navigable using the keyboard. -- The action can be triggered using the `Enter` or `Space` key for users relying on keyboard - interaction. - -### Screen Reader Compatibility - -- The semantic elements used in the `CipherAction` component ensure that assistive technologies can - interpret the component correctly. -- Text associated with the `notificationType` is programmatically linked, providing clarity for - screen reader users. - -### Focus Management - -- The component includes focus styles to ensure visibility during navigation. -- Proper focus management ensures the component works seamlessly with keyboard navigation. - -### Visual Feedback - -- Provides distinct visual states for different themes and states: - - **Hover:** Adjustments to background, border, and text for enhanced visibility. - - **Active:** Highlights the button with a focus state when activated. - - **Disabled:** Grays out the component to indicate inactivity. - -## Usage Example - -Here's an example of how to integrate the `CipherAction` component: - -```ts -import { CipherAction } from "../../cipher/cipher-action"; -import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; -import { NotificationTypes } from "../../../../notification/abstractions/notification-bar"; - -const handleAction = (e: Event) => { - console.log("Cipher action triggered!", e); -}; - -; -``` diff --git a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/cipher-icon.mdx b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/cipher-icon.mdx deleted file mode 100644 index a1a94efde7b..00000000000 --- a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/cipher-icon.mdx +++ /dev/null @@ -1,90 +0,0 @@ -import { Meta, Controls, Primary } from "@storybook/addon-docs"; - -import * as stories from "./cipher-icon.lit-stories"; - - - -## Cipher Icon - -The `CipherIcon` component is a versatile icon renderer designed for secure environments. It -dynamically supports custom icons provided via URIs or displays a default icon (`Globe`) styled -based on the theme and provided properties. - - - - -## Props - -| **Prop** | **Type** | **Required** | **Description** | -| -------- | ------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `color` | `string` | Yes | A contextual color override applied when the `uri` is not provided, ensuring consistent styling of the default icon. | -| `size` | `string` | Yes | A valid CSS `width` value representing the width basis of the graphic. The height adjusts to maintain the original aspect ratio of the graphic. | -| `theme` | `Theme` | Yes | The styling theme for the icon, matching the `Theme` enum. | -| `uri` | `string` (optional) | No | A URL to an external graphic. If provided, the component displays this icon. If omitted, a default icon (`Globe`) styled with the provided `color` and `theme`. | - -## Installation and Setup - -1. Ensure the necessary dependencies are installed: - - - `lit`: Renders the component. - - `@emotion/css`: Styles the component. - -2. Pass the necessary props when using the component: - - `color`: Used when no `uri` is provided to style the default icon. - - `size`: Defines the width of the icon. Height maintains aspect ratio. - - `theme`: Specifies the theme for styling. - - `uri` (optional): If provided, this URI is used to display a custom icon. - -## Accessibility (WCAG) Compliance - -The `CipherIcon` component ensures accessible and user-friendly interactions through thoughtful -design: - -### Semantic Rendering - -- When the `uri` is provided, the component renders an `` element, which is semantically - appropriate for external graphics. -- If no `uri` is provided, the default icon is wrapped in a ``, ensuring proper context for - screen readers. - -### Visual Feedback - -- The component visually adjusts based on the `size`, `color`, and `theme`, ensuring the icon - remains clear and legible across different environments. - -### Keyboard and Screen Reader Support - -- Ensure that any container or parent component provides appropriate `alt` text or labeling when - `uri` is used with an `` tag for additional accessibility. - -## Usage Example - -Here's an example of how to integrate the `CipherIcon` component: - -```ts -import { CipherIcon } from "./cipher-icon"; -import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; - -; -``` - -This configuration displays a custom icon from the provided URI with a width of 32px, styled for the -light theme. If the URI is omitted, the Globe icon is used as the fallback, colored in blue. - -### Default Styles - -- The default styles ensure responsive and clean design: - -- Width: Defined by the size prop. -- Height: Automatically adjusts to maintain the aspect ratio. -- Fit Content: Ensures the icon does not overflow or distort its container. - -### Notes - -- Always validate the uri provided to ensure it points to a secure and accessible location. -- Use the color and theme props for consistent fallback styling when uri is not provided. diff --git a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/cipher-indicator-icon.mdx b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/cipher-indicator-icon.mdx deleted file mode 100644 index 6c338276c02..00000000000 --- a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/cipher-indicator-icon.mdx +++ /dev/null @@ -1,81 +0,0 @@ -import { Meta, Controls, Primary } from "@storybook/addon-docs"; - -import * as stories from "./cipher-indicator-icon.lit-stories"; - - - -## Cipher Info Indicator Icons - -The `CipherInfoIndicatorIcons` component displays a set of icons indicating specific attributes -related to cipher information. It supports business and family organization indicators, styled -dynamically based on the provided theme. - - - - -## Props - -| **Prop** | **Type** | **Required** | **Description** | -| ------------------ | --------- | ------------ | ----------------------------------------------------------------------- | -| `showBusinessIcon` | `boolean` | No | Displays the business organization icon when set to `true`. | -| `showFamilyIcon` | `boolean` | No | Displays the family organization icon when set to `true`. | -| `theme` | `Theme` | Yes | Defines the theme used to style the icons. Must match the `Theme` enum. | - -## Installation and Setup - -1. Ensure the necessary dependencies are installed: - - - `lit`: Renders the component. - - `@emotion/css`: Used for styling. - -2. Pass the required props when using the component: - - `showBusinessIcon`: A boolean that, when `true`, displays the business icon. - - `showFamilyIcon`: A boolean that, when `true`, displays the family icon. - - `theme`: Specifies the theme for styling the icons. - -## Accessibility (WCAG) Compliance - -The `CipherInfoIndicatorIcons` component ensures accessibility and usability through its design: - -### Screen Reader Compatibility - -- Icons are rendered as `` elements, and parent components should provide appropriate labeling - or descriptions to convey their meaning to screen readers. - -### Visual Feedback - -- Icons are styled dynamically based on the `theme` to ensure visual clarity and contrast in all - supported themes. -- The size of the icons is fixed at `12px` in height to maintain a consistent visual appearance. - -## Usage Example - -Here's an example of how to integrate the `CipherInfoIndicatorIcons` component: - -```ts -import { CipherInfoIndicatorIcons } from "./cipher-info-indicator-icons"; -import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; - -; -``` - -This example displays the business organization icon, styled for the dark theme, and omits the -family organization icon. - -### Styling Details - -- The component includes the following styles: - -- Icons: Rendered as SVGs with a height of 12px and a width that adjusts to maintain their aspect - ratio. -- Color: Icons are dynamically styled based on the theme, using muted text colors for a subtle - appearance. - -### Notes - -- If neither showBusinessIcon nor showFamilyIcon is set to true, the component renders nothing. This - behavior should be handled by the parent component. diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts deleted file mode 100644 index 99b7e9e0acb..00000000000 --- a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Meta, StoryObj } from "@storybook/web-components"; - -import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; - -import { NotificationTypes } from "../../../../notification/abstractions/notification-bar"; -import { CipherAction, CipherActionProps } from "../../cipher/cipher-action"; -import { mockI18n } from "../mock-data"; - -export default { - title: "Components/Ciphers/Cipher Action", - argTypes: { - theme: { control: "select", options: [...Object.values(ThemeTypes)] }, - notificationType: { - control: "select", - options: [NotificationTypes.Change, NotificationTypes.Add], - }, - handleAction: { control: false }, - }, - args: { - theme: ThemeTypes.Light, - notificationType: NotificationTypes.Change, - handleAction: () => alert("Action triggered!"), - i18n: mockI18n, - }, -} as Meta; - -const Template = (args: CipherActionProps) => CipherAction({ ...args }); - -export const Default: StoryObj = { - render: Template, -}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-icon.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-icon.lit-stories.ts deleted file mode 100644 index d0396b013c8..00000000000 --- a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-icon.lit-stories.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Meta, StoryObj } from "@storybook/web-components"; -import { html } from "lit"; - -import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; - -import { CipherIcon, CipherIconProps } from "../../cipher/cipher-icon"; - -export default { - title: "Components/Ciphers/Cipher Icon", - argTypes: { - color: { control: "color" }, - size: { control: "text" }, - theme: { control: "select", options: [...Object.values(ThemeTypes)] }, - uri: { control: "text" }, - }, - args: { - size: "50px", - theme: ThemeTypes.Light, - uri: "", - }, -} as Meta; - -const Template = (args: CipherIconProps) => { - return html` -
- ${CipherIcon({ ...args })} -
- `; -}; - -export const Default: StoryObj = { - render: Template, -}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icons.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icons.lit-stories.ts deleted file mode 100644 index 7b10aeee8de..00000000000 --- a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icons.lit-stories.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Meta, StoryObj } from "@storybook/web-components"; -import { html } from "lit"; - -import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; - -import { - CipherInfoIndicatorIcons, - CipherInfoIndicatorIconsProps, -} from "../../cipher/cipher-indicator-icons"; -import { OrganizationCategories } from "../../cipher/types"; - -export default { - title: "Components/Ciphers/Cipher Indicator Icons", - argTypes: { - theme: { control: "select", options: [...Object.values(ThemeTypes)] }, - }, - args: { - theme: ThemeTypes.Light, - organizationCategories: [...Object.values(OrganizationCategories)], - }, -} as Meta; - -const Template: StoryObj["render"] = (args) => - html`
${CipherInfoIndicatorIcons({ ...args })}
`; - -export const Default: StoryObj = { - render: Template, -}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-info.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-info.lit-stories.ts deleted file mode 100644 index b56c3193bb8..00000000000 --- a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-info.lit-stories.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Meta, StoryObj } from "@storybook/web-components"; - -import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; - -import { CipherInfo, CipherInfoProps } from "../../cipher/cipher-info"; -import { mockCiphers } from "../mock-data"; - -export default { - title: "Components/Ciphers/Cipher Info", - argTypes: { - theme: { control: "select", options: [...Object.values(ThemeTypes)] }, - }, - args: { - cipher: mockCiphers[0], - theme: ThemeTypes.Light, - }, -} as Meta; - -const Template = (args: CipherInfoProps) => CipherInfo({ ...args }); - -export const Default: StoryObj = { - render: Template, -}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/rows/button-row.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/rows/button-row.lit-stories.ts index 83b498df7cb..343f36d5e11 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/rows/button-row.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/rows/button-row.lit-stories.ts @@ -4,6 +4,7 @@ import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; import { themes } from "../../constants/styles"; import { ButtonRow, ButtonRowProps } from "../../rows/button-row"; +import { mockBrowserI18nGetMessage } from "../mock-data"; export default { title: "Components/Rows/Button Row", @@ -15,6 +16,49 @@ export default { window.alert("Button clicked!"); }, }, + selectButtons: [ + { + id: "select-1", + label: "select 1", + options: [ + { + text: "item 1", + value: 1, + }, + { + default: true, + text: "item 2", + value: 2, + }, + { + text: "item 3", + value: 3, + }, + ], + }, + { + id: "select-2", + label: "select 2", + options: [ + { + text: "item a", + value: "a", + }, + { + text: "item b", + value: "b", + }, + { + text: "item c", + value: "c", + }, + { + text: "item d", + value: "d", + }, + ], + }, + ], }, } as Meta; @@ -51,3 +95,10 @@ export const Dark: StoryObj = { }, }, }; + +window.chrome = { + ...window.chrome, + i18n: { + getMessage: mockBrowserI18nGetMessage, + }, +} as typeof chrome; diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-item.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/rows/cipher-item-row.lit-stories.ts similarity index 62% rename from apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-item.lit-stories.ts rename to apps/browser/src/autofill/content/components/lit-stories/rows/cipher-item-row.lit-stories.ts index 67915db0a7b..59c38c56745 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-item.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/rows/cipher-item-row.lit-stories.ts @@ -3,30 +3,30 @@ import { Meta, StoryObj } from "@storybook/web-components"; import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; import { NotificationTypes } from "../../../../notification/abstractions/notification-bar"; -import { CipherItem, CipherItemProps } from "../../cipher/cipher-item"; +import { CipherItemRow, CipherItemRowProps } from "../../rows/cipher-item-row"; import { mockCiphers, mockI18n } from "../mock-data"; export default { - title: "Components/Ciphers/Cipher Item", + title: "Components/Rows/Cipher Item Row", argTypes: { theme: { control: "select", options: [...Object.values(ThemeTypes)] }, - handleAction: { control: false }, notificationType: { control: "select", - options: [NotificationTypes.Change, NotificationTypes.Add], + options: [...Object.values(NotificationTypes)], }, + handleAction: { control: false }, }, args: { cipher: mockCiphers[0], - theme: ThemeTypes.Light, - notificationType: NotificationTypes.Change, - handleAction: () => alert("Clicked"), i18n: mockI18n, + notificationType: NotificationTypes.Change, + theme: ThemeTypes.Light, + handleAction: () => window.alert("clicked!"), }, -} as Meta; +} as Meta; -const Template = (args: CipherItemProps) => CipherItem({ ...args }); +const Template = (props: CipherItemRowProps) => CipherItemRow({ ...props }); -export const Default: StoryObj = { +export const Default: StoryObj = { render: Template, }; diff --git a/apps/browser/src/autofill/content/components/lit-stories/rows/item-row.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/rows/item-row.lit-stories.ts deleted file mode 100644 index 375b793d016..00000000000 --- a/apps/browser/src/autofill/content/components/lit-stories/rows/item-row.lit-stories.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Meta, StoryObj } from "@storybook/web-components"; - -import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; - -import { ItemRow, ItemRowProps } from "../../rows/item-row"; - -export default { - title: "Components/Rows/Item Row", - argTypes: { - theme: { control: "select", options: [...Object.values(ThemeTypes)] }, - children: { control: "object" }, - }, - args: { - theme: ThemeTypes.Light, - }, -} as Meta; - -const Template = (args: ItemRowProps) => ItemRow({ ...args }); - -export const Default: StoryObj = { - render: Template, -}; diff --git a/apps/browser/src/autofill/content/components/notification/body.ts b/apps/browser/src/autofill/content/components/notification/body.ts index 4d8019b0a55..b1ce7cdba63 100644 --- a/apps/browser/src/autofill/content/components/notification/body.ts +++ b/apps/browser/src/autofill/content/components/notification/body.ts @@ -4,11 +4,10 @@ import { html } from "lit"; import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; import { NotificationType } from "../../../notification/abstractions/notification-bar"; -import { CipherItem } from "../cipher"; import { NotificationCipherData } from "../cipher/types"; import { I18n } from "../common-types"; import { scrollbarStyles, spacing, themes, typography } from "../constants/styles"; -import { ItemRow } from "../rows/item-row"; +import { CipherItemRow } from "../rows/cipher-item-row"; export const componentClassPrefix = "notification-body"; @@ -37,15 +36,12 @@ export function NotificationBody({ return html`
${ciphers.map((cipher) => - ItemRow({ + CipherItemRow({ + cipher, theme, - children: CipherItem({ - cipher, - i18n, - notificationType, - theme, - handleAction: handleEditOrUpdateAction, - }), + i18n, + notificationType, + handleAction: handleEditOrUpdateAction, }), )}
diff --git a/apps/browser/src/autofill/content/components/rows/cipher-item-row.ts b/apps/browser/src/autofill/content/components/rows/cipher-item-row.ts new file mode 100644 index 00000000000..0600fc9ac4b --- /dev/null +++ b/apps/browser/src/autofill/content/components/rows/cipher-item-row.ts @@ -0,0 +1,78 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { NotificationType } from "../../../notification/abstractions/notification-bar"; +import { CipherItem } from "../cipher/cipher-item"; +import { NotificationCipherData } from "../cipher/types"; +import { I18n } from "../common-types"; +import { spacing, themes, typography } from "../constants/styles"; + +export type CipherItemRowProps = { + cipher: NotificationCipherData; + i18n: I18n; + notificationType?: NotificationType; + theme: Theme; + handleAction: (e: Event) => void; +}; + +export function CipherItemRow({ + cipher, + i18n, + notificationType, + theme, + handleAction, +}: CipherItemRowProps) { + return html` +
+ ${CipherItem({ + cipher, + i18n, + notificationType, + theme, + handleAction, + })} +
+ `; +} + +const cipherItemRowStyles = ({ theme }: { theme: Theme }) => css` + ${typography.body1} + + gap: ${spacing["2"]}; + display: flex; + align-items: center; + justify-content: space-between; + border-width: 0 0 0.5px 0; + border-style: solid; + border-radius: ${spacing["2"]}; + border-color: ${themes[theme].secondary["300"]}; + background-color: ${themes[theme].background.DEFAULT}; + padding: ${spacing["2"]} ${spacing["3"]}; + min-height: min-content; + max-height: 52px; + overflow-x: hidden; + white-space: nowrap; + color: ${themes[theme].text.main}; + font-weight: 400; + + > div { + :first-child { + flex: 3 3 75%; + min-width: 25%; + } + + :not(:first-child) { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-end; + max-width: 25%; + + > button { + max-width: min-content; + } + } + } +`; diff --git a/apps/browser/src/autofill/content/components/rows/item-row.ts b/apps/browser/src/autofill/content/components/rows/item-row.ts deleted file mode 100644 index 8e9a870002e..00000000000 --- a/apps/browser/src/autofill/content/components/rows/item-row.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { css } from "@emotion/css"; -import { html, TemplateResult } from "lit"; - -import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; - -import { spacing, themes, typography } from "../../../content/components/constants/styles"; - -export type ItemRowProps = { - theme: Theme; - children: TemplateResult | TemplateResult[]; -}; - -export function ItemRow({ theme = ThemeTypes.Light, children }: ItemRowProps) { - return html`
${children}
`; -} - -export const itemRowStyles = ({ theme }: { theme: Theme }) => css` - ${typography.body1} - - gap: ${spacing["2"]}; - display: flex; - align-items: center; - justify-content: space-between; - border-width: 0 0 0.5px 0; - border-style: solid; - border-radius: ${spacing["2"]}; - border-color: ${themes[theme].secondary["300"]}; - background-color: ${themes[theme].background.DEFAULT}; - padding: ${spacing["2"]} ${spacing["3"]}; - min-height: min-content; - max-height: 52px; - overflow-x: hidden; - white-space: nowrap; - color: ${themes[theme].text.main}; - font-weight: 400; - - > div { - :first-child { - flex: 3 3 75%; - min-width: 25%; - } - - :not(:first-child) { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - justify-content: flex-end; - max-width: 25%; - - > button { - max-width: min-content; - } - } - } -`; From 57911f210bcebb68557c31633ae8c9ff2c277731 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Thu, 22 May 2025 10:20:33 -0700 Subject: [PATCH 128/163] [PM-21896] - prevent double reprompt for copy password in desktop cipher form (#14883) * prevent double reprompt for copy password in desktop cipher form * adjust name * fix input name --- apps/desktop/src/vault/app/vault/vault.component.html | 1 + apps/desktop/src/vault/app/vault/view.component.ts | 4 ++++ libs/angular/src/vault/components/view.component.ts | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/vault/app/vault/vault.component.html b/apps/desktop/src/vault/app/vault/vault.component.html index 99131a848cc..9a25619b1a8 100644 --- a/apps/desktop/src/vault/app/vault/vault.component.html +++ b/apps/desktop/src/vault/app/vault/vault.component.html @@ -15,6 +15,7 @@ *ngIf="cipherId && action === 'view'" [cipherId]="cipherId" [collectionId]="activeFilter?.selectedCollectionId" + [masterPasswordAlreadyPrompted]="cipherRepromptId === cipherId" (onCloneCipher)="cloneCipherWithoutPasswordPrompt($event)" (onEditCipher)="editCipher($event)" (onViewCipherPasswordHistory)="viewCipherPasswordHistory($event)" diff --git a/apps/desktop/src/vault/app/vault/view.component.ts b/apps/desktop/src/vault/app/vault/view.component.ts index 084a9a747ed..68bf8d6d1a3 100644 --- a/apps/desktop/src/vault/app/vault/view.component.ts +++ b/apps/desktop/src/vault/app/vault/view.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectorRef, Component, EventEmitter, + Input, NgZone, OnChanges, OnDestroy, @@ -46,6 +47,7 @@ const BroadcasterSubscriptionId = "ViewComponent"; }) export class ViewComponent extends BaseViewComponent implements OnInit, OnDestroy, OnChanges { @Output() onViewCipherPasswordHistory = new EventEmitter(); + @Input() masterPasswordAlreadyPrompted: boolean = false; constructor( cipherService: CipherService, @@ -120,6 +122,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro } }); }); + this.passwordReprompted = this.masterPasswordAlreadyPrompted; } ngOnDestroy() { @@ -134,6 +137,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro }); return; } + this.passwordReprompted = this.masterPasswordAlreadyPrompted; } viewHistory() { diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index 8915cb6b671..fd3f92c6c73 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -95,7 +95,7 @@ export class ViewComponent implements OnDestroy, OnInit { cipherType = CipherType; private previousCipherId: string; - private passwordReprompted = false; + protected passwordReprompted = false; /** * Represents TOTP information including display formatting and timing From bd29397fd8859a6bf48b40f769682d835979823e Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Thu, 22 May 2025 13:55:26 -0500 Subject: [PATCH 129/163] [PM-21611] Require userId on KeyService clear methods (#14788) --- .../settings/account-security.component.ts | 3 +- .../service-container/service-container.ts | 2 +- .../src/app/accounts/settings.component.ts | 3 +- apps/web/src/app/app.component.ts | 2 +- .../vault-timeout-settings.service.ts | 2 +- .../vault-timeout-settings.service.ts | 2 +- .../src/abstractions/key.service.ts | 6 ++- libs/key-management/src/key.service.spec.ts | 44 +++++++++++-------- libs/key-management/src/key.service.ts | 12 ++--- 9 files changed, 41 insertions(+), 35 deletions(-) diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index d7b8aa573d4..26a805b3624 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -480,7 +480,8 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { }); await this.vaultNudgesService.dismissNudge(NudgeType.AccountSecurity, userId); } else { - await this.vaultTimeoutSettingsService.clear(); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.vaultTimeoutSettingsService.clear(userId); } } diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index cdf6c4bbfda..bc5db09da26 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -874,7 +874,7 @@ export class ServiceContainer { const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); await Promise.all([ this.eventUploadService.uploadEvents(userId as UserId), - this.keyService.clearKeys(), + this.keyService.clearKeys(userId), this.cipherService.clear(userId), this.folderService.clear(userId), this.collectionService.clear(userId), diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 83c982fbaba..fd0585e805e 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -506,7 +506,8 @@ export class SettingsComponent implements OnInit, OnDestroy { await this.updateRequirePasswordOnStart(); } - await this.vaultTimeoutSettingsService.clear(); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.vaultTimeoutSettingsService.clear(userId); } this.messagingService.send("redrawMenu"); diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index cac0487d05d..3de9bf0a8c8 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -282,7 +282,7 @@ export class AppComponent implements OnDestroy, OnInit { ); await Promise.all([ - this.keyService.clearKeys(), + this.keyService.clearKeys(userId), this.cipherService.clear(userId), this.folderService.clear(userId), this.collectionService.clear(userId), diff --git a/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout-settings.service.ts b/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout-settings.service.ts index 7094a2c2f83..9ff362e4009 100644 --- a/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout-settings.service.ts +++ b/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout-settings.service.ts @@ -59,5 +59,5 @@ export abstract class VaultTimeoutSettingsService { */ isBiometricLockSet: (userId?: string) => Promise; - clear: (userId?: string) => Promise; + clear: (userId: UserId) => Promise; } diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts index 0716bf0bb93..07b8a8c297d 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts @@ -287,7 +287,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA return availableActions; } - async clear(userId?: string): Promise { + async clear(userId: UserId): Promise { await this.keyService.clearPinKeys(userId); } diff --git a/libs/key-management/src/abstractions/key.service.ts b/libs/key-management/src/abstractions/key.service.ts index d67fec4c98e..95b79890c6a 100644 --- a/libs/key-management/src/abstractions/key.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -370,8 +370,9 @@ export abstract class KeyService { * Note: This will remove the stored pin and as a result, * disable pin protection for the user * @param userId The desired user + * @throws Error when provided userId is null or undefined */ - abstract clearPinKeys(userId?: string): Promise; + abstract clearPinKeys(userId: UserId): Promise; /** * @param keyMaterial The key material to derive the send key from * @returns A new send key @@ -380,8 +381,9 @@ export abstract class KeyService { /** * Clears all of the user's keys from storage * @param userId The user's Id + * @throws Error when provided userId is null or undefined */ - abstract clearKeys(userId?: string): Promise; + abstract clearKeys(userId: UserId): Promise; abstract randomNumber(min: number, max: number): Promise; /** * Generates a new cipher key diff --git a/libs/key-management/src/key.service.spec.ts b/libs/key-management/src/key.service.spec.ts index 25d8aff99fb..f1f1286dfc5 100644 --- a/libs/key-management/src/key.service.spec.ts +++ b/libs/key-management/src/key.service.spec.ts @@ -1,5 +1,5 @@ import { mock } from "jest-mock-extended"; -import { BehaviorSubject, bufferCount, firstValueFrom, lastValueFrom, of, take, tap } from "rxjs"; +import { BehaviorSubject, bufferCount, firstValueFrom, lastValueFrom, of, take } from "rxjs"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data"; @@ -380,16 +380,12 @@ describe("keyService", () => { }); describe("clearKeys", () => { - it("resolves active user id when called with no user id", async () => { - let callCount = 0; - stateProvider.activeUserId$ = stateProvider.activeUserId$.pipe(tap(() => callCount++)); - - await keyService.clearKeys(); - expect(callCount).toBe(1); - - // revert to the original state - accountService.activeAccount$ = accountService.activeAccountSubject.asObservable(); - }); + test.each([null as unknown as UserId, undefined as unknown as UserId])( + "throws when the provided userId is %s", + async (userId) => { + await expect(keyService.clearKeys(userId)).rejects.toThrow("UserId is required"); + }, + ); describe.each([ USER_ENCRYPTED_ORGANIZATION_KEYS, @@ -397,14 +393,6 @@ describe("keyService", () => { USER_ENCRYPTED_PRIVATE_KEY, USER_KEY, ])("key removal", (key: UserKeyDefinition) => { - it(`clears ${key.key} for active user when unspecified`, async () => { - await keyService.clearKeys(); - - const encryptedOrgKeyState = stateProvider.singleUser.getFake(mockUserId, key); - expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1); - expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledWith(null); - }); - it(`clears ${key.key} for the specified user when specified`, async () => { const userId = "someOtherUser" as UserId; await keyService.clearKeys(userId); @@ -416,6 +404,24 @@ describe("keyService", () => { }); }); + describe("clearPinKeys", () => { + test.each([null as unknown as UserId, undefined as unknown as UserId])( + "throws when the provided userId is %s", + async (userId) => { + await expect(keyService.clearPinKeys(userId)).rejects.toThrow("UserId is required"); + }, + ); + it("calls pin service to clear", async () => { + const userId = "someOtherUser" as UserId; + + await keyService.clearPinKeys(userId); + + expect(pinService.clearPinKeyEncryptedUserKeyPersistent).toHaveBeenCalledWith(userId); + expect(pinService.clearPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(userId); + expect(pinService.clearUserKeyEncryptedPin).toHaveBeenCalledWith(userId); + }); + }); + describe("userPrivateKey$", () => { type SetupKeysParams = { makeMasterKey: boolean; diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index 849b9db6a50..9372dafd3ea 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -564,11 +564,9 @@ export class DefaultKeyService implements KeyServiceAbstraction { await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId); } - async clearPinKeys(userId?: UserId): Promise { - userId ??= await firstValueFrom(this.stateProvider.activeUserId$); - + async clearPinKeys(userId: UserId): Promise { if (userId == null) { - throw new Error("Cannot clear PIN keys, no user Id resolved."); + throw new Error("UserId is required"); } await this.pinService.clearPinKeyEncryptedUserKeyPersistent(userId); @@ -588,11 +586,9 @@ export class DefaultKeyService implements KeyServiceAbstraction { return (await this.keyGenerationService.createKey(512)) as CipherKey; } - async clearKeys(userId?: UserId): Promise { - userId ??= await firstValueFrom(this.stateProvider.activeUserId$); - + async clearKeys(userId: UserId): Promise { if (userId == null) { - throw new Error("Cannot clear keys, no user Id resolved."); + throw new Error("UserId is required"); } await this.masterPasswordService.clearMasterKeyHash(userId); From 5702a17772f44b60ea6637efd4c34a6ced12ac82 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 11:57:58 +0200 Subject: [PATCH 130/163] Autosync the updated translations (#14892) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 50 +- apps/browser/src/_locales/az/messages.json | 50 +- apps/browser/src/_locales/be/messages.json | 50 +- apps/browser/src/_locales/bg/messages.json | 50 +- apps/browser/src/_locales/bn/messages.json | 50 +- apps/browser/src/_locales/bs/messages.json | 50 +- apps/browser/src/_locales/ca/messages.json | 52 +- apps/browser/src/_locales/cs/messages.json | 50 +- apps/browser/src/_locales/cy/messages.json | 50 +- apps/browser/src/_locales/da/messages.json | 50 +- apps/browser/src/_locales/de/messages.json | 50 +- apps/browser/src/_locales/el/messages.json | 50 +- apps/browser/src/_locales/en_GB/messages.json | 50 +- apps/browser/src/_locales/en_IN/messages.json | 50 +- apps/browser/src/_locales/es/messages.json | 76 +- apps/browser/src/_locales/et/messages.json | 50 +- apps/browser/src/_locales/eu/messages.json | 50 +- apps/browser/src/_locales/fa/messages.json | 1310 +++++++++-------- apps/browser/src/_locales/fi/messages.json | 92 +- apps/browser/src/_locales/fil/messages.json | 50 +- apps/browser/src/_locales/fr/messages.json | 50 +- apps/browser/src/_locales/gl/messages.json | 50 +- apps/browser/src/_locales/he/messages.json | 50 +- apps/browser/src/_locales/hi/messages.json | 50 +- apps/browser/src/_locales/hr/messages.json | 50 +- apps/browser/src/_locales/hu/messages.json | 50 +- apps/browser/src/_locales/id/messages.json | 50 +- apps/browser/src/_locales/it/messages.json | 50 +- apps/browser/src/_locales/ja/messages.json | 50 +- apps/browser/src/_locales/ka/messages.json | 50 +- apps/browser/src/_locales/km/messages.json | 50 +- apps/browser/src/_locales/kn/messages.json | 50 +- apps/browser/src/_locales/ko/messages.json | 50 +- apps/browser/src/_locales/lt/messages.json | 92 +- apps/browser/src/_locales/lv/messages.json | 50 +- apps/browser/src/_locales/ml/messages.json | 50 +- apps/browser/src/_locales/mr/messages.json | 50 +- apps/browser/src/_locales/my/messages.json | 50 +- apps/browser/src/_locales/nb/messages.json | 50 +- apps/browser/src/_locales/ne/messages.json | 50 +- apps/browser/src/_locales/nl/messages.json | 50 +- apps/browser/src/_locales/nn/messages.json | 50 +- apps/browser/src/_locales/or/messages.json | 50 +- apps/browser/src/_locales/pl/messages.json | 50 +- apps/browser/src/_locales/pt_BR/messages.json | 50 +- apps/browser/src/_locales/pt_PT/messages.json | 50 +- apps/browser/src/_locales/ro/messages.json | 50 +- apps/browser/src/_locales/ru/messages.json | 78 +- apps/browser/src/_locales/si/messages.json | 50 +- apps/browser/src/_locales/sk/messages.json | 52 +- apps/browser/src/_locales/sl/messages.json | 50 +- apps/browser/src/_locales/sr/messages.json | 102 +- apps/browser/src/_locales/sv/messages.json | 50 +- apps/browser/src/_locales/te/messages.json | 50 +- apps/browser/src/_locales/th/messages.json | 50 +- apps/browser/src/_locales/tr/messages.json | 50 +- apps/browser/src/_locales/uk/messages.json | 50 +- apps/browser/src/_locales/vi/messages.json | 50 +- apps/browser/src/_locales/zh_CN/messages.json | 64 +- apps/browser/src/_locales/zh_TW/messages.json | 50 +- 60 files changed, 3254 insertions(+), 1214 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index c96f4a5803b..48ae14fc1ce 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "لم يتم العثور على معرف فريد." }, - "convertOrganizationEncryptionDesc": { - "message": "يستخدم $ORGANIZATION$ SSO مع خادم مفتاح الاستضافة الذاتية. لم تعد هناك حاجة إلى كلمة مرور رئيسية لتسجيل الدخول لأعضاء هذه المنظمة.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "مغادرة المؤسسة" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 51eb1345466..c9096bce0d6 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Unikal identifikator tapılmadı." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$, self-hosted açar serveri ilə SSO istifadə edir. Bu təşkilatın üzvlərinin giriş etməsi üçün artıq ana parol tələb edilməyəcək.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Aşağıdakı təşkilatların üzvləri üçün artıq ana parol tələb olunmur. Lütfən aşağıdakı domeni təşkilatınızın inzibatçısı ilə təsdiqləyin." + }, + "organizationName": { + "message": "Təşkilat adı" + }, + "keyConnectorDomain": { + "message": "Key Connector domeni" }, "leaveOrganization": { "message": "Təşkilatı tərk et" @@ -3615,6 +3615,14 @@ "message": "Şifrələnmiş məlumatları hər kəslə güvənli şəkildə paylaşmaq üçün \"Send\"i istifadə edin.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send, həssas məlumatlar təhlükəsizdir", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsBodyNoItems": { + "message": "İstənilən platformada faylları və dataları hər kəslə paylaşın. İfşa olunmağı məhdudlaşdıraraq məlumatlarınız ucdan-uca şifrələnmiş qalacaq.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Giriş lazımdır." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Bilinməyən bilinməyən bir səbəbə görə biometrik kilid açma əlçatmazdır." }, + "unlockVault": { + "message": "Seyfinizin kilidini saniyələr ərzində açın" + }, + "unlockVaultDesc": { + "message": "Seyfinizə daha cəld müraciət etmək üçün kilid açma və bitmə vaxtı ayarlarını özəlləşdirə bilərsiniz." + }, + "unlockPinSet": { + "message": "PIN ilə kilid açma təyini" + }, "authenticating": { "message": "Kimlik doğrulama" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Cəld parol yaradın" + }, + "generatorNudgeBodyOne": { + "message": "Klikləyərək güclü və unikal parolları asanlıqla yaradın", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyTwo": { + "message": "və girişlərinizi güvənli şəkildə saxlayın.", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyAria": { + "message": "Girişlərinizi güvənli şəkildə saxlamağınıza kömək etməsi üçün Parol yarat düyməsinə klikləyərək güclü və unikal parolları asanlıqla yaradın.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "Bu səhifəyə baxmaq icazəniz yoxdur. Fərqli hesabla giriş etməyə çalışın." } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index a51b96547da..0dd5ff4163f 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Не знойдзены ўнікальны ідэнтыфікатар." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ выкарыстоўвае SSO з уласным серверам ключоў. Асноўны пароль для ўдзельнікаў гэтай арганізацыі больш не патрабуецца.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Выйсці з арганізацыі" @@ -3615,6 +3615,14 @@ "message": "Выкарыстоўвайце Send'ы, каб бяспечна абагуляць зашыфраваную інфармацыю з іншымі.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Неабходны ўвод даных." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 09e5260f9b3..0bf8dbce2b0 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Няма намерен уникален идентификатор." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ използва еднократно удостоверяване със собствен сървър за ключове. Членовете на тази организация вече нямат нужда от главна парола за вписване.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "За членовете на следната организация вече не се изисква главна парола. Потвърдете домейна по-долу с администратора на организацията си." + }, + "organizationName": { + "message": "Име на организацията" + }, + "keyConnectorDomain": { + "message": "Домейн на конектора за ключове" }, "leaveOrganization": { "message": "Напускане на организацията" @@ -3615,6 +3615,14 @@ "message": "Използвайте Изпращане, за да споделите безопасно шифрована информация с някого.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Изпращайте чувствителна информация сигурно", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsBodyNoItems": { + "message": "Споделяйте сигурно файлове и данни с всекиго, през всяка система. Информацията Ви ще бъде защитена с шифроване от край до край, а видимостта ѝ ще бъде ограничена.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Полето е задължително да бъде попълнено." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Отключването с биометрични данни не е налично по неизвестна причина." }, + "unlockVault": { + "message": "Отключвайте трезора си за секунди" + }, + "unlockVaultDesc": { + "message": "Можете да персонализирате настройките си за отключване и време на активност, за да получавате достъп до трезора си по-бързо." + }, + "unlockPinSet": { + "message": "Зададен е ПИН код за отключване" + }, "authenticating": { "message": "Удостоверяване" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Създавайте пароли бързо" + }, + "generatorNudgeBodyOne": { + "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": "за да защитите данните си за вписване.", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyAria": { + "message": "Създавайте лесно сложни и уникални пароли като щракнете върху бутона за генериране на парола, за да защитите данните си за вписване.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "Нямате права за преглед на тази страница. Опитайте да се впишете с друг акаунт." } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 5e19936e975..f7f28115faa 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index f48037814e5..1dc35addd8e 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 151412c02ed..9517257ac3a 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -1057,7 +1057,7 @@ "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "notificationAddDesc": { - "message": "Bitwarden ha de recordar aquesta contrasenya per a vosaltres?" + "message": "Ha de recordar Bitwarden aquesta contrasenya per a vosaltres?" }, "notificationAddSave": { "message": "Guarda" @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No s'ha trobat cap identificador únic." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ està utilitzant SSO amb un servidor autoallotjat de claus. Ja no es requereix una contrasenya mestra d'inici de sessió per als membres d'aquesta organització.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Abandona l'organització" @@ -3615,6 +3615,14 @@ "message": "Utilitzeu Send per compartir informació xifrada de manera segura amb qualsevol persona.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "L'entrada és obligatòria." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "S'està autenticant" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index effc4c950c6..b213e0aee7d 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Nenalezen žádný jedinečný identifikátor." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ používá SSO s vlastním serverem s klíči. Hlavní heslo pro členy této organizace již není vyžadováno.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Hlavní heslo již není vyžadováno pro členy následující organizace. Potvrďte níže uvedenou doménu u správce Vaší organizace." + }, + "organizationName": { + "message": "Název organizace" + }, + "keyConnectorDomain": { + "message": "Doména Key Connectoru" }, "leaveOrganization": { "message": "Opustit organizaci" @@ -3615,6 +3615,14 @@ "message": "Použijte Send pro bezpečné sdílení šifrovaných informací s kýmkoliv.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Posílejte citlivé informace bezpečně", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsBodyNoItems": { + "message": "Sdílejte bezpečně soubory a data s kýmkoli na libovolné platformě. Vaše informace zůstanou šifrovány a zároveň omezují expozici.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Je vyžadován vstup." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometrické odemknutí je momentálně z neznámého důvodu nedostupné." }, + "unlockVault": { + "message": "Rychlé odemknutí trezoru" + }, + "unlockVaultDesc": { + "message": "Pro rychlejší přístup k trezoru můžete upravit nastavení odemknutí a vypršení časového limitu." + }, + "unlockPinSet": { + "message": "PIN pro odemknutí byl nastaven" + }, "authenticating": { "message": "Ověřování" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Rychlé vytvoření hesla" + }, + "generatorNudgeBodyOne": { + "message": "Jednoduše vytvořte silná a unikátní hesla klepnutím na", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyTwo": { + "message": "aby Vám pomohlo udržet Vaše přihlašovací údaje v bezpečí.", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyAria": { + "message": "Jednoduše vytvořte silná a unikátní hesla klepnutím na tlačítko Generovat heslo, které Vám pomůže udržet Vaše přihlašovací údaje v bezpečí.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "Nemáte oprávnění k zobrazení této stránky. Zkuste se přihlásit jiným účtem." } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 047b45dd9b8..ee74d1e45e2 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index e6233b1f8db..da24e1bcfb9 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Ingen entydig identifikator fundet." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ bruger SSO med en selv-hostet nøgleserver. En hovedadgangskode er ikke længere påkrævet for at logge ind for medlemmer af denne organisation.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Forlad organisation" @@ -3615,6 +3615,14 @@ "message": "Brug Send til at dele krypterede oplysninger sikkert med nogen.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input obligatorisk." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometrisk oplåsning er p.t. utilgængelig grundet en ukendt årsag." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Godkender" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 1eac22c919c..f7303885551 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Keine eindeutige Kennung gefunden." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ verwendet SSO mit einem selbst gehosteten Schlüsselserver. Ein Master-Passwort ist nicht mehr erforderlich, damit sich Mitglieder dieser Organisation anmelden können.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Für Mitglieder der folgenden Organisation ist kein Master-Passwort mehr erforderlich. Bitte bestätige die folgende Domain bei deinem Organisations-Administrator." + }, + "organizationName": { + "message": "Name der Organisation" + }, + "keyConnectorDomain": { + "message": "Key Connector-Domain" }, "leaveOrganization": { "message": "Organisation verlassen" @@ -3615,6 +3615,14 @@ "message": "Verwende Send, um verschlüsselte Informationen sicher mit anderen zu teilen.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Sensible Informationen sicher versenden", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsBodyNoItems": { + "message": "Teile Dateien und Daten sicher mit jedem auf jeder Plattform. Deine Informationen bleiben Ende-zu-Ende-Verschlüsselt, während die Verbreitung begrenzt wird.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Eingabe ist erforderlich." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometrisches Entsperren ist derzeit aus einem unbekannten Grund nicht verfügbar." }, + "unlockVault": { + "message": "Entsperre deinen Tresor in Sekunden" + }, + "unlockVaultDesc": { + "message": "Du kannst deine Entsperr- und Timeout-Einstellungen anpassen, um schneller auf deinen Tresor zuzugreifen." + }, + "unlockPinSet": { + "message": "Entsperr-PIN festgelegt" + }, "authenticating": { "message": "Authentifizierung" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Passwörter schnell erstellen" + }, + "generatorNudgeBodyOne": { + "message": "Generiere ganz einfach starke und einzigartige Passwörter, indem du auf den", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyTwo": { + "message": ", um deine Zugangsdaten sicher aufzubewahren.", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyAria": { + "message": "Generiere ganz einfach starke und einzigartige Passwörter, indem du auf den \"Passwort generieren\"-Button klickst, um deine Zugangsdaten sicher aufzubewahren.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "Du hast keine Berechtigung, diese Seite anzuzeigen. Versuche dich mit einem anderen Konto anzumelden." } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index faad1a90a07..ed81dfa744e 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Δε βρέθηκε μοναδικό αναγνωριστικό." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ χρησιμοποιεί SSO με έναν αυτοεξυπηρετητή κλειδιών. Ένας κύριος κωδικός πρόσβασης δεν απαιτείται πλέον για να συνδεθείτε για τα μέλη αυτού του οργανισμού.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Αποχώρηση από τον οργανισμό" @@ -3615,6 +3615,14 @@ "message": "Χρήση Send για ασφαλή κοινοποίηση κρυπτογραφημένων πληροφοριών με οποιονδήποτε.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Απαιτείται εισαγωγή." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Ταυτοποίηση" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index d131d3ec33d..5eecf989894 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organisation.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organisation. Please confirm the domain below with your organisation administrator." + }, + "organizationName": { + "message": "Organisation name" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organisation" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customise your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index aa1c076b2e9..adb3cf6ec32 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organisation. Please confirm the domain below with your organisation administrator." + }, + "organizationName": { + "message": "Organisation name" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave Organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customise your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 3018cd36ed4..94d02848b5f 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -3,7 +3,7 @@ "message": "Bitwarden" }, "appLogoLabel": { - "message": "Bitwarden logo" + "message": "Logo de Bitwarden" }, "extName": { "message": "Bitwarden - Administrador de contraseñas", @@ -656,7 +656,7 @@ "message": "Tu navegador web no soporta copiar al portapapeles facilmente. Cópialo manualmente." }, "verifyYourIdentity": { - "message": "Verify your identity" + "message": "Verifica tu identidad" }, "weDontRecognizeThisDevice": { "message": "No reconocemos este dispositivo. Introduce el código enviado a tu correo electrónico para verificar tu identidad." @@ -872,16 +872,16 @@ "message": "Iniciar sesión en Bitwarden" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "Introduce el código enviado a tu correo electrónico" }, "enterTheCodeFromYourAuthenticatorApp": { "message": "Introduce el código de tu aplicación de autenticación" }, "pressYourYubiKeyToAuthenticate": { - "message": "Press your YubiKey to authenticate" + "message": "Pulsa tu YubiKey para identificarte" }, "duoTwoFactorRequiredPageSubtitle": { - "message": "Duo two-step login is required for your account. Follow the steps below to finish logging in." + "message": "Se requiere inicio de sesión en dos pasos para tu cuenta. Sigue los pasos siguientes para terminar de iniciar sesión." }, "followTheStepsBelowToFinishLoggingIn": { "message": "Follow the steps below to finish logging in." @@ -911,7 +911,7 @@ "message": "No" }, "location": { - "message": "Location" + "message": "Ubicación" }, "unexpectedError": { "message": "Ha ocurrido un error inesperado." @@ -1063,7 +1063,7 @@ "message": "Guardar" }, "notificationViewAria": { - "message": "View $ITEMNAME$, opens in new window", + "message": "Ver $ITEMNAME$, se abre en una nueva ventana", "placeholders": { "itemName": { "content": "$1" @@ -1072,7 +1072,7 @@ "description": "Aria label for the view button in notification bar confirmation message" }, "notificationNewItemAria": { - "message": "New Item, opens in new window", + "message": "Nuevo Elemento, se abre en una nueva ventana", "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" }, "notificationEditTooltip": { @@ -2594,7 +2594,7 @@ "message": "Illustration of the Bitwarden autofill menu displaying a generated password." }, "updateInBitwarden": { - "message": "Update in Bitwarden" + "message": "Actualizar en Bitwarden" }, "updateInBitwardenSlideDesc": { "message": "Bitwarden will then prompt you to update the password in the password manager.", @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Identificador único no encontrado." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ está usando SSO con un servidor de claves autoalojado. Los miembros de esta organización ya no necesitarán una contraseña maestra para iniciar sesión.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Ya no es necesaria una contraseña maestra para los miembros de la siguiente organización. Confirma el dominio que aparece a continuación con el administrador de tu organización." + }, + "organizationName": { + "message": "Nombre de la organización" + }, + "keyConnectorDomain": { + "message": "Dominio del conector de clave" }, "leaveOrganization": { "message": "Abandonar organización" @@ -3406,7 +3406,7 @@ "message": "Inicio de sesión en proceso" }, "logInRequestSent": { - "message": "Request sent" + "message": "Solicitud enviada" }, "exposedMasterPassword": { "message": "Contraseña maestra comprometida" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Entrada requerida." }, @@ -3817,7 +3825,7 @@ "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { - "message": "New card", + "message": "Nueva tarjeta", "description": "Button text to display within inline menu when there are no matching items on a credit card field" }, "addNewCardItemAria": { @@ -4553,10 +4561,10 @@ "message": "Download from bitwarden.com now" }, "getItOnGooglePlay": { - "message": "Get it on Google Play" + "message": "Consíguela en Google Play" }, "downloadOnTheAppStore": { - "message": "Download on the App Store" + "message": "Descarga en la App Store" }, "permanentlyDeleteAttachmentConfirmation": { "message": "¿Estás seguro de que deseas eliminar permanentemente este adjunto?" @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Autenticando" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 5bce2142219..26b4bf69fb4 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Unikaalset identifikaatorit ei leitud." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ kasutab SSO-d koos enda majutatud võtmeserveriga. Selle organisatsiooni liikmed ei pea sisselogimisel enam ülemparooli kasutama.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Lahku organisatsioonist" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Sisestus on nõutav." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 54b7b9b234c..b3f525d7be5 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Ez da identifikatzaile bakarrik aurkitu." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ SSO erabiltzen ari da ostatatze propioa duen gako-zerbitzari batekin. Dagoeneko ez da pasahitz nagusirik behar erakunde honetako kideentzat saioa hasteko.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Utzi erakundea" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index f132b61fc4e..33a779a7909 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -6,7 +6,7 @@ "message": "لوگو Bitwarden" }, "extName": { - "message": "مدیریت رمز عبور Bitwarden", + "message": "مدیریت کلمه عبور Bitwarden", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { @@ -35,10 +35,10 @@ "message": "خوش آمدید" }, "setAStrongPassword": { - "message": "تنظیم رمز عبور قوی" + "message": "تنظیم کلمه عبور قوی" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "ایجاد حساب خود را با تنظیم رمز عبور تکمیل کنید" + "message": "ایجاد حساب کاربری خود را با تنظیم کلمه عبور تکمیل کنید" }, "enterpriseSingleSignOn": { "message": "ورود به سیستم پروژه" @@ -303,7 +303,7 @@ "message": "درباره استفاده از Bitwarden در مرکز راهنما بیشتر بیاموزید." }, "continueToBrowserExtensionStore": { - "message": "آیا میخواهید به فروشگاه افزونه مرورگر ادامه دهید?" + "message": "آیا می‌خواهید به فروشگاه افزونه مرورگر ادامه دهید؟" }, "continueToBrowserExtensionStoreDesc": { "message": "به دیگران کمک کنید تا بفهمند آیا Bitwarden برایشان مناسب است یا نه. به فروشگاه افزونه مرورگر خود بروید و نظر خود را به اشتراک بگذارید." @@ -341,7 +341,7 @@ "message": "Bitwarden برای کسب و کارها" }, "bitwardenAuthenticator": { - "message": "تاییدکننده هویت Bitwarden" + "message": "تأییدکننده هویت Bitwarden" }, "continueToAuthenticatorPageDesc": { "message": "احراز هویت کننده Bitwarden به شما امکان می‌دهد کلیدهای احراز هویت را ذخیره کرده و کدهای TOTP را برای فرآیندهای تأیید دومرحله‌ای تولید کنید. برای اطلاعات بیشتر به وب‌سایت bitwarden.com مراجعه کنید" @@ -796,7 +796,7 @@ "message": "حساب کاربری جدید شما ساخته شد! حالا می‌توانید وارد شوید." }, "newAccountCreated2": { - "message": "حساب جدید شما ایجاد شده است!" + "message": "حساب کاربری جدید شما ایجاد شده است!" }, "youHaveBeenLoggedIn": { "message": "شما با موفقیت وارد شدید!" @@ -839,55 +839,55 @@ "message": "کلید احراز هویت اضافه شد" }, "totpCapture": { - "message": "Scan authenticator QR code from current webpage" + "message": "اسکن کد QR احراز هویت کننده از صفحه وب فعلی" }, "totpHelperTitle": { - "message": "Make 2-step verification seamless" + "message": "تأیید دو مرحله‌ای را بدون دردسر کنید" }, "totpHelper": { - "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + "message": "Bitwarden می‌تواند کدهای تأیید دو مرحله‌ای را ذخیره و پر کند. کلید را کپی کرده و در این فیلد قرار دهید." }, "totpHelperWithCapture": { - "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." + "message": "Bitwarden می‌تواند کدهای تأیید دو مرحله‌ای را ذخیره و پر کند. برای اسکن کد QR احراز هویت کننده این وب‌سایت، روی آیکون دوربین کلیک کنید یا کلید را کپی کرده و در این فیلد قرار دهید." }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "درباره احراز هویت کننده‌ها بیشتر بدانید" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "کپی کلید احراز هویت (TOTP)" }, "loggedOut": { "message": "خارج شد" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "شما از حساب خود خارج شده‌اید." }, "loginExpired": { "message": "نشست ورود شما منقضی شده است." }, "logIn": { - "message": "Log in" + "message": "ورود" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "وارد Bitwarden شوید" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "کدی را که به ایمیل شما ارسال شده وارد کنید" }, "enterTheCodeFromYourAuthenticatorApp": { - "message": "Enter the code from your authenticator app" + "message": "کد سامانه تأیید کننده را وارد نمایید" }, "pressYourYubiKeyToAuthenticate": { - "message": "Press your YubiKey to authenticate" + "message": "برای احراز هویت، کلید YubiKey خود را فشار دهید" }, "duoTwoFactorRequiredPageSubtitle": { - "message": "Duo two-step login is required for your account. Follow the steps below to finish logging in." + "message": "برای حساب کاربری شما ورود دو مرحله‌ای Duo لازم است. مراحل زیر را دنبال کنید تا ورود خود را کامل کنید." }, "followTheStepsBelowToFinishLoggingIn": { - "message": "Follow the steps below to finish logging in." + "message": "مراحل زیر را دنبال کنید تا وارد سیستم شوید." }, "followTheStepsBelowToFinishLoggingInWithSecurityKey": { - "message": "Follow the steps below to finish logging in with your security key." + "message": "مراحل زیر را برای کامل کردن ورود با کلید امنیتی خود دنبال کنید." }, "restartRegistration": { "message": "ثبت‌نام را دوباره آغاز کنید" @@ -896,10 +896,10 @@ "message": "پیوند منقضی شد" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "لطفاً ثبت نام را مجدداً شروع کنید یا دوباره وارد شوید." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "ممکن است قبلاً حساب کاربری داشته باشید" }, "logOutConfirmation": { "message": "آیا مطمئنید که می‌خواهید خارج شوید؟" @@ -926,10 +926,10 @@ "message": "ورود دو مرحله ای باعث می‌شود که حساب کاربری شما با استفاده از یک دستگاه دیگر مانند کلید امنیتی، برنامه احراز هویت، پیامک، تماس تلفنی و یا ایمیل، اعتبار خود را با ایمنی بیشتر اثبات کند. ورود دو مرحله ای می تواند در bitwarden.com فعال شود. آیا می‌خواهید از سایت بازدید کنید؟" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "با راه‌اندازی ورود دو مرحله‌ای در برنامه وب Bitwarden، حساب کاربری خود را ایمن‌تر کنید." }, "twoStepLoginConfirmationTitle": { - "message": "Continue to web app?" + "message": "با برنامه وب ادامه می‌دهید؟" }, "editedFolder": { "message": "پوشه ذخیره شد" @@ -1016,16 +1016,16 @@ "message": "درخواست افزودن ورود به سیستم" }, "vaultSaveOptionsTitle": { - "message": "Save to vault options" + "message": "گزینه‌های ذخیره در گاوصندوق" }, "addLoginNotificationDesc": { "message": "در صورتی که موردی در گاوصندوق شما یافت نشد، درخواست افزودن کنید." }, "addLoginNotificationDescAlt": { - "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." + "message": "اگر موردی در گاوصندوق شما یافت نشد، درخواست افزودن آن را بدهید. این مورد برای همه حساب‌های وارد شده اعمال می‌شود." }, "showCardsInVaultViewV2": { - "message": "Always show cards as Autofill suggestions on Vault view" + "message": "همیشه کارت‌ها را به‌عنوان پیشنهادهای پر کردن خودکار در نمای گاوصندوق نمایش بده" }, "showCardsCurrentTab": { "message": "نمایش کارت‌ها در صفحه برگه" @@ -1034,7 +1034,7 @@ "message": "برای پر کردن خودکار آسان، موارد کارت را در صفحه برگه فهرست کن." }, "showIdentitiesInVaultViewV2": { - "message": "Always show identities as Autofill suggestions on Vault view" + "message": "همیشه هویت‌ها را به‌عنوان پیشنهادهای پر کردن خودکار در نمای گاوصندوق نمایش بده" }, "showIdentitiesCurrentTab": { "message": "نشان دادن هویت در صفحه برگه" @@ -1043,10 +1043,10 @@ "message": "موارد هویتی را در صفحه برگه برای پر کردن خودکار آسان فهرست کن." }, "clickToAutofillOnVault": { - "message": "Click items to autofill on Vault view" + "message": "برای پر کردن خودکار، روی موردها در نمای گاوصندوق کلیک کنید" }, "clickToAutofill": { - "message": "Click items in autofill suggestion to fill" + "message": "برای پر کردن، روی موردها در پیشنهادهای پرکردن خودکار کلیک کنید" }, "clearClipboard": { "message": "پاکسازی کلیپ بورد", @@ -1063,7 +1063,7 @@ "message": "ذخیره" }, "notificationViewAria": { - "message": "View $ITEMNAME$, opens in new window", + "message": "مشاهده $ITEMNAME$، در پنجره جدید باز می‌شود", "placeholders": { "itemName": { "content": "$1" @@ -1072,18 +1072,18 @@ "description": "Aria label for the view button in notification bar confirmation message" }, "notificationNewItemAria": { - "message": "New Item, opens in new window", + "message": "مورد جدید، در پنجره جدید باز می‌شود", "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" }, "notificationEditTooltip": { - "message": "Edit before saving", + "message": "ویرایش قبل ذخیره کردن", "description": "Tooltip and Aria label for edit button on cipher item" }, "newNotification": { - "message": "New notification" + "message": "اعلان جدید" }, "labelWithNotification": { - "message": "$LABEL$: New notification", + "message": "$LABEL$: اعلان جدید", "description": "Label for the notification with a new login suggestion.", "placeholders": { "label": { @@ -1093,15 +1093,15 @@ } }, "notificationLoginSaveConfirmation": { - "message": "saved to Bitwarden.", + "message": "در Bitwarden ذخیره شد.", "description": "Shown to user after item is saved." }, "notificationLoginUpdatedConfirmation": { - "message": "updated in Bitwarden.", + "message": "در Bitwarden به‌روزرسانی شد.", "description": "Shown to user after item is updated." }, "selectItemAriaLabel": { - "message": "Select $ITEMTYPE$, $ITEMNAME$", + "message": "انتخاب $ITEMTYPE$، $ITEMNAME$", "description": "Used by screen readers. $1 is the item type (like vault or folder), $2 is the selected item name.", "placeholders": { "itemType": { @@ -1113,35 +1113,35 @@ } }, "saveAsNewLoginAction": { - "message": "Save as new login", + "message": "ذخیره به عنوان ورود جدید", "description": "Button text for saving login details as a new entry." }, "updateLoginAction": { - "message": "Update login", + "message": "به‌روزرسانی ورود", "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "برای ذخیره این ورود، قفل را باز کنید", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { - "message": "Save login", + "message": "ذخیره ورود", "description": "Prompt asking the user if they want to save their login details." }, "updateLogin": { - "message": "Update existing login", + "message": "به‌روزرسانی ورود به سیستم موجود", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { - "message": "Login saved", + "message": "ورود ذخیره شد", "description": "Message displayed when login details are successfully saved." }, "loginUpdateSuccess": { - "message": "Login updated", + "message": "ورود به‌روزرسانی شد", "description": "Message displayed when login details are successfully updated." }, "loginUpdateTaskSuccess": { - "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "message": "آفرین! شما اقداماتی را انجام دادید تا خودتان و $ORGANIZATION$ را ایمن‌تر کنید.", "placeholders": { "organization": { "content": "$1" @@ -1150,7 +1150,7 @@ "description": "Shown to user after login is updated." }, "loginUpdateTaskSuccessAdditional": { - "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "message": "ممنون که $ORGANIZATION$ را ایمن‌تر کردید. شما $TASK_COUNT$ کلمات عبور دیگر برای به‌روزرسانی دارید.", "placeholders": { "organization": { "content": "$1" @@ -1162,15 +1162,15 @@ "description": "Shown to user after login is updated." }, "nextSecurityTaskAction": { - "message": "Change next password", + "message": "تغییر کلمه عبور بعدی", "description": "Message prompting user to undertake completion of another security task." }, "saveFailure": { - "message": "Error saving", + "message": "خطای ذخیره", "description": "Error message shown when the system fails to save login details." }, "saveFailureDetails": { - "message": "Oh no! We couldn't save this. Try entering the details manually.", + "message": "اوه نه! نتوانستیم این را ذخیره کنیم. لطفاً جزئیات را به‌صورت دستی وارد کنید.", "description": "Detailed error message shown when saving login details fails." }, "enableChangedPasswordNotification": { @@ -1180,13 +1180,13 @@ "message": "هنگامی که تغییری در یک وب‌سایت شناسایی شد، درخواست به‌روزرسانی کلمه عبور ورود کن." }, "changedPasswordNotificationDescAlt": { - "message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts." + "message": "هنگامی که تغییری در کلمه عبور یک ورود در وب‌سایت شناسایی شود، درخواست به‌روزرسانی آن را بده. این مورد برای همه حساب‌های وارد شده اعمال می‌شود." }, "enableUsePasskeys": { "message": "برای ذخیره و استفاده از passkey اجازه بگیر" }, "usePasskeysDesc": { - "message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts." + "message": "درخواست ذخیره کلیدهای عبور جدید یا ورود با کلیدهای عبوری که در گاوصندوق شما ذخیره شده‌اند. این مورد برای همه حساب‌های وارد شده اعمال می‌شود." }, "notificationChangeDesc": { "message": "آیا مایل به به‌روزرسانی این کلمه عبور در Bitwarden هستید؟" @@ -1210,7 +1210,7 @@ "message": "از یک کلیک ثانویه برای دسترسی به تولید کلمه عبور و ورودهای منطبق برای وب سایت استفاده کن." }, "contextMenuItemDescAlt": { - "message": "Use a secondary click to access password generation and matching logins for the website. Applies to all logged in accounts." + "message": "برای دسترسی به تولید کلمه عبور و ورودهای منطبق با وب‌سایت، از کلیک ثانویه استفاده کنید. این مورد برای همه حساب‌های کاربری وارد شده اعمال می‌شود." }, "defaultUriMatchDetection": { "message": "بررسی مطابقت نشانی اینترنتی پیش‌فرض", @@ -1226,7 +1226,7 @@ "message": "تغییر رنگ پوسته برنامه." }, "themeDescAlt": { - "message": "Change the application's color theme. Applies to all logged in accounts." + "message": "تغییر تم رنگی برنامه. این مورد برای همه حساب‌های کاربری وارد شده اعمال می‌شود." }, "dark": { "message": "تاریک", @@ -1237,7 +1237,7 @@ "description": "Light color" }, "exportFrom": { - "message": "صادرات از" + "message": "برون ریزی از" }, "exportVault": { "message": "برون ریزی گاوصندوق" @@ -1246,35 +1246,35 @@ "message": "فرمت پرونده" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "این پرونده برون ریزی با کلمه عبور محافظت می‌شود و برای رمزگشایی به کلمه عبور پرونده نیاز دارد." }, "filePassword": { - "message": "رمز فایل" + "message": "کلمه عبور پرونده" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "این کلمه عبور برای برون ریزی و درون ریزی این پرونده استفاده می‌شود" }, "accountRestrictedOptionDescription": { - "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + "message": "برای رمزگذاری برون ریزی و محدود کردن درون ریزی فقط به حساب کاربری فعلی Bitwarden، از کلید رمزگذاری حساب خود که از نام کاربری و کلمه عبور اصلی حساب شما مشتق شده است استفاده کنید." }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "یک کلمه عبور برای پرونده به‌منظور رمزگذاری تنظیم کنید تا برون ریزی و درون ریزی آن به هر حساب Bitwarden با استفاده از کلمه عبور رمزگشایی شود." }, "exportTypeHeading": { - "message": "نوع صادرات" + "message": "نوع برون ریزی" }, "accountRestricted": { "message": "حساب کاربری محدود شده است" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "عدم تطابق \"رمز فایل\" و \"تایید رمز فایل\" با یکدیگر." + "message": "عدم تطابق \"کلمه عبور پرونده\" و \"تأیید کلمه عبور پرونده\" با یکدیگر." }, "warning": { "message": "اخطار", "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "هشدار", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1296,7 +1296,7 @@ "message": "اشتراک گذاری شد" }, "bitwardenForBusinessPageDesc": { - "message": "Bitwarden for Business allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website." + "message": "Bitwarden for Business به شما اجازه می‌دهد تا با استفاده از یک سازمان، موارد گاوصندوق خود را با دیگران به اشتراک بگذارید. در وب سایت bitwarden.com بیشتر بیاموزید." }, "moveToOrganization": { "message": "انتقال به سازمان" @@ -1354,7 +1354,7 @@ "message": "پرونده" }, "fileToShare": { - "message": "File to share" + "message": "پرونده‌ای برای اشتراک‌گذاری" }, "selectFile": { "message": "ﺍﻧﺘﺨﺎﺏ یک ﭘﺮﻭﻧﺪﻩ" @@ -1390,13 +1390,13 @@ "message": "۱ گیگابایت فضای ذخیره سازی رمزگذاری شده برای پیوست های پرونده." }, "premiumSignUpEmergency": { - "message": "Emergency access." + "message": "دسترسی اضطراری." }, "premiumSignUpTwoStepOptions": { "message": "گزینه های ورود اضافی دو مرحله ای مانند YubiKey و Duo." }, "ppremiumSignUpReports": { - "message": "گزارش‌های بهداشت رمز عبور، سلامت حساب و نقض داده‌ها برای ایمن نگهداشتن گاوصندوق شما." + "message": "گزارش‌های بهداشت کلمه عبور، سلامت حساب و نقض داده‌ها برای ایمن نگهداشتن گاوصندوق شما." }, "ppremiumSignUpTotp": { "message": "تولید کننده کد تأیید (2FA) از نوع TOTP برای ورودهای در گاوصندوقتان." @@ -1411,7 +1411,7 @@ "message": "خرید پرمیوم" }, "premiumPurchaseAlertV2": { - "message": "You can purchase Premium from your account settings on the Bitwarden web app." + "message": "می‌توانید نسخه پرمیوم را از تنظیمات حساب کاربری خود در اپلیکیشن وب Bitwarden خریداری کنید." }, "premiumCurrentMember": { "message": "شما یک عضو پرمیوم هستید!" @@ -1420,7 +1420,7 @@ "message": "برای حمایتتان از Bitwarden سپاسگزاریم." }, "premiumFeatures": { - "message": "Upgrade to Premium and receive:" + "message": "ارتقا به نسخه پرمیوم و دریافت:" }, "premiumPrice": { "message": "تمامش فقط $PRICE$ در سال!", @@ -1432,7 +1432,7 @@ } }, "premiumPriceV2": { - "message": "All for just $PRICE$ per year!", + "message": "تمامش فقط $PRICE$ در سال!", "placeholders": { "price": { "content": "$1", @@ -1459,10 +1459,10 @@ "message": "برای استفاده از این ویژگی عضویت پرمیوم لازم است." }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "پایان زمان احراز هویت" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "نشست احراز هویت منقضی شد. لطفاً فرایند ورود را دوباره شروع کنید." }, "verificationCodeEmailSent": { "message": "ایمیل تأیید به $EMAIL$ ارسال شد.", @@ -1474,29 +1474,29 @@ } }, "dontAskAgainOnThisDeviceFor30Days": { - "message": "Don't ask again on this device for 30 days" + "message": "در این دستگاه به مدت ۳۰ روز دوباره نپرس" }, "selectAnotherMethod": { - "message": "Select another method", + "message": "انتخاب روش دیگر", "description": "Select another two-step login method" }, "useYourRecoveryCode": { - "message": "Use your recovery code" + "message": "از کد بازیابی‌تان استفاده کنید" }, "insertU2f": { "message": "کلید امنیتی خود را وارد پورت USB رایانه کنید، اگر دکمه ای دارد آن را بفشارید." }, "openInNewTab": { - "message": "Open in new tab" + "message": "گشودن در زبانهٔ جدید" }, "webAuthnAuthenticate": { "message": "تأیید اعتبار در WebAuthn" }, "readSecurityKey": { - "message": "Read security key" + "message": "خواندن کلید امنیتی" }, "awaitingSecurityKeyInteraction": { - "message": "Awaiting security key interaction..." + "message": "در انتظار تعامل با کلید امنیتی..." }, "loginUnavailable": { "message": "ورود به سیستم در دسترس نیست" @@ -1511,7 +1511,7 @@ "message": "گزینه‌های ورود دو مرحله‌ای" }, "selectTwoStepLoginMethod": { - "message": "Select two-step login method" + "message": "انتخاب ورود دو مرحله‌ای" }, "recoveryCodeDesc": { "message": "دسترسی به تمامی ارائه‌دهندگان دو مرحله‌ای را از دست داده‌اید؟ از کد بازیابی خود برای غیرفعال‌سازی ارائه‌دهندگان دو مرحله‌ای از حسابتان استفاده کنید." @@ -1523,17 +1523,17 @@ "message": "برنامه احراز هویت" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "کدی را وارد کنید که توسط یک برنامه احراز هویت مانند Bitwarden Authenticator تولید شده است.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "کلید امنیت Yubico OTP" }, "yubiKeyDesc": { "message": "از یک YubiKey برای دسترسی به حسابتان استفاده کنید. همراه با دستگاه‌های YubiKey 4 ،4 Nano ،NEO کار می‌کند." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "کدی را وارد کنید که توسط Duo Security تولید شده است.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1550,19 +1550,19 @@ "message": "ایمیل" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "کدی را که به ایمیل شما ارسال شده وارد کنید." }, "selfHostedEnvironment": { "message": "محیط خود میزبان" }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "نشانی اینترنتی پایه نصب Bitwarden خود را که به‌صورت داخلی میزبانی شده مشخص کنید.\nمثال: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "برای پیکربندی پیشرفته، می‌توانید نشانی اینترنتی پایه هر سرویس را به‌صورت مستقل مشخص کنید." }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "شما باید یا نشانی اینترنتی پایه سرور را اضافه کنید، یا حداقل یک محیط سفارشی تعریف کنید." }, "customEnvironment": { "message": "محیط سفارشی" @@ -1571,7 +1571,7 @@ "message": "نشانی اینترنتی سرور" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "نشانی اینترنتی سرور خود میزبان", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1593,20 +1593,20 @@ "message": "نشانی‌های اینترنتی محیط ذخیره شد" }, "showAutoFillMenuOnFormFields": { - "message": "Show autofill menu on form fields", + "message": "نمایش منوی پر کردن خودکار روی فیلدهای فرم", "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "پیشنهادهای پر کردن خودکار" }, "autofillSpotlightTitle": { - "message": "Easily find autofill suggestions" + "message": "یافتن آسان پیشنهادهای پر کردن خودکار" }, "autofillSpotlightDesc": { - "message": "Turn off your browser's autofill settings, so they don't conflict with Bitwarden." + "message": "تنظیمات پر کردن خودکار مرورگر خود را غیرفعال کنید تا با Bitwarden تداخل نداشته باشد." }, "turnOffBrowserAutofill": { - "message": "Turn off $BROWSER$ autofill", + "message": "پر کردن خودکار $BROWSER$ را غیرفعال کنید", "placeholders": { "browser": { "content": "$1", @@ -1615,25 +1615,25 @@ } }, "turnOffAutofill": { - "message": "Turn off autofill" + "message": "پر کردن خودکار را خاموش کنید" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "نمایش پیشنهادهای پر کردن خودکار روی فیلدهای فرم" }, "showInlineMenuIdentitiesLabel": { - "message": "Display identities as suggestions" + "message": "نمایش هویت‌ها به‌عنوان پیشنهاد" }, "showInlineMenuCardsLabel": { - "message": "Display cards as suggestions" + "message": "نمایش کارت‌ها به‌عنوان پیشنهاد" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "نمایش پیشنهادها هنگام انتخاب آیکون" }, "showInlineMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "message": "برای تمام حساب‌های کاربری وارد شده اعمال می‌شود." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser's built in password manager settings to avoid conflicts." + "message": "تنظیمات مدیر کلمه عبور داخلی مرورگر خود را غیرفعال کنید تا از بروز تداخل جلوگیری شود." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "ویرایش تنظیمات مرورگر." @@ -1643,15 +1643,15 @@ "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { - "message": "When field is selected (on focus)", + "message": "وقتی فیلد انتخاب شد (در حالت فوکوس)", "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When autofill icon is selected", + "message": "وقتی آیکون پر کردن خودکار انتخاب شود", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Autofill on page load" + "message": "پر کردن خودکار هنگام بارگذاری صفحه" }, "enableAutoFillOnPageLoad": { "message": "پر کردن خودکار هنگام بارگذاری صفحه" @@ -1663,7 +1663,7 @@ "message": "وب‌سایت‌های در معرض خطر یا نامعتبر می‌توانند از پر کردن خودکار در بارگذاری صفحه سوء استفاده کنند." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "درباره‌ خطراتش بیش‌تر بدانید" }, "learnMoreAboutAutofill": { "message": "درباره پر کردن خودکار بیشتر بدانید" @@ -1693,13 +1693,13 @@ "message": "باز کردن گاوصندوق در نوار کناری" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "آخرین ورودی مورد استفاده برای وب سایت فعلی را به صورت خودکار پر کنید" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "آخرین کارت مورد استفاده برای وب سایت فعلی را به صورت خودکار پر کنید" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "آخرین هویت مورد استفاده برای وب سایت فعلی را به صورت خودکار پر کنید" }, "commandGeneratePasswordDesc": { "message": "یک کلمه عبور تصادفی جدید ایجاد کنید و آن را در کلیپ بورد کپی کنید" @@ -1723,7 +1723,7 @@ "message": "برای مرتب‌سازی بکشید" }, "dragToReorder": { - "message": "Drag to reorder" + "message": "برای تغییر ترتیب، بکشید و رها کنید" }, "cfTypeText": { "message": "متن" @@ -1735,7 +1735,7 @@ "message": "منطقی" }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "کادر انتخاب" }, "cfTypeLinked": { "message": "پیوند شده", @@ -1758,7 +1758,7 @@ "message": "یک تصویر قابل تشخیص در کنار هر ورود نشان دهید." }, "faviconDescAlt": { - "message": "Show a recognizable image next to each login. Applies to all logged in accounts." + "message": "نمایش تصویر قابل تشخیص کنار هر ورود به سیستم. برای تمام حساب‌های کاربری وارد شده اعمال می‌شود." }, "enableBadgeCounter": { "message": "نمایش شمارنده نشان" @@ -1920,7 +1920,7 @@ "message": "هویت" }, "typeSshKey": { - "message": "SSH key" + "message": "کلید SSH" }, "newItemHeader": { "message": "$TYPE$ جدید", @@ -1941,7 +1941,7 @@ } }, "viewItemHeader": { - "message": "View $TYPE$", + "message": "مشاهده $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1953,13 +1953,13 @@ "message": "تاریخچه کلمه عبور" }, "generatorHistory": { - "message": "Generator history" + "message": "تاریخچه تولید کننده" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "پاک کردن تاریخچه تولید کننده" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "اگر ادامه دهید، تمام ورودی‌ها به‌طور دائمی از تاریخچه تولید کننده حذف خواهند شد. آیا مطمئن هستید که می‌خواهید ادامه دهید؟" }, "back": { "message": "بازگشت" @@ -1968,7 +1968,7 @@ "message": "مجموعه‌ها" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ مجموعه‌ها", "placeholders": { "count": { "content": "$1", @@ -1998,7 +1998,7 @@ "message": "یادداشت‌های امن" }, "sshKeys": { - "message": "SSH Keys" + "message": "کلید‌‌های SSH" }, "clear": { "message": "پاک کردن", @@ -2024,7 +2024,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "دامنه پایه (پیشنهادی)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -2078,13 +2078,13 @@ "message": "هیچ کلمه عبوری برای فهرست کردن وجود ندارد." }, "clearHistory": { - "message": "Clear history" + "message": "پاک کردن تاریخچه" }, "nothingToShow": { - "message": "Nothing to show" + "message": "چیزی برای نشان دادن موجود نیست" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "شما اخیراً چیزی تولید نکرده‌اید" }, "remove": { "message": "حذف" @@ -2145,16 +2145,16 @@ "message": "باز کردن با پین" }, "setYourPinTitle": { - "message": "Set PIN" + "message": "تنظیم کد پین" }, "setYourPinButton": { - "message": "Set PIN" + "message": "تنظیم کد پین" }, "setYourPinCode": { "message": "کد پین خود را برای باز کردن Bitwarden تنظیم کنید. اگر به طور کامل از برنامه خارج شوید، تنظیمات پین شما از بین می‌رود." }, "setYourPinCode1": { - "message": "Your PIN will be used to unlock Bitwarden instead of your master password. Your PIN will reset if you ever fully log out of Bitwarden." + "message": "کد پین شما برای باز کردن Bitwarden به جای کلمه عبور اصلی استفاده خواهد شد. در صورتی که کاملاً از Bitwarden خارج شوید، کد پین شما ریست خواهد شد." }, "pinRequired": { "message": "کد پین الزامیست." @@ -2163,13 +2163,13 @@ "message": "کد پین معتبر نیست." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "تعداد تلاش‌های ناموفق پین زیاد شد. خارج می‌شوید." }, "unlockWithBiometrics": { "message": "با استفاده از بیومتریک باز کنید" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "باز کردن قفل با کلمه عبور اصلی" }, "awaitDesktop": { "message": "در انتظار تأیید از دسکتاپ" @@ -2181,7 +2181,7 @@ "message": "در زمان شروع مجدد، با کلمه عبور اصلی قفل کن" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "در زمان شروع مجدد، کلمه عبور اصلی نیاز است" }, "selectOneCollection": { "message": "شما باید حداقل یک مجموعه را انتخاب کنید." @@ -2193,39 +2193,39 @@ "message": "شبیه سازی" }, "passwordGenerator": { - "message": "Password generator" + "message": "تولید کننده کلمه عبور" }, "usernameGenerator": { - "message": "Username generator" + "message": "تولید کننده نام کاربری" }, "useThisEmail": { - "message": "Use this email" + "message": "از این ایمیل استفاده شود" }, "useThisPassword": { - "message": "Use this password" + "message": "از این کلمه عبور استفاده کن" }, "useThisUsername": { - "message": "Use this username" + "message": "از این نام کاربری استفاده کن" }, "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'" }, "vaultCustomization": { - "message": "Vault customization" + "message": "سفارشی‌سازی گاوصندوق" }, "vaultTimeoutAction": { "message": "عمل متوقف شدن گاو‌صندوق" }, "vaultTimeoutAction1": { - "message": "Timeout action" + "message": "اقدام در صورت پایان زمان" }, "lock": { "message": "قفل", @@ -2254,7 +2254,7 @@ "message": "مورد بازیابی شد" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "در حال حاضر حساب کاربری دارید؟" }, "vaultTimeoutLogOutConfirmation": { "message": "خروج از سیستم، تمام دسترسی ها به گاو‌صندوق شما را از بین می‌برد و نیاز به احراز هویت آنلاین پس از مدت زمان توقف دارد. آیا مطمئن هستید که می‌خواهید از این تنظیمات استفاده کنید؟" @@ -2347,7 +2347,7 @@ "message": "کلمه عبور اصلی جدید شما از شرایط سیاست پیروی نمی‌کند." }, "receiveMarketingEmailsV2": { - "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." + "message": "پیشنهادها، اعلان‌ها و فرصت‌های تحقیقاتی Bitwarden را در صندوق ورودی خود دریافت کنید." }, "unsubscribe": { "message": "لغو اشتراک" @@ -2374,7 +2374,7 @@ "message": "سیاست حفظ حریم خصوصی" }, "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { - "message": "Your new password cannot be the same as your current password." + "message": "کلمه عبور جدید شما نمی‌تواند با کلمه عبور فعلی‌تان یکسان باشد." }, "hintEqualsPassword": { "message": "اشاره به کلمه عبور شما نمی‌تواند همان کلمه عبور شما باشد." @@ -2383,10 +2383,10 @@ "message": "تأیید" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "خطای به‌روزرسانی توکن دسترسی" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "هیچ توکن به‌روزرسانی یا کلید API یافت نشد. لطفاً از حساب کاربری خود خارج شده و دوباره وارد شوید." }, "desktopSyncVerificationTitle": { "message": "تأیید همگام‌سازی دسکتاپ" @@ -2425,10 +2425,10 @@ "message": "عدم مطابقت حساب کاربری" }, "nativeMessagingWrongUserKeyTitle": { - "message": "Biometric key missmatch" + "message": "عدم تطابق کلید بیومتریک" }, "nativeMessagingWrongUserKeyDesc": { - "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + "message": "باز کردن قفل بیومتریک ناموفق بود. کلید مخفی بیومتریک نتوانست گاوصندوق را باز کند. لطفاً دوباره تنظیمات بیومتریک را انجام دهید." }, "biometricsNotEnabledTitle": { "message": "بیومتریک برپا نشده" @@ -2446,13 +2446,13 @@ "message": "کاربر قفل یا خارج شد" }, "biometricsNotUnlockedDesc": { - "message": "Please unlock this user in the desktop application and try again." + "message": "لطفاً این کاربر را در برنامه دسکتاپ باز کنید و دوباره تلاش کنید." }, "biometricsNotAvailableTitle": { - "message": "Biometric unlock unavailable" + "message": "باز کردن قفل بیومتریک در دسترس نیست" }, "biometricsNotAvailableDesc": { - "message": "Biometric unlock is currently unavailable. Please try again later." + "message": "باز کردن قفل بیومتریک در حال حاضر در دسترس نیست. لطفاً بعداً دوباره تلاش کنید." }, "biometricsFailedTitle": { "message": "زیست‌سنجی ناموفق بود" @@ -2479,17 +2479,17 @@ "message": "سیاست سازمانی بر تنظیمات مالکیت شما تأثیر می‌گذارد." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "یک سیاست سازمانی، درون ریزی موارد به گاوصندوق فردی شما را مسدود کرده است." }, "domainsTitle": { - "message": "Domains", + "message": "دامنه‌ها", "description": "A category title describing the concept of web domains" }, "blockedDomains": { - "message": "Blocked domains" + "message": "دامنه‌های مسدود شده" }, "learnMoreAboutBlockedDomains": { - "message": "Learn more about blocked domains" + "message": "اطلاعات بیشتر درباره دامنه‌های مسدود شده" }, "excludedDomains": { "message": "دامنه های مستثنی" @@ -2498,22 +2498,22 @@ "message": "Bitwarden برای ذخیره جزئیات ورود به سیستم این دامنه ها سوال نمی‌کند. برای اینکه تغییرات اعمال شود باید صفحه را تازه کنید." }, "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." + "message": "Bitwarden برای هیچ یک از حساب‌های کاربری وارد شده، درخواست ذخیره اطلاعات ورود برای این دامنه‌ها را نخواهد داد. برای اعمال تغییرات باید صفحه را تازه‌سازی کنید." }, "blockedDomainsDesc": { - "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + "message": "ویژگی‌های پر کردن خودکار و سایر قابلیت‌های مرتبط برای این وب‌سایت‌ها ارائه نخواهند شد. برای اعمال تغییرات باید صفحه را تازه‌سازی کنید." }, "autofillBlockedNoticeV2": { - "message": "Autofill is blocked for this website." + "message": "پر کردن خودکار برای این وب‌سایت مسدود شده است." }, "autofillBlockedNoticeGuidance": { - "message": "Change this in settings" + "message": "این مورد را در تنظیمات تغییر دهید" }, "change": { - "message": "Change" + "message": "تغییر" }, "changeButtonTitle": { - "message": "Change password - $ITEMNAME$", + "message": "تغییر کلمه عبور - $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -2522,10 +2522,10 @@ } }, "atRiskPasswords": { - "message": "At-risk passwords" + "message": "کلمات عبور در معرض خطر" }, "atRiskPasswordDescSingleOrg": { - "message": "$ORGANIZATION$ is requesting you change one password because it is at-risk.", + "message": "$ORGANIZATION$ از شما درخواست کرده یک کلمه عبور را به دلیل در معرض خطر بودن تغییر دهید.", "placeholders": { "organization": { "content": "$1", @@ -2534,7 +2534,7 @@ } }, "atRiskPasswordsDescSingleOrgPlural": { - "message": "$ORGANIZATION$ is requesting you change the $COUNT$ passwords because they are at-risk.", + "message": "$ORGANIZATION$ از شما درخواست کرده $COUNT$ کلمه عبور را به دلیل در معرض خطر بودن تغییر دهید.", "placeholders": { "organization": { "content": "$1", @@ -2547,7 +2547,7 @@ } }, "atRiskPasswordsDescMultiOrgPlural": { - "message": "Your organizations are requesting you change the $COUNT$ passwords because they are at-risk.", + "message": "سازمان‌های شما از شما درخواست کرده‌اند که $COUNT$ کلمه عبور را به دلیل در معرض خطر بودن تغییر دهید.", "placeholders": { "count": { "content": "$1", @@ -2556,10 +2556,10 @@ } }, "reviewAndChangeAtRiskPassword": { - "message": "Review and change one at-risk password" + "message": "بررسی و تغییر یک کلمه عبور در معرض خطر" }, "reviewAndChangeAtRiskPasswordsPlural": { - "message": "Review and change $COUNT$ at-risk passwords", + "message": "بررسی و تغییر $COUNT$ کلمه عبور در معرض خطر", "placeholders": { "count": { "content": "$1", @@ -2568,52 +2568,52 @@ } }, "changeAtRiskPasswordsFaster": { - "message": "Change at-risk passwords faster" + "message": "تغییر سریع‌تر کلمات عبور در معرض خطر" }, "changeAtRiskPasswordsFasterDesc": { - "message": "Update your settings so you can quickly autofill your passwords and generate new ones" + "message": "تنظیمات خود را به‌روزرسانی کنید تا بتوانید به‌سرعت کلمات عبور خود را به‌صورت خودکار وارد کرده و کلمات جدید تولید کنید" }, "reviewAtRiskLogins": { - "message": "Review at-risk logins" + "message": "بررسی ورودهای در معرض خطر" }, "reviewAtRiskPasswords": { - "message": "Review at-risk passwords" + "message": "بررسی کلمات عبور در معرض خطر" }, "reviewAtRiskLoginsSlideDesc": { - "message": "Your organization passwords are at-risk because they are weak, reused, and/or exposed.", + "message": "کلمات عبور سازمان شما در معرض خطر هستند زیرا ضعیف، تکراری و یا افشا شده‌اند.", "description": "Description of the review at-risk login slide on the at-risk password page carousel" }, "reviewAtRiskLoginSlideImgAltPeriod": { - "message": "Illustration of a list of logins that are at-risk." + "message": "تصویری از فهرست ورودهایی که در معرض خطر هستند." }, "generatePasswordSlideDesc": { - "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", + "message": "با استفاده از منوی پر کردن خودکار Bitwarden در سایت در معرض خطر، به‌سرعت یک کلمه عبور قوی و منحصر به فرد تولید کنید.", "description": "Description of the generate password slide on the at-risk password page carousel" }, "generatePasswordSlideImgAltPeriod": { - "message": "Illustration of the Bitwarden autofill menu displaying a generated password." + "message": "تصویری از منوی پر کردن خودکار Bitwarden که یک کلمه عبور تولید شده را نمایش می‌دهد." }, "updateInBitwarden": { - "message": "Update in Bitwarden" + "message": "به‌روزرسانی در Bitwarden" }, "updateInBitwardenSlideDesc": { - "message": "Bitwarden will then prompt you to update the password in the password manager.", + "message": "سپس Bitwarden از شما می‌خواهد کلمه عبور را در مدیریت کلمه عبور به‌روزرسانی کنید.", "description": "Description of the update in Bitwarden slide on the at-risk password page carousel" }, "updateInBitwardenSlideImgAltPeriod": { - "message": "Illustration of a Bitwarden’s notification prompting the user to update the login." + "message": "تصویری از اعلان Bitwarden که از کاربر می‌خواهد ورود خود را به‌روزرسانی کند." }, "turnOnAutofill": { - "message": "Turn on autofill" + "message": "پر کردن خودکار را فعال کنید" }, "turnedOnAutofill": { - "message": "Turned on autofill" + "message": "پر کردن خودکار فعال شد" }, "dismiss": { - "message": "Dismiss" + "message": "نادیده گرفتن" }, "websiteItemLabel": { - "message": "Website $number$ (URI)", + "message": "وب‌سایت $number$ (نشانی اینترنتی)", "placeholders": { "number": { "content": "$1", @@ -2631,20 +2631,20 @@ } }, "blockedDomainsSavedSuccess": { - "message": "Blocked domain changes saved" + "message": "تغییرات دامنه مسدود شده ذخیره شد" }, "excludedDomainsSavedSuccess": { - "message": "Excluded domain changes saved" + "message": "تغییرات دامنه مستثنی شده ذخیره شد" }, "limitSendViews": { - "message": "Limit views" + "message": "محدود کردن نمایش‌ها" }, "limitSendViewsHint": { - "message": "No one can view this Send after the limit is reached.", + "message": "هیچ‌کس نمی‌تواند این را پس از رسیدن به محدودیت مشاهده کند.", "description": "Displayed under the limit views field on Send" }, "limitSendViewsCount": { - "message": "$ACCESSCOUNT$ views left", + "message": "$ACCESSCOUNT$ بازدید باقی مانده", "description": "Displayed under the limit views field on Send", "placeholders": { "accessCount": { @@ -2658,14 +2658,14 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { - "message": "Send details", + "message": "جزئیات ارسال", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { "message": "متن" }, "sendTypeTextToShare": { - "message": "Text to share" + "message": "متن برای اشتراک گذاری" }, "sendTypeFile": { "message": "پرونده" @@ -2675,7 +2675,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "hideTextByDefault": { - "message": "Hide text by default" + "message": "متن را به‌صورت پیش‌فرض مخفی کن" }, "expired": { "message": "منقضی شده" @@ -2684,7 +2684,7 @@ "message": "محافظت ‌شده با کلمه عبور" }, "copyLink": { - "message": "Copy link" + "message": "کپی پیوند" }, "copySendLink": { "message": "پیوند ارسال را کپی کن", @@ -2722,7 +2722,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendPermanentConfirmation": { - "message": "Are you sure you want to permanently delete this Send?", + "message": "مطمئن هستید می‌خواهید این ارسال را برای همیشه پاک کنید؟", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { @@ -2733,7 +2733,7 @@ "message": "تاریخ حذف" }, "deletionDateDescV2": { - "message": "The Send will be permanently deleted on this date.", + "message": "ارسال در این تاریخ به‌طور دائمی حذف خواهد شد.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { @@ -2755,7 +2755,7 @@ "message": "سفارشی" }, "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", + "message": "یک کلمه عبور اختیاری برای دریافت‌کنندگان اضافه کنید تا بتوانند به این ارسال دسترسی داشته باشند.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createSend": { @@ -2778,15 +2778,15 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSendSuccessfully": { - "message": "Send created successfully!", + "message": "ارسال با موفقیت ساخته شد!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { - "message": "The Send will be available to anyone with the link for the next 1 hour.", + "message": "این ارسال به مدت ۱ ساعت برای هر کسی که لینک را دارد در دسترس خواهد بود.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHours": { - "message": "The Send will be available to anyone with the link for the next $HOURS$ hours.", + "message": "این ارسال به مدت $HOURS$ ساعت برای هر کسی که لینک را دارد در دسترس خواهد بود.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "hours": { @@ -2796,11 +2796,11 @@ } }, "sendExpiresInDaysSingle": { - "message": "The Send will be available to anyone with the link for the next 1 day.", + "message": "این ارسال به مدت ۱ روز برای هر کسی که لینک را دارد در دسترس خواهد بود.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInDays": { - "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "message": "این ارسال به مدت $DAYS$ روز برای هر کسی که لینک را دارد در دسترس خواهد بود.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "days": { @@ -2810,7 +2810,7 @@ } }, "sendLinkCopied": { - "message": "Send link copied", + "message": "لینک ارسال کپی شد", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { @@ -2818,11 +2818,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogText": { - "message": "Pop out extension?", + "message": "باز کردن پنجره جداگانه افزونه؟", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogDesc": { - "message": "To create a file Send, you need to pop out the extension to a new window.", + "message": "برای ایجاد یک ارسال پرونده، باید افزونه را در پنجره‌ای جدید باز کنید.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { @@ -2835,7 +2835,7 @@ "message": "برای انتخاب پرونده ای با استفاده از Safari، با کلیک روی این بنر پنجره جدیدی باز کنید." }, "popOut": { - "message": "Pop out" + "message": "باز کردن در پنجره جداگانه" }, "sendFileCalloutHeader": { "message": "قبل از اینکه شروع کنی" @@ -2856,7 +2856,7 @@ "message": "هنگام ذخیره حذف و تاریخ انقضاء شما خطایی روی داد." }, "hideYourEmail": { - "message": "Hide your email address from viewers." + "message": "آدرس ایمیل خود را از بینندگان مخفی کنید." }, "passwordPrompt": { "message": "درخواست مجدد کلمه عبور اصلی" @@ -2871,7 +2871,7 @@ "message": "تأیید ایمیل لازم است" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "ایمیل تأیید شد" }, "emailVerificationRequiredDesc": { "message": "برای استفاده از این ویژگی باید ایمیل خود را تأیید کنید. می‌توانید ایمیل خود را در گاوصندوق وب تأیید کنید." @@ -2889,7 +2889,7 @@ "message": "کلمه عبور اصلی شما با یک یا چند سیاست سازمان‌تان مطابقت ندارد. برای دسترسی به گاوصندوق، باید همین حالا کلمه عبور اصلی خود را به‌روز کنید. در صورت ادامه، شما از نشست فعلی خود خارج می‌شوید و باید دوباره وارد سیستم شوید. نشست فعال در دستگاه های دیگر ممکن است تا یک ساعت همچنان فعال باقی بمانند." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "سازمان شما رمزگذاری دستگاه‌های مورد اعتماد را غیرفعال کرده است. لطفاً برای دسترسی به گاوصندوق خود یک کلمه عبور اصلی تنظیم کنید." }, "resetPasswordPolicyAutoEnroll": { "message": "ثبت نام خودکار" @@ -2905,15 +2905,15 @@ "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "مجوزهای سازمان شما به‌روزرسانی شد، باید یک کلمه عبور اصلی تنظیم کنید.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "سازمانتان از شما می‌خواهد که یک کلمه عبور اصلی تنظیم کنید.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "از میان $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -2932,7 +2932,7 @@ "message": "دقیقه" }, "vaultTimeoutPolicyAffectingOptions": { - "message": "Enterprise policy requirements have been applied to your timeout options" + "message": "نیازمندی‌های سیاست سازمانی بر گزینه‌های زمان پایان نشست شما اعمال شده است" }, "vaultTimeoutPolicyInEffect": { "message": "سیاست‌های سازمانتان بر مهلت زمانی گاوصندوق شما تأثیر می‌گذارد. حداکثر زمان مجاز گاوصندوق $HOURS$ ساعت و $MINUTES$ دقیقه است", @@ -2948,7 +2948,7 @@ } }, "vaultTimeoutPolicyInEffect1": { - "message": "$HOURS$ hour(s) and $MINUTES$ minute(s) maximum.", + "message": "حداکثر $HOURS$ ساعت و $MINUTES$ دقیقه.", "placeholders": { "hours": { "content": "$1", @@ -2961,7 +2961,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", @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "شناسه منحصر به فردی یافت نشد." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ در حال استفاده از SSO با یک سرور کلید خود میزبان است. برای ورود اعضای این سازمان دیگر نیازی به کلمه عبور اصلی نیست.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "برای اعضای سازمان زیر، کلمه عبور اصلی دیگر لازم نیست. لطفاً دامنه زیر را با مدیر سازمان خود تأیید کنید." + }, + "organizationName": { + "message": "نام سازمان" + }, + "keyConnectorDomain": { + "message": "دامنه رابط کلید" }, "leaveOrganization": { "message": "ترک سازمان" @@ -3057,7 +3057,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", @@ -3066,10 +3066,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "در حال برون ریزی گاوصندوق سازمان" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "فقط گاوصدوق سازمان مرتبط با $ORGANIZATION$ برون ریزی خواهد شد. موارد موجود در گاوصندوق‌های فردی یا سایر سازمان‌ها شامل نمی‌شوند.", "placeholders": { "organization": { "content": "$1", @@ -3081,27 +3081,27 @@ "message": "خطا" }, "decryptionError": { - "message": "Decryption error" + "message": "خطای رمزگشایی" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden نتوانست مورد(های) گاوصندوق فهرست شده زیر را رمزگشایی کند." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "با بخش پشتیبانی مشتریان تماس بگیرید", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "برای جلوگیری از، از دست دادن داده‌های بیشتر.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "ایجاد نام کاربری" }, "generateEmail": { - "message": "Generate email" + "message": "تولید ایمیل" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "مقدار باید بین $MIN$ و $MAX$ باشد.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -3115,7 +3115,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " برای تولید یک کلمه عبور قوی، از $RECOMMENDED$ کاراکتر یا بیشتر استفاده کنید.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -3125,7 +3125,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " برای تولید یک عبارت عبور قوی، از $RECOMMENDED$ واژه یا بیشتر استفاده کنید.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -3166,15 +3166,15 @@ "message": "یک نام مستعار ایمیل با یک سرویس ارسال خارجی ایجاد کنید." }, "forwarderDomainName": { - "message": "Email domain", + "message": "دامنه ایمیل", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Choose a domain that is supported by the selected service", + "message": "دامنه‌ای را انتخاب کنید که توسط سرویس انتخاب شده پشتیبانی می‌شود", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "$SERVICENAME$ خطا: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -3188,11 +3188,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "تولید شده توسط Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "وب‌سایت: $WEBSITE$. تولید شده توسط Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -3202,7 +3202,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "توکن API نامعتبر برای $SERVICENAME$", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -3212,7 +3212,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "توکن API نامعتبر برای $SERVICENAME$: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -3226,7 +3226,7 @@ } }, "forwaderInvalidOperation": { - "message": "$SERVICENAME$ refused your request. Please contact your service provider for assistance.", + "message": "$SERVICENAME$ درخواست شما را رد کرد. لطفاً برای دریافت کمک با ارائه‌دهنده سرویس خود تماس بگیرید.", "description": "Displayed when the user is forbidden from using the API by the forwarding service.", "placeholders": { "servicename": { @@ -3236,7 +3236,7 @@ } }, "forwaderInvalidOperationWithMessage": { - "message": "$SERVICENAME$ refused your request: $ERRORMESSAGE$", + "message": "$SERVICENAME$ درخواست شما را رد کرد: $ERRORMESSAGE$", "description": "Displayed when the user is forbidden from using the API by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -3250,7 +3250,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "دریافت شناسه حساب ایمیل ماسک شده از $SERVICENAME$ امکان‌پذیر نیست.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -3290,7 +3290,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "فرواردکننده ناشناخته: $SERVICENAME$.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -3379,34 +3379,34 @@ "message": "ارسال مجدد اعلان" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "مشاهده همه گزینه‌های ورود به سیستم" }, "notificationSentDevice": { "message": "یک اعلان به دستگاه شما ارسال شده است." }, "notificationSentDevicePart1": { - "message": "Unlock Bitwarden on your device or on the" + "message": "قفل Bitwarden را در دستگاه خود یا در... باز کنید" }, "notificationSentDeviceAnchor": { - "message": "web app" + "message": "برنامه وب" }, "notificationSentDevicePart2": { - "message": "Make sure the Fingerprint phrase matches the one below before approving." + "message": "اطمینان حاصل کنید که عبارت اثر انگشت با عبارت زیر مطابقت دارد قبل از تأیید." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "یک اعلان به دستگاه شما ارسال شده است" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "به‌محض تأیید درخواست، به شما اطلاع داده خواهد شد" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "به گزینه دیگری نیاز دارید؟" }, "loginInitiated": { "message": "ورود به سیستم آغاز شد" }, "logInRequestSent": { - "message": "Request sent" + "message": "درخواست ارسال شد" }, "exposedMasterPassword": { "message": "کلمه عبور اصلی افشا شده" @@ -3445,7 +3445,7 @@ "message": "نحوه پر کردن خودکار" }, "autofillSelectInfoWithCommand": { - "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", + "message": "یک مورد را از این صفحه انتخاب کنید، از میان‌بر $COMMAND$ استفاده کنید، یا گزینه‌های دیگر را در تنظیمات بررسی کنید.", "placeholders": { "command": { "content": "$1", @@ -3454,7 +3454,7 @@ } }, "autofillSelectInfoWithoutCommand": { - "message": "Select an item from this screen, or explore other options in settings." + "message": "یک مورد را از این صفحه انتخاب کنید یا گزینه‌های دیگر را در تنظیمات بررسی کنید." }, "gotIt": { "message": "متوجه شدم" @@ -3463,22 +3463,22 @@ "message": "تنظیمات پر کردن خودکار" }, "autofillKeyboardShortcutSectionTitle": { - "message": "Autofill shortcut" + "message": "میان‌بر پرکردن خودکار" }, "autofillKeyboardShortcutUpdateLabel": { - "message": "Change shortcut" + "message": "تغییر میان‌بر" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "مدیریت میان‌برها" }, "autofillShortcut": { "message": "میانبر صفحه کلید پر کردن خودکار" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "میان‌بر ورود خودکار تنظیم نشده است. این مورد را در تنظیمات مرورگر تغییر دهید." }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "میان‌بر ورود خودکار $COMMAND$ است. همه میان‌برها را می‌توانید در تنظیمات مرورگر مدیریت کنید.", "placeholders": { "command": { "content": "$1", @@ -3499,16 +3499,16 @@ "message": "در پنجره جدید باز می‌شود" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "این دستگاه را به خاطر بسپار تا ورودهای بعدی بدون مشکل انجام شود" }, "deviceApprovalRequired": { "message": "تأیید دستگاه لازم است. یک روش تأیید انتخاب کنید:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "تأیید دستگاه لازم است" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "یکی از گزینه‌های تأیید زیر را انتخاب کنید" }, "rememberThisDevice": { "message": "این دستگاه را به خاطر بسپار" @@ -3538,13 +3538,13 @@ "message": "و به ساختن حساب‌تان ادامه دهید." }, "noEmail": { - "message": "No email?" + "message": "ایمیلی دریافت نکردید؟" }, "goBack": { - "message": "Go back" + "message": "بازگشت به عقب" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "برای ویرایش آدرس ایمیل خود." }, "eu": { "message": "اروپا", @@ -3578,41 +3578,49 @@ "message": "ایمیل کاربر وجود ندارد" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "ایمیل کاربر فعال پیدا نشد. در حال خارج کردن شما از سیستم هستیم." }, "deviceTrusted": { "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" + "message": "برای امنیت حساب کاربری شما، فقط در صورتی تأیید کنید که دسترسی اضطراری به این کاربر داده‌اید و اثر انگشت او با آنچه در حسابش نمایش داده شده، مطابقت دارد" }, "orgTrustWarning": { - "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + "message": "برای امنیت حساب کاربری شما، فقط در صورتی ادامه دهید که عضو این سازمان باشید، بازیابی حساب کاربری را فعال کرده باشید و اثر انگشت نمایش داده شده در زیر با اثر انگشت سازمان مطابقت داشته باشد." }, "orgTrustWarning1": { - "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." + "message": "این سازمان دارای سیاست سازمانی است که شما را در بازیابی حساب کاربری ثبت‌نام می‌کند. ثبت‌نام به مدیران سازمان اجازه می‌دهد کلمه عبور شما را تغییر دهند. فقط در صورتی ادامه دهید که این سازمان را می‌شناسید و عبارت اثر انگشت نمایش داده شده در زیر با اثر انگشت سازمان مطابقت دارد." }, "trustUser": { - "message": "Trust user" + "message": "به کاربر اعتماد کنید" }, "sendsNoItemsTitle": { - "message": "No active Sends", + "message": "ارسال‌های فعالی نیست", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsNoItemsMessage": { - "message": "Use Send to securely share encrypted information with anyone.", + "message": "از ارسال برای اشتراک‌گذاری امن اطلاعات رمزگذاری شده با هر کسی استفاده کنید.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsTitleNoItems": { + "message": "اطلاعات حساس را به‌صورت ایمن ارسال کنید", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsBodyNoItems": { + "message": "پرونده‌ها و داده‌های خود را به‌صورت امن با هر کسی، در هر پلتفرمی به اشتراک بگذارید. اطلاعات شما در حین اشتراک‌گذاری به‌طور کامل رمزگذاری انتها به انتها باقی خواهد ماند و میزان افشا محدود می‌شود.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -3689,10 +3697,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "۱ فیلد به توجه شما نیاز دارد." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "فیلدهای $COUNT$ به توجه شما نیاز دارند.", "placeholders": { "count": { "content": "$1", @@ -3747,53 +3755,53 @@ "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "تغییر وضعیت ناوبری کناری" }, "skipToContent": { - "message": "Skip to content" + "message": "پرش به محتوا" }, "bitwardenOverlayButton": { - "message": "Bitwarden autofill menu button", + "message": "دکمه منوی پر کردن خودکار Bitwarden", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden autofill menu", + "message": "نمایش یا مخفی کردن منوی پر کردن خودکار Bitwarden", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden autofill menu", + "message": "منوی پر کردن خودکار Bitwarden", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { - "message": "Unlock your account to view matching logins", + "message": "برای مشاهده ورودهای منطبق، قفل حساب کاربری خود را باز کنید", "description": "Text to display in overlay when the account is locked." }, "unlockYourAccountToViewAutofillSuggestions": { - "message": "Unlock your account to view autofill suggestions", + "message": "برای مشاهده پیشنهادهای پر کردن خودکار، قفل حساب کاربری خود را باز کنید", "description": "Text to display in overlay when the account is locked." }, "unlockAccount": { - "message": "Unlock account", + "message": "قفل حساب کاربری را باز کنید", "description": "Button text to display in overlay when the account is locked." }, "unlockAccountAria": { - "message": "Unlock your account, opens in a new window", + "message": "قفل حساب کاربری خود را باز کنید، در پنجره‌ای جدید باز می‌شود", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, "totpCodeAria": { - "message": "Time-based One-Time Password Verification Code", + "message": "کد تأیید رمز یک‌بار مصرف مبتنی بر زمان", "description": "Aria label for the totp code displayed in the inline menu for autofill" }, "totpSecondsSpanAria": { - "message": "Time remaining before current TOTP expires", + "message": "زمان باقی‌مانده تا انقضای کد TOTP فعلی", "description": "Aria label for the totp seconds displayed in the inline menu for autofill" }, "fillCredentialsFor": { - "message": "Fill credentials for", + "message": "پر کردن اطلاعات ورود برای", "description": "Screen reader text for when overlay item is in focused" }, "partialUsername": { - "message": "Partial username", + "message": "نام کاربری جزئی", "description": "Screen reader text for when a login item is focused where a partial username is displayed. SR will announce this phrase before reading the text of the partial username" }, "noItemsToShow": { @@ -3809,31 +3817,31 @@ "description": "Screen reader text (aria-label) for new item button in overlay" }, "newLogin": { - "message": "New login", + "message": "ورود جدید", "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { - "message": "Add new vault login item, opens in a new window", + "message": "افزودن مورد ورود جدید به گاوصندوق، در پنجره‌ای جدید باز می‌شود", "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { - "message": "New card", + "message": "کارت جدید", "description": "Button text to display within inline menu when there are no matching items on a credit card field" }, "addNewCardItemAria": { - "message": "Add new vault card item, opens in a new window", + "message": "افزودن کارت جدید به گاوصندوق، در پنجره‌ای جدید باز می‌شود", "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { - "message": "New identity", + "message": "هویت جدید", "description": "Button text to display within inline menu when there are no matching items on an identity field" }, "addNewIdentityItemAria": { - "message": "Add new vault identity item, opens in a new window", + "message": "افزودن هویت جدید به گاوصندوق، در پنجره‌ای جدید باز می‌شود", "description": "Screen reader text (aria-label) for new identity button within inline menu" }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden autofill menu available. Press the down arrow key to select.", + "message": "منوی پر کردن خودکار Bitwarden در دسترس است. برای انتخاب، کلید فلش پایین را فشار دهید.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3847,13 +3855,13 @@ "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { - "message": "خطای وارد کردن" + "message": "خطای درون ریزی" }, "importErrorDesc": { "message": "مشکلی با داده‌هایی که سعی کردید وارد کنید وجود داشت. لطفاً خطاهای فهرست شده زیر را در فایل منبع خود برطرف کرده و دوباره امتحان کنید." }, "resolveTheErrorsBelowAndTryAgain": { - "message": "Resolve the errors below and try again." + "message": "خطاهای زیر را برطرف کرده و دوباره امتحان کنید." }, "description": { "message": "توضیحات" @@ -3874,10 +3882,10 @@ "message": "دوباره سعی کنید" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "برای این اقدام تأیید لازم است. یک کد پین تعیین کنید تا ادامه دهید." }, "setPin": { - "message": "تنظیم PIN" + "message": "تنظیم کد پین" }, "verifyWithBiometrics": { "message": "تایید با استفاده از بیومتریک" @@ -3892,25 +3900,25 @@ "message": "نیازمند روش دیگری هستید؟" }, "useMasterPassword": { - "message": "استفاده از رمز عبور اصلی" + "message": "استفاده از کلمه عبور اصلی" }, "usePin": { - "message": "استفاده از PIN" + "message": "استفاده از کد پین" }, "useBiometrics": { "message": "استفاده از بیومتریک" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "کد تأییدی را که به ایمیل شما ارسال شده است وارد کنید." }, "resendCode": { - "message": "Resend code" + "message": "ارسال دوباره کد" }, "total": { "message": "مجموع" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "شما در حال درون ریزی داده به $ORGANIZATION$ هستید. داده‌های شما ممکن است با اعضای این سازمان به اشتراک گذاشته شود. آیا می‌خواهید ادامه دهید؟", "placeholders": { "organization": { "content": "$1", @@ -3919,13 +3927,13 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + "message": "خطا در اتصال به سرویس Duo. از روش ورود دو مرحله‌ای دیگری استفاده کنید یا برای دریافت کمک با Duo تماس بگیرید." }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "ورود دو مرحله ای Duo برای حساب کاربری شما لازم است." }, "popoutExtension": { - "message": "Popout extension" + "message": "باز کردن پنجره جداگانه افزونه" }, "launchDuo": { "message": "اجرای Duo" @@ -3937,25 +3945,25 @@ "message": "چیزی وارد نشد." }, "importEncKeyError": { - "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." + "message": "خطا در رمزگشایی پرونده‌ی درون ریزی شده. کلید رمزگذاری شما با کلید رمزگذاری استفاده شده برای درون ریزی داده‌ها مطابقت ندارد." }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "کلمه عبور پرونده نامعتبر است، لطفاً از کلمه عبوری که هنگام ایجاد پرونده‌ی برون ریزی وارد کردید استفاده کنید." }, "destination": { - "message": "Destination" + "message": "مقصد" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "درباره گزینه‌های برون ریزی خود بیاموزید" }, "selectImportFolder": { "message": "یک پوشه انتخاب کنید" }, "selectImportCollection": { - "message": "Select a collection" + "message": "انتخاب یک مجموعه" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "اگر می‌خواهید محتوای فایل وارد شده به $DESTINATION$ منتقل شود، این گزینه را انتخاب کنید", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -3965,25 +3973,25 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "پرونده حاوی موارد اختصاص نیافته است." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "فرمت پرونده‌ی درون ریزی را انتخاب کنید" }, "selectImportFile": { - "message": "Select the import file" + "message": "پرونده‌ی درون ریزی را انتخاب کنید" }, "chooseFile": { - "message": "Choose File" + "message": "انتخاب پرونده" }, "noFileChosen": { - "message": "No file chosen" + "message": "هیچ پرونده‌ای انتخاب نشد" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "یا محتویات پرونده‌ی درون ریزی را کپی/پیست کنید" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "دستورالعمل‌های $NAME$", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -3993,25 +4001,25 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "درون ریزی گاوصندوق را تأیید کنید" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "این پرونده با کلمه عبور محافظت شده است. لطفاً کلمه عبور پرونده را برای درون ریزی داده‌ها وارد کنید." }, "confirmFilePassword": { - "message": "Confirm file password" + "message": "تأیید کلمه عبور پرونده" }, "exportSuccess": { - "message": "Vault data exported" + "message": "داده های گاوصندوق برون ریزی شد" }, "typePasskey": { - "message": "Passkey" + "message": "کلید عبور" }, "accessing": { - "message": "Accessing" + "message": "در حال دسترسی" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "وارد شده!" }, "passkeyNotCopied": { "message": "کلید عبور کپی نمی‌شود" @@ -4023,7 +4031,7 @@ "message": "تأیید توسط سایت آغازگر الزامی است. این ویژگی هنوز برای حساب‌های بدون کلمه عبور اصلی اجرا نشده است." }, "logInWithPasskeyQuestion": { - "message": "Log in with passkey?" + "message": "با کلید عبور وارد می‌شوید؟" }, "passkeyAlreadyExists": { "message": "یک کلید عبور از قبل برای این برنامه وجود دارد." @@ -4035,10 +4043,10 @@ "message": "شما هیچ ورود مشابهی برای این سایت ندارید." }, "noMatchingLoginsForSite": { - "message": "No matching logins for this site" + "message": "ورود منطبق برای این سایت یافت نشد" }, "searchSavePasskeyNewLogin": { - "message": "Search or save passkey as new login" + "message": "جستجو یا ذخیره کلید عبور به عنوان ورود جدید" }, "confirm": { "message": "تأیید" @@ -4050,10 +4058,10 @@ "message": "کلید عبور را به عنوان ورود جدید ذخیره کن" }, "chooseCipherForPasskeySave": { - "message": "Choose a login to save this passkey to" + "message": "یک ورود برای ذخیره این کلید عبور انتخاب کنید" }, "chooseCipherForPasskeyAuth": { - "message": "Choose a passkey to log in with" + "message": "یک کلید عبور برای ورود انتخاب کنید" }, "passkeyItem": { "message": "مورد کلید عبور" @@ -4071,125 +4079,125 @@ "message": "برای استفاده از کلید عبور، احراز هویت لازم است. برای ادامه، هویت خود را تأیید کنید." }, "multifactorAuthenticationCancelled": { - "message": "Multifactor authentication cancelled" + "message": "تأیید هویت چند مرحله‌ای کنسل شد" }, "noLastPassDataFound": { - "message": "No LastPass data found" + "message": "هیچ داده‌ای از LastPass یافت نشد" }, "incorrectUsernameOrPassword": { - "message": "Incorrect username or password" + "message": "نام کاربری یا کلمه عبور اشتباه است" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "کلمه عبور اشتباه است" }, "incorrectCode": { - "message": "Incorrect code" + "message": "کد اشتباه است" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "کد پین نادرست است" }, "multifactorAuthenticationFailed": { - "message": "Multifactor authentication failed" + "message": "احراز هویت چند مرحله‌ای ناموفق بود" }, "includeSharedFolders": { - "message": "Include shared folders" + "message": "شامل پوشه‌های به‌ اشتراک گذاری‌ شده" }, "lastPassEmail": { - "message": "LastPass Email" + "message": "ایمیل LastPass" }, "importingYourAccount": { - "message": "Importing your account..." + "message": "در حال درون ریزی حساب کاربری شما..." }, "lastPassMFARequired": { - "message": "LastPass multifactor authentication required" + "message": "احراز هویت چند مرحله‌ای LastPass مورد نیاز است" }, "lastPassMFADesc": { - "message": "Enter your one-time passcode from your authentication app" + "message": "کد یک‌بار مصرف خود را از برنامه احراز هویت وارد کنید" }, "lastPassOOBDesc": { - "message": "Approve the login request in your authentication app or enter a one-time passcode." + "message": "درخواست ورود را در برنامه احراز هویت خود تأیید کنید یا یک کد یک‌بار مصرف وارد کنید." }, "passcode": { - "message": "Passcode" + "message": "کد عبور" }, "lastPassMasterPassword": { - "message": "LastPass master password" + "message": "کلمه عبور اصلی LastPass" }, "lastPassAuthRequired": { - "message": "LastPass authentication required" + "message": "احراز هویت LastPass مورد نیاز است" }, "awaitingSSO": { - "message": "Awaiting SSO authentication" + "message": "در انتظار احراز هویت SSO" }, "awaitingSSODesc": { - "message": "Please continue to log in using your company credentials." + "message": "لطفاً برای ورود، از اطلاعات کاربری شرکت خود استفاده کنید." }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "دستورالعمل‌های کامل را در سایت راهنمای ما مشاهده کنید در", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { - "message": "Import directly from LastPass" + "message": "درون ریزی مستقیم از LastPass" }, "importFromCSV": { - "message": "Import from CSV" + "message": "درون ریزی از CSV" }, "lastPassTryAgainCheckEmail": { - "message": "Try again or look for an email from LastPass to verify it's you." + "message": "دوباره تلاش کنید یا ایمیلی از LastPass را بررسی کنید تا هویت شما تأیید شود." }, "collection": { - "message": "Collection" + "message": "مجموعه" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "کلید YubiKey مربوط به حساب کاربری LastPass خود را در درگاه USB کامپیوتر قرار دهید، سپس دکمه آن را لمس کنید." }, "switchAccount": { - "message": "Switch account" + "message": "تعویض حساب کاربری" }, "switchAccounts": { - "message": "Switch accounts" + "message": "تعویض حساب‌ها" }, "switchToAccount": { - "message": "Switch to account" + "message": "تعویض به حساب کاربری" }, "activeAccount": { - "message": "Active account" + "message": "حساب کاربری فعال" }, "bitwardenAccount": { - "message": "Bitwarden account" + "message": "حساب کاربری Bitwarden" }, "availableAccounts": { - "message": "Available accounts" + "message": "حساب کاربری در درسترس" }, "accountLimitReached": { - "message": "Account limit reached. Log out of an account to add another." + "message": "محدودیت حساب کاربری تکمیل شد. برای افزودن حساب کاربری دیگر، از یک حساب خارج شوید." }, "active": { - "message": "active" + "message": "فعال" }, "locked": { - "message": "locked" + "message": "قفل شد" }, "unlocked": { - "message": "unlocked" + "message": "باز شد" }, "server": { - "message": "server" + "message": "سرور" }, "hostedAt": { - "message": "hosted at" + "message": "میزبانی شده در" }, "useDeviceOrHardwareKey": { - "message": "Use your device or hardware key" + "message": "از دستگاه یا کلید سخت‌افزاری خود استفاده کنید" }, "justOnce": { - "message": "Just once" + "message": "فقط یک بار" }, "alwaysForThisSite": { - "message": "Always for this site" + "message": "همیشه برای این سایت" }, "domainAddedToExcludedDomains": { - "message": "$DOMAIN$ added to excluded domains.", + "message": "دامنه $DOMAIN$ به دامنه‌های مستثنی اضافه شد.", "placeholders": { "domain": { "content": "$1", @@ -4198,106 +4206,106 @@ } }, "commonImportFormats": { - "message": "Common formats", + "message": "فرمت‌های رایج", "description": "Label indicating the most common import formats" }, "confirmContinueToBrowserSettingsTitle": { - "message": "Continue to browser settings?", + "message": "ادامه به تنظیمات مرورگر؟", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { - "message": "Continue to Help Center?", + "message": "به مرکز راهنمایی ادامه می‌دهید؟", "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "تنظیمات پر کردن خودکار و مدیریت کلمه عبور مرورگر خود را تغییر دهید.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings" }, "confirmContinueToHelpCenterKeyboardShortcutsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "می‌توانید میان‌برهای افزونه را در تنظیمات مرورگر خود مشاهده و تنظیم کنید.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings" }, "confirmContinueToBrowserPasswordManagementSettingsContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "تنظیمات پر کردن خودکار و مدیریت کلمه عبور مرورگر خود را تغییر دهید.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" }, "confirmContinueToBrowserKeyboardShortcutSettingsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "می‌توانید میان‌برهای افزونه را در تنظیمات مرورگر خود مشاهده و تنظیم کنید.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" }, "overrideDefaultBrowserAutofillTitle": { - "message": "Make Bitwarden your default password manager?", + "message": "می‌خواهید Bitwarden را مدیر کلمه عبور پیش‌فرض خود کنید؟", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", + "message": "نادیده گرفتن این گزینه ممکن است باعث تداخل بین پیشنهادهای پر کردن خودکار Bitwarden و مرورگر شما شود.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Make Bitwarden your default password manager", + "message": "Bitwarden را به عنوان مدیر کلمه عبور پیش‌فرض خود تنظیم کنید", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "Unable to set Bitwarden as the default password manager", + "message": "امکان تنظیم Bitwarden به‌عنوان مدیر کلمه عبور پیش‌فرض وجود ندارد", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { - "message": "You must grant browser privacy permissions to Bitwarden to set it as the default password manager.", + "message": "برای تنظیم Bitwarden به‌عنوان مدیر کلمه عبور پیش‌فرض، باید دسترسی‌های حفظ حریم خصوصی مرورگر را به آن بدهید.", "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Make default", + "message": "تنظیم به‌عنوان پیش‌فرض", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "اطلاعات ورود با موفقیت ذخیره شد!", "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "کلمه عبور ذخیره شد!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "اطلاعات ورود با موفقیت به‌روزرسانی شد!", "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { - "message": "Password updated!", + "message": "کلمه عبور به‌روزرسانی شد!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { - "message": "Error saving credentials. Check console for details.", + "message": "خطا در ذخیره اطلاعات ورود. جزئیات را در کنسول بررسی کنید.", "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "موفقیت آمیز بود" }, "removePasskey": { - "message": "Remove passkey" + "message": "حذف کلید عبور" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "کلید عبور حذف شد" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "پیشنهادهای پر کردن خودکار" }, "itemSuggestions": { - "message": "Suggested items" + "message": "موارد پیشنهادی" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to autofill" + "message": "یک مورد ورود برای این سایت ذخیره کنید تا به‌صورت خودکار پر شود" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "گاوصندوق‌تان خالی است" }, "noItemsMatchSearch": { - "message": "No items match your search" + "message": "موردی با جستجوی شما مطابقت نداشت" }, "clearFiltersOrTryAnother": { - "message": "Clear filters or try another search term" + "message": "فیلترها را پاک کنید یا عبارت جستجوی دیگری را امتحان کنید" }, "copyInfoTitle": { - "message": "Copy info - $ITEMNAME$", + "message": "کپی اطلاعات - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -4307,7 +4315,7 @@ } }, "copyNoteTitle": { - "message": "Copy Note - $ITEMNAME$", + "message": "کپی یادداشت - $ITEMNAME$", "description": "Title for a button copies a note to the clipboard.", "placeholders": { "itemname": { @@ -4317,7 +4325,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "گزینه‌های بیشتر، $ITEMNAME$", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4327,7 +4335,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "گزینه‌های بیشتر - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4337,7 +4345,7 @@ } }, "viewItemTitle": { - "message": "View item - $ITEMNAME$", + "message": "مشاهده آیتم - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4347,7 +4355,7 @@ } }, "viewItemTitleWithField": { - "message": "View item - $ITEMNAME$ - $FIELD$", + "message": "مشاهده مورد - $ITEMNAME$ - $FIELD$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4361,7 +4369,7 @@ } }, "autofillTitle": { - "message": "Autofill - $ITEMNAME$", + "message": "پر کردن خودکار - $ITEMNAME$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4371,7 +4379,7 @@ } }, "autofillTitleWithField": { - "message": "Autofill - $ITEMNAME$ - $FIELD$", + "message": "پر کردن خودکار - $ITEMNAME$ - $FIELD$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4385,7 +4393,7 @@ } }, "copyFieldValue": { - "message": "Copy $FIELD$, $VALUE$", + "message": "کپی $FIELD$، $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -4399,40 +4407,40 @@ } }, "noValuesToCopy": { - "message": "No values to copy" + "message": "هیچ مقداری برای کپی وجود ندارد" }, "assignToCollections": { - "message": "Assign to collections" + "message": "اختصاص به مجموعه‌ها" }, "copyEmail": { - "message": "Copy email" + "message": "کپی ایمیل" }, "copyPhone": { - "message": "Copy phone" + "message": "کپی تلفن" }, "copyAddress": { - "message": "Copy address" + "message": "کپی آدرس" }, "adminConsole": { - "message": "Admin Console" + "message": "کنسول مدیر" }, "accountSecurity": { - "message": "Account security" + "message": "امنیت حساب کاربری" }, "notifications": { - "message": "Notifications" + "message": "اعلان‌ها" }, "appearance": { - "message": "Appearance" + "message": "ظاهر" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "خطا در اختصاص مجموعه هدف." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "خطا در اختصاص پوشه هدف." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "مشاهده موارد در $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -4442,7 +4450,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "بازگشت به $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -4452,10 +4460,10 @@ } }, "new": { - "message": "New" + "message": "جدید" }, "removeItem": { - "message": "Remove $NAME$", + "message": "حذف $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -4465,56 +4473,56 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "موارد بدون پوشه" }, "itemDetails": { - "message": "Item details" + "message": "جزئیات مورد" }, "itemName": { - "message": "Item name" + "message": "نام مورد" }, "organizationIsDeactivated": { - "message": "Organization is deactivated" + "message": "سازمان غیرفعال شده است" }, "owner": { - "message": "Owner" + "message": "مالک" }, "selfOwnershipLabel": { - "message": "You", + "message": "شما", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { - "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." + "message": "به موردهای موجود در سازمان‌های غیرفعال نمی‌توان دسترسی داشت. برای دریافت کمک با مالک سازمان تماس بگیرید." }, "additionalInformation": { - "message": "Additional information" + "message": "اطلاعات بیشتر" }, "itemHistory": { - "message": "Item history" + "message": "تاریخچه مورد" }, "lastEdited": { - "message": "Last edited" + "message": "آخرین ویرایش" }, "ownerYou": { - "message": "Owner: You" + "message": "مالک: شما" }, "linked": { - "message": "Linked" + "message": "پیوند شده" }, "copySuccessful": { - "message": "Copy Successful" + "message": "کپی موفق بود" }, "upload": { - "message": "Upload" + "message": "بارگذاری" }, "addAttachment": { - "message": "Add attachment" + "message": "افزودن پیوست" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "بیشترین حجم پرونده ۵۰۰ مگابایت است" }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "حذف پیوست $NAME$", "placeholders": { "name": { "content": "$1", @@ -4523,7 +4531,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "بارگیری $NAME$", "placeholders": { "name": { "content": "$1", @@ -4532,52 +4540,52 @@ } }, "downloadBitwarden": { - "message": "Download Bitwarden" + "message": "بارگیری Bitwarden" }, "downloadBitwardenOnAllDevices": { - "message": "Download Bitwarden on all devices" + "message": "Bitwarden را روی همه دستگاه‌ها بارگیری کنید" }, "getTheMobileApp": { - "message": "Get the mobile app" + "message": "دریافت برنامه‌ی تلفن همراه" }, "getTheMobileAppDesc": { - "message": "Access your passwords on the go with the Bitwarden mobile app." + "message": "با برنامه موبایل Bitwarden، به کلمات عبور خود در هر زمان و مکان دسترسی داشته باشید." }, "getTheDesktopApp": { - "message": "Get the desktop app" + "message": "برنامه دسکتاپ را دریافت کنید" }, "getTheDesktopAppDesc": { - "message": "Access your vault without a browser, then set up unlock with biometrics to expedite unlocking in both the desktop app and browser extension." + "message": "بدون استفاده از مرورگر به گاوصندوق خود دسترسی پیدا کنید و سپس باز کردن قفل با بیومتریک را تنظیم کنید تا باز کردن سریع‌تر قفل در اپ دسکتاپ و افزونه مرورگر انجام شود." }, "downloadFromBitwardenNow": { - "message": "Download from bitwarden.com now" + "message": "هم‌اکنون از bitwarden.com بارگیری کنید" }, "getItOnGooglePlay": { - "message": "Get it on Google Play" + "message": "از Google Play دریافت کنید" }, "downloadOnTheAppStore": { - "message": "Download on the App Store" + "message": "از AppStore بارگیری کنید" }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "آیا مطمئن هستید که می‌خواهید این پرونده پیوست را به‌طور دائمی حذف کنید؟" }, "premium": { - "message": "Premium" + "message": "پرمیوم" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "سازمان‌های رایگان نمی‌توانند از پرونده‌های پیوست استفاده کنند" }, "filters": { - "message": "Filters" + "message": "فیلترها" }, "filterVault": { - "message": "Filter vault" + "message": "فیلتر گاوصندوق" }, "filterApplied": { - "message": "One filter applied" + "message": "یک فیلتر اعمال شده است" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "$COUNT$ فیلتر اعمال شده است", "placeholders": { "count": { "content": "$1", @@ -4586,16 +4594,16 @@ } }, "personalDetails": { - "message": "Personal details" + "message": "جزئیات شخصی" }, "identification": { - "message": "Identification" + "message": "شناسایی" }, "contactInfo": { - "message": "Contact info" + "message": "اطلاعات مخاطب" }, "downloadAttachment": { - "message": "Download - $ITEMNAME$", + "message": "بارگیری - $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -4604,23 +4612,23 @@ } }, "cardNumberEndsWith": { - "message": "card number ends with", + "message": "شماره کارت پایان می‌یابد با", "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { - "message": "Login credentials" + "message": "اطلاعات ورود" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "کلید احراز هویت" }, "autofillOptions": { - "message": "Autofill options" + "message": "گزینه‌های پر کردن خودکار" }, "websiteUri": { - "message": "Website (URI)" + "message": "وب‌سایت (نشانی اینترنتی)" }, "websiteUriCount": { - "message": "Website (URI) $COUNT$", + "message": "وب‌سایت (نشانی اینترنتی) $COUNT$", "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", "placeholders": { "count": { @@ -4630,16 +4638,16 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "وب‌سایت اضافه شد" }, "addWebsite": { - "message": "Add website" + "message": "افزودن وب‌سایت" }, "deleteWebsite": { - "message": "Delete website" + "message": "حذف وبسایت" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "پیش‌فرض ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -4649,7 +4657,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "نمایش تطبیق سایت $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -4658,7 +4666,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "مخفی کردن تطبیق سایت $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -4667,19 +4675,19 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "پر کردن خودکار هنگام بارگذاری صفحه؟" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "تاریخ کارت منقضی شده است" }, "cardExpiredMessage": { - "message": "If you've renewed it, update the card's information" + "message": "اگر تمدید کرده‌اید، اطلاعات کارت را به‌روزرسانی کنید" }, "cardDetails": { - "message": "Card details" + "message": "جزئیات کارت" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$ جزئیات", "placeholders": { "brand": { "content": "$1", @@ -4688,43 +4696,43 @@ } }, "enableAnimations": { - "message": "Enable animations" + "message": "فعال‌سازی انیمیشن‌ها" }, "showAnimations": { - "message": "Show animations" + "message": "نمایش انیمیشن‌ها" }, "addAccount": { - "message": "Add account" + "message": "افزودن حساب کاربری" }, "loading": { - "message": "Loading" + "message": "در حال بارگذاری" }, "data": { - "message": "Data" + "message": "داده‌" }, "passkeys": { - "message": "Passkeys", + "message": "کلیدهای عبور", "description": "A section header for a list of passkeys." }, "passwords": { - "message": "Passwords", + "message": "کلمات عبور", "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { - "message": "Log in with passkey", + "message": "با کلید عبور وارد شوید", "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { - "message": "Assign" + "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": "فقط اعضای سازمانی که به این مجموعه‌ها دسترسی دارند قادر به مشاهده این موارد خواهند بود." }, "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", @@ -4736,37 +4744,37 @@ } }, "addField": { - "message": "Add field" + "message": "افزودن فیلد" }, "add": { - "message": "Add" + "message": "افزودن" }, "fieldType": { - "message": "Field type" + "message": "نوع فیلد" }, "fieldLabel": { - "message": "Field label" + "message": "برچسب فیلد" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "برای داده‌هایی مانند سوالات امنیتی از فیلدهای متنی استفاده کنید" }, "hiddenHelpText": { - "message": "Use hidden fields for sensitive data like a password" + "message": "برای داده‌های حساس مانند کلمه عبور از فیلدهای مخفی استفاده کنید" }, "checkBoxHelpText": { - "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + "message": "اگر می‌خواهید فیلدهای تیک‌دار فرم را به‌صورت خودکار پر کنید، مانند گزینه به یاد سپردن ایمیل، از کادرهای انتخاب استفاده کنید" }, "linkedHelpText": { - "message": "Use a linked field when you are experiencing autofill issues for a specific website." + "message": "وقتی در پر کردن خودکار برای یک وب‌سایت خاص به مشکل برخوردید، از فیلد مرتبط استفاده کنید." }, "linkedLabelHelpText": { - "message": "Enter the the field's html id, name, aria-label, or placeholder." + "message": "شناسه Html، نام، aria-label یا محل نگهدار فیلد را وارد کنید." }, "editField": { - "message": "Edit field" + "message": "ویرایش فیلد" }, "editFieldLabel": { - "message": "Edit $LABEL$", + "message": "ویرایش $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4775,7 +4783,7 @@ } }, "deleteCustomField": { - "message": "Delete $LABEL$", + "message": "حذف $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4784,7 +4792,7 @@ } }, "fieldAdded": { - "message": "$LABEL$ added", + "message": "$LABEL$ افزوده شد", "placeholders": { "label": { "content": "$1", @@ -4793,7 +4801,7 @@ } }, "reorderToggleButton": { - "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "message": "مرتب‌سازی مجدد $LABEL$. برای جابجایی مورد به بالا یا پایین از کلیدهای جهت‌نما استفاده کنید.", "placeholders": { "label": { "content": "$1", @@ -4802,10 +4810,10 @@ } }, "reorderWebsiteUriButton": { - "message": "Reorder website URI. Use arrow key to move item up or down." + "message": "مرتب‌سازی مجدد نشانی اینترنتی وب‌سایت. برای جابجایی مورد به بالا یا پایین از کلیدهای جهت‌نما استفاده کنید." }, "reorderFieldUp": { - "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "message": "$LABEL$ به بالا منتقل شد، موقعیت $INDEX$ از $LENGTH$", "placeholders": { "label": { "content": "$1", @@ -4822,13 +4830,13 @@ } }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "مجموعه‌ها را برای اختصاص انتخاب کنید" }, "personalItemTransferWarningSingular": { - "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + "message": "۱ مورد به طور دائمی به سازمان انتخاب شده منتقل خواهد شد. شما دیگر مالک این مورد نخواهید بود." }, "personalItemsTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items 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", @@ -4837,7 +4845,7 @@ } }, "personalItemWithOrgTransferWarningSingular": { - "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "message": "۱ مورد به طور دائمی به $ORG$ منتقل خواهد شد. شما دیگر مالک این مورد نخواهید بود.", "placeholders": { "org": { "content": "$1", @@ -4846,7 +4854,7 @@ } }, "personalItemsWithOrgTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ مورد به طور دائمی به $ORG$ منتقل خواهند شد. شما دیگر مالک این موارد نخواهید بود.", "placeholders": { "personal_items_count": { "content": "$1", @@ -4859,13 +4867,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", @@ -4874,7 +4882,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "مورد به $ORGNAME$ منتقل شد", "placeholders": { "orgname": { "content": "$1", @@ -4883,7 +4891,7 @@ } }, "reorderFieldDown": { - "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "message": "$LABEL$ به پایین منتقل شد، موقعیت $INDEX$ از $LENGTH$", "placeholders": { "label": { "content": "$1", @@ -4900,46 +4908,46 @@ } }, "itemLocation": { - "message": "Item Location" + "message": "مکان مورد" }, "fileSend": { - "message": "File Send" + "message": "پرونده ارسال" }, "fileSends": { - "message": "File Sends" + "message": "پرونده ارسال‌ها" }, "textSend": { - "message": "Text Send" + "message": "ارسال متن" }, "textSends": { - "message": "Text Sends" + "message": "ارسال‌های متن" }, "accountActions": { - "message": "Account actions" + "message": "فعالیت‌های حساب کاربری" }, "showNumberOfAutofillSuggestions": { - "message": "Show number of login autofill suggestions on extension icon" + "message": "نمایش تعداد پیشنهادهای پر کردن خودکار ورود در آیکون افزونه" }, "showQuickCopyActions": { - "message": "Show quick copy actions on Vault" + "message": "نمایش عملیات کپی سریع در گاوصندوق" }, "systemDefault": { - "message": "System default" + "message": "پیش‌فرض سیستم" }, "enterprisePolicyRequirementsApplied": { - "message": "Enterprise policy requirements have been applied to this setting" + "message": "الزامات سیاست سازمانی روی این تنظیم اعمال شده است" }, "sshPrivateKey": { - "message": "Private key" + "message": "کلید خصوصی" }, "sshPublicKey": { - "message": "Public key" + "message": "کلید عمومی" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "اثر انگشت" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "نوع کلید" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4954,61 +4962,61 @@ "message": "RSA 4096-Bit" }, "retry": { - "message": "Retry" + "message": "تلاش مجدد" }, "vaultCustomTimeoutMinimum": { - "message": "Minimum custom timeout is 1 minute." + "message": "حداقل مهلت زمانی سفارشی ۱ دقیقه است." }, "additionalContentAvailable": { - "message": "Additional content is available" + "message": "محتوای اضافی در دسترس است" }, "fileSavedToDevice": { - "message": "File saved to device. Manage from your device downloads." + "message": "پرونده در دستگاه ذخیره شد. از بخش بارگیری‌های دستگاه خود مدیریت کنید." }, "showCharacterCount": { - "message": "Show character count" + "message": "نمایش تعداد کاراکترها" }, "hideCharacterCount": { - "message": "Hide character count" + "message": "مخفی کردن تعداد کاراکترها" }, "itemsInTrash": { - "message": "Items in trash" + "message": "موراد در سطل زباله" }, "noItemsInTrash": { - "message": "No items in trash" + "message": "موردی در سطل زباله نیست" }, "noItemsInTrashDesc": { - "message": "Items you delete will appear here and be permanently deleted after 30 days" + "message": "مواردی که حذف می‌کنید اینجا نمایش داده می‌شوند و پس از ۳۰ روز به طور دائمی حذف خواهند شد" }, "trashWarning": { - "message": "Items that have been in trash more than 30 days will automatically be deleted" + "message": "مواردی که بیش از ۳۰ روز در سطل زباله بوده‌اند به‌طور خودکار حذف خواهند شد" }, "restore": { - "message": "Restore" + "message": "بازیابی" }, "deleteForever": { - "message": "Delete forever" + "message": "حذف برای همیشه" }, "noEditPermissions": { - "message": "You don't have permission to edit this item" + "message": "شما مجوز ویرایش این مورد را ندارید" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "بازکردن با بیومتریک در دسترس نیست زیرا ابتدا باید بازکردن قفل با کد پین یا کلمه عبور انجام شود." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "باز کردن قفل با بیومتریک در حال حاضر در دسترس نیست." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "باز کردن قفل با بیومتریک به دلیل پیکربندی نادرست پرونده‌های سیستم در دسترس نیست." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "باز کردن قفل با بیومتریک به دلیل پیکربندی نادرست پرونده‌های سیستم در دسترس نیست." }, "biometricsStatusHelptextDesktopDisconnected": { - "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + "message": "باز کردن قفل با بیومتریک در دسترس نیست زیرا برنامه دسکتاپ Bitwarden بسته است." }, "biometricsStatusHelptextNotEnabledInDesktop": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "باز کردن قفل با بیومتریک در دسترس نیست زیرا برای $EMAIL$ در برنامه دسکتاپ Bitwarden فعال نشده است.", "placeholders": { "email": { "content": "$1", @@ -5017,208 +5025,217 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "باز کردن قفل با بیومتریک در حال حاضر به دلیل نامعلومی در دسترس نیست." + }, + "unlockVault": { + "message": "قفل گاوصندوق خود را در چند ثانیه باز کنید" + }, + "unlockVaultDesc": { + "message": "می‌توانید تنظیمات باز کردن قفل و زمان‌بندی خودکار بسته شدن گاوصندوق را سفارشی کنید تا سریع‌تر به گاوصندوق خود دسترسی پیدا کنید." + }, + "unlockPinSet": { + "message": "بازکردن قفل کد پین تنظیم شد" }, "authenticating": { - "message": "Authenticating" + "message": "در حال احراز هویت" }, "fillGeneratedPassword": { - "message": "Fill generated password", + "message": "پر کردن کلمه عبور تولید شده", "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { - "message": "Password regenerated", + "message": "کلمه عبور تولید شد", "description": "Notification message for when a password has been regenerated" }, "saveToBitwarden": { - "message": "Save to Bitwarden", + "message": "ذخیره در Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "فاصله", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { - "message": "Tilde", + "message": "تیلد", "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "بک‌تیک", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "علامت تعجب", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "علامت @", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { - "message": "Hash sign", + "message": "علامت #", "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "علامت دلار", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Percent sign", + "message": "علامت ٪", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { - "message": "Caret", + "message": "علامت ^", "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "علامت &", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "علامت *", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "پرانتز چپ", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "پرانتز راست", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "خط زیر", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "خط تیره", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "به علاوه", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { - "message": "Equals", + "message": "مساوی", "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { - "message": "Left brace", + "message": "آکولادِ چپ", "description": "Represents the { key in screen reader content as a readable word" }, "braceRightCharacterDescriptor": { - "message": "Right brace", + "message": "آکولادِ راست", "description": "Represents the } key in screen reader content as a readable word" }, "bracketLeftCharacterDescriptor": { - "message": "Left bracket", + "message": "کروشه‌ی چپ", "description": "Represents the [ key in screen reader content as a readable word" }, "bracketRightCharacterDescriptor": { - "message": "Right bracket", + "message": "کروشه‌ی راست", "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { - "message": "Pipe", + "message": "علامت |", "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "بک اسلش", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "دو نقطه", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "نقطه ویرگول", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { - "message": "Double quote", + "message": "دابل کوت", "description": "Represents the double quote key in screen reader content as a readable word" }, "singleQuoteCharacterDescriptor": { - "message": "Single quote", + "message": "سینگل کوت", "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "کمتر از", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "بیش‌تر از", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "کاما", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "دوره", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "علامت سوال", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "ممیز", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "حروف کوچک" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "حروف بزرگ" }, "generatedPassword": { - "message": "Generated password" + "message": "کلمه عبور تولید شد" }, "compactMode": { - "message": "Compact mode" + "message": "حالت فشرده" }, "beta": { - "message": "Beta" + "message": "آزمایشی" }, "extensionWidth": { - "message": "Extension width" + "message": "عرض افزونه" }, "wide": { - "message": "Wide" + "message": "عریض" }, "extraWide": { - "message": "Extra wide" + "message": "خیلی عریض" }, "sshKeyWrongPassword": { - "message": "The password you entered is incorrect." + "message": "کلمه عبور وارد شده اشتباه است." }, "importSshKey": { - "message": "Import" + "message": "درون ریزی" }, "confirmSshKeyPassword": { - "message": "Confirm password" + "message": "تأیید کلمه عبور" }, "enterSshKeyPasswordDesc": { - "message": "Enter the password for the SSH key." + "message": "کلمه عبور کلید SSH را وارد کنید." }, "enterSshKeyPassword": { - "message": "Enter password" + "message": "کلمه عبور را وارد کنید" }, "invalidSshKey": { - "message": "The SSH key is invalid" + "message": "کلید SSH نامعتبر است" }, "sshKeyTypeUnsupported": { - "message": "The SSH key type is not supported" + "message": "نوع کلید SSH پشتیبانی نمی‌شود" }, "importSshKeyFromClipboard": { - "message": "Import key from clipboard" + "message": "وارد کردن کلید از حافظه موقت" }, "sshKeyImported": { - "message": "SSH key imported successfully" + "message": "کلید SSH با موفقیت وارد شد" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "نمی‌توانید مجموعه‌هایی را که فقط دسترسی مشاهده دارند حذف کنید: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -5227,121 +5244,138 @@ } }, "updateDesktopAppOrDisableFingerprintDialogTitle": { - "message": "Please update your desktop application" + "message": "لطفاً برنامه دسکتاپ خود را به‌روزرسانی کنید" }, "updateDesktopAppOrDisableFingerprintDialogMessage": { - "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." + "message": "برای استفاده از باز کردن قفل با بیومتریک، لطفاً برنامه دسکتاپ خود را به‌روزرسانی کنید یا باز کردن قفل با اثر انگشت را در تنظیمات دسکتاپ غیرفعال کنید." }, "changeAtRiskPassword": { - "message": "Change at-risk password" + "message": "تغییر کلمه عبور در معرض خطر" }, "settingsVaultOptions": { - "message": "Vault options" + "message": "گزینه‌های گاوصندوق" }, "emptyVaultDescription": { - "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + "message": "گاوصندوق تنها کلمات عبور را محافظت نمی‌کند. ورودهای امن، شناسه‌ها، کارت‌ها و یادداشت‌ها را نیز به صورت امن در اینجا ذخیره کنید." }, "introCarouselLabel": { - "message": "Welcome to Bitwarden" + "message": "به Bitwarden خوش آمدید" }, "securityPrioritized": { - "message": "Security, prioritized" + "message": "امنیت، در اولویت" }, "securityPrioritizedBody": { - "message": "Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you." + "message": "ورودها، کارت‌ها و هویت‌ها را در گاوصندوق امن خود ذخیره کنید. Bitwarden از رمزگذاری انتها به انتهای دانش صفر برای محافظت از آنچه برای شما مهم است استفاده می‌کند." }, "quickLogin": { - "message": "Quick and easy login" + "message": "ورود سریع و آسان" }, "quickLoginBody": { - "message": "Set up biometric unlock and autofill to log into your accounts without typing a single letter." + "message": "قفل بیومتریک و پر کردن خودکار را تنظیم کنید تا بدون وارد کردن حتی یک حرف وارد حساب‌های خود شوید." }, "secureUser": { - "message": "Level up your logins" + "message": "ورودهای خود را ارتقا دهید" }, "secureUserBody": { - "message": "Use the generator to create and save strong, unique passwords for all your accounts." + "message": "از تولید کننده برای ایجاد و ذخیره کلمه‌های عبور قوی و منحصر به فرد برای تمام حساب‌های کاربری خود استفاده کنید." }, "secureDevices": { - "message": "Your data, when and where you need it" + "message": "داده‌های شما، زمانی که نیاز دارید و در جایی که نیاز دارید" }, "secureDevicesBody": { - "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + "message": "کلمه‌های عبور نامحدود را در دستگاه‌های نامحدود با اپلیکیشن‌های موبایل، مرورگر و دسکتاپ Bitwarden ذخیره کنید." }, "nudgeBadgeAria": { - "message": "1 notification" + "message": "۱ اعلان" }, "emptyVaultNudgeTitle": { - "message": "Import existing passwords" + "message": "درون ریزی کلمات عبور موجود" }, "emptyVaultNudgeBody": { - "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + "message": "از ابزار درون ریزی استفاده کنید تا ورودها را سریعاً به Bitwarden منتقل کنید بدون نیاز به افزودن دستی آن‌ها." }, "emptyVaultNudgeButton": { - "message": "Import now" + "message": "الان درودن ریزی کن" }, "hasItemsVaultNudgeTitle": { - "message": "Welcome to your vault!" + "message": "به گاوصندوق خود خوش آمدید!" }, "hasItemsVaultNudgeBodyOne": { - "message": "Autofill items for the current page" + "message": "پر کردن خودکار موارد برای صفحه فعلی" }, "hasItemsVaultNudgeBodyTwo": { - "message": "Favorite items for easy access" + "message": "موارد مورد علاقه برای دسترسی آسان" }, "hasItemsVaultNudgeBodyThree": { - "message": "Search your vault for something else" + "message": "در گاوصندوق خود به دنبال چیز دیگری بگردید" }, "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" }, + "generatorNudgeTitle": { + "message": "ساخت سریع کلمات عبور" + }, + "generatorNudgeBodyOne": { + "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": "برای کمک به حفظ امنیت ورودهای شما.", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyAria": { + "message": "با کلیک روی دکمه تولید رمز عبور، به‌راحتی کلمات عبور قوی و منحصر به‌ فرد ایجاد کنید تا ورودهای شما ایمن باقی بمانند.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { - "message": "You do not have permissions to view this page. Try logging in with a different account." + "message": "شما اجازه دسترسی به این صفحه را ندارید. لطفاً با حساب کاربری دیگری وارد شوید." } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index ead59384ff8..8e5eede202a 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -1072,7 +1072,7 @@ "description": "Aria label for the view button in notification bar confirmation message" }, "notificationNewItemAria": { - "message": "New Item, opens in new window", + "message": "Uusi kohde, avautuu uudessa ikkunassa", "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" }, "notificationEditTooltip": { @@ -1606,7 +1606,7 @@ "message": "Turn off your browser's autofill settings, so they don't conflict with Bitwarden." }, "turnOffBrowserAutofill": { - "message": "Turn off $BROWSER$ autofill", + "message": "Poista automaattitäyttö käytöstä selaimessa $BROWSER$", "placeholders": { "browser": { "content": "$1", @@ -1615,7 +1615,7 @@ } }, "turnOffAutofill": { - "message": "Turn off autofill" + "message": "Poista automaattitäyttö käytöstä" }, "showInlineMenuLabel": { "message": "Näytä automaattitäytön ehdotukset lomakekentissä" @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Yksilöllistä tunnistetta ei löytynyt." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ käyttää kertakirjautumista (SSO) itse ylläpidetyllä avainpalvelimella. Organisaation jäsenet eivät enää tarvitse pääsalasanaa kirjautumiseen.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Poistu organisaatiosta" @@ -3584,16 +3584,16 @@ "message": "Laitteeseen luotettu" }, "trustOrganization": { - "message": "Trust organization" + "message": "Luota organisaatioon" }, "trust": { - "message": "Trust" + "message": "Luota" }, "doNotTrust": { - "message": "Do not trust" + "message": "Älä luota" }, "organizationNotTrusted": { - "message": "Organization is not trusted" + "message": "Organisaatio ei ole luotettu" }, "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" @@ -3605,7 +3605,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": "Luota käyttäjään" }, "sendsNoItemsTitle": { "message": "Aktiivisia Sendejä ei ole", @@ -3615,6 +3615,14 @@ "message": "Sendillä voit jakaa salattuja tietoja turvallisesti kenelle tahansa.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Syöte vaaditaan." }, @@ -4532,19 +4540,19 @@ } }, "downloadBitwarden": { - "message": "Download Bitwarden" + "message": "Lataa Bitwarden" }, "downloadBitwardenOnAllDevices": { - "message": "Download Bitwarden on all devices" + "message": "Lataa Bitwarden kaikille laitteille" }, "getTheMobileApp": { - "message": "Get the mobile app" + "message": "Hanki mobiilisovellus" }, "getTheMobileAppDesc": { "message": "Access your passwords on the go with the Bitwarden mobile app." }, "getTheDesktopApp": { - "message": "Get the desktop app" + "message": "Hanki työpöytäsovellus" }, "getTheDesktopAppDesc": { "message": "Access your vault without a browser, then set up unlock with biometrics to expedite unlocking in both the desktop app and browser extension." @@ -4553,10 +4561,10 @@ "message": "Download from bitwarden.com now" }, "getItOnGooglePlay": { - "message": "Get it on Google Play" + "message": "Hanki se Google Playstä" }, "downloadOnTheAppStore": { - "message": "Download on the App Store" + "message": "Lataa App Storesta" }, "permanentlyDeleteAttachmentConfirmation": { "message": "Haluatko varmasti poistaa liitteen pysyvästi?" @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometrinen avaus ei ole tällä hetkellä käytettävissä tuntemattomasta syystä." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Todennetaan" }, @@ -5236,7 +5253,7 @@ "message": "Vaihda vaarantunut salasana" }, "settingsVaultOptions": { - "message": "Vault options" + "message": "Holvin asetukset" }, "emptyVaultDescription": { "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." @@ -5269,19 +5286,19 @@ "message": "Tallenna rajattomasti salasanoja, rajattomalla määrällä laitteita, Bitwardenin mobiili-, selain- ja työpöytäsovelluksilla." }, "nudgeBadgeAria": { - "message": "1 notification" + "message": "1 ilmoitus" }, "emptyVaultNudgeTitle": { - "message": "Import existing passwords" + "message": "Tuo olemassa olevat salasanat" }, "emptyVaultNudgeBody": { "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." }, "emptyVaultNudgeButton": { - "message": "Import now" + "message": "Tuo nyt" }, "hasItemsVaultNudgeTitle": { - "message": "Welcome to your vault!" + "message": "Tervetuloa holviisi!" }, "hasItemsVaultNudgeBodyOne": { "message": "Autofill items for the current page" @@ -5293,7 +5310,7 @@ "message": "Search your vault for something else" }, "newLoginNudgeTitle": { - "message": "Save time with autofill" + "message": "Säästä aikaa automaattitäytöllä" }, "newLoginNudgeBodyOne": { "message": "Include a", @@ -5301,7 +5318,7 @@ "example": "Include a Website so this login appears as an autofill suggestion." }, "newLoginNudgeBodyBold": { - "message": "Website", + "message": "Verkkosivusto", "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." }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index a7734899843..e8e45249773 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Walang natagpuang natatanging nag-identipikar." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ ay gumagamit ng SSO na may sariling-hosted na key server. Walang kinakailangang master password para mag-log in para sa mga miyembro ng organisasyon na ito.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Umalis sa organisasyon" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 8a825082b5f..736fd21349e 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Aucun identifiant unique trouvé." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ utilise le SSO avec un serveur de clés auto-hébergé. Un mot de passe principal n'est plus nécessaire aux membres de cette organisation pour se connecter.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Quitter l'organisation" @@ -3615,6 +3615,14 @@ "message": "Utilisez Send pour partager en toute sécurité des informations chiffrées avec tout le monde.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Saisie requise." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Le déverrouillage par biométrie n'est pas disponible actuellement pour une raison inconnue." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authentification" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 91a5121db16..e10287f3e7b 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Non se atopou ningún identificador único." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ emprega SSO con un servidor de claves propio. Os membros da organización xa non precisan dun contrasinal mestre para iniciar sesión.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Deixar a organización" @@ -3615,6 +3615,14 @@ "message": "Usar send para compartir información cifrada con quen queiras.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Requírese algunha entrada." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "O desbloqueo biométrico non está dispoñible por algunha razón non prevista." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Autenticando" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 2e42a819b65..1b264d9a70a 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "לא נמצא מזהה ייחודי." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ משתמש/ת ב־SSO עם שרת מפתחות באירוח עצמי. סיסמה ראשית כבר לא נדרשת כדי להיכנס עבור חברים של ארגון זה.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "עזוב ארגון" @@ -3615,6 +3615,14 @@ "message": "השתמש בסֵנְד כדי לשתף באופן מאובטח מידע מוצפן עם כל אחד.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "נדרש קלט." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "ביטול נעילה ביומטרי אינו זמין כעת מסיבה לא ידועה." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "מאמת" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 8af1aa70cc3..61c0fbe9963 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 557e465ea19..ad32937c740 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Nije nađen jedinstveni identifikator." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ koristi jedinstvenu prijavu SSO s vlastitim poslužiteljem. Članovima organizacije glavna lozinka više nije potrebna.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Napusti organizaciju" @@ -3615,6 +3615,14 @@ "message": "Koristi Send za sigurno slanje šifriranih podataka bilo kome.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Potreban je unos." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometrijsko otključavanje trenutno nije dostupno iz nepoznatog razloga." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Autentifikacija" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index b47093bc2ee..50607787cd7 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Nincs egyedi azonosító." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ jelenleg saját tárolású aláíráskulcsú SSO szervert használ. A mesterjelszó a továbbiakban nem szükséges a szervezeti tagsági bejelentkezéshez.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A következő szervezet tagjai számára már nincs szükség mesterjelszóra. Erősítsük meg az alábbi tartományt a szervezet adminisztrátorával." + }, + "organizationName": { + "message": "Szervezet neve" + }, + "keyConnectorDomain": { + "message": "Key Connector tartomány" }, "leaveOrganization": { "message": "Szervezet elhagyása" @@ -3615,6 +3615,14 @@ "message": "A Send használatával biztonságosan megoszthatjuk a titkosított információkat bárkivel.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Érzékeny információt küldése biztonságosan", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsBodyNoItems": { + "message": "Fájlok vagy adatok megosztása biztonságosan bárkivel, bármilyen platformon. Az információk titkosítva maradnak a végpontokon, korlátozva a kitettséget.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Az adatbevitel kötelező." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "A biometrikus feloldás jelenleg ismeretlen okból nem érhető el." }, + "unlockVault": { + "message": "A széf feloldása másodpercek alatt" + }, + "unlockVaultDesc": { + "message": "Testreszabhatjuk a feloldási és időtúllépési beállításokat a széf gyorsabb elérése érdekében." + }, + "unlockPinSet": { + "message": "PIN beállítás feloldása" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Jelszavak gyors létrehozása" + }, + "generatorNudgeBodyOne": { + "message": "Könnyen létrehozhatunk erős és egyedi jelszavakat a gombra kattintva.", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyTwo": { + "message": "a bejelentkezések biztonságának megőrzéséhez.", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyAria": { + "message": "Könnyedén hozhatunk létre erős és egyedi jelszavakat a Jelszó generálása gombra kattintva, amely segít megőrizni a bejelentkezések biztonságát.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "Nincs jogosultság az oldal megtekintéséhez. Próbáljunk meg másik fiókkal bejelentkezni." } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 03e47fca575..a12137d696a 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Tidak ada pengidentifikasi unik yang ditemukan." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ menggunakan SSO dengan server kunci yang dihosting sendiri. Kata sandi utama tidak lagi diperlukan untuk masuk untuk anggota organisasi ini.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Tinggalkan Organisasi" @@ -3615,6 +3615,14 @@ "message": "Gunakan Send untuk membagikan informasi terenkripsi secara aman dengan siapapun.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Masukan ini harus diisi." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index d3d0dc9ec43..9a5d2c65bb6 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Nessun identificatore univoco trovato." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ sta usando SSO con un server self-hosted. Una password principale non è più necessaria per accedere per i membri di questa organizzazione.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Lascia organizzazione" @@ -3615,6 +3615,14 @@ "message": "Utilizza un Send per condividere in modo sicuro le informazioni con qualsiasi utente.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input obbligatorio." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Lo sblocco biometrico non è attualmente disponibile per un motivo sconosciuto." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Autenticazione" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index e9c22c1286f..5c235bdea43 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "一意の識別子が見つかりませんでした。" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ は自己ホストの鍵サーバで SSO を使用しています。この組織のメンバーのログインにマスターパスワードは必要ありません。", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "組織から脱退する" @@ -3615,6 +3615,14 @@ "message": "Send を使用すると暗号化された情報を誰とでも安全に共有できます。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "入力が必要です。" }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "生体認証によるロック解除は、不明な理由により現在利用できません。" }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "認証中" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index d8397db5da8..7f36bb3568f 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "ავთენტიკაცია" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index f436c45ab75..4775d1f7af0 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 0459e007126..ac36d911e89 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 8add57d61ec..5a496cde98a 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "고유 식별자를 찾을 수 없습니다." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ 조직은 자체 호스팅 키 서버로 SSO를 사용하고 있습니다 이 조직의 멤버들은 로그인할 때에 마스터 비밀번호를 필요로 하지 않습니다.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "조직 나가기" @@ -3615,6 +3615,14 @@ "message": "Send를 사용하여 암호화된 정보를 어느 사람과도 안전하게 공유합니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "입력이 필요합니다." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "인증 중" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 06ff7cda8fa..f790c226437 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -3,7 +3,7 @@ "message": "Bitwarden" }, "appLogoLabel": { - "message": "Bitwarden logo" + "message": "Bitwarden logotipas" }, "extName": { "message": "„Bitwarden“ slaptažodžių tvarkyklė", @@ -23,16 +23,16 @@ "message": "Sukurti paskyrą" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Pirmą kartą Bitwarden?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Prisijungti naudojant prieigos raktą" }, "useSingleSignOn": { "message": "Use single sign-on" }, "welcomeBack": { - "message": "Welcome back" + "message": "Sveiki sugrįžę" }, "setAStrongPassword": { "message": "Nustatyti stiprų slaptažodį" @@ -84,7 +84,7 @@ "message": "Pagrindinio slaptažodžio užuomina (neprivaloma)" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "Slaptažodžio stiprumas $SCORE$", "placeholders": { "score": { "content": "$1", @@ -93,10 +93,10 @@ } }, "joinOrganization": { - "message": "Join organization" + "message": "Prisijungti prie organizacijos" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "Prisijungti prie $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -105,7 +105,7 @@ } }, "finishJoiningThisOrganizationBySettingAMasterPassword": { - "message": "Finish joining this organization by setting a master password." + "message": "Baigėte prisijungimą prie organizacijos nustatant pagrindinį slaptažodį." }, "tab": { "message": "Skirtukas" @@ -132,7 +132,7 @@ "message": "Kopijuoti slaptažodį" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "Kopijuoti slaptažodžio frazę" }, "copyNote": { "message": "Kopijuoti pastabą" @@ -150,31 +150,31 @@ "message": "Kopijuoti saugos kodą" }, "copyName": { - "message": "Copy name" + "message": "Kopijuoti vardą" }, "copyCompany": { - "message": "Copy company" + "message": "Kopijuoti įmonę" }, "copySSN": { - "message": "Copy Social Security number" + "message": "Kopijuoti socialinės apsaugos numerį" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Kopijuoti paso numerį" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "Kopijuoti licenzijos numerį" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Kopijuoti privatų raktą" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Kopijuoti viešą raktą" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "Kopijuoti piršto antspaudą" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Kopijuoti $FIELD$", "placeholders": { "field": { "content": "$1", @@ -183,13 +183,13 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Kopijuoti svetainę" }, "copyNotes": { - "message": "Copy notes" + "message": "Kopijuoti pastabas" }, "copy": { - "message": "Copy", + "message": "Kopijuoti", "description": "Copy to clipboard" }, "fill": { @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Unikalus identifikatorius nerastas." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ naudoja SSO su savarankiškai sukurtu raktų serveriu. Pagrindinis slaptažodis nebėra reikalingas norint šios organizacijos nariams prisijungti.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Palikti organizaciją" @@ -3615,6 +3615,14 @@ "message": "Naudokite „Send“, kad saugiai bendrintumėte užšifruotą informaciją su bet kuriuo asmeniu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index e9044f4c041..f47b5f7645e 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Nav atrasts neviens neatkārtojams identifikators" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ izmanto vienoto pieteikšanos ar pašmitinātu atslēgu serveri. Tās dalībniekiem vairs nav nepieciešama galvenā parole, lai pieteiktos.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Galvenā parole vairs nav nepieciešama turpmāk minētās apvienības dalībniekiem. Lūgums saskaņot zemāk esošo domēnu ar savas apvienības pārvaldītāju." + }, + "organizationName": { + "message": "Apvienības nosaukums" + }, + "keyConnectorDomain": { + "message": "Key Connector domēns" }, "leaveOrganization": { "message": "Pamest apvienību" @@ -3615,6 +3615,14 @@ "message": "Send ir izmantojams, lai ar ikvienu droši kopīgotu šifrētu informāciju.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Drošā veidā nosūti jūtīgu informāciju", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsBodyNoItems": { + "message": "Drošā veidā kopīgo datnes un datus ar ikvienu jebkurā platformā! Tava informācija paliks pilnībā šifrēta, vienlaikus ierobežojot riskantumu.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Jāievada vērtība." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Atslēgšana ar biometriju pašlaik nav pieejama nezināma iemesla dēļ." }, + "unlockVault": { + "message": "Atslēdz savu glabātavu dažās sekundēs" + }, + "unlockVaultDesc": { + "message": "Tu vari pielāgot savus atslēgšanas un noildzes iestatījumus, lai ātrāk piekļūtu savai glabātavai." + }, + "unlockPinSet": { + "message": "Atslēgšanas PIN iestatīts" + }, "authenticating": { "message": "Autentificē" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Ātra paroļu izveidošana" + }, + "generatorNudgeBodyOne": { + "message": "Vienkārša spēcīgu un neatkārtojamu paroļu izveidošana ar pogu", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyTwo": { + "message": ", lai palīdzētu uzturērt pieteikšanās vienumus drošus.", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyAria": { + "message": "Vienkārša spēcīgu un neatkārtojamu paroļu izveidošana ar pogu \"Izveidot paroli\", lai palīdzētu uzturēt pieteikšanās vienumus drošus.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "Nav atļaujas apskatīt šo lapu. Jāmēģina pieteikties ar citu kontu." } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index e6de809dfbd..ab0120a40e2 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index dd347d822c7..625bf093ba9 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index f436c45ab75..4775d1f7af0 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 62b55eb6501..a11d5de6e2a 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Ingen unik identifikator ble funnet." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ bruker SSO med en selvdrevet nøkkelserver. Et hovedpassord er ikke lenger nødvendig for å logge inn for medlemmer av denne organisasjonen.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Forlat organisasjonen" @@ -3615,6 +3615,14 @@ "message": "Bruk Send til å dele kryptert informasjon med noen.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Inndata er påkrevd." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Autentiserer" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index f436c45ab75..4775d1f7af0 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index af637b2f4cd..67a784459b2 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Geen unieke id gevonden." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ gebruikt SSO met een zelf gehoste sleutelserver. Leden van deze organisatie kunnen inloggen zonder hoofdwachtwoord.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Organisatie verlaten" @@ -3615,6 +3615,14 @@ "message": "Gebruik Send voor het veilig delen van versleutelde informatie met wie dan ook.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Gevoelige informatie veilig versturen", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsBodyNoItems": { + "message": "Deel bestanden en gegevens veilig met iedereen, op elk platform. Je informatie blijft end-to-end versleuteld terwijl en blootstelling beperkt.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Invoer vereist." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometrisch ontgrendelen is momenteel niet beschikbaar om een onbekende reden." }, + "unlockVault": { + "message": "Ontgrendel je kluis in seconden" + }, + "unlockVaultDesc": { + "message": "Je kunt de instellingen voor ontgrendelen en time-out aanpassen om sneller toegang te krijgen tot je kluis." + }, + "unlockPinSet": { + "message": "PIN-code ontgrendelen instellen" + }, "authenticating": { "message": "Aan het inloggen" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Snel wachtwoorden maken" + }, + "generatorNudgeBodyOne": { + "message": "Maak eenvoudig sterke en unieke wachtwoorden door te klikken op", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyTwo": { + "message": "om je te helpen je inloggegevens veilig te houden.", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyAria": { + "message": "Maak eenvoudig sterke en unieke wachtwoorden door op de knop Wachtwoord genereren te klikken om je logins veilig te houden.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "Je hebt geen rechten om deze pagina te bekijken. Probeer in te loggen met een ander account." } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index f436c45ab75..4775d1f7af0 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index f436c45ab75..4775d1f7af0 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 16696e5f828..eb391f5e34b 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Nie znaleziono unikatowego identyfikatora." }, - "convertOrganizationEncryptionDesc": { - "message": "Organizacja $ORGANIZATION$ używa jednokrotnego logowania SSO z własnym serwerem kluczy. Użytkownicy nie muszą logować się za pomocą hasła głównego.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Hasło główne nie jest już wymagane dla członków następującej organizacji. Proszę potwierdzić poniższą domenę u administratora organizacji." + }, + "organizationName": { + "message": "Nazwa organizacji" + }, + "keyConnectorDomain": { + "message": "Domena Key Connector'a" }, "leaveOrganization": { "message": "Opuść organizację" @@ -3615,6 +3615,14 @@ "message": "Użyj wysyłki, aby bezpiecznie dzielić się zaszyfrowanymi informacjami ze wszystkimi.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Wysyłaj bezpiecznie poufne informacje", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsBodyNoItems": { + "message": "Udostępniaj pliki i dane bezpiecznie każdemu, na każdej platformie. Twoje dane pozostaną zaszyfrowane end-to-end przy jednoczesnym ograniczeniu narażenia.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Dane wejściowe są wymagane." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Odblokowanie biometryczne jest obecnie niedostępne z nieznanego powodu." }, + "unlockVault": { + "message": "Odblokuj swój sejf w kilka sekund" + }, + "unlockVaultDesc": { + "message": "Możesz dostosować ustawienia odblokowania i limitu czasu, aby szybciej uzyskać dostęp do sejfu." + }, + "unlockPinSet": { + "message": "Ustaw kod PIN odblokowujący" + }, "authenticating": { "message": "Uwierzytelnianie" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Szybko twórz hasła" + }, + "generatorNudgeBodyOne": { + "message": "Łatwo twórz silne i unikalne hasła, klikając na", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyTwo": { + "message": ", aby pomóc Ci zachować bezpieczeństwo Twoich danych logowania.", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyAria": { + "message": "Łatwo twórz silne i unikalne hasła, klikając na Wygeneruj Hasło, aby pomóc Ci zachować bezpieczeństwo Twoich danych logowania.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "Nie masz uprawnień do przeglądania tej strony. Spróbuj zalogować się na inne konto." } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index e5ca45500a5..c0b83248edf 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Nenhum identificador exclusivo encontrado." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ está usando SSO com um servidor de chaves auto-hospedado. Não é mais necessária uma senha mestra para os membros desta organização entrarem.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Sair da Organização" @@ -3615,6 +3615,14 @@ "message": "Use o Send para compartilhar informação criptografa com qualquer um.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Entrada necessária." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "O desbloqueio por biometria está indisponível por razões desconhecidas." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Autenticando" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index a4ed095c4db..a144f5767d4 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Não foi encontrado um identificador único." }, - "convertOrganizationEncryptionDesc": { - "message": "A $ORGANIZATION$ está a utilizar o SSO com um servidor de chaves auto-hospedado. Já não é necessária uma palavra-passe mestra para iniciar sessão para os membros desta organização.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Já não é necessária uma palavra-passe mestra para os membros da seguinte organização. Por favor, confirme o domínio abaixo com o administrador da sua organização." + }, + "organizationName": { + "message": "Nome da organização" + }, + "keyConnectorDomain": { + "message": "Domínio do Key Connector" }, "leaveOrganization": { "message": "Sair da organização" @@ -3615,6 +3615,14 @@ "message": "Utilize o Send para partilhar de forma segura informações encriptadas com qualquer pessoa.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Envie informações sensíveis com segurança", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsBodyNoItems": { + "message": "Partilhe ficheiros e dados de forma segura com qualquer pessoa, em qualquer plataforma. As suas informações permanecerão encriptadas ponto a ponto, limitando a exposição.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Campo obrigatório." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "O desbloqueio biométrico está atualmente indisponível por um motivo desconhecido." }, + "unlockVault": { + "message": "Desbloqueie o seu cofre em segundos" + }, + "unlockVaultDesc": { + "message": "Pode personalizar as suas definições de desbloqueio e de tempo limite para aceder mais rapidamente ao seu cofre." + }, + "unlockPinSet": { + "message": "Definição do PIN de desbloqueio" + }, "authenticating": { "message": "A autenticar" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Criar rapidamente palavras-passe" + }, + "generatorNudgeBodyOne": { + "message": "Crie facilmente palavras-passe fortes e únicas clicando em", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyTwo": { + "message": "para o ajudar a manter as suas credenciais seguras.", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyAria": { + "message": "Crie facilmente palavras-passe fortes e únicas clicando no botão Gerar palavra-passe para o ajudar a manter as suas credenciais seguras.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "Não tem permissões para ver esta página. Tente iniciar sessão com uma conta diferente." } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 71990435ad2..2aabf825399 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Nu a fost găsit niciun identificator unic." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ folosește SSO cu un server de chei autogăzduit. Membrii acestei organizații nu mai au nevoie de o parolă principală pentru autentificare.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Părăsire organizație" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Este necesară o intrare." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 2d23c5352e5..a23ab4172f8 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -206,7 +206,7 @@ "message": "Автозаполнение карты" }, "autoFillIdentity": { - "message": "Автозаполнение личности" + "message": "Автозаполнение личной информации" }, "fillVerificationCode": { "message": "Заполнить код подтверждения" @@ -228,7 +228,7 @@ "message": "Нет карт" }, "noIdentities": { - "message": "Нет личностей" + "message": "Нет личной информации" }, "addLoginMenu": { "message": "Добавить логин" @@ -237,7 +237,7 @@ "message": "Добавить карту" }, "addIdentityMenu": { - "message": "Добавить личность" + "message": "Добавить личную информацию" }, "unlockVaultMenu": { "message": "Разблокировать хранилище" @@ -1037,10 +1037,10 @@ "message": "Всегда показывать личности как предложения автозаполнения при просмотре хранилища" }, "showIdentitiesCurrentTab": { - "message": "Показывать Личности на вкладке" + "message": "Показывать Личную информацию на вкладке" }, "showIdentitiesCurrentTabDesc": { - "message": "Личности будут отображены на вкладке для удобного автозаполнения." + "message": "Личная информация будет отображена на вкладке для удобного автозаполнения." }, "clickToAutofillOnVault": { "message": "Кликните элементы для автозаполнения в режиме просмотра хранилища" @@ -1621,7 +1621,7 @@ "message": "Показывать предположения автозаполнения в полях формы" }, "showInlineMenuIdentitiesLabel": { - "message": "Показывать Личности как предложения" + "message": "Показывать Личную информацию как предложения" }, "showInlineMenuCardsLabel": { "message": "Показывать Карты как предложения" @@ -1699,7 +1699,7 @@ "message": "Автозаполнение последней использованной карты для текущего сайта" }, "commandAutofillIdentityDesc": { - "message": "Автозаполнение последней использованной личности для текущего сайта" + "message": "Автозаполнение последних использованных личных данных для текущего сайта" }, "commandGeneratePasswordDesc": { "message": "Сгенерировать и скопировать новый случайный пароль в буфер обмена." @@ -1857,7 +1857,7 @@ "message": "Полное имя" }, "identityName": { - "message": "Название личности" + "message": "Название личной информации" }, "company": { "message": "Компания" @@ -1989,7 +1989,7 @@ "message": "Карты" }, "identities": { - "message": "Личные данные" + "message": "Личная информация" }, "logins": { "message": "Логины" @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Уникальный идентификатор не найден." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ использует SSO с собственным сервером ключей. Для авторизации пользователям этой организации больше не требуется мастер-пароль.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Мастер-пароль больше не требуется для членов следующей организации. Пожалуйста, подтвердите указанный ниже домен у администратора вашей организации." + }, + "organizationName": { + "message": "Название организации" + }, + "keyConnectorDomain": { + "message": "Домен соединителя ключей" }, "leaveOrganization": { "message": "Покинуть организацию" @@ -3615,6 +3615,14 @@ "message": "Используйте Send для безопасного обмена зашифрованной информацией с кем угодно.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Безопасная отправка конфиденциальной информации", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsBodyNoItems": { + "message": "Безопасно обменивайтесь файлами и данными с кем угодно на любой платформе. Ваша информация надежно шифруется и доступ к ней ограничен.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Необходимо ввести данные." }, @@ -3825,11 +3833,11 @@ "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { - "message": "Новая личность", + "message": "Новая личная информация", "description": "Button text to display within inline menu when there are no matching items on an identity field" }, "addNewIdentityItemAria": { - "message": "Добавление новой личности в хранилище, откроется в новом окне", + "message": "Добавление новой личной информации в хранилище, откроется в новом окне", "description": "Screen reader text (aria-label) for new identity button within inline menu" }, "bitwardenOverlayMenuAvailable": { @@ -4068,7 +4076,7 @@ "message": "Функция пока не поддерживается" }, "yourPasskeyIsLocked": { - "message": "Для использования passkey необходима аутентификация. Для продолжения работы подтвердите свою личность." + "message": "Для использования passkey необходима аутентификация. Для продолжения работы подтвердите вашу личность." }, "multifactorAuthenticationCancelled": { "message": "Многофакторная аутентификация отменена" @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Биометрическая разблокировка в настоящее время недоступна по неизвестной причине." }, + "unlockVault": { + "message": "Разблокируйте свое хранилище за считанные секунды" + }, + "unlockVaultDesc": { + "message": "Вы можете настроить параметры разблокировки и тайм-аута для более быстрого доступа к хранилищу." + }, + "unlockPinSet": { + "message": "Установить PIN--код разблокировки" + }, "authenticating": { "message": "Аутентификация" }, @@ -5248,7 +5265,7 @@ "message": "Безопасность, приоритет" }, "securityPrioritizedBody": { - "message": "Сохраняйте логины, карты и личные данные в своем защищенном хранилище. Bitwarden использует сквозное шифрование, чтобы защитить то, что для вас важно." + "message": "Сохраняйте логины, карты и личную информацию в своем защищенном хранилище. Bitwarden использует сквозное шифрование, чтобы защитить то, что для вас важно." }, "quickLogin": { "message": "Быстрая и простая авторизация" @@ -5320,7 +5337,7 @@ "message": "Упрощение создания аккаунтов" }, "newIdentityNudgeBody": { - "message": "С помощью личностей можно быстро заполнять длинные регистрационные или контактные формы." + "message": "С помощью личной информации можно быстро заполнять длинные регистрационные или контактные формы." }, "newNoteNudgeTitle": { "message": "Храните ваши конфиденциальные данные в безопасности" @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Быстрое создание паролей" + }, + "generatorNudgeBodyOne": { + "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": "чтобы обеспечить безопасность ваших логинов.", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyAria": { + "message": "Легко создавайте надежные и уникальные пароли, нажатием на кнопку 'Сгенерировать пароль', чтобы обеспечить безопасность ваших логинов.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "У вас нет прав для просмотра этой страницы. Попробуйте авторизоваться под другим аккаунтом." } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 308404946b3..eafbf9c584e 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "අද්විතීය හඳුනාගැනීමක් සොයාගත නොහැකි විය." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ ස්වයං සත්කාරක යතුරු සේවාදායකයක් සමඟ SSO භාවිතා කරයි. මෙම සංවිධානයේ සාමාජිකයන් සඳහා ප්රවිෂ්ට වීමට ප්රධාන මුරපදයක් තවදුරටත් අවශ්ය නොවේ.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "සංවිධානය හැරයන්න" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index e8d2993a8df..016f4ca1f5a 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -2766,7 +2766,7 @@ "message": "Nové heslo" }, "sendDisabled": { - "message": "Send zakázaný", + "message": "Send bol odstránený", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDisabledWarning": { @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Nenašiel sa žiadny jedinečný identifikátor." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ používa SSO s vlastným kľúčovým serverom. Na prihlásenie členov tejto organizácie už nie je potrebné hlavné heslo.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Hlavné heslo sa už nevyžaduje pre členov tejto organizácie. Nižšie uvedenú doménu potvrďte u správcu organizácie." + }, + "organizationName": { + "message": "Názov organizácie" + }, + "keyConnectorDomain": { + "message": "Doména Key Connectora" }, "leaveOrganization": { "message": "Opustiť organizáciu" @@ -3615,6 +3615,14 @@ "message": "Použite Send na bezpečné zdieľanie zašifrovaných informácii s kýmkoľvek.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send, citlivé informácie bezpečne", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsBodyNoItems": { + "message": "Bezpečne zdieľajte súbory a údaje s kýmkoľvek a na akejkoľvek platforme. Vaše informácie zostanú end-to-end zašifrované a zároveň sa obmedzí ich odhalenie.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Vstup je povinný." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Odomykanie biometrickými údajmi je momentálne z neznámych dôvodov nedostupné." }, + "unlockVault": { + "message": "Odomknite trezor za pár sekúnd" + }, + "unlockVaultDesc": { + "message": "Pre rýchlejší prístup k trezoru si môžete upraviť nastavenia odomknutia a časový limit." + }, + "unlockPinSet": { + "message": "PIN na odomknutie nastavený" + }, "authenticating": { "message": "Overuje sa" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Rýchle vytváranie hesiel" + }, + "generatorNudgeBodyOne": { + "message": "Jednoducho vytvorte silné a jedinečné heslá kliknutím na", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyTwo": { + "message": "aby ste mohli ochrániť prihlasovacie údaje.", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyAria": { + "message": "Jednoducho vytvorte silné a jedinečné heslá kliknutím na tlačidlo Generovať heslo, aby zabezpečili prihlasovacie údaje.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "Nemáte oprávnenie na zobrazenie tejto stránky. Skúste sa prihlásiť pomocou iného účtu." } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 65ee31c888c..3f84455be88 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 255b8dfb924..dae425fcfd3 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -1072,7 +1072,7 @@ "description": "Aria label for the view button in notification bar confirmation message" }, "notificationNewItemAria": { - "message": "New Item, opens in new window", + "message": "Нова ставка, отвара се у новом прозору", "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" }, "notificationEditTooltip": { @@ -1093,15 +1093,15 @@ } }, "notificationLoginSaveConfirmation": { - "message": "saved to Bitwarden.", + "message": "сачувано у Bitwarden.", "description": "Shown to user after item is saved." }, "notificationLoginUpdatedConfirmation": { - "message": "updated in Bitwarden.", + "message": "ажурирано у Bitwarden.", "description": "Shown to user after item is updated." }, "selectItemAriaLabel": { - "message": "Select $ITEMTYPE$, $ITEMNAME$", + "message": "Одабрати $ITEMTYPE$, $ITEMNAME$", "description": "Used by screen readers. $1 is the item type (like vault or folder), $2 is the selected item name.", "placeholders": { "itemType": { @@ -1121,7 +1121,7 @@ "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "Откључајте да бисте сачували ову пријаву", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { @@ -1600,10 +1600,10 @@ "message": "Предлог за ауто-попуњавања" }, "autofillSpotlightTitle": { - "message": "Easily find autofill suggestions" + "message": "Једноставно пронађите предлоге за ауто-пуњење" }, "autofillSpotlightDesc": { - "message": "Turn off your browser's autofill settings, so they don't conflict with Bitwarden." + "message": "Искључите подешавања ауто-пуњења прегледача, тако да се не сукобљавај са Bitwarden." }, "turnOffBrowserAutofill": { "message": "Turn off $BROWSER$ autofill", @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Није пронађен ниједан јединствени идентификатор." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ користи SSO уз сопствени сервер за кључеве. Главна лозинка за пријаву више није неопходна за чланове ове организације.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Главна лозинка више није потребна за чланове следеће организације. Молимо потврдите домен са администратором организације." + }, + "organizationName": { + "message": "Назив организације" + }, + "keyConnectorDomain": { + "message": "Домен конектора кључа" }, "leaveOrganization": { "message": "Напусти организацију" @@ -3615,6 +3615,14 @@ "message": "Употребите Send да безбедно делите шифроване информације са било ким.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Унос је потребан." }, @@ -4532,31 +4540,31 @@ } }, "downloadBitwarden": { - "message": "Download Bitwarden" + "message": "Преузети Bitwarden" }, "downloadBitwardenOnAllDevices": { - "message": "Download Bitwarden on all devices" + "message": "Преузети Bitwarden на све уређаје" }, "getTheMobileApp": { - "message": "Get the mobile app" + "message": "Узети мобилну апликацију" }, "getTheMobileAppDesc": { - "message": "Access your passwords on the go with the Bitwarden mobile app." + "message": "Приступите лозинци у покрету са Bitwarden мобилном апликацијом." }, "getTheDesktopApp": { - "message": "Get the desktop app" + "message": "Узети desktop апликацију" }, "getTheDesktopAppDesc": { - "message": "Access your vault without a browser, then set up unlock with biometrics to expedite unlocking in both the desktop app and browser extension." + "message": "Приступите свом сефу без прегледача, а затим поставите откључавање са биометристима да бисте убрзали откључавање и у програму и у додатку." }, "downloadFromBitwardenNow": { - "message": "Download from bitwarden.com now" + "message": "Преузети сада са bitwarden.com" }, "getItOnGooglePlay": { - "message": "Get it on Google Play" + "message": "Набавите на Google Play" }, "downloadOnTheAppStore": { - "message": "Download on the App Store" + "message": "Преузмите са App Store" }, "permanentlyDeleteAttachmentConfirmation": { "message": "Да ли сте сигурни да желите да трајно избришете овај прилог?" @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Биометријско откључавање није доступно из непознатог разлога." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Аутентификација" }, @@ -5269,7 +5286,7 @@ "message": "Сачувајте неограничене лозинке на неограниченим уређајима помоћу Bitwarden мобилних апликација, претраживача и десктоп апликација." }, "nudgeBadgeAria": { - "message": "1 notification" + "message": "1 нотификација" }, "emptyVaultNudgeTitle": { "message": "Увоз постојеће лозинке" @@ -5284,29 +5301,29 @@ "message": "Добродошли у ваш сеф!" }, "hasItemsVaultNudgeBodyOne": { - "message": "Autofill items for the current page" + "message": "Ауто-пуњење предмета за тренутну страницу" }, "hasItemsVaultNudgeBodyTwo": { - "message": "Favorite items for easy access" + "message": "Предмети као омиљен за лак приступ" }, "hasItemsVaultNudgeBodyThree": { - "message": "Search your vault for something else" + "message": "Претражите сеф за нешто друго" }, "newLoginNudgeTitle": { "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." }, @@ -5332,16 +5349,33 @@ "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" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { - "message": "You do not have permissions to view this page. Try logging in with a different account." + "message": "Немате дозволе за преглед ове странице. Покушајте да се пријавите са другим налогом." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 3a063f8f066..f57209122ef 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Ingen unik identifierare hittades." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ använder SSO med en egen nyckelserver. Ett huvudlösenord krävs inte längre för att logga in för medlemmar i denna organisation.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Lämna organisation" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Inmatning är obligatoriskt." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index f436c45ab75..4775d1f7af0 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 19430463090..fd9bac62391 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "No unique identifier found." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3615,6 +3615,14 @@ "message": "Use Send to securely share encrypted information with anyone.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 700e9b73881..02d7d15b8a2 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Benzersiz tanımlayıcı bulunamadı." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ kendi barındırdığı bir anahtar sunucusuyla SSO kullanıyor. Bu kuruluşun üyelerinin artık ana parola kullanması gerekmiyor.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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": "Kuruluş adı" + }, + "keyConnectorDomain": { + "message": "Key Connector alan adı" }, "leaveOrganization": { "message": "Kuruluştan ayrıl" @@ -3615,6 +3615,14 @@ "message": "Şifrelenmiş bilgileri güvenle paylaşmak için Send'i kullanabilirsiniz.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Hassas bilgileri güvenle paylaşın", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Girdi gerekli." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Kasanızın kilidini saniyeler içinde açın" + }, + "unlockVaultDesc": { + "message": "Kasanıza daha hızlı ulaşmak için kilit açma ve zaman aşımı ayarlarınızı özelleştirebilirsiniz." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Kimlik doğrulanıyor" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "Bu sayfayı görüntüleme izniniz yok. Farklı bir hesapla giriş yapmayı deneyin." } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index b0c78a17106..9406aabe088 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Не знайдено унікальний ідентифікатор." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ використовує SSO з власним сервером ключів. Головний пароль для учасників цієї організації більше не вимагається.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Покинути організацію" @@ -3615,6 +3615,14 @@ "message": "Використовуйте відправлення, щоб безпечно надавати доступ іншим до зашифрованої інформації.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Необхідно ввести дані." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Біометричне розблокування зараз недоступне з невідомої причини." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Аутентифікація" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 14649828aaf..29a7b7109e7 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "Không tìm thấy danh tính duy nhất." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ đang sử dụng SSO với khóa máy chủ tự lưu trữ. Mật khẩu chính không còn cần để đăng nhập cho các thành viên của tổ chức này.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Rời tổ chức" @@ -3615,6 +3615,14 @@ "message": "Sử dụng Gửi để chia sẻ thông tin mã hóa một cách an toàn với bất kỳ ai.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Trường này là bắt buộc." }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "Biometric unlock is currently unavailable for an unknown reason." }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "Authenticating" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index e0bc34bd47d..8a537be08f2 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -1072,7 +1072,7 @@ "description": "Aria label for the view button in notification bar confirmation message" }, "notificationNewItemAria": { - "message": "新增项目,在新窗口中打开", + "message": "新增项目(在新窗口中打开)", "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" }, "notificationEditTooltip": { @@ -1101,7 +1101,7 @@ "description": "Shown to user after item is updated." }, "selectItemAriaLabel": { - "message": "Select $ITEMTYPE$, $ITEMNAME$", + "message": "选择 $ITEMTYPE$,$ITEMNAME$", "description": "Used by screen readers. $1 is the item type (like vault or folder), $2 is the selected item name.", "placeholders": { "itemType": { @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "未找到唯一的标识符。" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ 使用自托管密钥服务器 SSO。这个组织的成员登录时将不再需要主密码。", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "以下组织的成员不再需要主密码。请与您的组织管理员确认下面的域名。" + }, + "organizationName": { + "message": "组织名称" + }, + "keyConnectorDomain": { + "message": "Key Connector 域名" }, "leaveOrganization": { "message": "退出组织" @@ -3615,6 +3615,14 @@ "message": "使用 Send 与任何人安全地分享加密信息。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "安全地发送敏感信息", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsBodyNoItems": { + "message": "在任何平台上安全地与任何人共享文件和数据。您的信息将在限制曝光的同时保持端到端加密。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "必须输入内容。" }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "由于某个未知的原因,生物识别解锁当前不可用。" }, + "unlockVault": { + "message": "数秒内解锁您的密码库" + }, + "unlockVaultDesc": { + "message": "您可以自定义解锁和超时设置,以便更快地访问您的密码库。" + }, + "unlockPinSet": { + "message": "解锁 PIN 设置" + }, "authenticating": { "message": "正在验证" }, @@ -5284,24 +5301,24 @@ "message": "欢迎来到您的密码库!" }, "hasItemsVaultNudgeBodyOne": { - "message": "自动填充项目用于当前页面" + "message": "为当前页面自动填充项目" }, "hasItemsVaultNudgeBodyTwo": { - "message": "收藏项目用于轻松访问" + "message": "收藏项目以便快速访问" }, "hasItemsVaultNudgeBodyThree": { - "message": "搜索密码库用于其他" + "message": "在密码库中搜索其他内容" }, "newLoginNudgeTitle": { "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." }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "快速创建密码" + }, + "generatorNudgeBodyOne": { + "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": "帮助您保持登录安全。", + "description": "Two part message", + "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." + }, + "generatorNudgeBodyAria": { + "message": "一键创建强大且唯一的密码,帮助您保持登录安全。", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "您没有查看此页面的权限。请尝试使用其他账户登录。" } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index e9eab0a726d..9d4eabb1b49 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -3014,14 +3014,14 @@ "copyCustomFieldNameNotUnique": { "message": "找不到唯一識別碼。" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ 使用自我裝載金鑰伺服器 SSO。此組織的成員登入時將不再需要主密碼。", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "離開組織" @@ -3615,6 +3615,14 @@ "message": "使用 Send 可以與任何人安全地共用加密資訊。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoItems": { + "message": "Send sensitive information safely", + "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.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "必須輸入內容。" }, @@ -5019,6 +5027,15 @@ "biometricsStatusHelptextUnavailableReasonUnknown": { "message": "基於不明原因,生物辨識解鎖無法使用。" }, + "unlockVault": { + "message": "Unlock your vault in seconds" + }, + "unlockVaultDesc": { + "message": "You can customize your unlock and timeout settings to more quickly access your vault." + }, + "unlockPinSet": { + "message": "Unlock PIN set" + }, "authenticating": { "message": "驗證中" }, @@ -5341,6 +5358,23 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "generatorNudgeTitle": { + "message": "Quickly create passwords" + }, + "generatorNudgeBodyOne": { + "message": "Easily create strong and unique passwords by clicking on", + "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.", + "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.", + "description": "Aria label for the body content of the generator nudge" + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." } From 66a8c3ede3d18bde7f0a0c9165165daadea9f231 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 12:00:35 +0200 Subject: [PATCH 131/163] Autosync the updated translations (#14893) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 13 +- apps/web/src/locales/ar/messages.json | 13 +- apps/web/src/locales/az/messages.json | 13 +- apps/web/src/locales/be/messages.json | 13 +- apps/web/src/locales/bg/messages.json | 13 +- apps/web/src/locales/bn/messages.json | 13 +- apps/web/src/locales/bs/messages.json | 13 +- apps/web/src/locales/ca/messages.json | 13 +- apps/web/src/locales/cs/messages.json | 13 +- apps/web/src/locales/cy/messages.json | 13 +- apps/web/src/locales/da/messages.json | 13 +- apps/web/src/locales/de/messages.json | 13 +- apps/web/src/locales/el/messages.json | 13 +- apps/web/src/locales/en_GB/messages.json | 13 +- apps/web/src/locales/en_IN/messages.json | 13 +- apps/web/src/locales/eo/messages.json | 41 +++-- apps/web/src/locales/es/messages.json | 13 +- apps/web/src/locales/et/messages.json | 13 +- apps/web/src/locales/eu/messages.json | 13 +- apps/web/src/locales/fa/messages.json | 199 +++++++++++------------ apps/web/src/locales/fi/messages.json | 13 +- apps/web/src/locales/fil/messages.json | 13 +- apps/web/src/locales/fr/messages.json | 13 +- apps/web/src/locales/gl/messages.json | 13 +- apps/web/src/locales/he/messages.json | 13 +- apps/web/src/locales/hi/messages.json | 13 +- apps/web/src/locales/hr/messages.json | 13 +- apps/web/src/locales/hu/messages.json | 13 +- apps/web/src/locales/id/messages.json | 13 +- apps/web/src/locales/it/messages.json | 13 +- apps/web/src/locales/ja/messages.json | 13 +- apps/web/src/locales/ka/messages.json | 13 +- apps/web/src/locales/km/messages.json | 13 +- apps/web/src/locales/kn/messages.json | 13 +- apps/web/src/locales/ko/messages.json | 13 +- apps/web/src/locales/lv/messages.json | 13 +- apps/web/src/locales/ml/messages.json | 13 +- apps/web/src/locales/mr/messages.json | 13 +- apps/web/src/locales/my/messages.json | 13 +- apps/web/src/locales/nb/messages.json | 13 +- apps/web/src/locales/ne/messages.json | 13 +- apps/web/src/locales/nl/messages.json | 13 +- apps/web/src/locales/nn/messages.json | 13 +- apps/web/src/locales/or/messages.json | 13 +- apps/web/src/locales/pl/messages.json | 13 +- apps/web/src/locales/pt_BR/messages.json | 13 +- apps/web/src/locales/pt_PT/messages.json | 13 +- apps/web/src/locales/ro/messages.json | 13 +- apps/web/src/locales/ru/messages.json | 31 ++-- apps/web/src/locales/si/messages.json | 13 +- apps/web/src/locales/sk/messages.json | 17 +- apps/web/src/locales/sl/messages.json | 13 +- apps/web/src/locales/sr/messages.json | 39 ++--- apps/web/src/locales/sr_CS/messages.json | 13 +- apps/web/src/locales/sv/messages.json | 13 +- apps/web/src/locales/te/messages.json | 13 +- apps/web/src/locales/th/messages.json | 13 +- apps/web/src/locales/tr/messages.json | 13 +- apps/web/src/locales/uk/messages.json | 13 +- apps/web/src/locales/vi/messages.json | 13 +- apps/web/src/locales/zh_CN/messages.json | 27 ++- apps/web/src/locales/zh_TW/messages.json | 13 +- 62 files changed, 448 insertions(+), 634 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 152eb51d7d1..3d2c04f6673 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Ongeldige bevestigingskode" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ gebruik SSO met ’n sleutelbediener op ’n eie gasheer. ’n Hoofwagwoord word nie meer vereis vir aantekening vir lede van hierdie organisasie nie.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Verlaat organisasie" diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index c84858fce08..82907ed4df0 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "رمز التحقق غير صالح" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "مغادرة المؤسسة" diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 3518898335e..121f50f0e00 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Yararsız doğrulama kodu" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$, self-hosted açar serveri ilə SSO istifadə edir. Bu təşkilatın üzvlərinin giriş etməsi üçün artıq ana parol tələb edilməyəcək.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Aşağıdakı təşkilatların üzvləri üçün artıq ana parol tələb olunmur. Lütfən aşağıdakı domeni təşkilatınızın inzibatçısı ilə təsdiqləyin." + }, + "keyConnectorDomain": { + "message": "Key Connector domeni" }, "leaveOrganization": { "message": "Təşkilatı tərk et" diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 789f292aeaf..7409df6baff 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Памылковы праверачны код" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ выкарыстоўвае SSO з уласным серверам ключоў. Асноўны пароль для ўдзельнікаў гэтай арганізацыі больш не патрабуецца.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Выйсці з арганізацыі" diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index b26d6a21578..a8bd9d51e8f 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Грешен код за потвърждаване" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ използва еднократно удостоверяване със собствен сървър за ключове. Членовете на тази организация вече нямат нужда от главна парола за вписване.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "За членовете на следната организация вече не се изисква главна парола. Потвърдете домейна по-долу с администратора на организацията си." + }, + "keyConnectorDomain": { + "message": "Домейн на конектора за ключове" }, "leaveOrganization": { "message": "Напускане на организацията" diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index bb71e44f09d..1419d580837 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index a05e84245e8..31174ebbe5a 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index f24353d69b3..49aadd7951a 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Codi de verificació no vàlid" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ està utilitzant SSO amb un servidor autoallotjat de claus. Ja no es requereix una contrasenya mestra d'inici de sessió per als membres d'aquesta organització.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Abandona l'organització" diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 4ba5a9c3fea..615bfa4f09b 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Neplatný ověřovací kód" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ používá SSO s vlastním serverem s klíči. Hlavní heslo pro členy této organizace již není vyžadováno.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Hlavní heslo již není vyžadováno pro členy následující organizace. Potvrďte níže uvedenou doménu u správce Vaší organizace." + }, + "keyConnectorDomain": { + "message": "Doména Key Connectoru" }, "leaveOrganization": { "message": "Opustit organizaci" diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index f91dc727a84..87c852a8869 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index f40daec488f..ee2dc4d570c 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Ugyldig bekræftelseskode" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ anvender SSO med en selv-hostet nøgleserver. Organisationsmedlemmer afkræves ikke længere en hovedadgangskode for at logge ind.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Forlad organisation" diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index f39bb85a082..58b4c568c61 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Ungültiger Verifizierungscode" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ verwendet SSO mit einem selbst gehosteten Schlüsselserver. Ein Master-Passwort ist nicht mehr erforderlich, damit sich Mitglieder dieser Organisation anmelden können.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Für Mitglieder der folgenden Organisation ist kein Master-Passwort mehr erforderlich. Bitte bestätige die folgende Domain bei deinem Organisations-Administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector-Domain" }, "leaveOrganization": { "message": "Organisation verlassen" diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 24de28d39be..3790920f883 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Μη έγκυρος κωδικός επαλήθευσης" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ χρησιμοποιεί SSO με έναν αυτοεξυπηρετητή κλειδιών. Ένας κύριος κωδικός πρόσβασης δεν απαιτείται πλέον για να συνδεθείτε για τα μέλη αυτού του οργανισμού.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Αποχώρηση από τον οργανισμό" diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index d6a1b01f1d5..ab7bf3a1197 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organisation.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organisation. Please confirm the domain below with your organisation administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organisation" diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index a0e9f4de149..736cece1de3 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organisation.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organisation. Please confirm the domain below with your organisation administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organisation" diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 5b5e1f21582..0a4acdc5739 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -7446,11 +7443,11 @@ "description": "Label for a secret (key/value pair)" }, "serviceAccount": { - "message": "Servila konto", + "message": "Konto ĉe servo", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "serviceAccounts": { - "message": "Servilaj kontoj", + "message": "Kontoj ĉe servo", "description": "The title for the section that deals with service accounts." }, "secrets": { @@ -7474,7 +7471,7 @@ "description": "Title for creating a new secret." }, "newServiceAccount": { - "message": "Nova servila konto", + "message": "Nova konto ĉe servo", "description": "Title for creating a new service account." }, "secretsNoItemsTitle": { @@ -7501,7 +7498,7 @@ "description": "Placeholder text for searching secrets." }, "deleteServiceAccounts": { - "message": "Forigi servilajn kontojn", + "message": "Forigi kontojn ĉe servo", "description": "Title for the action to delete one or multiple service accounts." }, "deleteServiceAccount": { @@ -7509,7 +7506,7 @@ "description": "Title for the action to delete a single service account." }, "viewServiceAccount": { - "message": "Vidi servilan konton", + "message": "Vidi la konton ĉe servo", "description": "Action to view the details of a service account." }, "deleteServiceAccountDialogMessage": { @@ -8170,7 +8167,7 @@ "message": "This user can access Secrets Manager" }, "important": { - "message": "Grave" + "message": "Seriozaĵo:" }, "viewAll": { "message": "Vidi ĉiujn" @@ -8220,7 +8217,7 @@ "message": "Krei projekton" }, "createServiceAccount": { - "message": "Krei servilan konton" + "message": "Krei konton ĉe servo" }, "downloadThe": { "message": "Elŝuti la", @@ -9077,11 +9074,11 @@ "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { - "message": "Forĵeti maŝinan konton", + "message": "Forĵeti maŝinajn kontojn", "description": "Title for the action to delete one or multiple machine accounts." }, "deleteMachineAccount": { - "message": "Forĵeti maŝinajn kontojn", + "message": "Forĵeti maŝinan konton", "description": "Title for the action to delete a single machine account." }, "viewMachineAccount": { @@ -9580,7 +9577,7 @@ "message": "Vidi sekreton" }, "noClients": { - "message": "Estas neniu kliento por listo" + "message": "Estas neniu kliento por listi" }, "providerBillingEmailHint": { "message": "This email address will receive all invoices pertaining to this provider", @@ -9732,7 +9729,7 @@ "message": "Aldoni al dosierujo" }, "selectFolder": { - "message": "Elekti dosierujo" + "message": "Elekti dosierujon" }, "personalItemTransferWarningSingular": { "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." @@ -10102,7 +10099,7 @@ "message": "Non-compliant members will be revoked. Administrators can restore members once they leave all other organizations." }, "deleteOrganizationUser": { - "message": "Fo", + "message": "Forigi $NAME$", "placeholders": { "name": { "content": "$1", @@ -10177,7 +10174,7 @@ "message": "This action is not applicable to any of the selected members." }, "deletedSuccessfully": { - "message": "Sukcesis foriĝi" + "message": "Sukcese forigis" }, "freeFamiliesSponsorship": { "message": "Remove Free Bitwarden Families sponsorship" @@ -10246,7 +10243,7 @@ "message": "Claimed" }, "domainStatusUnderVerification": { - "message": "Sub aŭtentigo" + "message": "En konfirmado" }, "claimedDomainsDesc": { "message": "Claim a domain to own all member accounts whose email address matches the domain. Members will be able to skip the SSO identifier when logging in. Administrators will also be able to delete member accounts." diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index a535b45e721..1660429f7f4 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Código de verificación no válido" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ está usando SSO con un servidor de claves autoalojado. Una contraseña maestra ya no es necesaria para iniciar sesión para los miembros de esta organización.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Abandonar organización" diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 9ed4db4b0e3..19608e1fd82 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Vale kinnituskood" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Lahku organisatsioonist" diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 29f132852eb..b587e7f1611 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Egiaztatze-kodea ez da baliozkoa" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ ostatatze propioko gako-zerbitzari batekin SSO erabiltzen ari da. Dagoeneko ez da pasahitz nagusirik behar erakunde honetako kideentzat saioa hasteko.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Utzi erakundea" diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 2fbacc8dca2..a7c07192acc 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -1,30 +1,30 @@ { "allApplications": { - "message": "All applications" + "message": "همه‌ برنامه‌ها" }, "appLogoLabel": { - "message": "Bitwarden logo" + "message": "لوگو Bitwarden" }, "criticalApplications": { - "message": "Critical applications" + "message": "برنامه‌های حیاتی" }, "noCriticalAppsAtRisk": { - "message": "No critical applications at risk" + "message": "هیچ برنامه حیاتی در معرض خطر نیست" }, "accessIntelligence": { - "message": "Access Intelligence" + "message": "دسترسی به هوش مصنوعی" }, "riskInsights": { - "message": "Risk Insights" + "message": "دیدگاه‌های خطر" }, "passwordRisk": { - "message": "Password Risk" + "message": "خطر کلمه عبور" }, "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "message": "کلمات عبور در معرض خطر (ضعیف، افشا شده یا تکراری) را در برنامه‌ها بررسی کنید. برنامه‌های حیاتی خود را انتخاب کنید تا اقدامات امنیتی را برای کاربران‌تان اولویت‌بندی کنید و به کلمات عبور در معرض خطر رسیدگی کنید." }, "dataLastUpdated": { - "message": "Data last updated: $DATE$", + "message": "آخرین به‌روزرسانی داده‌ها: \\$DATE\\$", "placeholders": { "date": { "content": "$1", @@ -33,19 +33,19 @@ } }, "notifiedMembers": { - "message": "Notified members" + "message": "اعضای مطلع شده" }, "revokeMembers": { - "message": "Revoke members" + "message": "حذف اعضا" }, "restoreMembers": { - "message": "Restore members" + "message": "بازیابی اعضا" }, "cannotRestoreAccessError": { - "message": "Cannot restore organization access" + "message": "امکان بازیابی دسترسی به سازمان وجود ندارد" }, "allApplicationsWithCount": { - "message": "All applications ($COUNT$)", + "message": "تمام برنامه‌ها ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -54,10 +54,10 @@ } }, "createNewLoginItem": { - "message": "Create new login item" + "message": "ایجاد مورد ورود جدید" }, "criticalApplicationsWithCount": { - "message": "Critical applications ($COUNT$)", + "message": "برنامه‌های حیاتی ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -102,13 +102,13 @@ "message": "Applications marked as critical" }, "application": { - "message": "Application" + "message": "اپلیکیشن" }, "atRiskPasswords": { - "message": "At-risk passwords" + "message": "کلمات عبور در معرض خطر" }, "requestPasswordChange": { - "message": "Request password change" + "message": "درخواست تغییر کلمه عبور" }, "totalPasswords": { "message": "Total passwords" @@ -220,10 +220,10 @@ "message": "یادداشت‌ها" }, "privateNote": { - "message": "Private note" + "message": "یادداشت خصوصی" }, "note": { - "message": "Note" + "message": "یادداشت" }, "customFields": { "message": "فیلدهای سفارشی" @@ -232,19 +232,19 @@ "message": "نام صاحب کارت" }, "loginCredentials": { - "message": "Login credentials" + "message": "اطلاعات ورود" }, "personalDetails": { - "message": "Personal details" + "message": "جزئیات شخصی" }, "identification": { - "message": "Identification" + "message": "شناسایی" }, "contactInfo": { - "message": "Contact info" + "message": "اطلاعات مخاطب" }, "cardDetails": { - "message": "Card details" + "message": "جزئیات کارت" }, "cardBrandDetails": { "message": "$BRAND$ details", @@ -450,17 +450,17 @@ "message": "منطقی" }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "کادر انتخاب" }, "cfTypeLinked": { "message": "پیوند شده", "description": "This describes a field that is 'linked' (related) to another field." }, "fieldType": { - "message": "Field type" + "message": "نوع فیلد" }, "fieldLabel": { - "message": "Field label" + "message": "برچسب فیلد" }, "remove": { "message": "حذف" @@ -473,7 +473,7 @@ "description": "This is the folder for uncategorized items" }, "selfOwnershipLabel": { - "message": "You", + "message": "شما", "description": "Used as a label to indicate that the user is the owner of an item." }, "addFolder": { @@ -496,7 +496,7 @@ } }, "newFolder": { - "message": "New folder" + "message": "پوشه جدید" }, "folderName": { "message": "Folder name" @@ -687,7 +687,7 @@ "message": "نام کامل" }, "address": { - "message": "Address" + "message": "نشانی" }, "address1": { "message": "نشانی ۱" @@ -732,7 +732,7 @@ "message": "مشاهده مورد" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "$TYPE$ جدید", "placeholders": { "type": { "content": "$1", @@ -741,7 +741,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "ویرایش $TYPE$", "placeholders": { "type": { "content": "$1", @@ -750,7 +750,7 @@ } }, "viewItemType": { - "message": "View $ITEMTYPE$", + "message": "مشاهده $ITEMTYPE$", "placeholders": { "itemtype": { "content": "$1", @@ -766,10 +766,10 @@ "message": "مورد" }, "itemDetails": { - "message": "Item details" + "message": "جزئیات مورد" }, "itemName": { - "message": "Item name" + "message": "نام مورد" }, "ex": { "message": "مثال.", @@ -795,7 +795,7 @@ } }, "copySuccessful": { - "message": "Copy Successful" + "message": "کپی موفق بود" }, "copyValue": { "message": "کپی مقدار", @@ -806,11 +806,11 @@ "description": "Copy password to clipboard" }, "copyPassphrase": { - "message": "Copy passphrase", + "message": "کپی عبارت عبور", "description": "Copy passphrase to clipboard" }, "passwordCopied": { - "message": "Password copied" + "message": "کلمه عبور کپی شد" }, "copyUsername": { "message": "کپی نام کاربری", @@ -829,7 +829,7 @@ "description": "Copy URI to clipboard" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "کپی $FIELD$", "placeholders": { "field": { "content": "$1", @@ -838,34 +838,34 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "کپی وب‌سایت" }, "copyNotes": { - "message": "Copy notes" + "message": "کپی یادداشت‌ها" }, "copyAddress": { - "message": "Copy address" + "message": "کپی نشانی" }, "copyPhone": { - "message": "Copy phone" + "message": "کپی تلفن" }, "copyEmail": { - "message": "Copy email" + "message": "کپی ایمیل" }, "copyCompany": { - "message": "Copy company" + "message": "کپی شرکت" }, "copySSN": { - "message": "Copy Social Security number" + "message": "کپی شماره کد ملی" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "کپی شماره گذرنامه" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "کپی شماره گواهینامه" }, "copyName": { - "message": "Copy name" + "message": "کپی نام" }, "me": { "message": "من" @@ -944,7 +944,7 @@ } }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "موارد به $ORGNAME$ منتقل شدند", "placeholders": { "orgname": { "content": "$1", @@ -953,7 +953,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "مورد به $ORGNAME$ منتقل شد", "placeholders": { "orgname": { "content": "$1", @@ -1019,16 +1019,16 @@ "message": "نشست ورود شما منقضی شده است." }, "restartRegistration": { - "message": "Restart registration" + "message": "ثبت‌نام را دوباره آغاز کنید" }, "expiredLink": { - "message": "Expired link" + "message": "پیوند منقضی شد" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "لطفاً ثبت نام را مجدداً شروع کنید یا دوباره وارد شوید." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "ممکن است قبلاً حساب کاربری داشته باشید" }, "logOutConfirmation": { "message": "آیا مطمئنید که می‌خواهید خارج شوید؟" @@ -1046,7 +1046,7 @@ "message": "خیر" }, "location": { - "message": "Location" + "message": "موقعیت" }, "loginOrCreateNewAccount": { "message": "وارد شوید یا یک حساب کاربری بسازید تا به گاوصندوق امن‌تان دسترسی یابید." @@ -1058,28 +1058,28 @@ "message": "ورود به سیستم با دستگاه باید در تنظیمات برنامه‌ی Bitwarden تنظیم شود. به گزینه دیگری نیاز دارید؟" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "به گزینه دیگری نیاز دارید؟" }, "loginWithMasterPassword": { "message": "با کلمه عبور اصلی وارد شوید" }, "readingPasskeyLoading": { - "message": "Reading passkey..." + "message": "خواندن کلید عبور..." }, "readingPasskeyLoadingInfo": { - "message": "Keep this window open and follow prompts from your browser." + "message": "این پنجره را باز نگه دارید و دستورهای مرورگر خود را دنبال کنید." }, "useADifferentLogInMethod": { "message": "Use a different log in method" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "با کلید عبور وارد شوید" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "استفاده از ورود تک مرحله‌ای" }, "welcomeBack": { - "message": "Welcome back" + "message": "خوش آمدید" }, "invalidPasskeyPleaseTryAgain": { "message": "Invalid Passkey. Please try again." @@ -1163,13 +1163,13 @@ "message": "ایجاد حساب کاربری" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "در Bitwarden تازه وارد هستید؟" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "تنظیم کلمه عبور قوی" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "ایجاد حساب کاربری خود را با تنظیم کلمه عبور تکمیل کنید" }, "newAroundHere": { "message": "اینجا تازه واردی؟" @@ -1181,22 +1181,22 @@ "message": "ورود" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "وارد Bitwarden شوید" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "کدی را که به ایمیل شما ارسال شده وارد کنید" }, "enterTheCodeFromYourAuthenticatorApp": { - "message": "Enter the code from your authenticator app" + "message": "کد سامانه تأیید کننده را وارد نمایید" }, "pressYourYubiKeyToAuthenticate": { - "message": "Press your YubiKey to authenticate" + "message": "برای احراز هویت، کلید YubiKey خود را فشار دهید" }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "پایان زمان احراز هویت" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "نشست احراز هویت منقضی شد. لطفاً فرایند ورود را دوباره شروع کنید." }, "verifyYourIdentity": { "message": "Verify your Identity" @@ -1736,25 +1736,25 @@ "message": "تاریخچه کلمه عبور" }, "generatorHistory": { - "message": "Generator history" + "message": "تاریخچه تولید کننده" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "پاک کردن تاریخچه تولید کننده" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "اگر ادامه دهید، تمام ورودی‌ها به‌طور دائمی از تاریخچه تولید کننده حذف خواهند شد. آیا مطمئن هستید که می‌خواهید ادامه دهید؟" }, "noPasswordsInList": { "message": "هیچ کلمه عبوری برای فهرست کردن وجود ندارد." }, "clearHistory": { - "message": "Clear history" + "message": "پاک کردن تاریخچه" }, "nothingToShow": { - "message": "Nothing to show" + "message": "چیزی برای نشان دادن موجود نیست" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "شما اخیراً چیزی تولید نکرده‌اید" }, "clear": { "message": "پاک کردن", @@ -1794,10 +1794,10 @@ "message": "لطفاً دوباره وارد شوید." }, "currentSession": { - "message": "Current session" + "message": "نشست کنونی" }, "requestPending": { - "message": "Request pending" + "message": "درخواست در حال انتظار است" }, "logBackInOthersToo": { "message": "لطفاً دوباره وارد شوید. اگر از سایر برنامه‌های Bitwarden استفاده می‌کنید، از سیستم خارج شوید و دوباره به آن‌ها وارد شوید." @@ -1961,11 +1961,11 @@ "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsLink": { - "message": "new item", + "message": "مورد جدید", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsLoginLink": { - "message": "new login", + "message": "ورود جدید", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new login instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsPartTwoNoOrgs": { @@ -2016,7 +2016,7 @@ "message": "خطا در رمزگشایی پرونده‌ی درون ریزی شده. کلید رمزگذاری شما با کلید رمزگذاری استفاده شده برای درون ریزی داده‌ها مطابقت ندارد." }, "destination": { - "message": "Destination" + "message": "مقصد" }, "learnAboutImportOptions": { "message": "درباره گزینه‌های برون ریزی خود بیاموزید" @@ -2201,16 +2201,16 @@ "message": "مدیریت" }, "manageCollection": { - "message": "Manage collection" + "message": "مدیریت مجموعه" }, "viewItems": { - "message": "View items" + "message": "مشاهده موارد" }, "viewItemsHidePass": { "message": "View items, hidden passwords" }, "editItems": { - "message": "Edit items" + "message": "ویرایش موارد" }, "editItemsHidePass": { "message": "Edit items, hidden passwords" @@ -2222,7 +2222,7 @@ "message": "لغو دسترسی" }, "revoke": { - "message": "Revoke" + "message": "لغو" }, "twoStepLoginProviderEnabled": { "message": "این ارائه دهنده ورود به سیستم دو مرحله ای در حساب شما فعال است." @@ -2237,7 +2237,7 @@ "message": "," }, "twoStepAuthenticatorInstructionInfix2": { - "message": "or" + "message": "یا" }, "twoStepAuthenticatorInstructionSuffix": { "message": "." @@ -2255,10 +2255,10 @@ "message": "You are leaving Bitwarden and launching an external website in a new window." }, "twoStepContinueToBitwardenUrlTitle": { - "message": "Continue to bitwarden.com?" + "message": "به bitwarden.com ادامه می‌دهید؟" }, "twoStepContinueToBitwardenUrlDesc": { - "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website." + "message": "احراز هویت کننده Bitwarden به شما امکان می‌دهد کلیدهای احراز هویت را ذخیره کرده و کدهای TOTP را برای فرآیندهای تأیید دومرحله‌ای تولید کنید. برای اطلاعات بیشتر به وب‌سایت bitwarden.com مراجعه کنید." }, "twoStepAuthenticatorScanCodeV2": { "message": "Scan the QR code below with your authenticator app or enter the key." @@ -2270,7 +2270,7 @@ "message": "کلید" }, "twoStepAuthenticatorEnterCodeV2": { - "message": "Verification code" + "message": "کد تأیید" }, "twoStepAuthenticatorReaddDesc": { "message": "در صورت نیاز به افزودن آن به دستگاه دیگر، کد QR (یا کلید) مورد نیاز برنامه احراز هویت شما در زیر آمده است." @@ -2351,7 +2351,7 @@ "message": "اطلاعات برنامه Bitwarden را از پنل مدیریت Duo خود وارد کنید." }, "twoFactorDuoClientId": { - "message": "Client Id" + "message": "شناسه کاربر" }, "twoFactorDuoClientSecret": { "message": "Client Secret" @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "کد تأیید نامعتبر است" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ در حال استفاده از SSO با یک سرور کلید خود میزبان است. برای ورود اعضای این سازمان دیگر نیازی به کلمه عبور اصلی نیست.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "ترک سازمان" diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 83efcc197fc..ccbcf4f7cc3 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Virheellinen todennuskoodi" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ käyttää kertakirjautumista itse ylläpidetyllä avainpalvelimella. Organisaation jäsenet eivät enää tarvitse pääsalasanaa kirjautumiseen.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Eroa organisaatiosta" diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 15e0793d6ce..97e22e18045 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Maling verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ ay gumagamit ng SSO na may sariling-hosted na key server. Walang kinakailangang master password para mag-log in para sa mga miyembro ng organisasyon na ito.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Umalis sa organisasyon" diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 39fe5b3c1cf..d881ecfefd3 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Code de vérification invalide" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ utilise le SSO avec un serveur de clés auto-hébergé. Un mot de passe principal n'est plus nécessaire aux membres de cette organisation pour se connecter.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Un mot de passe maître n'est plus requis pour les membres de l'organisation suivante. Veuillez confirmer le domaine ci-dessous avec l'administrateur de votre organisation." + }, + "keyConnectorDomain": { + "message": "Domaine Key Connector" }, "leaveOrganization": { "message": "Quitter l'organisation" diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index eae36865876..707f9752ab6 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 286276f96e5..6dd2d9243b1 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "קוד אימות שגוי" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ משתמש/ת ב־SSO עם שרת מפתחות באירוח עצמי. סיסמה ראשית כבר לא נדרשת כדי להיכנס עבור חברים של ארגון זה.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "עזוב ארגון" diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index dd2073a924f..22a31e5df70 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 4f1c4c981b2..3c37dc9cedf 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Nevažeći kôd za provjeru" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ koristi jedinstvenu prijavu SSO s vlastitim poslužiteljem. Članovima organizacije glavna lozinka više nije potrebna.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Napusti organizaciju" diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 12fd95bbbaa..33ea6c3d9cd 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Érvénytelen ellenőrző kód" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ jelenleg saját tárolású aláíráskulcsú SSO szervert használ. A mesterjelszó a továbbiakban nem szükséges a szervezeti tagsági bejelentkezéshez.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A következő szervezet tagjai számára már nincs szükség mesterjelszóra. Erősítsük meg az alábbi tartományt a szervezet adminisztrátorával." + }, + "keyConnectorDomain": { + "message": "Key Connector tartomány" }, "leaveOrganization": { "message": "Szervezet elhagyása" diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 3706fb73f63..379a6424333 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Kode verifikasi tidak valid" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Tinggalkan Organisasi" diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index a30c1f81f93..8019ac7a750 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Codice di verifica non valido" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ sta usando SSO con un server self-hosted. Una password principale non è più necessaria per accedere per i membri di questa organizzazione.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Lascia organizzazione" diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 1bb8bf364b0..2de545cd27a 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "認証コードが間違っています" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ は自己ホストの鍵サーバで SSO を使用しています。この組織のメンバーのログインにマスターパスワードは必要ありません。", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "組織から脱退する" diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 72e9066806c..863f8d11dea 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 38740c78cba..27a160a9a3c 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 2758b022b64..98c317d377f 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index ee089584adf..2340ae849fa 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "유효하지 않은 확인 코드" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "조직 나가기" diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 61ff8651c72..27818e4e3b9 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Nederīgs apliecinājuma kods" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ izmanto vienoto pieteikšanos ar pašmitinātu atslēgu serveri. Tās dalībniekiem vairs nav nepieciešama galvenā parole, lai pieteiktos.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Galvenā parole vairs nav nepieciešama turpmāk minētās apvienības dalībniekiem. Lūgums saskaņot zemāk esošo domēnu ar savas apvienības pārvaldītāju." + }, + "keyConnectorDomain": { + "message": "Key Connector domēns" }, "leaveOrganization": { "message": "Pamest apvienību" diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index dd4ca0b94cf..54a837b83e1 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 38740c78cba..27a160a9a3c 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 38740c78cba..27a160a9a3c 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 93df01f3440..b2068d1d939 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Ugyldig bekreftelseskode" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ bruker SSO med en selvdrevet nøkkelserver. Et hovedpassord er ikke lenger nødvendig for å logge inn for medlemmer av denne organisasjonen.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Forlat organisasjonen" diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 215ebaf1849..c8c0c90dcde 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 9d192639377..d92b9707fc9 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Ongeldige verificatiecode" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ gebruikt SSO met een zelf gehoste sleutelserver. Leden van deze organisatie kunnen inloggen zonder hoofdwachtwoord.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Voor leden van de volgende organisatie is een hoofdwachtwoord niet langer nodig. Bevestig het domein hieronder met de beheerder van je organisatie." + }, + "keyConnectorDomain": { + "message": "Key Connector-domein" }, "leaveOrganization": { "message": "Organisatie verlaten" diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 3c03b2e7cb2..619c1776cb6 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 38740c78cba..27a160a9a3c 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index ab471e892ef..de9caf9bbce 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Kod weryfikacyjny jest nieprawidłowy" }, - "convertOrganizationEncryptionDesc": { - "message": "Organizacja $ORGANIZATION$ używa jednokrotnego logowania SSO z własnym serwerem kluczy. Użytkownicy nie muszą logować się za pomocą hasła głównego.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Hasło główne nie jest już wymagane dla członków następującej organizacji. Proszę potwierdzić poniższą domenę u administratora organizacji." + }, + "keyConnectorDomain": { + "message": "Domena Key Connector'a" }, "leaveOrganization": { "message": "Opuść organizację" diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index b3f6dd37a8c..04403ade2d9 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ está usando SSO com um servidor de chaves auto-hospedado. Não é mais necessária uma senha mestra para os membros desta organização entrarem.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Sair da Organização" diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 5f1e02c82e3..dc66923f629 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, - "convertOrganizationEncryptionDesc": { - "message": "A $ORGANIZATION$ está a utilizar o SSO com um servidor de chaves auto-hospedado. Já não é necessária uma palavra-passe mestra para iniciar sessão para os membros desta organização.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Já não é necessária uma palavra-passe mestra para os membros da seguinte organização. Por favor, confirme o domínio abaixo com o administrador da sua organização." + }, + "keyConnectorDomain": { + "message": "Domínio do Key Connector" }, "leaveOrganization": { "message": "Sair da organização" diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 64eb05dad22..632733642b1 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Cod de verificare nevalid" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ folosește SSO cu un server de chei auto-găzduit. Membrii acestei organizații nu mai au nevoie de o parolă principală pentru autentificare.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Părăsire organizație" diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index aaccc3b2145..c30371f7d86 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -141,13 +141,13 @@ "message": "Эти пользователи входят в приложения со слабыми, скомпрометированными или повторно используемыми паролями." }, "atRiskMembersDescriptionNone": { - "message": "These are no members logging into applications with weak, exposed, or reused passwords." + "message": "Нет пользователей, входящих в приложения со слабыми, скомпрометированными или повторно используемыми паролями." }, "atRiskApplicationsDescription": { "message": "Эти приложения имеют слабые, скомпрометированные или повторно используемые пароли." }, "atRiskApplicationsDescriptionNone": { - "message": "These are no applications with weak, exposed, or reused passwords." + "message": "Нет приложений со слабыми, скомпрометированными или повторно используемыми паролями." }, "atRiskMembersDescriptionWithApp": { "message": "Эти пользователи входят в $APPNAME$ со слабыми, скомпрометированными или повторно используемыми паролями.", @@ -159,7 +159,7 @@ } }, "atRiskMembersDescriptionWithAppNone": { - "message": "There are no at risk members for $APPNAME$.", + "message": "Нет пользователей, подверженных риску для $APPNAME$.", "placeholders": { "appname": { "content": "$1", @@ -235,7 +235,7 @@ "message": "Данные для авторизации" }, "personalDetails": { - "message": "Личные данные" + "message": "Личная информация" }, "identification": { "message": "Идентификация" @@ -333,7 +333,7 @@ "message": "Код безопасности / CVV" }, "identityName": { - "message": "Название личности" + "message": "Название личной информации" }, "company": { "message": "Компания" @@ -610,7 +610,7 @@ "description": "Search Card type" }, "searchIdentity": { - "message": "Поиск личностей", + "message": "Поиск личной информации", "description": "Search Identity type" }, "searchSecureNote": { @@ -2096,7 +2096,7 @@ "message": "Правила домена" }, "domainRulesDesc": { - "message": "Если у вас есть тот же логин на нескольких разных доменах сайта, вы можете отметить сайт как \"эквивалентный\". \"Глобальные\" - это домены, созданные для вас Bitwarden." + "message": "Если у вас есть такой же логин на нескольких разных доменах сайта, вы можете отметить сайт как \"эквивалентный\". \"Глобальные\" домены это домены, созданные Bitwarden для вас." }, "globalEqDomains": { "message": "Глобальные эквивалентные домены" @@ -4549,7 +4549,7 @@ "message": "После обновления ключа шифрования необходимо выйти из всех приложений Bitwarden, которые вы используете (например, из мобильного приложения или расширения браузера). Если этого не сделать, могут повредиться данные (так как при выходе и последующем входе загружается ваш новый ключ шифрования). Мы попытаемся автоматически осуществить завершение ваших сессий, однако это может произойти с задержкой." }, "updateEncryptionKeyAccountExportWarning": { - "message": "Any account restricted exports you have saved will become invalid." + "message": "Все сохраненные экспорты, затронутые ограничениями аккаунта, будут аннулированы." }, "subscription": { "message": "Подписка" @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Неверный код подтверждения" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ использует SSO с собственным сервером ключей. Для авторизации пользователям этой организации больше не требуется мастер-пароль.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Мастер-пароль больше не требуется для членов следующей организации. Пожалуйста, подтвердите указанный ниже домен у администратора вашей организации." + }, + "keyConnectorDomain": { + "message": "Домен соединителя ключей" }, "leaveOrganization": { "message": "Покинуть организацию" @@ -10584,7 +10581,7 @@ "message": "Упрощение создания аккаунтов" }, "newIdentityNudgeBody": { - "message": "С помощью личностей можно быстро заполнять длинные регистрационные или контактные формы." + "message": "С помощью личной информации можно быстро заполнять длинные регистрационные или контактные формы." }, "newNoteNudgeTitle": { "message": "Храните ваши конфиденциальные данные в безопасности" diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 2d270a54731..f75fd53c209 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 6bbf765ba78..e785bd3749b 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -159,7 +159,7 @@ } }, "atRiskMembersDescriptionWithAppNone": { - "message": "There are no at risk members for $APPNAME$.", + "message": "Pre $APPNAME$ neexistujú žiadni ohrození členovia.", "placeholders": { "appname": { "content": "$1", @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Neplatný verifikačný kód" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ používa SSO s vlastným kľúčovým serverom. Na prihlásenie členov tejto organizácie už nie je potrebné hlavné heslo.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Opustiť organizáciu" @@ -10612,6 +10609,6 @@ "message": "Platba prostredníctvom bankového účtu je dostupná len pre zákazníkov v Spojených Štátoch. Budete musieť overiť svoj bankový účet. V priebehu nasledujúcich 1-2 pracovných dní vykonáme mikro vklad. Na overenie bankového účtu zadajte kód popisu výpisu z tohto vkladu na fakturačnej stránke poskytovateľa. Neoverenie bankového účtu bude mať za následok neuskutočnenie platby a pozastavenie vášho predplatného." }, "clickPayWithPayPal": { - "message": "Please click the Pay with PayPal button to add your payment method." + "message": "Pre pridanie platobnej metódy kliknite prosím na Zaplatiť cez PayPal." } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 06c5339ad98..5a43561bf5a 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 2ca7e72e227..e54b6d148ca 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -99,7 +99,7 @@ "message": "Означите апликацију као критичну" }, "applicationsMarkedAsCriticalSuccess": { - "message": "Applications marked as critical" + "message": "Апликације означене као критичне" }, "application": { "message": "Апликација" @@ -141,13 +141,13 @@ "message": "Ови чланови се пријављују у апликације са слабим, откривеним или поново коришћеним лозинкама." }, "atRiskMembersDescriptionNone": { - "message": "These are no members logging into applications with weak, exposed, or reused passwords." + "message": "Нема чланова пријављена у апликације са слабим, откривеним или поново коришћеним лозинкама." }, "atRiskApplicationsDescription": { "message": "Ове апликације имају слабу, проваљену или често коришћену лозинку." }, "atRiskApplicationsDescriptionNone": { - "message": "These are no applications with weak, exposed, or reused passwords." + "message": "Ове апликације немају слабу, проваљену или често коришћену лозинку." }, "atRiskMembersDescriptionWithApp": { "message": "Ови чланови се пријављују у $APPNAME$ са слабим, откривеним или поново коришћеним лозинкама.", @@ -4549,7 +4549,7 @@ "message": "Након ажурирања кључа за шифровање, мораћете да се одјавите и вратите у све Bitwarden апликације које тренутно користите (као што су мобилна апликација или додаци прегледача). Ако се не одјавите и поново пријавите (чиме се преузима ваш нови кључ за шифровање), може доћи до оштећења података. Покушаћемо аутоматски да се одјавимо, али може доћи до одлагања." }, "updateEncryptionKeyAccountExportWarning": { - "message": "Any account restricted exports you have saved will become invalid." + "message": "Сваки рачун са ограничен извоз који сте сачували постаће неважећи." }, "subscription": { "message": "Претплата" @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Неисправан верификациони код" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Главна лозинка више није потребна за чланове следеће организације. Молимо потврдите домен са администратором организације." + }, + "keyConnectorDomain": { + "message": "Домен конектора кључа" }, "leaveOrganization": { "message": "Напусти организацију" @@ -9429,19 +9426,19 @@ "message": "Управљајте наплатом из Provider Portal" }, "continueSettingUp": { - "message": "Continue setting up Bitwarden" + "message": "Наставити са подешавањем Bitwarden-а" }, "continueSettingUpFreeTrial": { "message": "Наставите са подешавањем бесплатне пробне верзије Bitwarden-а" }, "continueSettingUpPasswordManager": { - "message": "Continue setting up Bitwarden Password Manager" + "message": "Наставите са подешавањем Bitwarden менаџер лозинки" }, "continueSettingUpFreeTrialPasswordManager": { "message": "Наставите са подешавањем бесплатне пробне верзије Bitwarden менаџер лозинки" }, "continueSettingUpSecretsManager": { - "message": "Continue setting up Bitwarden Secrets Manager" + "message": "Наставите са подешавањем Bitwarden Secrets Manager" }, "continueSettingUpFreeTrialSecretsManager": { "message": "Наставите са подешавањем бесплатне пробне верзије Bitwarden Secrets Manager" @@ -10560,17 +10557,17 @@ "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." }, @@ -10596,12 +10593,12 @@ "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" }, @@ -10612,6 +10609,6 @@ "message": "Плаћање са банковним рачуном доступно је само купцима у Сједињеним Државама. Од вас ће бити потребно да проверите свој банковни рачун. Направит ћемо микро депозит у наредних 1-2 радна дана. Унесите кôд за дескриптор изјаве са овог депозита на претплатничкој страници провајдера да бисте потврдили банковни рачун. Неуспех у верификацији банковног рачуна резултираће пропуштеним плаћањем и претплатом је суспендовано." }, "clickPayWithPayPal": { - "message": "Please click the Pay with PayPal button to add your payment method." + "message": "Кликните на Pay with PayPal да бисте додали начин лпаћања." } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 6b10fd30018..95b9348aa04 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 1ca059f95ec..3bbfc4577ea 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Ogiltig verifieringskod" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Lämna organisation" diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 38740c78cba..27a160a9a3c 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 393442e42e5..7651ae5bc06 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index c59f33bba45..7e6f3e6ab4e 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Geçersiz doğrulama kodu" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ kendi barındırdığı bir anahtar sunucusuyla SSO kullanıyor. Bu kuruluşun üyelerinin artık ana parola kullanması gerekmiyor.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector alan adı" }, "leaveOrganization": { "message": "Kuruluştan ayrıl" diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 288e8e840a0..a899bde3d04 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Недійсний код підтвердження" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ використовує SSO з власним сервером ключів. Головний пароль для учасників цієї організації більше не вимагається.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Покинути організацію" diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index cee1bf8ebbd..78a536991a7 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 9805eaac5fd..eb186f11360 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -1870,7 +1870,7 @@ "message": "取消会话授权" }, "deauthorizeSessionsDesc": { - "message": "您是否担心自己的账户在其他设备上登录过?请按照以下步骤取消对之前使用过的所有计算机或设备的授权。如果您以前使用过公共计算机或不小心曾将密码保存在不属于您的设备上,则建议执行此安全步骤。此步骤还将清除所有以前记住的两步登录会话。" + "message": "您是否担心自己的账户在其他设备上登录过?继续下面的操作以取消对之前使用过的所有计算机或设备的授权。如果您以前使用过公共计算机或不小心曾将密码保存在不属于您的设备上,则建议执行此安全步骤。此步骤还将清除所有以前记住的两步登录会话。" }, "deauthorizeSessionsWarning": { "message": "继续操作还将使您退出当前会话,并要求您重新登录。如果有设置两步登录,也需要重新验证。其他设备上的活动会话可能会继续保持活动状态长达一小时。" @@ -1885,10 +1885,10 @@ "message": "启用新设备登录保护" }, "turnOffNewDeviceLoginProtectionModalDesc": { - "message": "继续下面的操作以停用 Bitwarden 在您从新设备登录时发送验证电子邮件的功能。" + "message": "继续下面的操作以停用从新设备登录 Bitwarden 时发送验证电子邮件的功能。" }, "turnOnNewDeviceLoginProtectionModalDesc": { - "message": "继续下面的操作以启用 Bitwarden 在您从新设备登录时发送验证电子邮件的功能。" + "message": "继续下面的操作以启用从新设备登录 Bitwarden 时发送验证电子邮件的功能。" }, "turnOffNewDeviceLoginProtectionWarning": { "message": "停用新设备登录保护后,任何拥有您的主密码的人都可以从任何设备访问您的账户。要在没有验证电子邮件的情况下保护您的账户,请设置两步登录。" @@ -1918,10 +1918,10 @@ "message": "密码库被提供商访问。" }, "purgeVaultDesc": { - "message": "接下来的操作会删除密码库中的所有项目和文件夹。属于组织的共享项目将不会被删除。" + "message": "继续下面的操作以删除密码库中的所有项目和文件夹。属于组织的共享项目将不会被删除。" }, "purgeOrgVaultDesc": { - "message": "接下来的操作会删除组织密码库中的所有项目。" + "message": "继续下面的操作以删除组织密码库中的所有项目。" }, "purgeVaultWarning": { "message": "清空密码库是永久性操作,无法撤销!" @@ -1933,7 +1933,7 @@ "message": "删除账户" }, "deleteAccountDesc": { - "message": "接下来的操作会删除您的账户和所有密码库数据。" + "message": "继续下面的操作以删除您的账户和所有密码库数据。" }, "deleteAccountWarning": { "message": "删除账户是永久性操作,无法撤销!" @@ -4549,7 +4549,7 @@ "message": "更新加密密钥后,您需要注销所有当前使用的 Bitwarden 应用程序(例如移动 App 或浏览器扩展)然后重新登录。注销或者重新登录(这将下载新的加密密钥)失败可能会导致数据损坏。我们会尝试自动为您注销,但可能会有所延迟。" }, "updateEncryptionKeyAccountExportWarning": { - "message": "您已保存的所有账户限制的导出文件将失效。" + "message": "所有您已保存的账户限制的导出文件将失效。" }, "subscription": { "message": "订阅" @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "无效的验证码" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ 使用自托管密钥服务器 SSO。这个组织的成员登录时将不再需要主密码。", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "以下组织的成员不再需要主密码。请与您的组织管理员确认下面的域名。" + }, + "keyConnectorDomain": { + "message": "Key Connector 域名" }, "leaveOrganization": { "message": "退出组织" diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 57dd4c8631f..2f5567308d2 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -6476,14 +6476,11 @@ "invalidVerificationCode": { "message": "無效的驗證碼" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ 使用自我裝載金鑰伺服器 SSO。此組織的成員登入時將不再需要主密碼。", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "離開組織" From eb3e14b02211210daf1c77c7d26b52ff7dfc7527 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 12:29:05 +0200 Subject: [PATCH 132/163] Autosync the updated translations (#14891) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 25 +- apps/desktop/src/locales/ar/messages.json | 25 +- apps/desktop/src/locales/az/messages.json | 25 +- apps/desktop/src/locales/be/messages.json | 25 +- apps/desktop/src/locales/bg/messages.json | 25 +- apps/desktop/src/locales/bn/messages.json | 25 +- apps/desktop/src/locales/bs/messages.json | 25 +- apps/desktop/src/locales/ca/messages.json | 25 +- apps/desktop/src/locales/cs/messages.json | 25 +- apps/desktop/src/locales/cy/messages.json | 25 +- apps/desktop/src/locales/da/messages.json | 25 +- apps/desktop/src/locales/de/messages.json | 25 +- apps/desktop/src/locales/el/messages.json | 25 +- apps/desktop/src/locales/en_GB/messages.json | 25 +- apps/desktop/src/locales/en_IN/messages.json | 25 +- apps/desktop/src/locales/eo/messages.json | 25 +- apps/desktop/src/locales/es/messages.json | 87 ++- apps/desktop/src/locales/et/messages.json | 25 +- apps/desktop/src/locales/eu/messages.json | 25 +- apps/desktop/src/locales/fa/messages.json | 781 ++++++++++--------- apps/desktop/src/locales/fi/messages.json | 69 +- apps/desktop/src/locales/fil/messages.json | 25 +- apps/desktop/src/locales/fr/messages.json | 25 +- apps/desktop/src/locales/gl/messages.json | 25 +- apps/desktop/src/locales/he/messages.json | 25 +- apps/desktop/src/locales/hi/messages.json | 25 +- apps/desktop/src/locales/hr/messages.json | 25 +- apps/desktop/src/locales/hu/messages.json | 25 +- apps/desktop/src/locales/id/messages.json | 25 +- apps/desktop/src/locales/it/messages.json | 25 +- apps/desktop/src/locales/ja/messages.json | 25 +- apps/desktop/src/locales/ka/messages.json | 25 +- apps/desktop/src/locales/km/messages.json | 25 +- apps/desktop/src/locales/kn/messages.json | 25 +- apps/desktop/src/locales/ko/messages.json | 25 +- apps/desktop/src/locales/lt/messages.json | 25 +- apps/desktop/src/locales/lv/messages.json | 25 +- apps/desktop/src/locales/me/messages.json | 25 +- apps/desktop/src/locales/ml/messages.json | 25 +- apps/desktop/src/locales/mr/messages.json | 25 +- apps/desktop/src/locales/my/messages.json | 25 +- apps/desktop/src/locales/nb/messages.json | 25 +- apps/desktop/src/locales/ne/messages.json | 25 +- apps/desktop/src/locales/nl/messages.json | 25 +- apps/desktop/src/locales/nn/messages.json | 25 +- apps/desktop/src/locales/or/messages.json | 25 +- apps/desktop/src/locales/pl/messages.json | 25 +- apps/desktop/src/locales/pt_BR/messages.json | 25 +- apps/desktop/src/locales/pt_PT/messages.json | 25 +- apps/desktop/src/locales/ro/messages.json | 25 +- apps/desktop/src/locales/ru/messages.json | 29 +- apps/desktop/src/locales/si/messages.json | 25 +- apps/desktop/src/locales/sk/messages.json | 25 +- apps/desktop/src/locales/sl/messages.json | 25 +- apps/desktop/src/locales/sr/messages.json | 71 +- apps/desktop/src/locales/sv/messages.json | 25 +- apps/desktop/src/locales/te/messages.json | 25 +- apps/desktop/src/locales/th/messages.json | 25 +- apps/desktop/src/locales/tr/messages.json | 25 +- apps/desktop/src/locales/uk/messages.json | 25 +- apps/desktop/src/locales/vi/messages.json | 167 ++-- apps/desktop/src/locales/zh_CN/messages.json | 29 +- apps/desktop/src/locales/zh_TW/messages.json | 25 +- 63 files changed, 1600 insertions(+), 1033 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 479f8f47c1f..28cff8cae13 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Hoofwagwoord is verwyder" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ gebruik SSO met ’n sleutelbediener op ’n eie gasheer. ’n Hoofwagwoord word nie meer vereis vir aantekening vir lede van hierdie organisasie nie.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Verlaat organisasie" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 510c1aa918a..d9dfb7cdff5 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "تمت إزالة كلمة المرور الرئيسية." }, - "convertOrganizationEncryptionDesc": { - "message": "يستخدم $ORGANIZATION$ SSO مع خادم مفتاح الاستضافة الذاتية. لم تعد هناك حاجة إلى كلمة مرور رئيسية لتسجيل الدخول لأعضاء هذه المؤسسة.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "مغادرة المؤسسة" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 459458bdb2a..5c1f49cbfff 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Ana parol silindi." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$, self-hosted açar serveri ilə SSO istifadə edir. Bu təşkilatın üzvlərinin giriş etməsi üçün artıq ana parol tələb edilməyəcək.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Aşağıdakı təşkilatların üzvləri üçün artıq ana parol tələb olunmur. Lütfən aşağıdakı domeni təşkilatınızın inzibatçısı ilə təsdiqləyin." + }, + "organizationName": { + "message": "Təşkilat adı" + }, + "keyConnectorDomain": { + "message": "Key Connector domeni" }, "leaveOrganization": { "message": "Təşkilatı tərk et" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Riskli parolları dəyişdir" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Daşı" }, diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 4e2441ac168..9284f683e58 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Асноўны пароль выдалены." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ выкарыстоўвае SSO з уласным серверам ключоў. Асноўны пароль для ўдзельнікаў гэтай арганізацыі больш не патрабуецца.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Выйсці з арганізацыі" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 6300ed3391f..18a1d8cf2f0 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Главната парола е премахната." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ използва еднократно удостоверяване със собствен сървър за ключове. Членовете на тази организация вече нямат нужда от главна парола за вписване.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "За членовете на следната организация вече не се изисква главна парола. Потвърдете домейна по-долу с администратора на организацията си." + }, + "organizationName": { + "message": "Име на организацията" + }, + "keyConnectorDomain": { + "message": "Домейн на конектора за ключове" }, "leaveOrganization": { "message": "Напускане на организацията" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Промяна на парола в риск" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Преместване" }, diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 3ca32f51395..b3a2b6e21ee 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 4e77b51467b..89015368c11 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 2f13de7c33b..5550e2d0c62 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "S'ha suprimit la contrasenya mestra." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ està utilitzant SSO amb un servidor autoallotjat de claus. Ja no es requereix una contrasenya mestra d'inici de sessió per als membres d'aquesta organització.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Abandona l'organització" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 49cfc8823b7..b6c3edd783a 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Hlavní heslo bylo odebráno" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ používá SSO s vlastním serverem s klíči. Hlavní heslo pro členy této organizace již není vyžadováno.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Hlavní heslo již není vyžadováno pro členy následující organizace. Potvrďte níže uvedenou doménu u správce Vaší organizace." + }, + "organizationName": { + "message": "Název organizace" + }, + "keyConnectorDomain": { + "message": "Doména Key Connectoru" }, "leaveOrganization": { "message": "Opustit organizaci" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Změnit ohrožené heslo" }, + "cannotRemoveViewOnlyCollections": { + "message": "Nemůžete odebrat kolekce s oprávněními jen pro zobrazení: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Přesunout" }, diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 0031d2de121..28ed661423d 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index c92f0caf2d1..f547eea69a0 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Hovedadgangskode fjernet." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ bruger SSO med en selv-hostet nøgleserver. Organisationsmedlemmer behøver ikke længere hovedadgangskode for at logge ind.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Forlad organisation" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index da1272ef300..de39eba6f99 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master-Passwort entfernt" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ verwendet SSO mit einem selbst gehosteten Schlüsselserver. Ein Master-Passwort ist nicht mehr erforderlich, damit sich Mitglieder dieser Organisation anmelden können.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Für Mitglieder der folgenden Organisation ist kein Master-Passwort mehr erforderlich. Bitte bestätige die folgende Domain bei deinem Organisations-Administrator." + }, + "organizationName": { + "message": "Name der Organisation" + }, + "keyConnectorDomain": { + "message": "Key Connector-Domain" }, "leaveOrganization": { "message": "Organisation verlassen" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Gefährdetes Passwort ändern" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Verschieben" }, diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 1f4462ce4b5..3133488f805 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Ο κύριος κωδικός αφαιρέθηκε" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ χρησιμοποιεί SSO με έναν αυτοεξυπηρετητή κλειδιών. Ένας κύριος κωδικός πρόσβασης δεν απαιτείται πλέον για να συνδεθείτε για τα μέλη αυτού του οργανισμού.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Αποχώρηση από τον οργανισμό" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Μετακίνηση" }, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 77e4222ac1f..cf4d0986e1f 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organisation.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organisation. Please confirm the domain below with your organisation administrator." + }, + "organizationName": { + "message": "Organisation name" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organisation" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 109ace48efa..d82cb566a98 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A master password is no longer required for members of the following organisation. Please confirm the domain below with your organisation administrator." + }, + "organizationName": { + "message": "Organisation name" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 51c350831a2..8e8cc09341f 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index 3023fdd0359..154bb327689 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -232,7 +232,7 @@ "message": "Habilitar agente SSH" }, "enableSshAgentDesc": { - "message": "Enable the SSH agent to sign SSH requests right from your Bitwarden vault." + "message": "Activa el agente SSH para firmar peticiones SSH directamente desde tu caja fuerte de Bitwarden." }, "enableSshAgentHelp": { "message": "The SSH agent is a service targeted at developers that allows you to sign SSH requests directly from your Bitwarden vault." @@ -434,28 +434,28 @@ "message": "Website added" }, "addWebsite": { - "message": "Add website" + "message": "Añadir página web" }, "deleteWebsite": { - "message": "Delete website" + "message": "Eliminar página web" }, "owner": { - "message": "Owner" + "message": "Propietario" }, "addField": { "message": "Añadir campo" }, "editField": { - "message": "Edit field" + "message": "Editar campo" }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "¿Estás seguro de que quieres eliminar permanentemente este adjunto?" }, "fieldType": { - "message": "Field type" + "message": "Tipo de campo" }, "fieldLabel": { - "message": "Field label" + "message": "Etiqueta del campo" }, "add": { "message": "Añadir" @@ -931,11 +931,11 @@ "message": "No volver a preguntar en este dispositivo durante 30 días" }, "selectAnotherMethod": { - "message": "Select another method", + "message": "Selecciona otro método", "description": "Select another two-step login method" }, "useYourRecoveryCode": { - "message": "Use your recovery code" + "message": "Usar tu código de recuperación" }, "insertU2f": { "message": "Inserta tu llave de seguridad en el puerto USB de tu equipo. Si tiene un botón, púlsalo." @@ -1028,7 +1028,7 @@ "message": "The authentication session timed out. Please restart the login process." }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "URL del servidor autoalojado", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -2001,10 +2001,10 @@ "message": "Free organizations cannot use attachments" }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "1 campo necesita tu atención." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "$COUNT$ campos necesitan tu atención.", "placeholders": { "count": { "content": "$1", @@ -2013,10 +2013,10 @@ } }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Tarjeta expirada" }, "cardExpiredMessage": { - "message": "If you've renewed it, update the card's information" + "message": "Si la has renovado, actualiza la información de la tarjeta" }, "verificationRequired": { "message": "Verificación requerida", @@ -2167,10 +2167,10 @@ "message": "La biometría del navegador requiere habilitar primero la biometría de escritorio en los ajustes." }, "biometricsManualSetupTitle": { - "message": "Automatic setup not available" + "message": "Configuración automática no disponible" }, "biometricsManualSetupDesc": { - "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + "message": "Debido al método de instalación, el soporte biométrico no pudo activarse automáticamente. ¿Quieres abrir la documentación sobre cómo hacerlo manualmente?" }, "personalOwnershipSubmitError": { "message": "Debido a una política de organización, tiene restringido el guardar elementos a su caja fuerte personal. Cambie la configuración de propietario a organización y elija entre las colecciones disponibles." @@ -2188,13 +2188,13 @@ "message": "Una política organizacional ha bloqueado la importación de elementos a su caja fuerte personal." }, "personalDetails": { - "message": "Personal details" + "message": "Datos personales" }, "identification": { - "message": "Identification" + "message": "Identificación" }, "contactInfo": { - "message": "Contact information" + "message": "Información de contacto" }, "allSends": { "message": "Todos los Send", @@ -2363,7 +2363,7 @@ "message": "Leer clave de seguridad" }, "awaitingSecurityKeyInteraction": { - "message": "Awaiting security key interaction..." + "message": "Esperando interacción de la clave de seguridad..." }, "hideEmail": { "message": "Ocultar mi dirección de correo electrónico a los destinatarios." @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Contraseña maestra eliminada." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ está usando SSO con un servidor de claves autoalojado. Los miembros de esta organización ya no necesitarán una contraseña maestra para iniciar sesión.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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": "Nombre de la organización" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Abandonar organización" @@ -2946,7 +2946,7 @@ "message": "Are you trying to access your account?" }, "accessAttemptBy": { - "message": "Access attempt by $EMAIL$", + "message": "Intento de acceso de $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -3016,7 +3016,7 @@ "message": "Inicio de sesión solicitado" }, "accountAccessRequested": { - "message": "Account access requested" + "message": "Acceso a la cuenta solicitado" }, "creatingAccountOn": { "message": "Creando una cuenta en" @@ -3161,10 +3161,10 @@ "message": "Trust organization" }, "trust": { - "message": "Trust" + "message": "Confiar" }, "doNotTrust": { - "message": "Do not trust" + "message": "No confiar" }, "organizationNotTrusted": { "message": "Organization is not trusted" @@ -3623,7 +3623,7 @@ "message": "Login credentials" }, "additionalOptions": { - "message": "Additional options" + "message": "Opciones adicionales" }, "itemHistory": { "message": "Item history" @@ -3659,10 +3659,10 @@ "message": "authenticate to a server" }, "sshActionSign": { - "message": "sign a message" + "message": "firmar un mensaje" }, "sshActionGitSign": { - "message": "sign a git commit" + "message": "firmar una confirmación git" }, "unknownApplication": { "message": "Una aplicación" @@ -3683,7 +3683,7 @@ "message": "File saved to device. Manage from your device downloads." }, "allowScreenshots": { - "message": "Allow screen capture" + "message": "Permitir captura de pantalla" }, "allowScreenshotsDesc": { "message": "Allow the Bitwarden desktop application to be captured in screenshots and viewed in remote desktop sessions. Disabling this will prevent access on some external displays." @@ -3703,11 +3703,20 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Mover" }, "newFolder": { - "message": "New folder" + "message": "Nueva carpeta" }, "folderName": { "message": "Folder Name" @@ -3724,7 +3733,7 @@ "example": "Include a Website so this login appears as an autofill suggestion." }, "newLoginNudgeBodyBold": { - "message": "Website", + "message": "Página web", "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." }, diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 922ab7e36a6..1cc526ecfba 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Ülemparool on eemaldatud." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ kasutab SSO-d koos enda majutatud võtmeserveriga. Selle organisatsiooni liikmed ei pea sisselogimisel enam ülemparooli kasutama.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Lahku organisatsioonist" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 3bc76d8427c..ff8d91873b7 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Pasahitz nagusia ezabatua." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ ostatatze propioko gako-zerbitzari batekin SSO erabiltzen ari da. Dagoeneko ez da pasahitz nagusirik behar erakunde honetako kideentzat saioa hasteko.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Utzi erakundea" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 4bcbbd3ac33..e0050ea092a 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -27,7 +27,7 @@ "message": "یادداشت امن" }, "typeSshKey": { - "message": "SSH key" + "message": "کلید SSH" }, "folders": { "message": "پوشه‌ها" @@ -64,7 +64,7 @@ } }, "welcomeBack": { - "message": "Welcome back" + "message": "خوش آمدید" }, "moveToOrgDesc": { "message": "سازمانی را انتخاب کنید که می‌خواهید این مورد را به آن منتقل کنید. انتقال به یک سازمان، مالکیت مورد را به آن سازمان منتقل می‌کند. پس از انتقال این مورد، دیگر مالک مستقیم آن نخواهید بود." @@ -181,16 +181,16 @@ "message": "نشانی" }, "sshPrivateKey": { - "message": "Private key" + "message": "کلید خصوصی" }, "sshPublicKey": { - "message": "Public key" + "message": "کلید عمومی" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "اثر انگشت" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "نوع کلید" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -205,55 +205,55 @@ "message": "RSA 4096-Bit" }, "sshKeyGenerated": { - "message": "A new SSH key was generated" + "message": "یک کلید SSH جدید ایجاد شد" }, "sshKeyWrongPassword": { - "message": "The password you entered is incorrect." + "message": "کلمه عبور وارد شده اشتباه است." }, "importSshKey": { - "message": "Import" + "message": "درون ریزی" }, "confirmSshKeyPassword": { - "message": "Confirm password" + "message": "تأیید کلمه عبور" }, "enterSshKeyPasswordDesc": { - "message": "Enter the password for the SSH key." + "message": "کلمه عبور کلید SSH را وارد کنید." }, "enterSshKeyPassword": { - "message": "Enter password" + "message": "کلمه عبور را وارد کنید" }, "sshAgentUnlockRequired": { - "message": "Please unlock your vault to approve the SSH key request." + "message": "لطفاً برای تأیید درخواست کلید SSH، گاوصندوق خود را باز کنید." }, "sshAgentUnlockTimeout": { - "message": "SSH key request timed out." + "message": "درخواست کلید SSH منقضی شد." }, "enableSshAgent": { - "message": "Enable SSH agent" + "message": "عامل SSH را فعال کنید" }, "enableSshAgentDesc": { - "message": "Enable the SSH agent to sign SSH requests right from your Bitwarden vault." + "message": "عامل SSH را فعال کنید تا درخواست‌های SSH را مستقیماً از گاوصندوق Bitwarden خود امضا کنید." }, "enableSshAgentHelp": { - "message": "The SSH agent is a service targeted at developers that allows you to sign SSH requests directly from your Bitwarden vault." + "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": "Always" + "message": "همیشه" }, "sshAgentPromptBehaviorNever": { - "message": "Never" + "message": "هرگز" }, "sshAgentPromptBehaviorRememberUntilLock": { - "message": "Remember until vault is locked" + "message": "تا زمانی که گاوصندوق قفل شود، به خاطر بسپار" }, "premiumRequired": { "message": "در نسخه پرمیوم کار می‌کند" @@ -268,17 +268,17 @@ "message": "خطا" }, "decryptionError": { - "message": "Decryption error" + "message": "خطای رمزگشایی" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden نتوانست مورد(های) گاوصندوق فهرست شده زیر را رمزگشایی کند." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "با بخش پشتیبانی مشتریان تماس بگیرید", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "برای جلوگیری از، از دست دادن داده‌های بیشتر.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "january": { @@ -355,7 +355,7 @@ "message": "تولید کلمه عبور" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "تولید عبارت عبور" }, "type": { "message": "نوع" @@ -412,16 +412,16 @@ "message": "کلید احراز هویت (TOTP)" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "کلید احراز هویت" }, "autofillOptions": { - "message": "Autofill options" + "message": "گزینه‌های پر کردن خودکار" }, "websiteUri": { - "message": "Website (URI)" + "message": "وب‌سایت (نشانی اینترنتی)" }, "websiteUriCount": { - "message": "Website (URI) $COUNT$", + "message": "وب‌سایت (نشانی اینترنتی) $COUNT$", "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", "placeholders": { "count": { @@ -431,49 +431,49 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "وب‌سایت اضافه شد" }, "addWebsite": { - "message": "Add website" + "message": "افزودن وب‌سایت" }, "deleteWebsite": { - "message": "Delete website" + "message": "حذف وبسایت" }, "owner": { - "message": "Owner" + "message": "مالک" }, "addField": { - "message": "Add field" + "message": "افزودن فیلد" }, "editField": { - "message": "Edit field" + "message": "ویرایش فیلد" }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "آیا مطمئن هستید که می‌خواهید این پرونده پیوست را به‌طور دائمی حذف کنید؟" }, "fieldType": { - "message": "Field type" + "message": "نوع فیلد" }, "fieldLabel": { - "message": "Field label" + "message": "برچسب فیلد" }, "add": { - "message": "Add" + "message": "افزودن" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "برای داده‌هایی مانند سوالات امنیتی از فیلدهای متنی استفاده کنید" }, "hiddenHelpText": { - "message": "Use hidden fields for sensitive data like a password" + "message": "برای داده‌های حساس مانند کلمه عبور از فیلدهای مخفی استفاده کنید" }, "checkBoxHelpText": { - "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + "message": "اگر می‌خواهید فیلدهای تیک‌دار فرم را به‌صورت خودکار پر کنید، مانند گزینه به یاد سپردن ایمیل، از کادرهای انتخاب استفاده کنید" }, "linkedHelpText": { - "message": "Use a linked field when you are experiencing autofill issues for a specific website." + "message": "وقتی در پر کردن خودکار برای یک وب‌سایت خاص به مشکل برخوردید، از فیلد مرتبط استفاده کنید." }, "linkedLabelHelpText": { - "message": "Enter the the field's html id, name, aria-label, or placeholder." + "message": "شناسه Html، نام، aria-label یا محل نگهدار فیلد را وارد کنید." }, "folder": { "message": "پوشه" @@ -501,7 +501,7 @@ "description": "This describes a field that is 'linked' (related) to another field." }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "کادر انتخاب" }, "linkedValue": { "message": "مقدار پیوند شده", @@ -535,7 +535,7 @@ "message": "مورد به زباله‌ها فرستاده شد" }, "overwritePasswordConfirmation": { - "message": "آیا از بازنویسی بر روی پسورد فعلی مطمئن هستید؟" + "message": "آیا از بازنویسی بر روی کلمه عبور فعلی مطمئن هستید؟" }, "overwriteUsername": { "message": "بازنویسی نام کاربری" @@ -560,13 +560,13 @@ "message": "کپی کلمه عبور" }, "regenerateSshKey": { - "message": "Regenerate SSH key" + "message": "تولید مجدد کلید SSH" }, "copySshPrivateKey": { - "message": "Copy SSH private key" + "message": "کپی کلید خصوصی SSH" }, "copyPassphrase": { - "message": "Copy passphrase", + "message": "کپی عبارت عبور", "description": "Copy passphrase to clipboard" }, "copyUri": { @@ -579,7 +579,7 @@ "message": "طول" }, "passwordMinLength": { - "message": "حداقل طول رمز عبور" + "message": "حداقل طول کلمه عبور" }, "uppercase": { "message": "حروف بزرگ (A-Z)", @@ -597,11 +597,11 @@ "message": "نویسه‌های ویژه (!@#$%^&*)" }, "include": { - "message": "Include", + "message": "شامل", "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "شامل حروف بزرگ باشد", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -609,7 +609,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "شامل حروف کوچک باشد", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -617,7 +617,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "شامل اعداد", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -625,7 +625,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "افزودن کاراکترهای خاص", "description": "Full description for the password generator special characters checkbox" }, "numWords": { @@ -656,11 +656,11 @@ "description": "deprecated. Use avoidAmbiguous instead." }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "از کاراکترهای مبهم خودداری کن", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { - "message": "Enterprise policy requirements have been applied to your generator options.", + "message": "نیازمندی‌های سیاست سازمانی بر گزینه‌های تولید کننده شما اعمال شده‌اند.", "description": "Indicates that a policy limits the credential generator screen." }, "searchCollection": { @@ -719,37 +719,37 @@ "message": "ایجاد حساب کاربری" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "در Bitwarden تازه وارد هستید؟" }, "setAStrongPassword": { - "message": "تنظیم رمز عبور قوی" + "message": "تنظیم کلمه عبور قوی" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "ایجاد حساب‌تان را با تنظیم کردن رمز عبور تکمیل کنید" + "message": "ایجاد حساب کاربری خود را با تنظیم کلمه عبور تکمیل کنید" }, "logIn": { "message": "ورود" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "وارد Bitwarden شوید" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "کدی را که به ایمیل شما ارسال شده وارد کنید" }, "enterTheCodeFromYourAuthenticatorApp": { - "message": "Enter the code from your authenticator app" + "message": "کد سامانه تأیید کننده را وارد نمایید" }, "pressYourYubiKeyToAuthenticate": { - "message": "Press your YubiKey to authenticate" + "message": "برای احراز هویت، کلید YubiKey خود را فشار دهید" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "با کلید عبور وارد شوید" }, "loginWithDevice": { - "message": "Log in with device" + "message": "ورود با دستگاه" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "استفاده از ورود تک مرحله‌ای" }, "submit": { "message": "ثبت" @@ -770,7 +770,7 @@ "message": "یادآور کلمه عبور اصلی (اختیاری)" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "اگر کلمه عبور خود را فراموش کنید، یادآور کلمه عبور می‌تواند به ایمیل شما ارسال شود. حداکثر $CURRENT$/$MAXIMUM$ کاراکتر.", "placeholders": { "current": { "content": "$1", @@ -783,19 +783,19 @@ } }, "masterPassword": { - "message": "Master password" + "message": "کلمه عبور اصلی" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "کلمه‌های عبور اصلی در صورت فراموشی قابل بازیابی نیستند!" }, "confirmMasterPassword": { - "message": "Confirm master password" + "message": "تأیید کلمه عبور اصلی" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "یادآور کلمه عبور اصلی" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "امتیاز قدرت کلمه عبور $SCORE$", "placeholders": { "score": { "content": "$1", @@ -804,10 +804,10 @@ } }, "joinOrganization": { - "message": "Join organization" + "message": "به سازمان بپیوندید" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "به $ORGANIZATIONNAME$ بپیوندید", "placeholders": { "organizationName": { "content": "$1", @@ -816,22 +816,22 @@ } }, "finishJoiningThisOrganizationBySettingAMasterPassword": { - "message": "Finish joining this organization by setting a master password." + "message": "با تعیین یک کلمه عبور اصلی، عضویت خود در این سازمان را کامل کنید." }, "settings": { "message": "تنظیمات" }, "accountEmail": { - "message": "Account email" + "message": "حساب ایمیل" }, "requestHint": { - "message": "Request hint" + "message": "درخواست راهنمایی" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "درخواست یادآور کلمه عبور" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "نشانی ایمیل حساب کاربری خود را وارد کنید تا راهنمای کلمه عبور برای شما ارسال شود" }, "getMasterPasswordHint": { "message": "دریافت یادآور کلمه عبور اصلی" @@ -871,10 +871,10 @@ "message": "حساب کاربری جدید شما ساخته شد! حالا می‌توانید وارد شوید." }, "newAccountCreated2": { - "message": "Your new account has been created!" + "message": "حساب کاربری جدید شما ایجاد شده است!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "شما با موفقیت وارد شدید!" }, "masterPassSent": { "message": "ما یک ایمیل همراه با یادآور کلمه عبور اصلی برایتان ارسال کردیم." @@ -907,10 +907,10 @@ "message": "کد تأیید مورد نیاز است." }, "webauthnCancelOrTimeout": { - "message": "The authentication was cancelled or took too long. Please try again." + "message": "احراز هویت لغو شد یا بیش از حد طول کشید. لطفاً دوباره تلاش کنید." }, "openInNewTab": { - "message": "Open in new tab" + "message": "گشودن در زبانهٔ جدید" }, "invalidVerificationCode": { "message": "کد تأیید نامعتبر است" @@ -928,14 +928,14 @@ } }, "dontAskAgainOnThisDeviceFor30Days": { - "message": "Don't ask again on this device for 30 days" + "message": "در این دستگاه به مدت ۳۰ روز دوباره نپرس" }, "selectAnotherMethod": { - "message": "Select another method", + "message": "انتخاب روش دیگر", "description": "Select another two-step login method" }, "useYourRecoveryCode": { - "message": "Use your recovery code" + "message": "از کد بازیابی‌تان استفاده کنید" }, "insertU2f": { "message": "کلید امنیتی خود را وارد پورت USB رایانه کنید، اگر دکمه‌ای دارد آن را بفشارید." @@ -950,17 +950,17 @@ "message": "برنامه احراز هویت" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "کدی را وارد کنید که توسط یک برنامه احراز هویت مانند Bitwarden Authenticator تولید شده است.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "کلید امنیت Yubico OTP" }, "yubiKeyDesc": { "message": "از یک YubiKey برای دسترسی به حسابتان استفاده کنید. همراه با دستگاه‌های YubiKey 4 ،4 Nano ،NEO کار می‌کند." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "کدی را وارد کنید که توسط Duo Security تولید شده است.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -968,13 +968,13 @@ "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "verifyYourIdentity": { - "message": "Verify your Identity" + "message": "هویت خود را تأیید کنید" }, "weDontRecognizeThisDevice": { - "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + "message": "ما این دستگاه را نمی‌شناسیم. برای تأیید هویت خود، کدی را که به ایمیلتان ارسال شده وارد کنید." }, "continueLoggingIn": { - "message": "Continue logging in" + "message": "ادامه ورود" }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" @@ -986,7 +986,7 @@ "message": "ایمیل" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "کدی را که به ایمیل شما ارسال شده وارد کنید." }, "loginUnavailable": { "message": "ورود به سیستم در دسترس نیست" @@ -1001,19 +1001,19 @@ "message": "گزینه‌های ورود دو مرحله‌ای" }, "selectTwoStepLoginMethod": { - "message": "Select two-step login method" + "message": "انتخاب ورود دو مرحله‌ای" }, "selfHostedEnvironment": { "message": "محیط خود میزبان" }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "نشانی اینترنتی پایه نصب Bitwarden خود را که به‌صورت داخلی میزبانی شده مشخص کنید.\nمثال: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "برای پیکربندی پیشرفته، می‌توانید نشانی اینترنتی پایه هر سرویس را به‌صورت مستقل مشخص کنید." }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "شما باید یا نشانی اینترنتی پایه سرور را اضافه کنید، یا حداقل یک محیط سفارشی تعریف کنید." }, "customEnvironment": { "message": "محیط سفارشی" @@ -1022,13 +1022,13 @@ "message": "نشانی اینترنتی سرور" }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "پایان زمان احراز هویت" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "نشست احراز هویت منقضی شد. لطفاً فرایند ورود را دوباره شروع کنید." }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "نشانی اینترنتی سرور خود میزبان", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1059,7 +1059,7 @@ "message": "خیر" }, "location": { - "message": "Location" + "message": "موقعیت" }, "overwritePassword": { "message": "بازنویسی کلمه عبور" @@ -1080,16 +1080,16 @@ "message": "نشست ورود شما منقضی شده است." }, "restartRegistration": { - "message": "Restart registration" + "message": "ثبت‌نام را دوباره آغاز کنید" }, "expiredLink": { - "message": "Expired link" + "message": "پیوند منقضی شد" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "لطفاً ثبت نام را مجدداً شروع کنید یا دوباره وارد شوید." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "ممکن است قبلاً حساب کاربری داشته باشید" }, "logOutConfirmation": { "message": "آیا مطمئنید که می‌خواهید خارج شوید؟" @@ -1175,16 +1175,16 @@ "message": "گاوصندوق شما قفل شده است. برای ادامه هویت خود را تأیید کنید." }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "حساب کاربری شما قفل شده است" }, "or": { - "message": "or" + "message": "یا" }, "unlockWithBiometrics": { - "message": "Unlock with biometrics" + "message": "با استفاده از بیومتریک باز کنید" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "باز کردن قفل با کلمه عبور اصلی" }, "unlock": { "message": "باز کردن قفل" @@ -1215,7 +1215,7 @@ "message": "متوقف شدن گاو‌صندوق" }, "vaultTimeout1": { - "message": "Timeout" + "message": "پایان زمان" }, "vaultTimeoutDesc": { "message": "انتخاب کنید که گاو‌صندوق شما چه زمانی عمل توقف زمانی گاوصندوق را انجام دهد." @@ -1422,7 +1422,7 @@ "description": "Copy credit card number" }, "copyEmail": { - "message": "Copy email" + "message": "کپی ایمیل" }, "copySecurityCode": { "message": "کپی کد امنیتی", @@ -1453,7 +1453,7 @@ "message": "گزینه های ورود اضافی دو مرحله ای مانند YubiKey و Duo." }, "premiumSignUpReports": { - "message": "گزارش‌های بهداشت رمز عبور، سلامت حساب و نقض داده‌ها برای ایمن نگهداشتن گاوصندوق شما." + "message": "گزارش‌های بهداشت کلمه عبور، سلامت حساب و نقض داده‌ها برای ایمن نگهداشتن گاوصندوق شما." }, "premiumSignUpTotp": { "message": "تولید کننده کد تأیید (2FA) از نوع TOTP برای ورودهای موجود در گاوصندوقتان." @@ -1468,7 +1468,7 @@ "message": "خرید پرمیوم" }, "premiumPurchaseAlertV2": { - "message": "You can purchase Premium from your account settings on the Bitwarden web app." + "message": "می‌توانید نسخه پرمیوم را از تنظیمات حساب کاربری خود در اپلیکیشن وب Bitwarden خریداری کنید." }, "premiumCurrentMember": { "message": "شما یک عضو پرمیوم هستید!" @@ -1492,13 +1492,13 @@ "message": "تاریخچه کلمه عبور" }, "generatorHistory": { - "message": "Generator history" + "message": "تاریخچه تولید کننده" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "پاک کردن تاریخچه تولید کننده" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "اگر ادامه دهید، تمام ورودی‌ها به‌طور دائمی از تاریخچه تولید کننده حذف خواهند شد. آیا مطمئن هستید که می‌خواهید ادامه دهید؟" }, "clear": { "message": "پاک کردن", @@ -1508,13 +1508,13 @@ "message": "هیچ کلمه عبوری برای فهرست کردن وجود ندارد." }, "clearHistory": { - "message": "Clear history" + "message": "پاک کردن تاریخچه" }, "nothingToShow": { - "message": "Nothing to show" + "message": "چیزی برای نشان دادن موجود نیست" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "شما اخیراً چیزی تولید نکرده‌اید" }, "undo": { "message": "بازگرداندن" @@ -1591,13 +1591,13 @@ } }, "copySuccessful": { - "message": "Copy Successful" + "message": "کپی موفق بود" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "خطای به‌روزرسانی توکن دسترسی" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "هیچ توکن به‌روزرسانی یا کلید API یافت نشد. لطفاً از حساب کاربری خود خارج شده و دوباره وارد شوید." }, "help": { "message": "راهنما" @@ -1687,7 +1687,7 @@ "description": "ex. Date this password was updated" }, "exportFrom": { - "message": "صادرات از" + "message": "برون ریزی از" }, "exportVault": { "message": "برون ریزی گاوصندوق" @@ -1696,31 +1696,31 @@ "message": "فرمت پرونده" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "این پرونده برون ریزی با کلمه عبور محافظت می‌شود و برای رمزگشایی به کلمه عبور پرونده نیاز دارد." }, "filePassword": { - "message": "رمز فایل" + "message": "کلمه عبور پرونده" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "این کلمه عبور برای برون ریزی و درون ریزی این پرونده استفاده می‌شود" }, "accountRestrictedOptionDescription": { - "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + "message": "برای رمزگذاری برون ریزی و محدود کردن درون ریزی فقط به حساب کاربری فعلی Bitwarden، از کلید رمزگذاری حساب خود که از نام کاربری و کلمه عبور اصلی حساب شما مشتق شده است استفاده کنید." }, "passwordProtected": { "message": "محافظت ‌شده با کلمه عبور" }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "یک کلمه عبور برای پرونده به‌منظور رمزگذاری تنظیم کنید تا برون ریزی و درون ریزی آن به هر حساب Bitwarden با استفاده از کلمه عبور رمزگشایی شود." }, "exportTypeHeading": { - "message": "نوع صادرات" + "message": "نوع برون ریزی" }, "accountRestricted": { "message": "حساب کاربری محدود شده است" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "عدم تطابق \"رمز فایل\" و \"تایید رمز فایل\" با یکدیگر." + "message": "عدم تطابق \"کلمه عبور پرونده\" و \"تأیید کلمه عبور پرونده\" با یکدیگر." }, "done": { "message": "انجام شد" @@ -1739,7 +1739,7 @@ "message": "این برون ریزی با استفاده از کلید رمزگذاری حساب شما، اطلاعاتتان را رمزگذاری می کند. اگر زمانی کلید رمزگذاری حساب خود را بچرخانید، باید دوباره خروجی بگیرید، چون قادر به رمزگشایی این پرونده برون ریزی نخواهید بود." }, "encExportAccountWarningDesc": { - "message": "کلیدهای رمزگذاری حساب برای هر حساب کاربری Bitwarden منحصر به فرد است، بنابراین نمی‌توانید برون ریزی رمزگذاری شده را به حساب دیگری وارد کنید." + "message": "کلیدهای رمزگذاری حساب برای هر حساب کاربری Bitwarden منحصر به فرد است، بنابراین نمی‌توانید برون ریزی رمزگذاری شده را به حساب دیگری درون ریزی کنید." }, "noOrganizationsList": { "message": "شما به هیچ سازمانی تعلق ندارید. سازمان‌ها به شما اجازه می‌دهند تا داده‌های خود را با کاربران دیگر به صورت امن به اشتراک بگذارید." @@ -1776,7 +1776,7 @@ "description": "PIN code. Ex. The short code (often numeric) that you use to unlock a device." }, "unlockWithPin": { - "message": "باز کردن با پین" + "message": "باز کردن با کد پین" }, "setYourPinCode": { "message": "کد پین خود را برای باز کردن Bitwarden تنظیم کنید. اگر به طور کامل از برنامه خارج شوید، تنظیمات پین شما از بین می‌رود." @@ -1788,7 +1788,7 @@ "message": "کد پین غیر معتبر است." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "تعداد تلاش‌های ناموفق کد پین زیاد شد. خارج می‌شوید." }, "unlockWithWindowsHello": { "message": "باز کردن با Windows Hello" @@ -1797,7 +1797,7 @@ "message": "تنظیمات اضافی Windows Hello" }, "unlockWithPolkit": { - "message": "Unlock with system authentication" + "message": "باز کردن قفل با احراز هویت سیستم" }, "windowsHelloConsentMessage": { "message": "تأیید برای Bitwarden." @@ -1815,22 +1815,22 @@ "message": "درخواست Windows Hello در هنگام راه اندازی" }, "autoPromptPolkit": { - "message": "Ask for system authentication on launch" + "message": "در زمان اجرا درخواست احراز هویت سیستم را بده" }, "autoPromptTouchId": { "message": "درخواست Touch ID در هنگام راه اندازی" }, "requirePasswordOnStart": { - "message": "هنگام شروع برنامه، رمز عبور یا پین مورد نیاز است" + "message": "هنگام شروع برنامه، کلمه عبور یا کد پین مورد نیاز است" }, "requirePasswordWithoutPinOnStart": { - "message": "Require password on app start" + "message": "هنگام شروع برنامه، کلمه عبور مورد نیاز است" }, "recommendedForSecurity": { "message": "برای امنیت توصیه می‌شود." }, "lockWithMasterPassOnRestart1": { - "message": "Lock with master password on restart" + "message": "در زمان شروع مجدد، با کلمه عبور اصلی قفل کن" }, "deleteAccount": { "message": "حذف حساب" @@ -1842,10 +1842,10 @@ "message": "حذف حساب شما دائمی است. نمی‌توان آن را برگرداند." }, "cannotDeleteAccount": { - "message": "Cannot delete account" + "message": "قادر به حذف حساب کاربری نیستیم" }, "cannotDeleteAccountDesc": { - "message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details." + "message": "این اقدام قابل انجام نیست زیرا حساب کاربری شما متعلق به یک سازمان است. برای جزئیات بیشتر با مدیر سازمان خود تماس بگیرید." }, "accountDeleted": { "message": "حساب حذف شد" @@ -1916,7 +1916,7 @@ "description": "Verb form: to make secure or inaccessible by" }, "trash": { - "message": "زباله‌ها", + "message": "سطل زباله", "description": "Noun: a special folder to hold deleted items" }, "searchTrash": { @@ -1950,15 +1950,15 @@ "message": "تنظیم کلمه عبور اصلی" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "مجوزهای سازمان شما به‌روزرسانی شد، باید یک کلمه عبور اصلی تنظیم کنید.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "سازمانتان از شما می‌خواهد که یک کلمه عبور اصلی تنظیم کنید.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "از میان $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -1967,10 +1967,10 @@ } }, "cardDetails": { - "message": "Card details" + "message": "جزئیات کارت" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$ جزئیات", "placeholders": { "brand": { "content": "$1", @@ -1979,32 +1979,32 @@ } }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "درباره احراز هویت کننده‌ها بیشتر بدانید" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "کپی کلید احراز هویت (TOTP)" }, "totpHelperTitle": { - "message": "Make 2-step verification seamless" + "message": "تأیید دو مرحله‌ای را بدون دردسر کنید" }, "totpHelper": { - "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + "message": "Bitwarden می‌تواند کدهای تأیید دو مرحله‌ای را ذخیره و پر کند. کلید را کپی کرده و در این فیلد قرار دهید." }, "totpHelperWithCapture": { - "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." + "message": "Bitwarden می‌تواند کدهای تأیید دو مرحله‌ای را ذخیره و پر کند. برای اسکن کد QR احراز هویت کننده این وب‌سایت، روی آیکون دوربین کلیک کنید یا کلید را کپی کرده و در این فیلد قرار دهید." }, "premium": { - "message": "Premium", + "message": "پرمیوم", "description": "Premium membership" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "سازمان‌های رایگان نمی‌توانند از پرونده‌های پیوست استفاده کنند" }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "۱ فیلد به توجه شما نیاز دارد." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "فیلدهای $COUNT$ به توجه شما نیاز دارند.", "placeholders": { "count": { "content": "$1", @@ -2013,13 +2013,13 @@ } }, "cardExpiredTitle": { - "message": "Expired card" + "message": "تاریخ کارت منقضی شده است" }, "cardExpiredMessage": { - "message": "If you've renewed it, update the card's information" + "message": "اگر تمدید کرده‌اید، اطلاعات کارت را به‌روزرسانی کنید" }, "verificationRequired": { - "message": "تایید مورد نیاز است", + "message": "تأیید لازم است", "description": "Default title for the user verification dialog." }, "currentMasterPass": { @@ -2074,10 +2074,10 @@ "message": "کلمه عبور اصلی جدید شما از شرایط سیاست پیروی نمی‌کند." }, "receiveMarketingEmailsV2": { - "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." + "message": "پیشنهادها، اعلان‌ها و فرصت‌های تحقیقاتی Bitwarden را در صندوق ورودی خود دریافت کنید." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "لغو اشتراک" }, "atAnyTime": { "message": "در هر زمان." @@ -2098,7 +2098,7 @@ "message": "فعال کردن ادغام مرورگر" }, "enableBrowserIntegrationDesc1": { - "message": "Used to allow biometric unlock in browsers that are not Safari." + "message": "برای فعال‌سازی باز کردن قفل با بیومتریک در مرورگرهایی به‌جز Safari استفاده می‌شود." }, "enableDuckDuckGoBrowserIntegration": { "message": "اجازه ادغام مرورگر DuckDuckGo را بدهید" @@ -2110,10 +2110,10 @@ "message": "ادغام مرورگر پشتیبانی نمی‌شود" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "خطای فعال کردن ادغام مرورگر" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "خطایی هنگام فعال‌سازی یکپارچه سازی مرورگر رخ داده است." }, "browserIntegrationMasOnlyDesc": { "message": "متأسفانه در حال حاضر ادغام مرورگر فقط در نسخه Mac App Store پشتیبانی می‌شود." @@ -2134,7 +2134,7 @@ "message": "استفاده از شتاب سخت افزاری" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "به طور پیش‌فرض این تنظیم روشن است. فقط در صورت مواجهه با مشکلات گرافیکی آن را خاموش کنید. نیاز به راه‌اندازی مجدد دارد." }, "approve": { "message": "تأیید" @@ -2167,16 +2167,16 @@ "message": "بیومتریک مرورگر ابتدا نیاز به فعالسازی بیومتریک دسکتاپ در تنظیمات دارد." }, "biometricsManualSetupTitle": { - "message": "Automatic setup not available" + "message": "راه‌اندازی خودکار در دسترس نیست" }, "biometricsManualSetupDesc": { - "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + "message": "به دلیل روش نصب، پشتیبانی از بیومتریک به‌صورت خودکار فعال نشد. آیا می‌خواهید مستندات نحوه انجام این کار به‌صورت دستی را باز کنید؟" }, "personalOwnershipSubmitError": { "message": "به دلیل سیاست پرمیوم، برای ذخیره موارد در گاوصندوق شخصی خود محدود شده اید. گزینه مالکیت را به یک سازمان تغییر دهید و مجموعه های موجود را انتخاب کنید." }, "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { - "message": "Your new password cannot be the same as your current password." + "message": "کلمه عبور جدید شما نمی‌تواند با کلمه عبور فعلی‌تان یکسان باشد." }, "hintEqualsPassword": { "message": "اشاره به کلمه عبور شما نمی‌تواند همان کلمه عبور شما باشد." @@ -2185,16 +2185,16 @@ "message": "سیاست سازمانی بر تنظیمات مالکیت شما تأثیر می‌گذارد." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "یک سیاست سازمانی، درون ریزی موارد به گاوصندوق فردی شما را مسدود کرده است." }, "personalDetails": { - "message": "Personal details" + "message": "جزئیات شخصی" }, "identification": { - "message": "Identification" + "message": "شناسایی" }, "contactInfo": { - "message": "Contact information" + "message": "اطلاعات تماس" }, "allSends": { "message": "همه ارسال ها", @@ -2360,10 +2360,10 @@ "message": "تأیید اعتبار در WebAuthn" }, "readSecurityKey": { - "message": "Read security key" + "message": "خواندن کلید امنیتی" }, "awaitingSecurityKeyInteraction": { - "message": "Awaiting security key interaction..." + "message": "در انتظار تعامل با کلید امنیتی..." }, "hideEmail": { "message": "نشانی ایمیلم را از گیرندگان مخفی کن." @@ -2375,7 +2375,7 @@ "message": "تأیید ایمیل لازم است" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "ایمیل تأیید شد" }, "emailVerificationRequiredDesc": { "message": "برای استفاده از این ویژگی باید ایمیل خود را تأیید کنید." @@ -2402,22 +2402,22 @@ "message": "کلمه عبور اصلی شما با یک یا چند سیاست سازمان‌تان مطابقت ندارد. برای دسترسی به گاوصندوق، باید همین حالا کلمه عبور اصلی خود را به‌روز کنید. در صورت ادامه، شما از نشست فعلی خود خارج می‌شوید و باید دوباره وارد سیستم شوید. نشست فعال در دستگاه های دیگر ممکن است تا یک ساعت همچنان فعال باقی بمانند." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "سازمان شما رمزگذاری دستگاه‌های مورد اعتماد را غیرفعال کرده است. لطفاً برای دسترسی به گاوصندوق خود یک کلمه عبور اصلی تنظیم کنید." }, "tryAgain": { "message": "دوباره سعی کنید" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "برای این اقدام تأیید لازم است. یک کد پین تعیین کنید تا ادامه دهید." }, "setPin": { - "message": "تنظیم PIN" + "message": "تنظیم کد پین" }, "verifyWithBiometrics": { - "message": "تایید با استفاده از بیومتریک" + "message": "تأیید با استفاده از بیومتریک" }, "awaitingConfirmation": { - "message": "در انتظار تایید" + "message": "در انتظار تأیید" }, "couldNotCompleteBiometrics": { "message": "تکمیل بیومتریک ممکن نشد." @@ -2426,10 +2426,10 @@ "message": "نیازمند روش دیگری هستید؟" }, "useMasterPassword": { - "message": "استفاده از رمز عبور اصلی" + "message": "استفاده از کلمه عبور اصلی" }, "usePin": { - "message": "استفاده از پین" + "message": "استفاده از کد پین" }, "useBiometrics": { "message": "استفاده از بیومتریک" @@ -2447,7 +2447,7 @@ "message": "دقیقه" }, "vaultTimeoutPolicyInEffect1": { - "message": "$HOURS$ hour(s) and $MINUTES$ minute(s) maximum.", + "message": "حداکثر $HOURS$ ساعت و $MINUTES$ دقیقه.", "placeholders": { "hours": { "content": "$1", @@ -2489,7 +2489,7 @@ "message": "مهلت زمانی شما بیش از محدودیت های تعیین شده توسط سازمانتان است." }, "inviteAccepted": { - "message": "Invitation accepted" + "message": "دعوتنامه پذیرفته شد" }, "resetPasswordPolicyAutoEnroll": { "message": "ثبت نام خودکار" @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "کلمه عبور اصلی حذف شد" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ در حال استفاده از SSO با یک سرور کلید خود میزبان است. برای ورود اعضای این سازمان دیگر نیازی به کلمه عبور اصلی نیست.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "برای اعضای سازمان زیر، کلمه عبور اصلی دیگر لازم نیست. لطفاً دامنه زیر را با مدیر سازمان خود تأیید کنید." + }, + "organizationName": { + "message": "نام سازمان" + }, + "keyConnectorDomain": { + "message": "دامنه رابط کلید" }, "leaveOrganization": { "message": "ترک سازمان" @@ -2582,7 +2582,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", @@ -2591,10 +2591,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "در حال برون ریزی گاوصندوق سازمان" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "فقط گاوصدوق سازمان مرتبط با $ORGANIZATION$ برون ریزی خواهد شد. موارد موجود در گاوصندوق‌های فردی یا سایر سازمان‌ها شامل نمی‌شوند.", "placeholders": { "organization": { "content": "$1", @@ -2606,7 +2606,7 @@ "message": "قفل شده" }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "گاوصندوق‌تان قفل شد" }, "unlocked": { "message": "باز شده" @@ -2628,13 +2628,13 @@ "message": "ایجاد نام کاربری" }, "generateEmail": { - "message": "Generate email" + "message": "تولید ایمیل" }, "usernameGenerator": { - "message": "Username generator" + "message": "تولید کننده نام کاربری" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "مقدار باید بین $MIN$ و $MAX$ باشد.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2648,7 +2648,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " برای تولید یک کلمه عبور قوی، از $RECOMMENDED$ کاراکتر یا بیشتر استفاده کنید.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2658,7 +2658,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " برای تولید یک عبارت عبور قوی، از $RECOMMENDED$ واژه یا بیشتر استفاده کنید.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2684,7 +2684,7 @@ "message": "از صندوق ورودی پیکربندی شده دامنه خود استفاده کنید." }, "useThisEmail": { - "message": "Use this email" + "message": "از این ایمیل استفاده شود" }, "random": { "message": "تصادفی" @@ -2714,15 +2714,15 @@ "message": "یک نام مستعار ایمیل با یک سرویس ارسال خارجی ایجاد کنید." }, "forwarderDomainName": { - "message": "Email domain", + "message": "دامنه ایمیل", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Choose a domain that is supported by the selected service", + "message": "دامنه‌ای را انتخاب کنید که توسط سرویس انتخاب شده پشتیبانی می‌شود", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "$SERVICENAME$ خطا: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2736,11 +2736,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "تولید شده توسط Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "وب‌سایت: $WEBSITE$. تولید شده توسط Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2750,7 +2750,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "توکن API نامعتبر برای $SERVICENAME$", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2760,7 +2760,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "توکن API نامعتبر برای $SERVICENAME$: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2774,7 +2774,7 @@ } }, "forwaderInvalidOperation": { - "message": "$SERVICENAME$ refused your request. Please contact your service provider for assistance.", + "message": "$SERVICENAME$ درخواست شما را رد کرد. لطفاً برای دریافت کمک با ارائه‌دهنده سرویس خود تماس بگیرید.", "description": "Displayed when the user is forbidden from using the API by the forwarding service.", "placeholders": { "servicename": { @@ -2784,7 +2784,7 @@ } }, "forwaderInvalidOperationWithMessage": { - "message": "$SERVICENAME$ refused your request: $ERRORMESSAGE$", + "message": "$SERVICENAME$ درخواست شما را رد کرد: $ERRORMESSAGE$", "description": "Displayed when the user is forbidden from using the API by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2798,7 +2798,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "دریافت شناسه حساب ایمیل ماسک شده از $SERVICENAME$ امکان‌پذیر نیست.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2808,7 +2808,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "دامنه $SERVICENAME$ نامعتبر.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2818,7 +2818,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "آدرس $SERVICENAME$ نامعتبر.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2828,7 +2828,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "خطای $SERVICENAME$ نامعلومی رخ داد.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -2838,7 +2838,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "فرواردکننده ناشناخته: $SERVICENAME$.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -2897,25 +2897,25 @@ "message": "ورود به سیستم آغاز شد" }, "logInRequestSent": { - "message": "Request sent" + "message": "درخواست ارسال شد" }, "notificationSentDevice": { "message": "یک اعلان به دستگاه شما ارسال شده است." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "یک اعلان به دستگاه شما ارسال شده است" }, "notificationSentDevicePart1": { - "message": "Unlock Bitwarden on your device or on the " + "message": "قفل Bitwarden را روی دستگاه خود باز کنید یا روی " }, "notificationSentDeviceAnchor": { - "message": "web app" + "message": "برنامه وب" }, "notificationSentDevicePart2": { - "message": "Make sure the Fingerprint phrase matches the one below before approving." + "message": "اطمینان حاصل کنید که عبارت اثر انگشت با عبارت زیر مطابقت دارد قبل از تأیید." }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "به گزینه دیگری نیاز دارید؟" }, "fingerprintMatchInfo": { "message": "لطفاً مطمئن شوید که قفل گاوصندوق شما باز است و عبارت اثر انگشت در دستگاه دیگر مطابقت دارد." @@ -2924,13 +2924,13 @@ "message": "عبارت اثر انگشت" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "به‌محض تأیید درخواست، به شما اطلاع داده خواهد شد" }, "needAnotherOption": { "message": "ورود به سیستم با دستگاه باید در تنظیمات برنامه‌ی Bitwarden تنظیم شود. به گزینه دیگری نیاز دارید؟" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "مشاهده همه گزینه‌های ورود به سیستم" }, "viewAllLoginOptions": { "message": "مشاهده همه گزینه‌های ورود به سیستم" @@ -2943,10 +2943,10 @@ "description": "'Character count' describes a feature that displays a number next to each character of the password." }, "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" + "message": "آیا در تلاش برای دسترسی به حساب کاربری خود هستید؟" }, "accessAttemptBy": { - "message": "Access attempt by $EMAIL$", + "message": "تلاش برای دسترسی به سیستم توسط $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -2964,10 +2964,10 @@ "message": "زمان" }, "confirmAccess": { - "message": "Confirm access" + "message": "تأیید دسترسی" }, "denyAccess": { - "message": "Deny access" + "message": "دسترسی را رد کن" }, "logInConfirmedForEmailOnDevice": { "message": "ورود به سیستم برای $EMAIL$ در $DEVICE$ تأیید شد", @@ -3004,7 +3004,7 @@ "message": "این درخواست دیگر معتبر نیست." }, "confirmAccessAttempt": { - "message": "Confirm access attempt for $EMAIL$", + "message": "تأیید تلاش برای دسترسی به سیستم برای $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -3016,28 +3016,28 @@ "message": "ورود الزامیست" }, "accountAccessRequested": { - "message": "Account access requested" + "message": "درخواست دسترسی به حساب کاربری دریافت شد" }, "creatingAccountOn": { - "message": "Creating account on" + "message": "در حال ساخت حساب روی" }, "checkYourEmail": { - "message": "Check your email" + "message": "ایمیل خود را چک کنید" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "دنبال کنید لینکی را که در ایمیل فرستاده شده به" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "و به ساختن حساب‌تان ادامه دهید." }, "noEmail": { - "message": "No email?" + "message": "ایمیلی دریافت نکردید؟" }, "goBack": { - "message": "Go back" + "message": "بازگشت به عقب" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "برای ویرایش آدرس ایمیل خود." }, "exposedMasterPassword": { "message": "کلمه عبور اصلی افشا شده" @@ -3052,28 +3052,28 @@ "message": "کلمه عبور ضعیف شناسایی و در یک نقض داده پیدا شد. از یک کلمه عبور قوی و منحصر به فرد برای محافظت از حساب خود استفاده کنید. آیا مطمئنید که می‌خواهید از این کلمه عبور استفاده کنید؟" }, "useThisPassword": { - "message": "Use this password" + "message": "از این کلمه عبور استفاده کن" }, "useThisUsername": { - "message": "Use this username" + "message": "از این نام کاربری استفاده کن" }, "checkForBreaches": { "message": "نقض اطلاعات شناخته شده برای این کلمه عبور را بررسی کنید" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "وارد شده!" }, "important": { "message": "مهم:" }, "accessing": { - "message": "Accessing" + "message": "در حال دسترسی" }, "accessTokenUnableToBeDecrypted": { - "message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue." + "message": "شما خارج شده‌اید زیرا توکن دسترسی شما قابل رمزگشایی نبود. لطفاً برای رفع این مشکل دوباره وارد شوید." }, "refreshTokenSecureStorageRetrievalFailure": { - "message": "You have been logged out because your refresh token could not be retrieved. Please log in again to resolve this issue." + "message": "شما خارج شده‌اید زیرا توکن تازه‌سازی شما قابل بازیابی نبود. لطفاً برای رفع این مشکل دوباره وارد شوید." }, "masterPasswordHint": { "message": "کلمه عبور اصلی شما در صورت فراموشی قابل بازیابی نیست!" @@ -3088,22 +3088,22 @@ } }, "windowsBiometricUpdateWarning": { - "message": "Bitwarden توصیه می‌کند که تنظیمات بیومتریک خود را به‌روزرسانی کنید تا در اولین باز کردن قفل، به رمز عبور اصلی (یا پین) نیاز داشته باشید. آیا می‌خواهید تنظیمات خود را اکنون به‌روز کنید؟" + "message": "Bitwarden توصیه می‌کند که تنظیمات بیومتریک خود را به‌روزرسانی کنید تا در اولین باز کردن قفل، به کلمه عبور اصلی (یا کد پین) نیاز داشته باشید. آیا می‌خواهید تنظیمات خود را اکنون به‌روز کنید؟" }, "windowsBiometricUpdateWarningTitle": { "message": "به‌روز رسانی تنظیمات توصیه شده" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "این دستگاه را به خاطر بسپار تا ورودهای بعدی بدون مشکل انجام شود" }, "deviceApprovalRequired": { "message": "تأیید دستگاه لازم است. یک روش تأیید انتخاب کنید:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "تأیید دستگاه لازم است" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "یکی از گزینه‌های تأیید زیر را انتخاب کنید" }, "rememberThisDevice": { "message": "این دستگاه را به خاطر بسپار" @@ -3152,34 +3152,34 @@ "message": "ایمیل کاربر وجود ندارد" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "ایمیل کاربر فعال پیدا نشد. در حال خارج کردن شما از سیستم هستیم." }, "deviceTrusted": { "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" + "message": "برای امنیت حساب کاربری شما، فقط در صورتی تأیید کنید که دسترسی اضطراری به این کاربر داده‌اید و اثر انگشت او با آنچه در حسابش نمایش داده شده، مطابقت دارد" }, "orgTrustWarning": { - "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + "message": "برای امنیت حساب کاربری شما، فقط در صورتی ادامه دهید که عضو این سازمان باشید، بازیابی حساب کاربری را فعال کرده باشید و اثر انگشت نمایش داده شده در زیر با اثر انگشت سازمان مطابقت داشته باشد." }, "orgTrustWarning1": { - "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." + "message": "این سازمان دارای سیاست سازمانی است که شما را در بازیابی حساب کاربری ثبت‌نام می‌کند. ثبت‌نام به مدیران سازمان اجازه می‌دهد کلمه عبور شما را تغییر دهند. فقط در صورتی ادامه دهید که این سازمان را می‌شناسید و عبارت اثر انگشت نمایش داده شده در زیر با اثر انگشت سازمان مطابقت دارد." }, "trustUser": { - "message": "Trust user" + "message": "به کاربر اعتماد کنید" }, "inputRequired": { "message": "ورودی ضروری است." @@ -3282,7 +3282,7 @@ "message": "زیرمنو" }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "تغییر وضعیت ناوبری کناری" }, "skipToContent": { "message": "پرش به محتوا" @@ -3300,14 +3300,14 @@ "message": "دامنه مستعار" }, "importData": { - "message": "وارد کردن اطلاعات", + "message": "درون ریزی داده", "description": "Used for the desktop menu item and the header of the import dialog" }, "importError": { - "message": "خطای وارد کردن" + "message": "خطای درون ریزی" }, "importErrorDesc": { - "message": "مشکلی با داده‌هایی که سعی کردید وارد کنید وجود داشت. لطفاً خطاهای فهرست شده در زیر را در فایل منبع خود برطرف کرده و دوباره امتحان کنید." + "message": "مشکلی با داده‌هایی که سعی کردید درون ریزی کنید وجود داشت. لطفاً خطاهای فهرست شده در زیر را در پرونده منبع خود برطرف کرده و دوباره امتحان کنید." }, "resolveTheErrorsBelowAndTryAgain": { "message": "خطاهای زیر را برطرف کرده و دوباره امتحان کنید." @@ -3316,10 +3316,10 @@ "message": "توضیحات" }, "importSuccess": { - "message": "داده‌ها با موفقیت وارد شد" + "message": "داده‌ها با موفقیت درون ریزی شد" }, "importSuccessNumberOfItems": { - "message": "$AMOUNT$ مورد وارد شده است.", + "message": "$AMOUNT$ مورد درون ریزی شده است.", "placeholders": { "amount": { "content": "$1", @@ -3331,7 +3331,7 @@ "message": "مجموع" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "شما در حال درون ریزی داده به $ORGANIZATION$ هستید. داده‌های شما ممکن است با اعضای این سازمان به اشتراک گذاشته شود. آیا می‌خواهید ادامه دهید؟", "placeholders": { "organization": { "content": "$1", @@ -3340,49 +3340,49 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + "message": "خطا در اتصال به سرویس Duo. از روش ورود دو مرحله‌ای دیگری استفاده کنید یا برای دریافت کمک با Duo تماس بگیرید." }, "duoRequiredByOrgForAccount": { - "message": "Duo two-step login is required for your account." + "message": "ورود دو مرحله ای Duo برای حساب کاربری شما لازم است." }, "duoTwoFactorRequiredPageSubtitle": { - "message": "Duo two-step login is required for your account. Follow the steps below to finish logging in." + "message": "برای حساب کاربری شما ورود دو مرحله‌ای Duo لازم است. مراحل زیر را دنبال کنید تا ورود خود را کامل کنید." }, "followTheStepsBelowToFinishLoggingIn": { - "message": "Follow the steps below to finish logging in." + "message": "مراحل زیر را دنبال کنید تا وارد سیستم شوید." }, "followTheStepsBelowToFinishLoggingInWithSecurityKey": { - "message": "Follow the steps below to finish logging in with your security key." + "message": "مراحل زیر را برای کامل کردن ورود با کلید امنیتی خود دنبال کنید." }, "launchDuo": { - "message": "Launch Duo in Browser" + "message": "اجرای Duo در مرورگر" }, "importFormatError": { - "message": "Data is not formatted correctly. Please check your import file and try again." + "message": "داده‌ها به درستی قالب‌بندی نشده‌اند. لطفاً پرونده درون ریزی شده خود را بررسی و دوباره امتحان کنید." }, "importNothingError": { - "message": "چیزی وارد نشد." + "message": "چیزی درون ریزی نشد." }, "importEncKeyError": { - "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." + "message": "خطا در رمزگشایی پرونده‌ی درون ریزی شده. کلید رمزگذاری شما با کلید رمزگذاری استفاده شده برای درون ریزی داده‌ها مطابقت ندارد." }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "کلمه عبور پرونده نامعتبر است، لطفاً از کلمه عبوری که هنگام ایجاد پرونده‌ی برون ریزی وارد کردید استفاده کنید." }, "destination": { - "message": "Destination" + "message": "مقصد" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "درباره گزینه‌های درون ریزی خود بیاموزید" }, "selectImportFolder": { "message": "یک پوشه انتخاب کنید" }, "selectImportCollection": { - "message": "انتخاب یک مجموعه" + "message": "یک مجموعه انتخاب کنید" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "اگر می‌خواهید محتوای پرونده درون ریزی شده به $DESTINATION$ منتقل شود، این گزینه را انتخاب کنید", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -3392,25 +3392,25 @@ } }, "importUnassignedItemsError": { - "message": "فایل حاوی موارد اختصاص نیافته است." + "message": "پرونده حاوی موارد اختصاص نیافته است." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "فرمت پرونده‌ی درون ریزی را انتخاب کنید" }, "selectImportFile": { - "message": "انتخاب فایل وارد شده" + "message": "پرونده‌ی درون ریزی را انتخاب کنید" }, "chooseFile": { - "message": "انتخاب فایل" + "message": "انتخاب پرونده" }, "noFileChosen": { - "message": "هیچ فایلی انتخاب نشد" + "message": "هیچ پرونده‌ای انتخاب نشد" }, "orCopyPasteFileContents": { - "message": "یا محتویات فایل وارد شده را کپی/پیست کنید" + "message": "یا محتویات پرونده‌ی درون ریزی را کپی/پیست کنید" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "دستورالعمل‌های $NAME$", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -3420,120 +3420,120 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "درون ریزی گاوصندوق را تأیید کنید" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "این پرونده با کلمه عبور محافظت شده است. لطفاً کلمه عبور پرونده را برای درون ریزی داده‌ها وارد کنید." }, "confirmFilePassword": { - "message": "Confirm file password" + "message": "تأیید کلمه عبور پرونده" }, "exportSuccess": { - "message": "Vault data exported" + "message": "داده‌های گاوصندوق برون ریزی شد" }, "multifactorAuthenticationCancelled": { - "message": "تایید هویت چندمرحله‌ای کنسل شد" + "message": "تایید هویت چند مرحله‌ای کنسل شد" }, "noLastPassDataFound": { - "message": "No LastPass data found" + "message": "هیچ داده‌ای از LastPass یافت نشد" }, "incorrectUsernameOrPassword": { - "message": "نام کاربری یا گذرواژه نادرست است" + "message": "نام کاربری یا کلمه عبور اشتباه است" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "کلمه عبور اشتباه است" }, "incorrectCode": { - "message": "Incorrect code" + "message": "کد اشتباه است" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "کد پین نادرست است" }, "multifactorAuthenticationFailed": { - "message": "Multifactor authentication failed" + "message": "احراز هویت چند مرحله‌ای ناموفق بود" }, "includeSharedFolders": { - "message": "Include shared folders" + "message": "شامل پوشه‌های به‌ اشتراک گذاری‌ شده" }, "lastPassEmail": { - "message": "LastPass Email" + "message": "ایمیل LastPass" }, "importingYourAccount": { - "message": "Importing your account..." + "message": "در حال درون ریزی حساب کاربری شما..." }, "lastPassMFARequired": { - "message": "LastPass multifactor authentication required" + "message": "احراز هویت چند مرحله‌ای LastPass مورد نیاز است" }, "lastPassMFADesc": { - "message": "Enter your one-time passcode from your authentication app" + "message": "کد یک‌بار مصرف خود را از برنامه احراز هویت وارد کنید" }, "lastPassOOBDesc": { - "message": "Approve the login request in your authentication app or enter a one-time passcode." + "message": "درخواست ورود را در برنامه احراز هویت خود تأیید کنید یا یک کد یک‌بار مصرف وارد کنید." }, "passcode": { - "message": "Passcode" + "message": "کد عبور" }, "lastPassMasterPassword": { - "message": "LastPass master password" + "message": "کلمه عبور اصلی LastPass" }, "lastPassAuthRequired": { - "message": "LastPass authentication required" + "message": "احراز هویت LastPass مورد نیاز است" }, "awaitingSSO": { - "message": "Awaiting SSO authentication" + "message": "در انتظار احراز هویت SSO" }, "awaitingSSODesc": { - "message": "Please continue to log in using your company credentials." + "message": "لطفاً برای ورود، از اطلاعات کاربری شرکت خود استفاده کنید." }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "دستورالعمل‌های کامل را در سایت راهنمای ما مشاهده کنید در", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { - "message": "Import directly from LastPass" + "message": "درون ریزی مستقیم از LastPass" }, "importFromCSV": { - "message": "وارد کردن از CSV" + "message": "درون ریزی از CSV" }, "lastPassTryAgainCheckEmail": { - "message": "Try again or look for an email from LastPass to verify it's you." + "message": "دوباره تلاش کنید یا ایمیلی از LastPass را بررسی کنید تا هویت شما تأیید شود." }, "collection": { "message": "مجموعه" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "کلید YubiKey مربوط به حساب کاربری LastPass خود را در درگاه USB کامپیوتر قرار دهید، سپس دکمه آن را لمس کنید." }, "commonImportFormats": { "message": "فرمت‌های رایج", "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "موفقیت آمیز بود" }, "troubleshooting": { - "message": "Troubleshooting" + "message": "عیب یابی" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "شتاب‌دهی سخت‌افزاری را غیرفعال کنید و دوباره راه‌اندازی کنید" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "message": "شتاب‌دهی سخت‌افزاری را فعال کنید و دوباره راه‌اندازی کنید" }, "removePasskey": { - "message": "Remove passkey" + "message": "حذف کلید عبور" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "کلید عبور حذف شد" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "خطا در اختصاص مجموعه هدف." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "خطا در اختصاص پوشه هدف." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "مشاهده موارد در $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -3543,7 +3543,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "بازگشت به $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -3553,11 +3553,11 @@ } }, "back": { - "message": "Back", + "message": "بازگشت", "description": "Button text to navigate back" }, "removeItem": { - "message": "Remove $NAME$", + "message": "حذف $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -3567,42 +3567,42 @@ } }, "data": { - "message": "Data" + "message": "داده‌" }, "fileSends": { - "message": "File Sends" + "message": "پرونده ارسال‌ها" }, "textSends": { - "message": "Text Sends" + "message": "ارسال‌های متن" }, "ssoError": { - "message": "No free ports could be found for the sso login." + "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": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "بازکردن با بیومتریک در دسترس نیست زیرا ابتدا باید بازکردن قفل با کد پین یا کلمه عبور انجام شود." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "باز کردن قفل با بیومتریک در حال حاضر در دسترس نیست." }, "biometricsStatusHelptextAutoSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "باز کردن قفل با بیومتریک به دلیل پیکربندی نادرست پرونده‌های سیستم در دسترس نیست." }, "biometricsStatusHelptextManualSetupNeeded": { - "message": "Biometric unlock is unavailable due to misconfigured system files." + "message": "باز کردن قفل با بیومتریک به دلیل پیکربندی نادرست پرونده‌های سیستم در دسترس نیست." }, "biometricsStatusHelptextNotEnabledLocally": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "باز کردن قفل با بیومتریک در دسترس نیست زیرا برای $EMAIL$ در برنامه دسکتاپ Bitwarden فعال نشده است.", "placeholders": { "email": { "content": "$1", @@ -3611,156 +3611,165 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "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": "Authorize" + "message": "اجازه دادن" }, "deny": { - "message": "Deny" + "message": "رد کن" }, "sshkeyApprovalTitle": { - "message": "Confirm SSH key usage" + "message": "تأیید استفاده از کلید SSH" }, "agentForwardingWarningTitle": { - "message": "Warning: Agent Forwarding" + "message": "هشدار: انتقال عامل" }, "agentForwardingWarningText": { - "message": "This request comes from a remote device that you are logged into" + "message": "این درخواست از دستگاه راه دوری ارسال شده است که شما در آن وارد شده‌اید" }, "sshkeyApprovalMessageInfix": { - "message": "is requesting access to" + "message": "درخواست دسترسی دارد به" }, "sshkeyApprovalMessageSuffix": { - "message": "in order to" + "message": "برای اینکه بتواند" }, "sshActionLogin": { - "message": "authenticate to a server" + "message": "برای احراز هویت به یک سرور" }, "sshActionSign": { - "message": "sign a message" + "message": "امضای یک پیام" }, "sshActionGitSign": { - "message": "sign a git commit" + "message": "امضای یک کامیت گیت" }, "unknownApplication": { - "message": "An application" + "message": "یک برنامه" }, "invalidSshKey": { - "message": "The SSH key is invalid" + "message": "کلید SSH نامعتبر است" }, "sshKeyTypeUnsupported": { - "message": "The SSH key type is not supported" + "message": "نوع کلید SSH پشتیبانی نمی‌شود" }, "importSshKeyFromClipboard": { - "message": "Import key from clipboard" + "message": "وارد کردن کلید از حافظه موقت" }, "sshKeyImported": { - "message": "SSH key imported successfully" + "message": "کلید SSH با موفقیت وارد شد" }, "fileSavedToDevice": { - "message": "File saved to device. Manage from your device downloads." + "message": "پرونده در دستگاه ذخیره شد. از بخش بارگیری‌های دستگاه خود مدیریت کنید." }, "allowScreenshots": { - "message": "Allow screen capture" + "message": "اجازه ضبط صفحه" }, "allowScreenshotsDesc": { - "message": "Allow the Bitwarden desktop application to be captured in screenshots and viewed in remote desktop sessions. Disabling this will prevent access on some external displays." + "message": "اجازه دهید برنامه دسکتاپ Bitwarden در اسکرین‌شات‌ها ثبت شده و در جلسات دسکتاپ از راه دور مشاهده شود. غیرفعال کردن این گزینه دسترسی در برخی نمایشگرهای خارجی را مسدود می‌کند." }, "confirmWindowStillVisibleTitle": { - "message": "Confirm window still visible" + "message": "تأیید کنید که پنجره هنوز قابل مشاهده است" }, "confirmWindowStillVisibleContent": { - "message": "Please confirm that the window is still visible." + "message": "لطفاً تأیید کنید که پنجره هنوز قابل مشاهده است." }, "updateBrowserOrDisableFingerprintDialogTitle": { - "message": "Extension update required" + "message": "به‌روزرسانی افزونه لازم است" }, "updateBrowserOrDisableFingerprintDialogMessage": { - "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." + "message": "افزونه مرورگری که استفاده می‌کنید قدیمی است. لطفاً آن را به‌روزرسانی کنید یا اعتبارسنجی اثر انگشت یکپارچه‌سازی مرورگر را در تنظیمات برنامه دسکتاپ غیرفعال کنید." }, "changeAtRiskPassword": { - "message": "Change at-risk password" + "message": "تغییر کلمه عبور در معرض خطر" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } }, "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": "برای تو در تو کردن یک پوشه، نام پوشه والد را وارد کرده و سپس یک “/” اضافه کنید. مثال: Social/Forums" }, "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" } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 14cf9c95ffc..bffa90c8a17 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -247,10 +247,10 @@ "message": "Remember SSH authorizations" }, "sshAgentPromptBehaviorAlways": { - "message": "Always" + "message": "Aina" }, "sshAgentPromptBehaviorNever": { - "message": "Never" + "message": "Ei koskaan" }, "sshAgentPromptBehaviorRememberUntilLock": { "message": "Remember until vault is locked" @@ -446,7 +446,7 @@ "message": "Lisää kenttä" }, "editField": { - "message": "Edit field" + "message": "Muokkaa kenttää" }, "permanentlyDeleteAttachmentConfirmation": { "message": "Are you sure you want to permanently delete this attachment?" @@ -1998,13 +1998,13 @@ "description": "Premium membership" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "Ilmaiset organisaatiot eivät voi käyttää liitteitä" }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "Yksi kenttä vaatii huomiotasi." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "$COUNT$ kenttää vaatii huomiotasi.", "placeholders": { "count": { "content": "$1", @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Pääsalasana poistettiin" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ käyttää kertakirjautumista (SSO) itse ylläpidetyllä avainpalvelimella. Organisaation jäsenet eivät enää tarvitse pääsalasanaa kirjautumiseen.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Poistu organisaatiosta" @@ -3158,16 +3158,16 @@ "message": "Laitteeseen luotettu" }, "trustOrganization": { - "message": "Trust organization" + "message": "Luota organisaatioon" }, "trust": { - "message": "Trust" + "message": "Luota" }, "doNotTrust": { - "message": "Do not trust" + "message": "Älä luota" }, "organizationNotTrusted": { - "message": "Organization is not trusted" + "message": "Organisaatio ei ole luotettu" }, "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" @@ -3179,7 +3179,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": "Luota käyttäjään" }, "inputRequired": { "message": "Syöte vaaditaan." @@ -3614,25 +3614,25 @@ "message": "Biometrinen avaus ei ole tällä hetkellä käytettävissä tuntemattomasta syystä." }, "itemDetails": { - "message": "Item details" + "message": "Kohteen tiedot" }, "itemName": { - "message": "Item name" + "message": "Kohteen nimi" }, "loginCredentials": { - "message": "Login credentials" + "message": "Kirjautumistiedot" }, "additionalOptions": { - "message": "Additional options" + "message": "Lisävalinnat" }, "itemHistory": { - "message": "Item history" + "message": "Kohteen historia" }, "lastEdited": { - "message": "Last edited" + "message": "Viimeksi muokattu" }, "upload": { - "message": "Upload" + "message": "Lähetä" }, "authorize": { "message": "Valtuuta" @@ -3703,20 +3703,29 @@ "changeAtRiskPassword": { "message": "Vaihda vaarantunut salasana" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Siirrä" }, "newFolder": { - "message": "New folder" + "message": "Uusi kansio" }, "folderName": { - "message": "Folder Name" + "message": "Kansion nimi" }, "folderHintText": { "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" }, "newLoginNudgeTitle": { - "message": "Save time with autofill" + "message": "Säästä aikaa automaattitäytöllä" }, "newLoginNudgeBodyOne": { "message": "Include a", @@ -3724,7 +3733,7 @@ "example": "Include a Website so this login appears as an autofill suggestion." }, "newLoginNudgeBodyBold": { - "message": "Website", + "message": "Verkkosivusto", "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." }, diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index b3633cd1694..74c886a98b6 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Tinanggal ang password ng master" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ ay gumagamit ng SSO na may self-hosted key server. Ang isang master password ay hindi na kinakailangan upang mag log in para sa mga miyembro ng organisasyong ito.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Umalis sa organisasyon" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index e94f4837d99..2c53c0652bd 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Mot de passe principal supprimé" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ utilise le SSO avec un serveur de clés auto-hébergé. Un mot de passe principal n'est plus nécessaire aux membres de cette organisation pour se connecter.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Quitter l'organisation" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Changer le mot de passe à risque" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 471d7235945..17495f96785 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 954c50812be..3999caa1bbd 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "הסיסמה הראשית הוסרה" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ משתמש/ת ב־SSO עם שרת מפתחות באירוח עצמי. סיסמה ראשית כבר לא נדרשת כדי להיכנס עבור חברים של ארגון זה.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "עזוב ארגון" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "שנה סיסמה בסיכון" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 10307cc03ca..292e1c4b7d2 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 8bdfcb40534..cf90703a438 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Glavna lozinka uklonjena." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ koristi jedinstvenu prijavu SSO s vlastitim poslužiteljem. Članovima organizacije glavna lozinka više nije potrebna.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Napusti organizaciju" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Promijeni rizičnu lozinku" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index e56b62f9840..f7eb77766c1 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "A mesterjelszó eltávolításra került." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ jelenleg saját tárolású aláíráskulcsú SSO szervert használ. A mesterjelszó a továbbiakban nem szükséges a szervezeti tagsági bejelentkezéshez.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "A következő szervezet tagjai számára már nincs szükség mesterjelszóra. Erősítsük meg az alábbi tartományt a szervezet adminisztrátorával." + }, + "organizationName": { + "message": "Szervezet neve" + }, + "keyConnectorDomain": { + "message": "Key Connector tartomány" }, "leaveOrganization": { "message": "Szervezet elhagyása" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Kockázatos jelszó megváltoztatása" }, + "cannotRemoveViewOnlyCollections": { + "message": "Nem távolíthatók el a csak megtekintési engedéllyel bíró gyűjtemények: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Áthelyezés" }, diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 1eeb32fdcc6..70be61f4bf3 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Sandi utama dihapus" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ menggunakan SSO dengan server kunci yang dihosting sendiri. Kata sandi utama tidak lagi diperlukan untuk masuk untuk anggota organisasi ini.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Tinggalkan organisasi" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index db70740d96e..a3dcb13fa76 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Password principale rimossa" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ sta usando SSO con un server self-hosted. Una password principale non è più necessaria per accedere per i membri di questa organizzazione.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Lascia organizzazione" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Modifica la password non sicura o esposta" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 47ba77508bc..624a296f32b 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "マスターパスワードを削除しました。" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ は自己ホストの鍵サーバで SSO を使用しています。この組織のメンバーのログインにマスターパスワードは必要ありません。", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "組織から脱退する" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "危険なパスワードの変更" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "移動" }, diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 6a2912eed10..45047cd678f 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 471d7235945..17495f96785 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 767cdcf9d03..e74fc479016 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index b7b2a82b4b5..371f4cae1c8 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "마스터 비밀번호가 제거되었습니다." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ 조직은 자체 호스팅 키 서버로 SSO를 사용하고 있습니다 이 조직의 멤버들은 로그인할 때에 마스터 비밀번호를 필요로 하지 않습니다.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "조직 나가기" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index 66dcf1da155..f98a5dba2e1 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Pagrindinis slaptažodis pašalintas" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ naudoja SSO su savarankiškai sukurtu raktų serveriu. Pagrindinis slaptažaodis nebėra reikalingas norint šios organizacijos nariams prisijungti.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Palikti organizaciją" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 437aba39714..6c35520605d 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Galvenā parole tika noņemta." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ izmanto vienoto pieteikšanos ar pašmitinātu atslēgu serveri. Tās dalībniekiem vairs nav nepieciešama galvenā parole, lai pieteiktos.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Galvenā parole vairs nav nepieciešama turpmāk minētās apvienības dalībniekiem. Lūgums saskaņot zemāk esošo domēnu ar savas apvienības pārvaldītāju." + }, + "organizationName": { + "message": "Apvienības nosaukums" + }, + "keyConnectorDomain": { + "message": "Key Connector domēns" }, "leaveOrganization": { "message": "Pamest apvienību" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Mainīt riskam pakļautu paroli" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Pārvietot" }, diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 152877fa552..51310719f0a 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 3fcb52bade5..a760acb86ec 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 471d7235945..17495f96785 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 724194808cf..2179311c501 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 5522218037f..3ddf3e44e5f 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Hovedpassordet er fjernet." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ bruker SSO med en selvdrevet nøkkelserver. Et hovedpassord er ikke lenger nødvendig for å logge inn for medlemmer av denne organisasjonen.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Forlat organisasjon" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Flytt" }, diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index a56268aa671..9ae7b7af955 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index cad8a495dbf..b1e04600908 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Hoofdwachtwoord verwijderd." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ gebruikt SSO met een zelf gehoste sleutelserver. Leden van deze organisatie kunnen inloggen zonder hoofdwachtwoord.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Voor leden van de volgende organisatie is een hoofdwachtwoord niet langer nodig. Bevestig het domein hieronder met de beheerder van je organisatie." + }, + "organizationName": { + "message": "Organisatienaam" + }, + "keyConnectorDomain": { + "message": "Key Connector-domein" }, "leaveOrganization": { "message": "Organisatie verlaten" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Risicovol wachtwoord wijzigen" }, + "cannotRemoveViewOnlyCollections": { + "message": "Je kunt verzamelingen niet verwijderen met alleen rechten voor weergeven: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Verplaatsen" }, diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index b3f0f0c5d20..097da5886a6 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index d584cd5d117..a71275e64ac 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 61c438c0425..84ef8f0ca9a 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Hasło główne zostało usunięte" }, - "convertOrganizationEncryptionDesc": { - "message": "Organizacja $ORGANIZATION$ używa jednokrotnego logowania SSO z własnym serwerem kluczy. Użytkownicy nie muszą logować się za pomocą hasła głównego.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Opuść organizację" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Zmień zagrożone hasło" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index e44bb7abc8c..43fa1087988 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Senha mestra removida." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ está usando SSO com um servidor de chaves auto-hospedado. Não é mais necessária uma senha mestra para os membros desta organização entrarem.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Sair da Organização" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Alterar senhas vulneráveis" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Mover" }, diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index ef657f1b658..dc40c91960e 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Palavra-passe mestra removida" }, - "convertOrganizationEncryptionDesc": { - "message": "A $ORGANIZATION$ está a utilizar o SSO com um servidor de chaves auto-hospedado. Já não é necessária uma palavra-passe mestra para iniciar sessão para os membros desta organização.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Já não é necessária uma palavra-passe mestra para os membros da seguinte organização. Por favor, confirme o domínio abaixo com o administrador da sua organização." + }, + "organizationName": { + "message": "Nome da organização" + }, + "keyConnectorDomain": { + "message": "Domínio do Key Connector" }, "leaveOrganization": { "message": "Sair da organização" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Alterar palavra-passe em risco" }, + "cannotRemoveViewOnlyCollections": { + "message": "Não é possível remover coleções com permissões de Apenas visualização: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Mover" }, diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 9024aaa5533..f108d420829 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Parola principală înlăturată" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ folosește SSO cu un server de chei auto-găzduit. Membrii acestei organizații nu mai au nevoie de o parolă principală pentru autentificare.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Părăsire organizație" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 0e58487ae88..52dea4d205c 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -157,7 +157,7 @@ "message": "Код безопасности" }, "identityName": { - "message": "Название личности" + "message": "Название личной информации" }, "company": { "message": "Компания" @@ -2188,7 +2188,7 @@ "message": "Импорт элементов в ваше личное хранилище отключен политикой организации." }, "personalDetails": { - "message": "Личные данные" + "message": "Личная информация" }, "identification": { "message": "Идентификация" @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Мастер-пароль удален." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ использует SSO с собственным сервером ключей. Для авторизации пользователям этой организации больше не требуется мастер-пароль.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Мастер-пароль больше не требуется для членов следующей организации. Пожалуйста, подтвердите указанный ниже домен у администратора вашей организации." + }, + "organizationName": { + "message": "Название организации" + }, + "keyConnectorDomain": { + "message": "Домен соединителя ключей" }, "leaveOrganization": { "message": "Покинуть организацию" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Изменить пароль, подверженный риску" }, + "cannotRemoveViewOnlyCollections": { + "message": "Вы не можете удалить коллекции с правами только на просмотр: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Переместить" }, diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index bb843718e28..03124c345da 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index d84b164e749..3ee4a800a61 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Hlavné heslo bolo odstránené." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ používa SSO s vlastným kľúčovým serverom. Na prihlásenie členov tejto organizácie už nie je potrebné hlavné heslo.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Hlavné heslo sa už nevyžaduje pre členov tejto organizácie. Nižšie uvedenú doménu potvrďte u správcu organizácie." + }, + "organizationName": { + "message": "Názov organizácie" + }, + "keyConnectorDomain": { + "message": "Doména Key Connectora" }, "leaveOrganization": { "message": "Opustiť organizáciu" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Zmeniť rizikové heslá" }, + "cannotRemoveViewOnlyCollections": { + "message": "Zbierky, ktoré môžete len zobraziť nemôžete odstrániť: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Presunúť" }, diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 6cc9e6fc197..e1f7dcd578b 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Glavno geslo je bilo odstranjeno." }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Zapusti organizacijo" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 7509870b0a0..d13c28ae4da 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -238,22 +238,22 @@ "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": "Always" + "message": "Увек" }, "sshAgentPromptBehaviorNever": { - "message": "Never" + "message": "Никада" }, "sshAgentPromptBehaviorRememberUntilLock": { - "message": "Remember until vault is locked" + "message": "Запамти док сеф није блокиран" }, "premiumRequired": { "message": "Потребан Премијум" @@ -446,10 +446,10 @@ "message": "Додај поље" }, "editField": { - "message": "Edit field" + "message": "Уреди поље" }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "Да ли сте сигурни да желите да трајно избришете овај прилог?" }, "fieldType": { "message": "Врста поља" @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Главна лозинка уклоњена" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ користи SSO уз сопствени сервер за кључеве. Главна лозинка за пријаву више није неопходна за чланове ове организације.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "Главна лозинка више није потребна за чланове следеће организације. Молимо потврдите домен са администратором организације." + }, + "organizationName": { + "message": "Назив организације" + }, + "keyConnectorDomain": { + "message": "Домен конектора кључа" }, "leaveOrganization": { "message": "Напусти организацију" @@ -3614,25 +3614,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": "Ауторизуј" @@ -3703,33 +3703,42 @@ "changeAtRiskPassword": { "message": "Променити ризичну лозинку" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "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": "Угнездите фасциклу додавањем имена надређене фасцкле праћеног знаком „/“. Пример: Друштвени/Форуми" }, "newLoginNudgeTitle": { "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." }, @@ -3755,12 +3764,12 @@ "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" } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index eb0d2dcd547..e4fbc6f5773 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Huvudlösenord togs bort" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ använder SSO med en egen nyckelserver. Ett huvudlösenord krävs inte längre för att logga in för medlemmar i denna organisation.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Lämna organisation" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 471d7235945..17495f96785 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 43aaa697a8a..34669ba21c4 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Master password removed" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Leave organization" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index c6a59afda23..43a134abfb0 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Ana parola kaldırıldı" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ kendi barındırdığı bir anahtar sunucusuyla SSO kullanıyor. Bu kuruluşun üyelerinin artık ana parola kullanması gerekmiyor.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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": "Kuruluş adı" + }, + "keyConnectorDomain": { + "message": "Key Connector alan adı" }, "leaveOrganization": { "message": "Kuruluştan ayrıl" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Taşı" }, diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 156c8610ba2..a4a63da8089 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Головний пароль вилучено" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ використовує SSO з власним сервером ключів. Головний пароль для учасників цієї організації більше не вимагається.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Покинути організацію" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Змінити ризикований пароль" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Перемістити" }, diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 2fd354ed08b..4ab9257691c 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -27,7 +27,7 @@ "message": "Ghi chú bảo mật" }, "typeSshKey": { - "message": "SSH key" + "message": "Khóa SSH" }, "folders": { "message": "Thư mục" @@ -64,7 +64,7 @@ } }, "welcomeBack": { - "message": "Welcome back" + "message": "Chào mừng bạn trở lại" }, "moveToOrgDesc": { "message": "Chọn một tổ chức mà bạn muốn chuyển mục này đến. Việc chuyển đến một tổ chức sẽ chuyển quyền sở hữu mục này cho tổ chức đó. Bạn sẽ không còn là chủ sở hữu trực tiếp của mục này khi đã chuyển." @@ -181,16 +181,16 @@ "message": "Địa chỉ" }, "sshPrivateKey": { - "message": "Private key" + "message": "Khóa riêng SSH" }, "sshPublicKey": { - "message": "Public key" + "message": "Khóa công khai SSH" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "Mã vân tay SSH" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Loại thuật toán" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -205,55 +205,55 @@ "message": "RSA 4096-Bit" }, "sshKeyGenerated": { - "message": "A new SSH key was generated" + "message": "Khóa SSH mới đã được tạo" }, "sshKeyWrongPassword": { - "message": "The password you entered is incorrect." + "message": "Mật khẩu bạn đã nhập không chính xác." }, "importSshKey": { - "message": "Import" + "message": "Nhập khóa" }, "confirmSshKeyPassword": { - "message": "Confirm password" + "message": "Xác nhận mật khẩu" }, "enterSshKeyPasswordDesc": { - "message": "Enter the password for the SSH key." + "message": "Nhập mật khẩu cho khóa SSH." }, "enterSshKeyPassword": { - "message": "Enter password" + "message": "Nhập mật khẩu" }, "sshAgentUnlockRequired": { - "message": "Please unlock your vault to approve the SSH key request." + "message": "Vui lòng mở khóa kho lưu trữ của bạn để phê duyệt yêu cầu khóa SSH." }, "sshAgentUnlockTimeout": { - "message": "SSH key request timed out." + "message": "Hết thời gian chờ yêu cầu khóa SSH." }, "enableSshAgent": { - "message": "Enable SSH agent" + "message": "Bật SSH agent" }, "enableSshAgentDesc": { - "message": "Enable the SSH agent to sign SSH requests right from your Bitwarden vault." + "message": "Bật SSH agent để ký các yêu cầu SSH ngay từ kho lưu trữ Bitwarden của bạn." }, "enableSshAgentHelp": { - "message": "The SSH agent is a service targeted at developers that allows you to sign SSH requests directly from your Bitwarden vault." + "message": "SSH agent là một dịch vụ hướng tới các nhà phát triển, cho phép bạn ký các yêu cầu SSH trực tiếp từ kho lưu trữ Bitwarden của mình." }, "sshAgentPromptBehavior": { - "message": "Ask for authorization when using SSH agent" + "message": "Yêu cầu cấp quyền khi sử dụng SSH agent" }, "sshAgentPromptBehaviorDesc": { - "message": "Choose how to handle SSH-agent authorization requests." + "message": "Chọn cách xử lý các yêu cầu cấp quyền của SSH-agent." }, "sshAgentPromptBehaviorHelp": { - "message": "Remember SSH authorizations" + "message": "Ghi nhớ các quyền SSH" }, "sshAgentPromptBehaviorAlways": { - "message": "Always" + "message": "Luôn luôn" }, "sshAgentPromptBehaviorNever": { - "message": "Never" + "message": "Không bao giờ" }, "sshAgentPromptBehaviorRememberUntilLock": { - "message": "Remember until vault is locked" + "message": "Ghi nhớ cho đến khi kho lưu trữ bị khóa" }, "premiumRequired": { "message": "Cần có tài khoản cao cấp" @@ -268,17 +268,17 @@ "message": "Lỗi" }, "decryptionError": { - "message": "Decryption error" + "message": "Lỗi giải mã" }, "couldNotDecryptVaultItemsBelow": { - "message": "Bitwarden could not decrypt the vault item(s) listed below." + "message": "Bitwarden không thể giải mã các kho lưu trữ được liệt kê dưới đây." }, "contactCSToAvoidDataLossPart1": { - "message": "Contact customer success", + "message": "Liên hệ hỗ trợ khách hàng thành công", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "contactCSToAvoidDataLossPart2": { - "message": "to avoid additional data loss.", + "message": "để tránh mất mát thêm dữ liệu.", "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "january": { @@ -355,7 +355,7 @@ "message": "Tạo mật khẩu" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Tạo cụm mật khẩu" }, "type": { "message": "Loại" @@ -412,16 +412,16 @@ "message": "Khóa xác thực (TOTP)" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "Khóa xác thực" }, "autofillOptions": { - "message": "Autofill options" + "message": "Tùy chọn tự động điền" }, "websiteUri": { - "message": "Website (URI)" + "message": "Trang web (URI)" }, "websiteUriCount": { - "message": "Website (URI) $COUNT$", + "message": "Trang web (URI) $COUNT$", "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", "placeholders": { "count": { @@ -431,46 +431,46 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "Đã thêm trang web" }, "addWebsite": { - "message": "Add website" + "message": "Thêm trang web" }, "deleteWebsite": { - "message": "Delete website" + "message": "Xóa trang web" }, "owner": { - "message": "Owner" + "message": "Chủ sở hữu" }, "addField": { - "message": "Add field" + "message": "Thêm trường" }, "editField": { - "message": "Edit field" + "message": "Chỉnh sửa trường" }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "Bạn có chắc muốn xóa vĩnh viễn tệp đính kèm này không?" }, "fieldType": { - "message": "Field type" + "message": "Loại trường" }, "fieldLabel": { - "message": "Field label" + "message": "Nhãn trường" }, "add": { - "message": "Add" + "message": "Thêm" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "Sử dụng các trường văn bản cho dữ liệu như câu hỏi bảo mật" }, "hiddenHelpText": { - "message": "Use hidden fields for sensitive data like a password" + "message": "Sử dụng trường ẩn cho dữ liệu nhạy cảm như mật khẩu" }, "checkBoxHelpText": { - "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + "message": "Dùng các ô tích chọn nếu bạn muốn tự động điền vào ô tích chọn của biểu mẫu, chẳng hạn như ghi nhớ email" }, "linkedHelpText": { - "message": "Use a linked field when you are experiencing autofill issues for a specific website." + "message": "Sử dụng trường liên kết khi bạn gặp sự cố tự động điền trên một trang web cụ thể." }, "linkedLabelHelpText": { "message": "Enter the the field's html id, name, aria-label, or placeholder." @@ -1184,7 +1184,7 @@ "message": "Unlock with biometrics" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "Mở khóa bằng mật khẩu chính" }, "unlock": { "message": "Mở khóa" @@ -1508,13 +1508,13 @@ "message": "Chưa có mật khẩu." }, "clearHistory": { - "message": "Clear history" + "message": "Xóa lịch sử" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Không có gì để hiển thị" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "Bạn chưa tạo gì gần đây" }, "undo": { "message": "Hoàn tác" @@ -1591,7 +1591,7 @@ } }, "copySuccessful": { - "message": "Copy Successful" + "message": "Sao chép thành công" }, "errorRefreshingAccessToken": { "message": "Lỗi làm mới khoá truy cập" @@ -1824,13 +1824,13 @@ "message": "Yêu cầu mật khẩu hoặc mã PIN khi mở" }, "requirePasswordWithoutPinOnStart": { - "message": "Require password on app start" + "message": "Yêu cầu mật khẩu khi khởi động ứng dụng" }, "recommendedForSecurity": { "message": "Đề xuất để tăng cường bảo mật." }, "lockWithMasterPassOnRestart1": { - "message": "Lock with master password on restart" + "message": "Khóa bằng mật khẩu chính khi khởi động lại" }, "deleteAccount": { "message": "Xóa tài khoản" @@ -1842,10 +1842,10 @@ "message": "Việc xóa tài khoản là vĩnh viễn và không thể hoàn tác." }, "cannotDeleteAccount": { - "message": "Cannot delete account" + "message": "Không thể xóa tài khoản" }, "cannotDeleteAccountDesc": { - "message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details." + "message": "Không thể thực hiện hành động này vì tài khoản của bạn thuộc sở hữu của một tổ chức. Liên hệ với quản trị viên tổ chức của bạn để biết thêm chi tiết." }, "accountDeleted": { "message": "Đã xóa tài khoản" @@ -1958,7 +1958,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "trong tổng số $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -1967,10 +1967,10 @@ } }, "cardDetails": { - "message": "Card details" + "message": "Thông tin thẻ" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "Chi tiết của $BRAND$", "placeholders": { "brand": { "content": "$1", @@ -1979,32 +1979,32 @@ } }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "Tìm hiểu thêm về các công cụ xác thực" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "Sao chép khóa xác thực (TOTP)" }, "totpHelperTitle": { - "message": "Make 2-step verification seamless" + "message": "Làm cho xác thực 2 bước trở nên liền mạch" }, "totpHelper": { - "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + "message": "Bitwarden có thể lưu trữ và điền mã xác thực 2 bước. Hãy sao chép và dán khóa vào trường này." }, "totpHelperWithCapture": { - "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." + "message": "Bitwarden có thể lưu trữ và điền mã xác thực 2 bước. Hãy chọn biểu tượng máy ảnh để chụp mã QR xác thực trên trang web này, hoặc sao chép và dán khóa vào trường này." }, "premium": { - "message": "Premium", + "message": "Phiên bản cao cấp", "description": "Premium membership" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "Các tổ chức miễn phí không thể sử dụng tệp đính kèm" }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "1 trường cần bạn chú ý." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "$COUNT$ trường cần bạn chú ý.", "placeholders": { "count": { "content": "$1", @@ -2013,10 +2013,10 @@ } }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Thẻ hết hạn" }, "cardExpiredMessage": { - "message": "If you've renewed it, update the card's information" + "message": "Nếu bạn đã gia hạn, hãy cập nhật thông tin thẻ" }, "verificationRequired": { "message": "Yêu cầu xác minh", @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "Đã xóa mật khẩu chính" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ đang sử dụng SSO với khóa máy chủ tự lưu trữ. Mật khẩu chính không còn cần để đăng nhập cho các thành viên của tổ chức này.", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "Rời khỏi tổ chức" @@ -2606,7 +2606,7 @@ "message": "Đã khóa" }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "Kho lưu trữ đã bị khóa" }, "unlocked": { "message": "Đã mở khóa" @@ -3067,7 +3067,7 @@ "message": "Quan trọng:" }, "accessing": { - "message": "Accessing" + "message": "Đang truy cập" }, "accessTokenUnableToBeDecrypted": { "message": "Bạn đã bị đăng xuất do khoá truy cập của bạn không thể giải mã. Vui lòng đăng nhập lại để khắc phục sự cố này." @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 90cdb33fde5..69d5faa5fb7 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1836,7 +1836,7 @@ "message": "删除账户" }, "deleteAccountDesc": { - "message": "接下来的操作会删除您的账户和所有密码库数据。" + "message": "继续下面的操作以删除您的账户和所有密码库数据。" }, "deleteAccountWarning": { "message": "删除账户是永久性操作,无法撤销!" @@ -2363,7 +2363,7 @@ "message": "读取安全密钥" }, "awaitingSecurityKeyInteraction": { - "message": "等待安全密钥交互……" + "message": "等待安全密钥交互..." }, "hideEmail": { "message": "对接收者隐藏我的电子邮箱地址。" @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "主密码已移除" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ 使用自托管密钥服务器 SSO。这个组织的成员登录时将不再需要主密码。", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "message": "以下组织的成员不再需要主密码。请与您的组织管理员确认下面的域名。" + }, + "organizationName": { + "message": "组织名称" + }, + "keyConnectorDomain": { + "message": "Key Connector 域名" }, "leaveOrganization": { "message": "退出组织" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "更改有风险的密码" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "移动" }, diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index eeeaf9b152e..6080c3ee0f8 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -2512,14 +2512,14 @@ "removedMasterPassword": { "message": "主密碼已移除" }, - "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ 使用自我裝載金鑰伺服器 SSO。此組織的成員登入時將不再需要主密碼。", - "placeholders": { - "organization": { - "content": "$1", - "example": "My Org Name" - } - } + "removeMasterPasswordForOrganizationUserKeyConnector": { + "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" + }, + "keyConnectorDomain": { + "message": "Key Connector domain" }, "leaveOrganization": { "message": "離開組織" @@ -3703,6 +3703,15 @@ "changeAtRiskPassword": { "message": "Change at-risk password" }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "move": { "message": "Move" }, From 56a3b14583747a499d66960ac5b372ee0857246e Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 23 May 2025 08:01:25 -0400 Subject: [PATCH 133/163] Introduce eslint errors for risky/circular imports (#14804) * first draft at an idea dependency graph * ignore existing errors * remove conflicting rule regarding internal platform logic in libs * review: allow components to import from platform --- .../messaging/chrome-message.sender.ts | 2 + .../platform/sync/foreground-sync.service.ts | 2 + .../sync/sync-service.listener.spec.ts | 2 + .../utils/from-chrome-runtime-messaging.ts | 2 + .../electron-renderer-message.sender.ts | 2 + .../src/platform/utils/from-ipc-messaging.ts | 2 + eslint.config.mjs | 248 +++++++++++++++++- .../components/collections.component.ts | 2 + .../base-login-via-webauthn.component.ts | 2 + .../environment-selector.component.ts | 2 + .../auth/components/set-password.component.ts | 4 + .../src/auth/components/set-pin.component.ts | 2 + .../src/auth/guards/active-auth.guard.spec.ts | 2 + .../src/auth/guards/active-auth.guard.ts | 2 + ...vice-trust-toast.service.implementation.ts | 2 + .../device-trust-toast.service.spec.ts | 2 + .../angular/src/components/share.component.ts | 2 + libs/angular/src/services/injection-tokens.ts | 2 + .../src/services/jslib-services.module.ts | 8 + .../view-password-history.service.spec.ts | 2 + .../services/view-password-history.service.ts | 2 + .../deprecated-vault-filter.service.ts | 2 + .../vault/components/add-edit.component.ts | 4 + .../src/vault/components/view.component.ts | 2 + .../account-security-nudge.service.ts | 2 + .../empty-vault-nudge.service.ts | 2 + .../src/vault/services/nudges.service.spec.ts | 2 + .../components/collection-filter.component.ts | 2 + .../components/vault-filter.component.ts | 2 + .../services/vault-filter.service.ts | 2 + .../anon-layout-wrapper.component.ts | 2 + .../anon-layout-wrapper.stories.ts | 2 + .../change-password.component.ts | 2 + .../fingerprint-dialog.component.ts | 2 + .../src/angular/icons/bitwarden-logo.icon.ts | 2 + .../angular/icons/bitwarden-shield.icon.ts | 2 + .../angular/icons/device-verification.icon.ts | 2 + libs/auth/src/angular/icons/devices.icon.ts | 2 + libs/auth/src/angular/icons/lock.icon.ts | 2 + .../icons/registration-check-email.icon.ts | 2 + .../icons/registration-expired-link.icon.ts | 2 + .../icons/registration-lock-alt.icon.ts | 2 + .../icons/registration-user-add.icon.ts | 2 + libs/auth/src/angular/icons/sso-key.icon.ts | 2 + .../two-factor-auth-authenticator.icon.ts | 2 + .../two-factor-auth-duo.icon.ts | 2 + .../two-factor-auth-email.icon.ts | 2 + .../two-factor-auth-security-key.icon.ts | 2 + .../two-factor-auth-webauthn.icon.ts | 2 + .../two-factor-auth-yubico.icon.ts | 2 + .../angular/icons/two-factor-timeout.icon.ts | 2 + libs/auth/src/angular/icons/user-lock.icon.ts | 2 + ...erification-biometrics-fingerprint.icon.ts | 2 + libs/auth/src/angular/icons/vault.icon.ts | 2 + libs/auth/src/angular/icons/wave.icon.ts | 2 + .../input-password.component.ts | 2 + .../input-password/input-password.stories.ts | 2 + .../login-approval.component.spec.ts | 2 + .../login-approval.component.ts | 2 + .../login-decryption-options.component.ts | 2 + .../login-via-auth-request.component.ts | 2 + .../login-secondary-content.component.ts | 2 + .../auth/src/angular/login/login.component.ts | 2 + .../new-device-verification.component.ts | 2 + .../password-callout.component.ts | 2 + .../password-callout.stories.ts | 2 + .../password-hint/password-hint.component.ts | 2 + .../registration-env-selector.component.ts | 2 + .../registration-finish.component.ts | 2 + .../registration-link-expired.component.ts | 2 + .../registration-start-secondary.component.ts | 2 + .../registration-start.component.ts | 2 + .../registration-start.stories.ts | 2 + ...self-hosted-env-config-dialog.component.ts | 2 + .../default-set-password-jit.service.spec.ts | 2 + .../default-set-password-jit.service.ts | 2 + libs/auth/src/angular/sso/sso.component.ts | 2 + ...two-factor-auth-authenticator.component.ts | 2 + .../two-factor-auth-duo.component.ts | 2 + .../two-factor-auth-email.component.ts | 2 + .../two-factor-auth-webauthn.component.ts | 2 + .../two-factor-auth-yubikey.component.ts | 2 + .../two-factor-auth.component.spec.ts | 2 + .../two-factor-auth.component.ts | 2 + .../two-factor-options.component.ts | 2 + .../user-verification-dialog.component.ts | 2 + .../user-verification-dialog.types.ts | 2 + .../user-verification-form-input.component.ts | 2 + .../vault-timeout-input.component.ts | 2 + libs/common/src/abstractions/api.service.ts | 2 + .../response/organization-export.response.ts | 2 + .../provider-user-bulk-public-key.response.ts | 2 + .../registration/register-finish.request.ts | 2 + .../models/request/set-password.request.ts | 2 + ...update-tde-offboarding-password.request.ts | 2 + .../request/update-temp-password.request.ts | 2 + .../response/identity-token.response.ts | 2 + .../auth/models/response/prelogin.response.ts | 2 + .../response/protected-device.response.ts | 2 + .../src/auth/services/auth.service.spec.ts | 2 + libs/common/src/auth/services/auth.service.ts | 2 + .../master-password-api.service.spec.ts | 2 + ...-enrollment.service.implementation.spec.ts | 4 + ...reset-enrollment.service.implementation.ts | 4 + .../src/auth/services/token.service.spec.ts | 2 + .../common/src/auth/services/token.service.ts | 2 + .../user-verification.service.spec.ts | 4 + .../user-verification.service.ts | 4 + .../webauthn-login.service.spec.ts | 2 + .../webauthn-login/webauthn-login.service.ts | 2 + libs/common/src/auth/types/verification.ts | 2 + .../organization-billing.service.spec.ts | 2 + .../services/organization-billing.service.ts | 2 + .../device-trust.service.implementation.ts | 4 + .../services/device-trust.service.spec.ts | 4 + .../models/set-key-connector-key.request.ts | 2 + .../services/key-connector.service.spec.ts | 2 + .../services/key-connector.service.ts | 4 + .../default-process-reload.service.ts | 4 + .../vault-timeout-settings.service.spec.ts | 4 + .../vault-timeout-settings.service.ts | 4 + .../services/vault-timeout.service.spec.ts | 6 + .../services/vault-timeout.service.ts | 6 + .../export/collection-with-id.export.ts | 2 + .../src/models/export/collection.export.ts | 2 + .../import-organization-ciphers.request.ts | 2 + libs/common/src/models/request/kdf.request.ts | 2 + .../abstractions/key-generation.service.ts | 2 + libs/common/src/platform/misc/utils.ts | 2 + .../platform/models/domain/enc-string.spec.ts | 2 + .../default-notifications.service.spec.ts | 2 + .../internal/default-notifications.service.ts | 2 + .../platform/services/container.service.ts | 2 + .../services/key-generation.service.spec.ts | 2 + .../services/key-generation.service.ts | 2 + .../services/sdk/default-sdk.service.spec.ts | 2 + .../services/sdk/default-sdk.service.ts | 2 + .../user-auto-unlock-key.service.spec.ts | 2 + .../services/user-auto-unlock-key.service.ts | 2 + .../src/platform/sync/core-sync.service.ts | 2 + .../sync/default-sync.service.spec.ts | 6 + .../src/platform/sync/default-sync.service.ts | 4 + .../common/src/platform/sync/sync.response.ts | 2 + libs/common/src/services/api.service.spec.ts | 2 + libs/common/src/services/api.service.ts | 4 + ...-service-legacy-encryptor-provider.spec.ts | 2 + .../key-service-legacy-encryptor-provider.ts | 2 + .../src/tools/send/models/domain/send.spec.ts | 2 + .../send/services/send.service.abstraction.ts | 2 + .../tools/send/services/send.service.spec.ts | 2 + .../src/tools/send/services/send.service.ts | 2 + .../src/vault/abstractions/cipher.service.ts | 2 + .../folder/folder.service.abstraction.ts | 2 + .../vault/models/domain/attachment.spec.ts | 2 + .../src/vault/models/domain/cipher.spec.ts | 2 + .../cipher-authorization.service.spec.ts | 2 + .../services/cipher-authorization.service.ts | 2 + .../src/vault/services/cipher.service.spec.ts | 2 + .../src/vault/services/cipher.service.ts | 2 + .../services/folder/folder.service.spec.ts | 2 + .../vault/services/folder/folder.service.ts | 2 + .../src/components/import.component.ts | 2 + libs/importer/src/importers/base-importer.ts | 2 + .../bitwarden/bitwarden-json-importer.ts | 2 + .../src/importers/padlock-csv-importer.ts | 2 + .../src/importers/passpack-csv-importer.ts | 2 + libs/importer/src/models/import-result.ts | 2 + .../import-collection.service.abstraction.ts | 2 + .../services/import.service.abstraction.ts | 2 + .../src/services/import.service.spec.ts | 2 + libs/importer/src/services/import.service.ts | 2 + libs/key-management/src/key.service.spec.ts | 2 + libs/key-management/src/key.service.ts | 2 + .../cipher-form-config.service.ts | 2 + .../src/cipher-form/cipher-form.stories.ts | 2 + .../item-details-section.component.spec.ts | 2 + .../item-details-section.component.ts | 2 + .../default-cipher-form-config.service.ts | 2 + .../src/cipher-view/cipher-view.component.ts | 2 + .../item-details-v2.component.spec.ts | 2 + .../item-details/item-details-v2.component.ts | 2 + .../assign-collections.component.ts | 2 + 182 files changed, 652 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/platform/messaging/chrome-message.sender.ts b/apps/browser/src/platform/messaging/chrome-message.sender.ts index 4fc9c22e26b..c0736f2456e 100644 --- a/apps/browser/src/platform/messaging/chrome-message.sender.ts +++ b/apps/browser/src/platform/messaging/chrome-message.sender.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CommandDefinition, MessageSender } from "@bitwarden/common/platform/messaging"; +// 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 { getCommand } from "@bitwarden/common/platform/messaging/internal"; type ErrorHandler = (logger: LogService, command: string) => void; diff --git a/apps/browser/src/platform/sync/foreground-sync.service.ts b/apps/browser/src/platform/sync/foreground-sync.service.ts index ce776f53685..2ac75bbec2c 100644 --- a/apps/browser/src/platform/sync/foreground-sync.service.ts +++ b/apps/browser/src/platform/sync/foreground-sync.service.ts @@ -13,6 +13,8 @@ import { } from "@bitwarden/common/platform/messaging"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { StateProvider } from "@bitwarden/common/platform/state"; +// 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 { CoreSyncService } from "@bitwarden/common/platform/sync/internal"; import { SyncOptions } from "@bitwarden/common/platform/sync/sync.service"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; diff --git a/apps/browser/src/platform/sync/sync-service.listener.spec.ts b/apps/browser/src/platform/sync/sync-service.listener.spec.ts index 9682e2cdb57..dc0674a7ae5 100644 --- a/apps/browser/src/platform/sync/sync-service.listener.spec.ts +++ b/apps/browser/src/platform/sync/sync-service.listener.spec.ts @@ -3,6 +3,8 @@ import { Subject, firstValueFrom } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// 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 { tagAsExternal } from "@bitwarden/common/platform/messaging/helpers"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; diff --git a/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts b/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts index ebc01ad86fa..9489c5f2a4e 100644 --- a/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts +++ b/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts @@ -1,6 +1,8 @@ import { map, share } from "rxjs"; import { Message } from "@bitwarden/common/platform/messaging"; +// 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 { tagAsExternal } from "@bitwarden/common/platform/messaging/internal"; import { fromChromeEvent } from "../browser/from-chrome-event"; diff --git a/apps/desktop/src/platform/services/electron-renderer-message.sender.ts b/apps/desktop/src/platform/services/electron-renderer-message.sender.ts index 109a52a8d8f..a70542e7b20 100644 --- a/apps/desktop/src/platform/services/electron-renderer-message.sender.ts +++ b/apps/desktop/src/platform/services/electron-renderer-message.sender.ts @@ -1,4 +1,6 @@ import { MessageSender, CommandDefinition } from "@bitwarden/common/platform/messaging"; +// 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 { getCommand } from "@bitwarden/common/platform/messaging/internal"; export class ElectronRendererMessageSender implements MessageSender { diff --git a/apps/desktop/src/platform/utils/from-ipc-messaging.ts b/apps/desktop/src/platform/utils/from-ipc-messaging.ts index cdefbf5c506..f9c01c9487f 100644 --- a/apps/desktop/src/platform/utils/from-ipc-messaging.ts +++ b/apps/desktop/src/platform/utils/from-ipc-messaging.ts @@ -1,6 +1,8 @@ import { fromEventPattern, share } from "rxjs"; import { Message } from "@bitwarden/common/platform/messaging"; +// 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 { tagAsExternal } from "@bitwarden/common/platform/messaging/internal"; /** diff --git a/eslint.config.mjs b/eslint.config.mjs index 7928224dc00..aa0b475da0b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -278,14 +278,254 @@ export default tseslint.config( ]), }, }, - - /// Team overrides + /// Bandaids for keeping existing circular dependencies from getting worse and new ones from being created + /// Will be removed after Nx is implemented and existing circular dependencies are removed. { - files: ["**/src/platform/**/*.ts"], + files: ["libs/common/src/**/*.ts"], rules: { - "no-restricted-imports": buildNoRestrictedImports([], true), + "no-restricted-imports": buildNoRestrictedImports([ + // Common is at the base level - should not import from other libs except shared + "@bitwarden/admin-console", + "@bitwarden/angular", + "@bitwarden/auth", + "@bitwarden/billing", + "@bitwarden/components", + "@bitwarden/importer", + "@bitwarden/key-management", + "@bitwarden/key-management-ui", + "@bitwarden/node", + "@bitwarden/platform", + "@bitwarden/tools", + "@bitwarden/ui", + "@bitwarden/vault", + ]), }, }, + { + files: ["libs/shared/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + // Shared shouldnt have deps + "@bitwarden/admin-console", + "@bitwarden/angular", + "@bitwarden/auth", + "@bitwarden/billing", + "@bitwarden/common", + "@bitwarden/components", + "@bitwarden/importer", + "@bitwarden/key-management", + "@bitwarden/key-management-ui", + "@bitwarden/node", + "@bitwarden/platform", + "@bitwarden/tools", + "@bitwarden/ui", + "@bitwarden/vault", + ]), + }, + }, + { + files: ["libs/auth/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + // Auth can only depend on common, shared, angular, node, platform, eslint + "@bitwarden/admin-console", + "@bitwarden/billing", + "@bitwarden/components", + "@bitwarden/importer", + "@bitwarden/key-management-ui", + "@bitwarden/tools", + "@bitwarden/ui", + "@bitwarden/vault", + ]), + }, + }, + { + files: ["libs/key-management/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + // Key management can depend on common, node, angular, components, eslint, platform, ui + "@bitwarden/auth", + "@bitwarden/admin-console", + "@bitwarden/billing", + "@bitwarden/importer", + "@bitwarden/key-management-ui", + "@bitwarden/tools", + "@bitwarden/vault", + ]), + }, + }, + { + files: ["libs/billing/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + // Billing can depend on auth, common, angular, components, eslint, node, platform, ui + "@bitwarden/admin-console", + "@bitwarden/importer", + "@bitwarden/key-management-ui", + "@bitwarden/tools", + "@bitwarden/vault", + ]), + }, + }, + { + files: ["libs/components/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + // Components can depend on common, shared + "@bitwarden/admin-console", + "@bitwarden/auth", + "@bitwarden/billing", + "@bitwarden/eslint", + "@bitwarden/importer", + "@bitwarden/key-management-ui", + "@bitwarden/node", + "@bitwarden/tools", + "@bitwarden/vault", + ]), + }, + }, + { + files: ["libs/ui/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + // UI can depend on common, shared, auth + "@bitwarden/admin-console", + "@bitwarden/billing", + "@bitwarden/importer", + "@bitwarden/key-management-ui", + "@bitwarden/node", + "@bitwarden/platform", + "@bitwarden/tools", + "@bitwarden/vault", + ]), + }, + }, + { + files: ["libs/key-management-ui/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + // Key-management-ui can depend on key-management, common, angular, shared, auth, components, ui, eslint + "@bitwarden/admin-console", + "@bitwarden/billing", + "@bitwarden/importer", + "@bitwarden/node", + "@bitwarden/platform", + "@bitwarden/tools", + "@bitwarden/vault", + ]), + }, + }, + { + files: ["libs/angular/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + // Angular can depend on common, shared, components, ui + "@bitwarden/admin-console", + "@bitwarden/auth", + "@bitwarden/billing", + "@bitwarden/importer", + "@bitwarden/key-management-ui", + "@bitwarden/node", + "@bitwarden/platform", + "@bitwarden/tools", + "@bitwarden/vault", + ]), + }, + }, + { + files: ["libs/vault/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + // Vault can depend on most libs + "@bitwarden/admin-console", + "@bitwarden/importer", + "@bitwarden/tools", + ]), + }, + }, + { + files: ["libs/admin-console/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + // Admin console can depend on all libs + ]), + }, + }, + { + files: ["libs/tools/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + // Tools can depend on most libs + "@bitwarden/admin-console", + ]), + }, + }, + { + files: ["libs/platform/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + // Platform cant depend on most libs + "@bitwarden/admin-console", + "@bitwarden/auth", + "@bitwarden/billing", + "@bitwarden/importer", + "@bitwarden/key-management", + "@bitwarden/key-management-ui", + "@bitwarden/tools", + "@bitwarden/vault", + ]), + }, + }, + { + files: ["libs/importer/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + // Importer can depend on most libs but not other domain libs + "@bitwarden/admin-console", + "@bitwarden/tools", + ]), + }, + }, + { + files: ["libs/eslint/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + // ESLint should not depend on app code + "@bitwarden/admin-console", + "@bitwarden/angular", + "@bitwarden/auth", + "@bitwarden/billing", + "@bitwarden/components", + "@bitwarden/importer", + "@bitwarden/key-management", + "@bitwarden/key-management-ui", + "@bitwarden/node", + "@bitwarden/platform", + "@bitwarden/tools", + "@bitwarden/ui", + "@bitwarden/vault", + ]), + }, + }, + { + files: ["libs/node/src/**/*.ts"], + rules: { + "no-restricted-imports": buildNoRestrictedImports([ + // Node can depend on common, shared, auth + "@bitwarden/admin-console", + "@bitwarden/angular", + "@bitwarden/components", + "@bitwarden/importer", + "@bitwarden/key-management-ui", + "@bitwarden/platform", + "@bitwarden/tools", + "@bitwarden/ui", + "@bitwarden/vault", + ]), + }, + }, + + /// Team overrides { files: [ "apps/cli/src/admin-console/**/*.ts", diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 8ae90705f92..9d36ab4619f 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -3,6 +3,8 @@ import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { firstValueFrom, map } 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 import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; diff --git a/libs/angular/src/auth/components/base-login-via-webauthn.component.ts b/libs/angular/src/auth/components/base-login-via-webauthn.component.ts index 5d30fc997dc..53e29d4d940 100644 --- a/libs/angular/src/auth/components/base-login-via-webauthn.component.ts +++ b/libs/angular/src/auth/components/base-login-via-webauthn.component.ts @@ -4,6 +4,8 @@ import { Directive, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom } 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 import { LoginSuccessHandlerService } from "@bitwarden/auth/common"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view"; diff --git a/libs/angular/src/auth/components/environment-selector.component.ts b/libs/angular/src/auth/components/environment-selector.component.ts index b61feedd306..1831e513301 100644 --- a/libs/angular/src/auth/components/environment-selector.component.ts +++ b/libs/angular/src/auth/components/environment-selector.component.ts @@ -4,6 +4,8 @@ import { Component, EventEmitter, Output, Input, OnInit, OnDestroy } from "@angu import { ActivatedRoute } from "@angular/router"; import { Observable, map, Subject, takeUntil } 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 import { SelfHostedEnvConfigDialogComponent } from "@bitwarden/auth/angular"; import { EnvironmentService, diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index 230be90b7a4..53f6abaa33c 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -5,10 +5,14 @@ import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom, of } from "rxjs"; import { filter, first, switchMap, tap } from "rxjs/operators"; +// 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 { OrganizationUserApiService, OrganizationUserResetPasswordEnrollmentRequest, } 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 import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; diff --git a/libs/angular/src/auth/components/set-pin.component.ts b/libs/angular/src/auth/components/set-pin.component.ts index d2fceb34bb9..fba6ce2da86 100644 --- a/libs/angular/src/auth/components/set-pin.component.ts +++ b/libs/angular/src/auth/components/set-pin.component.ts @@ -4,6 +4,8 @@ import { Directive, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { firstValueFrom } 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 import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; diff --git a/libs/angular/src/auth/guards/active-auth.guard.spec.ts b/libs/angular/src/auth/guards/active-auth.guard.spec.ts index c3417b9d41d..566b2abc72a 100644 --- a/libs/angular/src/auth/guards/active-auth.guard.spec.ts +++ b/libs/angular/src/auth/guards/active-auth.guard.spec.ts @@ -5,6 +5,8 @@ import { RouterTestingModule } from "@angular/router/testing"; import { MockProxy, mock } from "jest-mock-extended"; import { BehaviorSubject } 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 import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; diff --git a/libs/angular/src/auth/guards/active-auth.guard.ts b/libs/angular/src/auth/guards/active-auth.guard.ts index 56213bbd979..83c648ae110 100644 --- a/libs/angular/src/auth/guards/active-auth.guard.ts +++ b/libs/angular/src/auth/guards/active-auth.guard.ts @@ -2,6 +2,8 @@ import { inject } from "@angular/core"; import { CanActivateFn, Router } from "@angular/router"; import { firstValueFrom } 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 import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; diff --git a/libs/angular/src/auth/services/device-trust-toast.service.implementation.ts b/libs/angular/src/auth/services/device-trust-toast.service.implementation.ts index 4013cf8df96..31e90548a7a 100644 --- a/libs/angular/src/auth/services/device-trust-toast.service.implementation.ts +++ b/libs/angular/src/auth/services/device-trust-toast.service.implementation.ts @@ -1,5 +1,7 @@ import { merge, Observable, tap } 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 import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; diff --git a/libs/angular/src/auth/services/device-trust-toast.service.spec.ts b/libs/angular/src/auth/services/device-trust-toast.service.spec.ts index 96b7db9d0ce..10ad3203b90 100644 --- a/libs/angular/src/auth/services/device-trust-toast.service.spec.ts +++ b/libs/angular/src/auth/services/device-trust-toast.service.spec.ts @@ -1,6 +1,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import { EMPTY, of } 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 import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts index 198cc7dc3a5..51827bfb9f2 100644 --- a/libs/angular/src/components/share.component.ts +++ b/libs/angular/src/components/share.component.ts @@ -3,6 +3,8 @@ import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { firstValueFrom, map, Observable, Subject, takeUntil } 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 import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index d82ff021962..4c29abe680a 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Observable, 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 import { LogoutReason } from "@bitwarden/auth/common"; import { ClientType } from "@bitwarden/common/enums"; import { VaultTimeout } from "@bitwarden/common/key-management/vault-timeout"; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index a8638efba18..08bcaa2165c 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -3,12 +3,16 @@ import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core"; import { 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 import { CollectionService, DefaultCollectionService, DefaultOrganizationUserApiService, OrganizationUserApiService, } 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 import { AnonLayoutWrapperDataService, DefaultAnonLayoutWrapperDataService, @@ -30,6 +34,8 @@ import { ChangePasswordService, DefaultChangePasswordService, } from "@bitwarden/auth/angular"; +// 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 { AuthRequestApiService, AuthRequestService, @@ -316,6 +322,8 @@ import { UserAsymmetricKeysRegenerationService, } from "@bitwarden/key-management"; import { SafeInjectionToken } from "@bitwarden/ui-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 import { PasswordRepromptService } from "@bitwarden/vault"; import { IndividualVaultExportService, diff --git a/libs/angular/src/services/view-password-history.service.spec.ts b/libs/angular/src/services/view-password-history.service.spec.ts index dec2b25b190..121aa7dc472 100644 --- a/libs/angular/src/services/view-password-history.service.spec.ts +++ b/libs/angular/src/services/view-password-history.service.spec.ts @@ -3,6 +3,8 @@ import { TestBed } from "@angular/core/testing"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; +// 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 { openPasswordHistoryDialog } from "@bitwarden/vault"; import { VaultViewPasswordHistoryService } from "./view-password-history.service"; diff --git a/libs/angular/src/services/view-password-history.service.ts b/libs/angular/src/services/view-password-history.service.ts index 88ca4d37287..ab4d6d4ddf1 100644 --- a/libs/angular/src/services/view-password-history.service.ts +++ b/libs/angular/src/services/view-password-history.service.ts @@ -3,6 +3,8 @@ import { Injectable } from "@angular/core"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; +// 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 { openPasswordHistoryDialog } from "@bitwarden/vault"; /** diff --git a/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts b/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts index 0a7d6397a04..3e82641fe90 100644 --- a/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts +++ b/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Observable } 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 import { CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index b04adc1fdfb..8175372cae5 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -4,6 +4,8 @@ import { DatePipe } from "@angular/common"; import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { concatMap, firstValueFrom, map, Observable, Subject, switchMap, takeUntil } 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 import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -40,6 +42,8 @@ import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { generate_ssh_key } from "@bitwarden/sdk-internal"; +// 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 { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault"; @Directive() diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index fd3f92c6c73..ac14c092490 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -54,6 +54,8 @@ import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cip import { TotpInfo } from "@bitwarden/common/vault/services/totp.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +// 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 { PasswordRepromptService } from "@bitwarden/vault"; const BroadcasterSubscriptionId = "BaseViewComponent"; diff --git a/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts b/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts index 862beb333d4..30bbd153c5e 100644 --- a/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts +++ b/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts @@ -3,6 +3,8 @@ import { Observable, combineLatest, from, of } from "rxjs"; import { catchError, map } from "rxjs/operators"; import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; +// 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 { PinServiceAbstraction } from "@bitwarden/auth/common"; import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; diff --git a/libs/angular/src/vault/services/custom-nudges-services/empty-vault-nudge.service.ts b/libs/angular/src/vault/services/custom-nudges-services/empty-vault-nudge.service.ts index a0e25aa841c..d579f8b1bb2 100644 --- a/libs/angular/src/vault/services/custom-nudges-services/empty-vault-nudge.service.ts +++ b/libs/angular/src/vault/services/custom-nudges-services/empty-vault-nudge.service.ts @@ -1,6 +1,8 @@ import { inject, Injectable } from "@angular/core"; import { combineLatest, Observable, of, switchMap } 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 import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; diff --git a/libs/angular/src/vault/services/nudges.service.spec.ts b/libs/angular/src/vault/services/nudges.service.spec.ts index 4edd57f5428..30e2ada6007 100644 --- a/libs/angular/src/vault/services/nudges.service.spec.ts +++ b/libs/angular/src/vault/services/nudges.service.spec.ts @@ -2,6 +2,8 @@ import { TestBed } from "@angular/core/testing"; import { mock } from "jest-mock-extended"; import { firstValueFrom, of } 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 import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; diff --git a/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts b/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts index d104026f2f6..feaaf74c96d 100644 --- a/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts +++ b/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Directive, EventEmitter, Input, Output } from "@angular/core"; +// 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 { CollectionView } from "@bitwarden/admin-console/common"; import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node"; diff --git a/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts b/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts index 0ce63c03f61..83304f8eae9 100644 --- a/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts +++ b/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts @@ -3,6 +3,8 @@ import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { firstValueFrom, Observable } 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 import { CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node"; diff --git a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts index 6c3ac21b162..9e3312b38ef 100644 --- a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts +++ b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts @@ -3,6 +3,8 @@ import { Injectable } from "@angular/core"; import { firstValueFrom, from, map, mergeMap, Observable, switchMap, take } 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 import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts index 04dc3b6dfd2..17f5ec8e3c6 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts @@ -6,6 +6,8 @@ import { Subject, filter, switchMap, takeUntil, tap } from "rxjs"; import { AnonLayoutComponent } from "@bitwarden/auth/angular"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +// 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 { Icon, Translation } from "@bitwarden/components"; import { AnonLayoutWrapperDataService } from "./anon-layout-wrapper-data.service"; diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts index 9f504c75d29..f106f9ee0dc 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts @@ -15,6 +15,8 @@ import { Environment, } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// 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 { ButtonModule } from "@bitwarden/components"; // FIXME: remove `/apps` import from `/libs` diff --git a/libs/auth/src/angular/change-password/change-password.component.ts b/libs/auth/src/angular/change-password/change-password.component.ts index 51c4d03d16f..86b7c884e92 100644 --- a/libs/auth/src/angular/change-password/change-password.component.ts +++ b/libs/auth/src/angular/change-password/change-password.component.ts @@ -8,6 +8,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { SyncService } from "@bitwarden/common/platform/sync"; import { UserId } from "@bitwarden/common/types/guid"; +// 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 { ToastService } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; diff --git a/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.ts b/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.ts index d8d76930302..17d7b343db9 100644 --- a/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.ts +++ b/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.ts @@ -1,6 +1,8 @@ import { Component, Inject } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +// 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 { DIALOG_DATA, ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; export type FingerprintDialogData = { diff --git a/libs/auth/src/angular/icons/bitwarden-logo.icon.ts b/libs/auth/src/angular/icons/bitwarden-logo.icon.ts index 2a1ae48526b..2df07c45ff9 100644 --- a/libs/auth/src/angular/icons/bitwarden-logo.icon.ts +++ b/libs/auth/src/angular/icons/bitwarden-logo.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const BitwardenLogo = svgIcon` diff --git a/libs/auth/src/angular/icons/bitwarden-shield.icon.ts b/libs/auth/src/angular/icons/bitwarden-shield.icon.ts index 86e3a0bb1b2..f40dc97e5ee 100644 --- a/libs/auth/src/angular/icons/bitwarden-shield.icon.ts +++ b/libs/auth/src/angular/icons/bitwarden-shield.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const BitwardenShield = svgIcon` diff --git a/libs/auth/src/angular/icons/device-verification.icon.ts b/libs/auth/src/angular/icons/device-verification.icon.ts index b1be4efdfb3..6c5313a8705 100644 --- a/libs/auth/src/angular/icons/device-verification.icon.ts +++ b/libs/auth/src/angular/icons/device-verification.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const DeviceVerificationIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/devices.icon.ts b/libs/auth/src/angular/icons/devices.icon.ts index 54acea5b087..dd268f0e6e2 100644 --- a/libs/auth/src/angular/icons/devices.icon.ts +++ b/libs/auth/src/angular/icons/devices.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const DevicesIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/lock.icon.ts b/libs/auth/src/angular/icons/lock.icon.ts index 198733d0dca..43ea2509e19 100644 --- a/libs/auth/src/angular/icons/lock.icon.ts +++ b/libs/auth/src/angular/icons/lock.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const LockIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/registration-check-email.icon.ts b/libs/auth/src/angular/icons/registration-check-email.icon.ts index 6f7dd6a2d63..d32964d8cb1 100644 --- a/libs/auth/src/angular/icons/registration-check-email.icon.ts +++ b/libs/auth/src/angular/icons/registration-check-email.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const RegistrationCheckEmailIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/registration-expired-link.icon.ts b/libs/auth/src/angular/icons/registration-expired-link.icon.ts index 3323c7f0b2b..099cf16d1d8 100644 --- a/libs/auth/src/angular/icons/registration-expired-link.icon.ts +++ b/libs/auth/src/angular/icons/registration-expired-link.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const RegistrationExpiredLinkIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/registration-lock-alt.icon.ts b/libs/auth/src/angular/icons/registration-lock-alt.icon.ts index 511f9710dc6..d312e909413 100644 --- a/libs/auth/src/angular/icons/registration-lock-alt.icon.ts +++ b/libs/auth/src/angular/icons/registration-lock-alt.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const RegistrationLockAltIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/registration-user-add.icon.ts b/libs/auth/src/angular/icons/registration-user-add.icon.ts index 69240cd0298..4f68453639d 100644 --- a/libs/auth/src/angular/icons/registration-user-add.icon.ts +++ b/libs/auth/src/angular/icons/registration-user-add.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const RegistrationUserAddIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/sso-key.icon.ts b/libs/auth/src/angular/icons/sso-key.icon.ts index 38ae8a66525..e00c3555906 100644 --- a/libs/auth/src/angular/icons/sso-key.icon.ts +++ b/libs/auth/src/angular/icons/sso-key.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const SsoKeyIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-authenticator.icon.ts b/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-authenticator.icon.ts index daef1f94dca..4961e7207cb 100644 --- a/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-authenticator.icon.ts +++ b/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-authenticator.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const TwoFactorAuthAuthenticatorIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-duo.icon.ts b/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-duo.icon.ts index c81433d0fc1..a64027c5fba 100644 --- a/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-duo.icon.ts +++ b/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-duo.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const TwoFactorAuthDuoIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-email.icon.ts b/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-email.icon.ts index 833ab3f8e98..380bc16a738 100644 --- a/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-email.icon.ts +++ b/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-email.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const TwoFactorAuthEmailIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-security-key.icon.ts b/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-security-key.icon.ts index f6ac90cfd5d..573dc890428 100644 --- a/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-security-key.icon.ts +++ b/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-security-key.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const TwoFactorAuthSecurityKeyIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-webauthn.icon.ts b/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-webauthn.icon.ts index 233533fc807..ff73bf2e255 100644 --- a/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-webauthn.icon.ts +++ b/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-webauthn.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const TwoFactorAuthWebAuthnIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-yubico.icon.ts b/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-yubico.icon.ts index 6bb989a9d15..8f6dca837ad 100644 --- a/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-yubico.icon.ts +++ b/libs/auth/src/angular/icons/two-factor-auth/two-factor-auth-yubico.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const TwoFactorAuthYubicoIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/two-factor-timeout.icon.ts b/libs/auth/src/angular/icons/two-factor-timeout.icon.ts index 71d0aa549dc..3681670c124 100644 --- a/libs/auth/src/angular/icons/two-factor-timeout.icon.ts +++ b/libs/auth/src/angular/icons/two-factor-timeout.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const TwoFactorTimeoutIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/user-lock.icon.ts b/libs/auth/src/angular/icons/user-lock.icon.ts index e85eac6fc2d..bc4fdd9e268 100644 --- a/libs/auth/src/angular/icons/user-lock.icon.ts +++ b/libs/auth/src/angular/icons/user-lock.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const UserLockIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/user-verification-biometrics-fingerprint.icon.ts b/libs/auth/src/angular/icons/user-verification-biometrics-fingerprint.icon.ts index f661f9330b1..e329d889574 100644 --- a/libs/auth/src/angular/icons/user-verification-biometrics-fingerprint.icon.ts +++ b/libs/auth/src/angular/icons/user-verification-biometrics-fingerprint.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const UserVerificationBiometricsIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/vault.icon.ts b/libs/auth/src/angular/icons/vault.icon.ts index e23944ab7db..a341bcd99f5 100644 --- a/libs/auth/src/angular/icons/vault.icon.ts +++ b/libs/auth/src/angular/icons/vault.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const VaultIcon = svgIcon` diff --git a/libs/auth/src/angular/icons/wave.icon.ts b/libs/auth/src/angular/icons/wave.icon.ts index 3e4483c1e05..5629c43fc8d 100644 --- a/libs/auth/src/angular/icons/wave.icon.ts +++ b/libs/auth/src/angular/icons/wave.icon.ts @@ -1,3 +1,5 @@ +// 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 { svgIcon } from "@bitwarden/components"; export const WaveIcon = svgIcon` diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index 135a6cfaaa8..8b0b6e4c159 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -17,6 +17,8 @@ import { HashPurpose } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +// 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 { AsyncActionsModule, ButtonModule, diff --git a/libs/auth/src/angular/input-password/input-password.stories.ts b/libs/auth/src/angular/input-password/input-password.stories.ts index 84ba39e2916..708b74b9925 100644 --- a/libs/auth/src/angular/input-password/input-password.stories.ts +++ b/libs/auth/src/angular/input-password/input-password.stories.ts @@ -14,6 +14,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +// 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 { DialogService, ToastService } from "@bitwarden/components"; import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management"; diff --git a/libs/auth/src/angular/login-approval/login-approval.component.spec.ts b/libs/auth/src/angular/login-approval/login-approval.component.spec.ts index 5fcc06c8476..afc2c67de12 100644 --- a/libs/auth/src/angular/login-approval/login-approval.component.spec.ts +++ b/libs/auth/src/angular/login-approval/login-approval.component.spec.ts @@ -14,6 +14,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { UserId } from "@bitwarden/common/types/guid"; +// 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 { DialogRef, DIALOG_DATA, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; diff --git a/libs/auth/src/angular/login-approval/login-approval.component.ts b/libs/auth/src/angular/login-approval/login-approval.component.ts index 43d9af64481..784bd93002e 100644 --- a/libs/auth/src/angular/login-approval/login-approval.component.ts +++ b/libs/auth/src/angular/login-approval/login-approval.component.ts @@ -17,6 +17,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +// 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 { DIALOG_DATA, DialogRef, diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts index 3ea9416b7e2..c49e54f8c19 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts @@ -26,6 +26,8 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { UserId } from "@bitwarden/common/types/guid"; +// 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 { AsyncActionsModule, ButtonModule, diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts index d74deb443f5..f23d49f0015 100644 --- a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts @@ -34,6 +34,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserId } from "@bitwarden/common/types/guid"; +// 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 { ButtonModule, LinkModule, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; diff --git a/libs/auth/src/angular/login/login-secondary-content.component.ts b/libs/auth/src/angular/login/login-secondary-content.component.ts index b608542b375..fba5e70d93c 100644 --- a/libs/auth/src/angular/login/login-secondary-content.component.ts +++ b/libs/auth/src/angular/login/login-secondary-content.component.ts @@ -4,6 +4,8 @@ import { RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { DefaultServerSettingsService } from "@bitwarden/common/platform/services/default-server-settings.service"; +// 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 { LinkModule } from "@bitwarden/components"; @Component({ diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index d5180f56785..8674453cf10 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -29,6 +29,8 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { UserId } from "@bitwarden/common/types/guid"; +// 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 { AsyncActionsModule, ButtonModule, diff --git a/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts index a2b0b23d05c..5d3ecc1751d 100644 --- a/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts +++ b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts @@ -9,6 +9,8 @@ import { LoginSuccessHandlerService } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +// 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 { AsyncActionsModule, ButtonModule, diff --git a/libs/auth/src/angular/password-callout/password-callout.component.ts b/libs/auth/src/angular/password-callout/password-callout.component.ts index 6968f384f07..03fba52336f 100644 --- a/libs/auth/src/angular/password-callout/password-callout.component.ts +++ b/libs/auth/src/angular/password-callout/password-callout.component.ts @@ -6,6 +6,8 @@ import { Component, Input } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +// 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 { CalloutModule } from "@bitwarden/components"; @Component({ diff --git a/libs/auth/src/angular/password-callout/password-callout.stories.ts b/libs/auth/src/angular/password-callout/password-callout.stories.ts index ce5b698b2e8..58862049e10 100644 --- a/libs/auth/src/angular/password-callout/password-callout.stories.ts +++ b/libs/auth/src/angular/password-callout/password-callout.stories.ts @@ -2,6 +2,8 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +// 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 { I18nMockService } from "@bitwarden/components"; import { PasswordCalloutComponent } from "./password-callout.component"; diff --git a/libs/auth/src/angular/password-hint/password-hint.component.ts b/libs/auth/src/angular/password-hint/password-hint.component.ts index cf24c68e10d..99eb06293e0 100644 --- a/libs/auth/src/angular/password-hint/password-hint.component.ts +++ b/libs/auth/src/angular/password-hint/password-hint.component.ts @@ -13,6 +13,8 @@ import { PasswordHintRequest } from "@bitwarden/common/auth/models/request/passw import { ClientType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// 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 { AsyncActionsModule, ButtonModule, diff --git a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts index 86f08e79748..8aac5f73ffe 100644 --- a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts +++ b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts @@ -15,6 +15,8 @@ import { } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// 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 { DialogService, FormFieldModule, SelectModule, ToastService } from "@bitwarden/components"; import { SelfHostedEnvConfigDialogComponent } from "../../self-hosted-env-config-dialog/self-hosted-env-config-dialog.component"; diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts index 981f7e554fb..353e3772c41 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts @@ -14,6 +14,8 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response" import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +// 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 { ToastService } from "@bitwarden/components"; import { diff --git a/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.ts b/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.ts index ef032b72562..d4ecade52fe 100644 --- a/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.ts +++ b/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.ts @@ -6,6 +6,8 @@ import { ActivatedRoute, RouterModule } from "@angular/router"; import { Subject, firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +// 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 { ButtonModule, IconModule } from "@bitwarden/components"; import { RegistrationExpiredLinkIcon } from "../../icons/registration-expired-link.icon"; diff --git a/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts b/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts index 4b13c6666e2..bc873593a83 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts @@ -6,6 +6,8 @@ import { ActivatedRoute, RouterModule } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +// 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 { LinkModule } from "@bitwarden/components"; /** diff --git a/libs/auth/src/angular/registration/registration-start/registration-start.component.ts b/libs/auth/src/angular/registration/registration-start/registration-start.component.ts index 44d1d720a8d..ea756c967c6 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start.component.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start.component.ts @@ -11,6 +11,8 @@ import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-a import { RegisterSendVerificationEmailRequest } from "@bitwarden/common/auth/models/request/registration/register-send-verification-email.request"; import { RegionConfig, Region } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// 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 { AsyncActionsModule, ButtonModule, diff --git a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts index 6047cc3d27a..e54e59a988a 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts @@ -15,6 +15,8 @@ import { Urls, } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// 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 { AsyncActionsModule, ButtonModule, diff --git a/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts b/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts index 64478d63447..189c669423d 100644 --- a/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts +++ b/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts @@ -16,6 +16,8 @@ import { EnvironmentService, Region, } from "@bitwarden/common/platform/abstractions/environment.service"; +// 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 { DialogRef, AsyncActionsModule, diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts index 6001cb39085..95d54d589bc 100644 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts +++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts @@ -1,6 +1,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { BehaviorSubject, of } 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 import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { FakeUserDecryptionOptions as UserDecryptionOptions, diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts index a4bc5f69b4c..ec274b9c4af 100644 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts +++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { firstValueFrom } 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 import { OrganizationUserApiService, OrganizationUserResetPasswordEnrollmentRequest, diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts index e0bdf8c26a1..f5a138af584 100644 --- a/libs/auth/src/angular/sso/sso.component.ts +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -33,6 +33,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +// 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 { AsyncActionsModule, ButtonModule, diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.ts index d8735d3fd54..cd4df5aee68 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.ts @@ -3,6 +3,8 @@ import { Component, Input, Output, EventEmitter } from "@angular/core"; import { ReactiveFormsModule, FormsModule, FormControl } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +// 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 { DialogModule, ButtonModule, diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo.component.ts index 96fee95940b..22d3906bb48 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo.component.ts @@ -6,6 +6,8 @@ import { ReactiveFormsModule, FormsModule } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// 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 { DialogModule, ButtonModule, diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts index 25235017bd1..2fff77b720e 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts @@ -12,6 +12,8 @@ import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.ser import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// 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 { DialogModule, ButtonModule, diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.ts index 4efab4629c3..a8b435375db 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.ts @@ -13,6 +13,8 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// 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 { DialogModule, ButtonModule, diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-yubikey.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-yubikey.component.ts index f334766bba9..abcbd557e0f 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-yubikey.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-yubikey.component.ts @@ -3,6 +3,8 @@ import { Component, Input } from "@angular/core"; import { ReactiveFormsModule, FormsModule, FormControl } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +// 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 { DialogModule, ButtonModule, diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts index e52f08941d4..a2769e37c87 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts @@ -34,6 +34,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; +// 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 { DialogService, ToastService } from "@bitwarden/components"; import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service"; diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index b14a368e066..57637fe9118 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -38,6 +38,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { UserId } from "@bitwarden/common/types/guid"; +// 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 { AsyncActionsModule, ButtonModule, diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-options.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-options.component.ts index ce6cef0d670..fb0d7101cad 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-options.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-options.component.ts @@ -7,6 +7,8 @@ import { TwoFactorService, } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; +// 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 { DialogRef, ButtonModule, diff --git a/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts b/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts index 58b1dca4cd1..8bb2f987d77 100644 --- a/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts +++ b/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts @@ -10,6 +10,8 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// 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 { DIALOG_DATA, DialogRef, diff --git a/libs/auth/src/angular/user-verification/user-verification-dialog.types.ts b/libs/auth/src/angular/user-verification/user-verification-dialog.types.ts index cb03f4e18f7..93ae6d7d77f 100644 --- a/libs/auth/src/angular/user-verification/user-verification-dialog.types.ts +++ b/libs/auth/src/angular/user-verification/user-verification-dialog.types.ts @@ -1,4 +1,6 @@ import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification"; +// 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 { ButtonType } from "@bitwarden/components"; /** diff --git a/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts b/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts index ff4af51f732..7ea191ba2f9 100644 --- a/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts +++ b/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts @@ -19,6 +19,8 @@ import { UserVerificationOptions } from "@bitwarden/common/auth/types/user-verif import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +// 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 { AsyncActionsModule, ButtonModule, diff --git a/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts b/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts index 82bc53bb147..c9f83902fe2 100644 --- a/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts +++ b/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts @@ -30,6 +30,8 @@ import { VaultTimeoutSettingsService, } from "@bitwarden/common/key-management/vault-timeout"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +// 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 { FormFieldModule, SelectModule } from "@bitwarden/components"; type VaultTimeoutForm = FormGroup<{ diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index e4453359015..49543e2f2ce 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +// 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 { CollectionAccessDetailsResponse, CollectionDetailsResponse, diff --git a/libs/common/src/admin-console/models/response/organization-export.response.ts b/libs/common/src/admin-console/models/response/organization-export.response.ts index 6e42fe14c0d..19a8dd9ad94 100644 --- a/libs/common/src/admin-console/models/response/organization-export.response.ts +++ b/libs/common/src/admin-console/models/response/organization-export.response.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +// 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 { CollectionResponse } from "@bitwarden/admin-console/common"; import { BaseResponse } from "../../../models/response/base.response"; diff --git a/libs/common/src/admin-console/models/response/provider/provider-user-bulk-public-key.response.ts b/libs/common/src/admin-console/models/response/provider/provider-user-bulk-public-key.response.ts index 1f497dd8c5f..7e6a2b2a505 100644 --- a/libs/common/src/admin-console/models/response/provider/provider-user-bulk-public-key.response.ts +++ b/libs/common/src/admin-console/models/response/provider/provider-user-bulk-public-key.response.ts @@ -1,3 +1,5 @@ +// 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 { OrganizationUserBulkPublicKeyResponse } from "@bitwarden/admin-console/common"; export class ProviderUserBulkPublicKeyResponse extends OrganizationUserBulkPublicKeyResponse {} diff --git a/libs/common/src/auth/models/request/registration/register-finish.request.ts b/libs/common/src/auth/models/request/registration/register-finish.request.ts index 645bcf05b1b..bb577053c5c 100644 --- a/libs/common/src/auth/models/request/registration/register-finish.request.ts +++ b/libs/common/src/auth/models/request/registration/register-finish.request.ts @@ -1,3 +1,5 @@ +// 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 { KdfType } from "@bitwarden/key-management"; import { KeysRequest } from "../../../../models/request/keys.request"; diff --git a/libs/common/src/auth/models/request/set-password.request.ts b/libs/common/src/auth/models/request/set-password.request.ts index 5aa74068591..7206cd98623 100644 --- a/libs/common/src/auth/models/request/set-password.request.ts +++ b/libs/common/src/auth/models/request/set-password.request.ts @@ -1,3 +1,5 @@ +// 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 { KdfType } from "@bitwarden/key-management"; import { KeysRequest } from "../../../models/request/keys.request"; diff --git a/libs/common/src/auth/models/request/update-tde-offboarding-password.request.ts b/libs/common/src/auth/models/request/update-tde-offboarding-password.request.ts index 8c90fa379b4..8a107aa6c32 100644 --- a/libs/common/src/auth/models/request/update-tde-offboarding-password.request.ts +++ b/libs/common/src/auth/models/request/update-tde-offboarding-password.request.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +// 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 { OrganizationUserResetPasswordRequest } from "@bitwarden/admin-console/common"; export class UpdateTdeOffboardingPasswordRequest extends OrganizationUserResetPasswordRequest { diff --git a/libs/common/src/auth/models/request/update-temp-password.request.ts b/libs/common/src/auth/models/request/update-temp-password.request.ts index 8f922f9008b..39d6707e198 100644 --- a/libs/common/src/auth/models/request/update-temp-password.request.ts +++ b/libs/common/src/auth/models/request/update-temp-password.request.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +// 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 { OrganizationUserResetPasswordRequest } from "@bitwarden/admin-console/common"; export class UpdateTempPasswordRequest extends OrganizationUserResetPasswordRequest { diff --git a/libs/common/src/auth/models/response/identity-token.response.ts b/libs/common/src/auth/models/response/identity-token.response.ts index 5f128d340bf..3e2896eec64 100644 --- a/libs/common/src/auth/models/response/identity-token.response.ts +++ b/libs/common/src/auth/models/response/identity-token.response.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +// 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 { KdfType } from "@bitwarden/key-management"; import { BaseResponse } from "../../../models/response/base.response"; diff --git a/libs/common/src/auth/models/response/prelogin.response.ts b/libs/common/src/auth/models/response/prelogin.response.ts index b5ca78c3b79..e7c962242ce 100644 --- a/libs/common/src/auth/models/response/prelogin.response.ts +++ b/libs/common/src/auth/models/response/prelogin.response.ts @@ -1,3 +1,5 @@ +// 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 { KdfType } from "@bitwarden/key-management"; import { BaseResponse } from "../../../models/response/base.response"; diff --git a/libs/common/src/auth/models/response/protected-device.response.ts b/libs/common/src/auth/models/response/protected-device.response.ts index 3cc3a3e0792..5695044c982 100644 --- a/libs/common/src/auth/models/response/protected-device.response.ts +++ b/libs/common/src/auth/models/response/protected-device.response.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +// 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 { RotateableKeySet } from "@bitwarden/auth/common"; import { DeviceType } from "../../../enums"; diff --git a/libs/common/src/auth/services/auth.service.spec.ts b/libs/common/src/auth/services/auth.service.spec.ts index fc236c91a21..5dcb8c372e5 100644 --- a/libs/common/src/auth/services/auth.service.spec.ts +++ b/libs/common/src/auth/services/auth.service.spec.ts @@ -1,6 +1,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom, of } 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 import { KeyService } from "@bitwarden/key-management"; import { diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index da70baf3999..9700efe02ca 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -11,6 +11,8 @@ import { switchMap, } 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 import { KeyService } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; diff --git a/libs/common/src/auth/services/master-password/master-password-api.service.spec.ts b/libs/common/src/auth/services/master-password/master-password-api.service.spec.ts index 64d4fdf1c7b..7a3b8762c13 100644 --- a/libs/common/src/auth/services/master-password/master-password-api.service.spec.ts +++ b/libs/common/src/auth/services/master-password/master-password-api.service.spec.ts @@ -2,6 +2,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +// 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 { KdfType } from "@bitwarden/key-management"; import { PasswordRequest } from "../../models/request/password.request"; diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts index 76c2d443d1d..d5b787d69f0 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts @@ -1,7 +1,11 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } 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 import { OrganizationUserApiService } 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 import { KeyService } from "@bitwarden/key-management"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts index ef9c5ad3265..f491d7d5eb0 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts @@ -2,10 +2,14 @@ // @ts-strict-ignore import { firstValueFrom, map } 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 import { OrganizationUserApiService, OrganizationUserResetPasswordEnrollmentRequest, } 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 import { KeyService } from "@bitwarden/key-management"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index a56853c479c..e67e522368f 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -3,6 +3,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom } 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 import { LogoutReason } from "@bitwarden/auth/common"; import { FakeSingleUserStateProvider, FakeGlobalStateProvider } from "../../../spec"; diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index 61c00f69215..2c6883272c3 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -3,6 +3,8 @@ import { Observable, combineLatest, firstValueFrom, map } from "rxjs"; import { Opaque } from "type-fest"; +// 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, decodeJwtTokenToJson } from "@bitwarden/auth/common"; import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts index d56dd6dda3b..33f276a87f2 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts @@ -1,12 +1,16 @@ import { mock } from "jest-mock-extended"; import { of } 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 import { PinLockType, PinServiceAbstraction, UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/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 import { BiometricsService, BiometricsStatus, diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index cfa6800deed..5837042b93f 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -2,7 +2,11 @@ // @ts-strict-ignore import { firstValueFrom, map } 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 import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/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 import { BiometricsService, BiometricsStatus, diff --git a/libs/common/src/auth/services/webauthn-login/webauthn-login.service.spec.ts b/libs/common/src/auth/services/webauthn-login/webauthn-login.service.spec.ts index 10444062349..56aa1139cda 100644 --- a/libs/common/src/auth/services/webauthn-login/webauthn-login.service.spec.ts +++ b/libs/common/src/auth/services/webauthn-login/webauthn-login.service.spec.ts @@ -1,5 +1,7 @@ import { mock } from "jest-mock-extended"; +// 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 { LoginStrategyServiceAbstraction, WebAuthnLoginCredentials } from "@bitwarden/auth/common"; import { LogService } from "../../../platform/abstractions/log.service"; diff --git a/libs/common/src/auth/services/webauthn-login/webauthn-login.service.ts b/libs/common/src/auth/services/webauthn-login/webauthn-login.service.ts index cea4bf29737..2d42329d27a 100644 --- a/libs/common/src/auth/services/webauthn-login/webauthn-login.service.ts +++ b/libs/common/src/auth/services/webauthn-login/webauthn-login.service.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +// 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 { LoginStrategyServiceAbstraction, WebAuthnLoginCredentials } from "@bitwarden/auth/common"; import { LogService } from "../../../platform/abstractions/log.service"; diff --git a/libs/common/src/auth/types/verification.ts b/libs/common/src/auth/types/verification.ts index 307a584fb36..4f45a6fdeed 100644 --- a/libs/common/src/auth/types/verification.ts +++ b/libs/common/src/auth/types/verification.ts @@ -1,3 +1,5 @@ +// 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 { KdfConfig } from "@bitwarden/key-management"; import { MasterKey } from "../../types/key"; diff --git a/libs/common/src/billing/services/organization-billing.service.spec.ts b/libs/common/src/billing/services/organization-billing.service.spec.ts index 7b194dff637..43457f810d1 100644 --- a/libs/common/src/billing/services/organization-billing.service.spec.ts +++ b/libs/common/src/billing/services/organization-billing.service.spec.ts @@ -12,6 +12,8 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SyncService } from "@bitwarden/common/platform/sync"; +// 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 { KeyService } from "@bitwarden/key-management"; describe("BillingAccountProfileStateService", () => { diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index fe5623fd5e6..b75134d28a8 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -5,6 +5,8 @@ import { Observable, of, switchMap } from "rxjs"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +// 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 { KeyService } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; diff --git a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts index 5e89e0a5cb7..84ebf981f03 100644 --- a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts +++ b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts @@ -2,7 +2,11 @@ // @ts-strict-ignore import { firstValueFrom, map, Observable, 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 import { RotateableKeySet, UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/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 import { KeyService } from "@bitwarden/key-management"; import { DeviceResponse } from "../../../auth/abstractions/devices/responses/device.response"; diff --git a/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts b/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts index de9de5d781a..c1b291c086a 100644 --- a/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts +++ b/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts @@ -3,11 +3,15 @@ import { matches, mock } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom, of } 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 import { UserDecryptionOptionsServiceAbstraction, UserDecryptionOptions, } from "@bitwarden/auth/common"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; +// 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 { KeyService } from "@bitwarden/key-management"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service"; diff --git a/libs/common/src/key-management/key-connector/models/set-key-connector-key.request.ts b/libs/common/src/key-management/key-connector/models/set-key-connector-key.request.ts index 14132ab79f2..fec38b1d72d 100644 --- a/libs/common/src/key-management/key-connector/models/set-key-connector-key.request.ts +++ b/libs/common/src/key-management/key-connector/models/set-key-connector-key.request.ts @@ -1,3 +1,5 @@ +// 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 { KdfConfig, KdfType } from "@bitwarden/key-management"; import { KeysRequest } from "../../../models/request/keys.request"; diff --git a/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts b/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts index d5443cfc52c..6049d4db5a1 100644 --- a/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts +++ b/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts @@ -3,6 +3,8 @@ import { firstValueFrom, of, timeout, TimeoutError } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; +// 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 { KeyService } from "@bitwarden/key-management"; import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; diff --git a/libs/common/src/key-management/key-connector/services/key-connector.service.ts b/libs/common/src/key-management/key-connector/services/key-connector.service.ts index fc0b64f707b..905bc42defe 100644 --- a/libs/common/src/key-management/key-connector/services/key-connector.service.ts +++ b/libs/common/src/key-management/key-connector/services/key-connector.service.ts @@ -2,8 +2,12 @@ // @ts-strict-ignore import { combineLatest, filter, firstValueFrom, Observable, of, switchMap } 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 import { LogoutReason } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +// 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 { Argon2KdfConfig, KdfConfig, diff --git a/libs/common/src/key-management/services/default-process-reload.service.ts b/libs/common/src/key-management/services/default-process-reload.service.ts index 860dac54855..e43fa5f0977 100644 --- a/libs/common/src/key-management/services/default-process-reload.service.ts +++ b/libs/common/src/key-management/services/default-process-reload.service.ts @@ -2,7 +2,11 @@ // @ts-strict-ignore import { firstValueFrom, map, timeout } 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 import { PinServiceAbstraction } from "@bitwarden/auth/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 import { BiometricStateService } from "@bitwarden/key-management"; import { AccountService } from "../../auth/abstractions/account.service"; diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.spec.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.spec.ts index b5e9544b01b..349aa474872 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.spec.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.spec.ts @@ -3,11 +3,15 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom, map, of } 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 import { PinServiceAbstraction, FakeUserDecryptionOptions as UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/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 import { BiometricStateService, KeyService } from "@bitwarden/key-management"; import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts index 07b8a8c297d..16e38ae0b52 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts @@ -14,10 +14,14 @@ import { tap, } 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 import { PinServiceAbstraction, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/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 import { BiometricStateService, KeyService } from "@bitwarden/key-management"; import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction"; diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts index 5fdae07b2d7..b17e85ca9c4 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts @@ -3,8 +3,14 @@ import { MockProxy, any, mock } from "jest-mock-extended"; import { BehaviorSubject, from, of } 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 import { CollectionService } 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 import { LogoutReason } from "@bitwarden/auth/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 import { BiometricsService } from "@bitwarden/key-management"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec"; diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts index d71b8972727..131f826fd33 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts @@ -2,8 +2,14 @@ // @ts-strict-ignore import { combineLatest, concatMap, filter, firstValueFrom, map, timeout } 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 import { CollectionService } 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 import { LogoutReason } from "@bitwarden/auth/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 import { BiometricsService } from "@bitwarden/key-management"; import { SearchService } from "../../../abstractions/search.service"; diff --git a/libs/common/src/models/export/collection-with-id.export.ts b/libs/common/src/models/export/collection-with-id.export.ts index ef850bc6039..a93f07b54e5 100644 --- a/libs/common/src/models/export/collection-with-id.export.ts +++ b/libs/common/src/models/export/collection-with-id.export.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +// 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 { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common"; import { CollectionExport } from "./collection.export"; diff --git a/libs/common/src/models/export/collection.export.ts b/libs/common/src/models/export/collection.export.ts index 89137c34875..3f913a8c3e3 100644 --- a/libs/common/src/models/export/collection.export.ts +++ b/libs/common/src/models/export/collection.export.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +// 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 { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common"; import { EncString } from "../../platform/models/domain/enc-string"; diff --git a/libs/common/src/models/request/import-organization-ciphers.request.ts b/libs/common/src/models/request/import-organization-ciphers.request.ts index 759b69b21e3..69f23f64b64 100644 --- a/libs/common/src/models/request/import-organization-ciphers.request.ts +++ b/libs/common/src/models/request/import-organization-ciphers.request.ts @@ -1,3 +1,5 @@ +// 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 { CollectionWithIdRequest } from "@bitwarden/admin-console/common"; import { CipherRequest } from "../../vault/models/request/cipher.request"; diff --git a/libs/common/src/models/request/kdf.request.ts b/libs/common/src/models/request/kdf.request.ts index f0bd376317f..7ffdbcb4a4b 100644 --- a/libs/common/src/models/request/kdf.request.ts +++ b/libs/common/src/models/request/kdf.request.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +// 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 { KdfType } from "@bitwarden/key-management"; import { PasswordRequest } from "../../auth/models/request/password.request"; diff --git a/libs/common/src/platform/abstractions/key-generation.service.ts b/libs/common/src/platform/abstractions/key-generation.service.ts index 8314efe3469..91c630ed638 100644 --- a/libs/common/src/platform/abstractions/key-generation.service.ts +++ b/libs/common/src/platform/abstractions/key-generation.service.ts @@ -1,3 +1,5 @@ +// 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 { KdfConfig } from "@bitwarden/key-management"; import { CsprngArray } from "../../types/csprng"; diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index f9c5ab4a843..a1e914da531 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -8,6 +8,8 @@ import { Observable, of, switchMap } from "rxjs"; import { getHostname, parse } from "tldts"; import { Merge } from "type-fest"; +// 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 { KeyService } from "@bitwarden/key-management"; import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; diff --git a/libs/common/src/platform/models/domain/enc-string.spec.ts b/libs/common/src/platform/models/domain/enc-string.spec.ts index 1ab61745eb3..f565174ba9c 100644 --- a/libs/common/src/platform/models/domain/enc-string.spec.ts +++ b/libs/common/src/platform/models/domain/enc-string.spec.ts @@ -1,5 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; +// 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 { KeyService } from "@bitwarden/key-management"; import { makeEncString, makeStaticByteArray } from "../../../../spec"; diff --git a/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts b/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts index bf834e8dd93..9dca079bdba 100644 --- a/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts +++ b/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts @@ -1,6 +1,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, bufferCount, firstValueFrom, ObservedValueOf, 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 import { LogoutReason } from "@bitwarden/auth/common"; import { awaitAsync } from "../../../../spec"; diff --git a/libs/common/src/platform/notifications/internal/default-notifications.service.ts b/libs/common/src/platform/notifications/internal/default-notifications.service.ts index 40c93f8f22a..4cbc8227364 100644 --- a/libs/common/src/platform/notifications/internal/default-notifications.service.ts +++ b/libs/common/src/platform/notifications/internal/default-notifications.service.ts @@ -11,6 +11,8 @@ import { switchMap, } 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 import { LogoutReason } from "@bitwarden/auth/common"; import { AccountService } from "../../../auth/abstractions/account.service"; diff --git a/libs/common/src/platform/services/container.service.ts b/libs/common/src/platform/services/container.service.ts index 1428b2bbd7c..501c8ace92c 100644 --- a/libs/common/src/platform/services/container.service.ts +++ b/libs/common/src/platform/services/container.service.ts @@ -1,3 +1,5 @@ +// 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 { KeyService } from "@bitwarden/key-management"; import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; diff --git a/libs/common/src/platform/services/key-generation.service.spec.ts b/libs/common/src/platform/services/key-generation.service.spec.ts index 0a9e997b428..4fdad48e0fa 100644 --- a/libs/common/src/platform/services/key-generation.service.spec.ts +++ b/libs/common/src/platform/services/key-generation.service.spec.ts @@ -1,5 +1,7 @@ import { mock } from "jest-mock-extended"; +// 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 { PBKDF2KdfConfig, Argon2KdfConfig } from "@bitwarden/key-management"; import { CryptoFunctionService } from "../../key-management/crypto/abstractions/crypto-function.service"; diff --git a/libs/common/src/platform/services/key-generation.service.ts b/libs/common/src/platform/services/key-generation.service.ts index dcd1f4f95d7..49f99eb79a9 100644 --- a/libs/common/src/platform/services/key-generation.service.ts +++ b/libs/common/src/platform/services/key-generation.service.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +// 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 { KdfConfig, PBKDF2KdfConfig, Argon2KdfConfig, KdfType } from "@bitwarden/key-management"; import { CryptoFunctionService } from "../../key-management/crypto/abstractions/crypto-function.service"; diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts index a66b2a9cb6f..6531be58f05 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts @@ -1,6 +1,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom, of } 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 import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management"; import { BitwardenClient } from "@bitwarden/sdk-internal"; diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index 5c381c7dd1b..7027b5134a0 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -14,6 +14,8 @@ import { throwIfEmpty, } 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 import { KeyService, KdfConfigService, KdfConfig, KdfType } from "@bitwarden/key-management"; import { BitwardenClient, diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts index 84511d1e71a..b9f189c5f06 100644 --- a/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts @@ -1,5 +1,7 @@ import { mock } from "jest-mock-extended"; +// 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 { DefaultKeyService } from "@bitwarden/key-management"; import { CsprngArray } from "../../types/csprng"; diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.ts index bf64c13b060..33537cd0e2d 100644 --- a/libs/common/src/platform/services/user-auto-unlock-key.service.ts +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.ts @@ -1,3 +1,5 @@ +// 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 { KeyService } from "@bitwarden/key-management"; import { UserId } from "../../types/guid"; diff --git a/libs/common/src/platform/sync/core-sync.service.ts b/libs/common/src/platform/sync/core-sync.service.ts index 4020c75f764..63f9ab17fb3 100644 --- a/libs/common/src/platform/sync/core-sync.service.ts +++ b/libs/common/src/platform/sync/core-sync.service.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { firstValueFrom, map, Observable, of, switchMap } 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 import { CollectionService } from "@bitwarden/admin-console/common"; import { ApiService } from "../../abstractions/api.service"; diff --git a/libs/common/src/platform/sync/default-sync.service.spec.ts b/libs/common/src/platform/sync/default-sync.service.spec.ts index ded06c8be6b..29543672dbe 100644 --- a/libs/common/src/platform/sync/default-sync.service.spec.ts +++ b/libs/common/src/platform/sync/default-sync.service.spec.ts @@ -1,12 +1,18 @@ import { mock, MockProxy } from "jest-mock-extended"; import { of } 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 import { CollectionService } 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 import { LogoutReason, UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/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 import { KeyService } from "@bitwarden/key-management"; import { Matrix } from "../../../spec/matrix"; diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index e9f6c60af64..6e1a8f28443 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -2,11 +2,15 @@ // @ts-strict-ignore import { firstValueFrom, map } 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 import { CollectionData, CollectionDetailsResponse, CollectionService, } 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 import { KeyService } from "@bitwarden/key-management"; // FIXME: remove `src` and fix import diff --git a/libs/common/src/platform/sync/sync.response.ts b/libs/common/src/platform/sync/sync.response.ts index bc94eff67a4..5055652a0d1 100644 --- a/libs/common/src/platform/sync/sync.response.ts +++ b/libs/common/src/platform/sync/sync.response.ts @@ -1,3 +1,5 @@ +// 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 { CollectionDetailsResponse } from "@bitwarden/admin-console/common"; import { PolicyResponse } from "../../admin-console/models/response/policy.response"; diff --git a/libs/common/src/services/api.service.spec.ts b/libs/common/src/services/api.service.spec.ts index eca6066b9b7..fffe0478254 100644 --- a/libs/common/src/services/api.service.spec.ts +++ b/libs/common/src/services/api.service.spec.ts @@ -1,6 +1,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import { of } 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 import { LogoutReason } from "@bitwarden/auth/common"; import { TokenService } from "../auth/abstractions/token.service"; diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 639daa7c658..95aea41e68b 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -2,12 +2,16 @@ // @ts-strict-ignore import { firstValueFrom } 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 import { CollectionAccessDetailsResponse, CollectionDetailsResponse, CollectionRequest, CollectionResponse, } 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 import { LogoutReason } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "../abstractions/api.service"; diff --git a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts index 66edc5a4838..255fd9b4aff 100644 --- a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts +++ b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts @@ -1,6 +1,8 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, 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 import { KeyService } from "@bitwarden/key-management"; import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; diff --git a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts index c91181a004a..c2e9b305618 100644 --- a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts +++ b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts @@ -12,6 +12,8 @@ import { takeWhile, } 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 import { KeyService } from "@bitwarden/key-management"; import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; diff --git a/libs/common/src/tools/send/models/domain/send.spec.ts b/libs/common/src/tools/send/models/domain/send.spec.ts index 7112ad7f751..8df9a144108 100644 --- a/libs/common/src/tools/send/models/domain/send.spec.ts +++ b/libs/common/src/tools/send/models/domain/send.spec.ts @@ -1,5 +1,7 @@ import { mock } from "jest-mock-extended"; +// 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 { KeyService } from "@bitwarden/key-management"; import { makeStaticByteArray, mockEnc } from "../../../../../spec"; diff --git a/libs/common/src/tools/send/services/send.service.abstraction.ts b/libs/common/src/tools/send/services/send.service.abstraction.ts index 921f0565624..0cf951e4197 100644 --- a/libs/common/src/tools/send/services/send.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send.service.abstraction.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Observable } 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 import { UserKeyRotationDataProvider } from "@bitwarden/key-management"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index 611cc9c7b76..777bc54f299 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -1,6 +1,8 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, of } 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 import { KeyService } from "@bitwarden/key-management"; import { diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 3a5bcbe997b..2556fa2e908 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Observable, concatMap, distinctUntilChanged, firstValueFrom, map } 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 import { PBKDF2KdfConfig, KeyService } from "@bitwarden/key-management"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index a67dfcef8b9..fc809058161 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Observable } 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 import { UserKeyRotationDataProvider } from "@bitwarden/key-management"; import { UriMatchStrategySetting } from "../../models/domain/domain-service"; diff --git a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts index b7241e3ae37..7324fe22c8d 100644 --- a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Observable } 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 import { UserKeyRotationDataProvider } from "@bitwarden/key-management"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; diff --git a/libs/common/src/vault/models/domain/attachment.spec.ts b/libs/common/src/vault/models/domain/attachment.spec.ts index eab67320679..f03ce927082 100644 --- a/libs/common/src/vault/models/domain/attachment.spec.ts +++ b/libs/common/src/vault/models/domain/attachment.spec.ts @@ -1,5 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; +// 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 { KeyService } from "@bitwarden/key-management"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec"; diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index a889f0b969c..5c98ceda9f7 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -2,6 +2,8 @@ import { mock } from "jest-mock-extended"; import { Jsonify } from "type-fest"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +// 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 { KeyService } from "@bitwarden/key-management"; import { CipherType as SdkCipherType, diff --git a/libs/common/src/vault/services/cipher-authorization.service.spec.ts b/libs/common/src/vault/services/cipher-authorization.service.spec.ts index 33af28842ca..01574d04df5 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.spec.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.spec.ts @@ -1,6 +1,8 @@ import { mock } from "jest-mock-extended"; import { Observable, firstValueFrom, of } 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 import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; diff --git a/libs/common/src/vault/services/cipher-authorization.service.ts b/libs/common/src/vault/services/cipher-authorization.service.ts index b415760a035..f30e80cdfb8 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.ts @@ -1,5 +1,7 @@ import { combineLatest, map, Observable, of, shareReplay, switchMap } 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 import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index b15bc4a9112..9e56bac2ca0 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -2,6 +2,8 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, map, of } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +// 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 { CipherDecryptionKeys, KeyService } from "@bitwarden/key-management"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 6bea56baa5e..2693d9d4644 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -4,6 +4,8 @@ import { combineLatest, filter, firstValueFrom, map, Observable, Subject, switch import { SemVer } from "semver"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +// 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 { KeyService } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; diff --git a/libs/common/src/vault/services/folder/folder.service.spec.ts b/libs/common/src/vault/services/folder/folder.service.spec.ts index 0c61efc288a..38dea851456 100644 --- a/libs/common/src/vault/services/folder/folder.service.spec.ts +++ b/libs/common/src/vault/services/folder/folder.service.spec.ts @@ -1,6 +1,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom } 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 import { KeyService } from "@bitwarden/key-management"; import { makeEncString } from "../../../../spec"; diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index ba87f1c3148..12d02958049 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Observable, Subject, firstValueFrom, map, shareReplay, switchMap, merge } 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 import { KeyService } from "@bitwarden/key-management"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index 2fee88852b5..7b8f49b796b 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -18,6 +18,8 @@ import * as JSZip from "jszip"; import { Observable, Subject, lastValueFrom, combineLatest, firstValueFrom } from "rxjs"; import { combineLatestWith, filter, map, switchMap, takeUntil } from "rxjs/operators"; +// 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 { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { safeProvider, SafeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; diff --git a/libs/importer/src/importers/base-importer.ts b/libs/importer/src/importers/base-importer.ts index 0594b6014e8..9033997a475 100644 --- a/libs/importer/src/importers/base-importer.ts +++ b/libs/importer/src/importers/base-importer.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import * as papa from "papaparse"; +// 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 { CollectionView } from "@bitwarden/admin-console/common"; import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; diff --git a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts index af29d8263c6..4291f7b1ab2 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { firstValueFrom, map } 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 import { CollectionView } from "@bitwarden/admin-console/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; diff --git a/libs/importer/src/importers/padlock-csv-importer.ts b/libs/importer/src/importers/padlock-csv-importer.ts index f4943e5979b..86b569fbc52 100644 --- a/libs/importer/src/importers/padlock-csv-importer.ts +++ b/libs/importer/src/importers/padlock-csv-importer.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +// 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 { CollectionView } from "@bitwarden/admin-console/common"; import { ImportResult } from "../models/import-result"; diff --git a/libs/importer/src/importers/passpack-csv-importer.ts b/libs/importer/src/importers/passpack-csv-importer.ts index a426d6db416..17b0c148896 100644 --- a/libs/importer/src/importers/passpack-csv-importer.ts +++ b/libs/importer/src/importers/passpack-csv-importer.ts @@ -1,3 +1,5 @@ +// 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 { CollectionView } from "@bitwarden/admin-console/common"; import { ImportResult } from "../models/import-result"; diff --git a/libs/importer/src/models/import-result.ts b/libs/importer/src/models/import-result.ts index 9fddbb420a1..9d94b410e7b 100644 --- a/libs/importer/src/models/import-result.ts +++ b/libs/importer/src/models/import-result.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +// 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 { CollectionView } from "@bitwarden/admin-console/common"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; diff --git a/libs/importer/src/services/import-collection.service.abstraction.ts b/libs/importer/src/services/import-collection.service.abstraction.ts index 539232ef094..48e6e7a4a8c 100644 --- a/libs/importer/src/services/import-collection.service.abstraction.ts +++ b/libs/importer/src/services/import-collection.service.abstraction.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +// 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 { CollectionView } from "@bitwarden/admin-console/common"; export abstract class ImportCollectionServiceAbstraction { diff --git a/libs/importer/src/services/import.service.abstraction.ts b/libs/importer/src/services/import.service.abstraction.ts index 5f7a31bcc14..d869dc71cc7 100644 --- a/libs/importer/src/services/import.service.abstraction.ts +++ b/libs/importer/src/services/import.service.abstraction.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +// 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 { CollectionView } from "@bitwarden/admin-console/common"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index 908f062ecc1..30309a3d9c2 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -1,6 +1,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import { of } 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 import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index bd18e78d542..adfa427c660 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { firstValueFrom, map } 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 import { CollectionService, CollectionWithIdRequest, diff --git a/libs/key-management/src/key.service.spec.ts b/libs/key-management/src/key.service.spec.ts index f1f1286dfc5..6d2e8fd20ec 100644 --- a/libs/key-management/src/key.service.spec.ts +++ b/libs/key-management/src/key.service.spec.ts @@ -1,6 +1,8 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, bufferCount, firstValueFrom, lastValueFrom, of, take } 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 import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index 9372dafd3ea..1d4fcc86a0c 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -10,6 +10,8 @@ import { switchMap, } 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 import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data"; import { BaseEncryptedOrganizationKey } from "@bitwarden/common/admin-console/models/domain/encrypted-organization-key"; diff --git a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts index 639cf562caa..71f12340ebc 100644 --- a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts @@ -1,3 +1,5 @@ +// 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 { CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; diff --git a/libs/vault/src/cipher-form/cipher-form.stories.ts b/libs/vault/src/cipher-form/cipher-form.stories.ts index c5256c841d9..3d68b7124c1 100644 --- a/libs/vault/src/cipher-form/cipher-form.stories.ts +++ b/libs/vault/src/cipher-form/cipher-form.stories.ts @@ -12,6 +12,8 @@ import { } from "@storybook/angular"; import { BehaviorSubject } 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 import { CollectionView } from "@bitwarden/admin-console/common"; import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { NudgeStatus, NudgesService } from "@bitwarden/angular/vault"; 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 074b4b9e4b4..1e9916e76a4 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 @@ -5,6 +5,8 @@ import { By } from "@angular/platform-browser"; import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } 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 import { CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 82615368b91..a1cf33c449b 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 @@ -6,6 +6,8 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms"; import { concatMap, map } 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 import { CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts index 8e1dde22324..a91a84e91c1 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts @@ -3,6 +3,8 @@ import { inject, Injectable } from "@angular/core"; import { combineLatest, filter, firstValueFrom, map, switchMap } 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 import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index cf78e78a65f..02968d6d149 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -2,6 +2,8 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnChanges, OnDestroy } from "@angular/core"; import { firstValueFrom, Observable, Subject, takeUntil } 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 import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts index da3d790368c..f093cd020b5 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts @@ -1,6 +1,8 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; +// 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 { CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts index 1335df74bb9..fbedcbb54df 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts @@ -3,6 +3,8 @@ import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; +// 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 { CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; diff --git a/libs/vault/src/components/assign-collections.component.ts b/libs/vault/src/components/assign-collections.component.ts index db62f096faa..faa2dae072a 100644 --- a/libs/vault/src/components/assign-collections.component.ts +++ b/libs/vault/src/components/assign-collections.component.ts @@ -24,6 +24,8 @@ import { tap, } 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 import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { From 0e0be0a3decf2ae93976ebf6257086a09de718c2 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 23 May 2025 09:07:32 -0400 Subject: [PATCH 134/163] ignore one eslint error (#14896) --- libs/vault/src/components/assign-collections.component.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/vault/src/components/assign-collections.component.spec.ts b/libs/vault/src/components/assign-collections.component.spec.ts index d6707cd1064..800bf5393ad 100644 --- a/libs/vault/src/components/assign-collections.component.spec.ts +++ b/libs/vault/src/components/assign-collections.component.spec.ts @@ -3,6 +3,8 @@ import { By } from "@angular/platform-browser"; import { mock } from "jest-mock-extended"; import { of } 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 import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; From c6af80f3eb812006e56c57be7f1c3c290a06ce1b Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Fri, 23 May 2025 11:48:23 -0400 Subject: [PATCH 135/163] PM-21651 [For Automation Purposes] add test IDs to notification bar (#14863) * PM-21651 [For Automation Purposes] Please add IDs to some of the main components * option items * dynamic test id * mitigate feedback * clean up logic --- .../buttons/option-selection-button.ts | 3 ++ .../option-selection-button.lit-stories.ts | 2 + .../confirmation/container.lit-stories.ts | 8 +++- .../notification/container.lit-stories.ts | 5 ++- .../option-selection.lit-stories.ts | 5 ++- .../notification/confirmation/container.ts | 4 +- .../components/notification/container.ts | 4 +- .../option-selection/option-item.ts | 3 ++ .../option-selection/option-items.ts | 3 ++ .../option-selection/option-selection.ts | 5 +++ .../content/components/rows/button-row.ts | 1 + apps/browser/src/autofill/notification/bar.ts | 42 +++++++++++++++---- 12 files changed, 70 insertions(+), 15 deletions(-) diff --git a/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts b/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts index 3912c791d34..ddefd02f6e1 100644 --- a/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts @@ -10,6 +10,7 @@ import { AngleUp, AngleDown } from "../icons"; export type OptionSelectionButtonProps = { disabled: boolean; icon?: Option["icon"]; + id: string; text?: string; theme: Theme; toggledOn: boolean; @@ -19,6 +20,7 @@ export type OptionSelectionButtonProps = { export function OptionSelectionButton({ disabled, icon, + id, text, theme, toggledOn, @@ -31,6 +33,7 @@ export function OptionSelectionButton({ return html` diff --git a/apps/browser/src/auth/popup/account-switching/account.component.html b/apps/browser/src/auth/popup/account-switching/account.component.html index d2e15d31899..d22ce9c9366 100644 --- a/apps/browser/src/auth/popup/account-switching/account.component.html +++ b/apps/browser/src/auth/popup/account-switching/account.component.html @@ -32,13 +32,13 @@
- + From 04ed114e0ef2845527535778284142daa7ef35ed Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 26 May 2025 00:30:52 +0200 Subject: [PATCH 140/163] [BEEEP/PM-8492] Add autostart for flatpak (#12016) * Add autostart for flatpak via ashpd * Fix clippy errors * Cargo fmt * Fix clippy --- apps/desktop/desktop_native/Cargo.lock | 1 + apps/desktop/desktop_native/Cargo.toml | 1 + apps/desktop/desktop_native/core/Cargo.toml | 1 + .../core/src/autostart/linux.rs | 21 ++++++++++ .../desktop_native/core/src/autostart/mod.rs | 5 +++ .../core/src/autostart/unimplemented.rs | 5 +++ apps/desktop/desktop_native/core/src/lib.rs | 1 + apps/desktop/desktop_native/napi/index.d.ts | 3 ++ apps/desktop/desktop_native/napi/src/lib.rs | 10 +++++ apps/desktop/src/main/messaging.main.ts | 39 ++++++++++++------- 10 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 apps/desktop/desktop_native/core/src/autostart/linux.rs create mode 100644 apps/desktop/desktop_native/core/src/autostart/mod.rs create mode 100644 apps/desktop/desktop_native/core/src/autostart/unimplemented.rs diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index e5d90446ddc..34819a3981b 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -863,6 +863,7 @@ dependencies = [ "anyhow", "arboard", "argon2", + "ashpd", "base64", "bitwarden-russh", "byteorder", diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 71da53c867d..c4a2ed98e70 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -13,6 +13,7 @@ aes = "=0.8.4" anyhow = "=1.0.94" arboard = { version = "=3.5.0", default-features = false } argon2 = "=0.5.3" +ashpd = "=0.11.0" base64 = "=0.22.1" bindgen = "=0.71.1" bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "3d48f140fd506412d186203238993163a8c4e536" } diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index b71081aaa1f..7cd67dbad6a 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -85,6 +85,7 @@ desktop_objc = { path = "../objc" } [target.'cfg(target_os = "linux")'.dependencies] oo7 = { workspace = true } libc = { workspace = true } +ashpd = { workspace = true } zbus = { workspace = true, optional = true } zbus_polkit = { workspace = true, optional = true } diff --git a/apps/desktop/desktop_native/core/src/autostart/linux.rs b/apps/desktop/desktop_native/core/src/autostart/linux.rs new file mode 100644 index 00000000000..1fd02a6ea5d --- /dev/null +++ b/apps/desktop/desktop_native/core/src/autostart/linux.rs @@ -0,0 +1,21 @@ +use anyhow::Result; +use ashpd::desktop::background::Background; + +pub async fn set_autostart(autostart: bool, params: Vec) -> Result<()> { + let request = if params.is_empty() { + Background::request().auto_start(autostart) + } else { + Background::request().command(params).auto_start(autostart) + }; + + match request.send().await.and_then(|r| r.response()) { + Ok(response) => { + println!("[ASHPD] Autostart enabled: {:?}", response); + Ok(()) + } + Err(err) => { + println!("[ASHPD] Error enabling autostart: {}", err); + Err(anyhow::anyhow!("error enabling autostart {}", err)) + } + } +} diff --git a/apps/desktop/desktop_native/core/src/autostart/mod.rs b/apps/desktop/desktop_native/core/src/autostart/mod.rs new file mode 100644 index 00000000000..78e27eb433e --- /dev/null +++ b/apps/desktop/desktop_native/core/src/autostart/mod.rs @@ -0,0 +1,5 @@ +#[cfg_attr(target_os = "linux", path = "linux.rs")] +#[cfg_attr(target_os = "windows", path = "unimplemented.rs")] +#[cfg_attr(target_os = "macos", path = "unimplemented.rs")] +mod autostart_impl; +pub use autostart_impl::*; diff --git a/apps/desktop/desktop_native/core/src/autostart/unimplemented.rs b/apps/desktop/desktop_native/core/src/autostart/unimplemented.rs new file mode 100644 index 00000000000..14f567bdc65 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/autostart/unimplemented.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +pub async fn set_autostart(_autostart: bool, _params: Vec) -> Result<()> { + unimplemented!(); +} diff --git a/apps/desktop/desktop_native/core/src/lib.rs b/apps/desktop/desktop_native/core/src/lib.rs index 0a16ee65be3..a72ec04e9c2 100644 --- a/apps/desktop/desktop_native/core/src/lib.rs +++ b/apps/desktop/desktop_native/core/src/lib.rs @@ -1,4 +1,5 @@ pub mod autofill; +pub mod autostart; pub mod biometric; pub mod clipboard; pub mod crypto; diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 952f2571c5d..b3c6f715e98 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -111,6 +111,9 @@ export declare namespace ipc { send(message: string): number } } +export declare namespace autostart { + export function setAutostart(autostart: boolean, params: Array): Promise +} export declare namespace autofill { export function runCommand(value: string): Promise export const enum UserVerification { diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 37796ef6f59..079872a3b03 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -477,6 +477,16 @@ pub mod ipc { } } +#[napi] +pub mod autostart { + #[napi] + pub async fn set_autostart(autostart: bool, params: Vec) -> napi::Result<()> { + desktop_core::autostart::set_autostart(autostart, params) + .await + .map_err(|e| napi::Error::from_reason(format!("Error setting autostart - {e} - {e:?}"))) + } +} + #[napi] pub mod autofill { use desktop_core::ipc::server::{Message, MessageType}; diff --git a/apps/desktop/src/main/messaging.main.ts b/apps/desktop/src/main/messaging.main.ts index bb4063d64fd..556fa293108 100644 --- a/apps/desktop/src/main/messaging.main.ts +++ b/apps/desktop/src/main/messaging.main.ts @@ -6,8 +6,11 @@ import * as path from "path"; import { app, ipcMain } from "electron"; import { firstValueFrom } from "rxjs"; +import { autostart } from "@bitwarden/desktop-napi"; + import { Main } from "../main"; import { DesktopSettingsService } from "../platform/services/desktop-settings.service"; +import { isFlatpak } from "../utils"; import { MenuUpdateRequest } from "./menu/menu.updater"; @@ -122,20 +125,24 @@ export class MessagingMain { private addOpenAtLogin() { if (process.platform === "linux") { - const data = `[Desktop Entry] -Type=Application -Version=${app.getVersion()} -Name=Bitwarden -Comment=Bitwarden startup script -Exec=${app.getPath("exe")} -StartupNotify=false -Terminal=false`; + if (isFlatpak()) { + autostart.setAutostart(true, []).catch((e) => {}); + } else { + const data = `[Desktop Entry] + Type=Application + Version=${app.getVersion()} + Name=Bitwarden + Comment=Bitwarden startup script + Exec=${app.getPath("exe")} + StartupNotify=false + Terminal=false`; - const dir = path.dirname(this.linuxStartupFile()); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); + const dir = path.dirname(this.linuxStartupFile()); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + fs.writeFileSync(this.linuxStartupFile(), data); } - fs.writeFileSync(this.linuxStartupFile(), data); } else { app.setLoginItemSettings({ openAtLogin: true }); } @@ -143,8 +150,12 @@ Terminal=false`; private removeOpenAtLogin() { if (process.platform === "linux") { - if (fs.existsSync(this.linuxStartupFile())) { - fs.unlinkSync(this.linuxStartupFile()); + if (isFlatpak()) { + autostart.setAutostart(false, []).catch((e) => {}); + } else { + if (fs.existsSync(this.linuxStartupFile())) { + fs.unlinkSync(this.linuxStartupFile()); + } } } else { app.setLoginItemSettings({ openAtLogin: false }); From 56b565a47ebaf6fc49a021a86127f42d554aa7d3 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 26 May 2025 03:21:30 -0400 Subject: [PATCH 141/163] Add lowdb to ignored dependencies (#14907) * Added lowdb to ignored dependencies * Changed to allowedVersions instead of ignoring for more context. --- .github/renovate.json5 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 0ebc2c210a2..1b84ccdab01 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -405,6 +405,12 @@ commitMessagePrefix: "[deps] KM:", reviewers: ["team:team-key-management-dev"], }, + { + // Any versions of lowdb above 1.0.0 are not compatible with CommonJS. + matchPackageNames: ["lowdb"], + allowedVersions: "1.0.0", + description: "Higher versions of lowdb are not compatible with CommonJS", + }, ], ignoreDeps: ["@types/koa-bodyparser", "bootstrap", "node-ipc", "@bitwarden/sdk-internal"], } From 745ab219467fbe31ce0e05308ff3232c0067e1e1 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 26 May 2025 14:38:02 +0200 Subject: [PATCH 142/163] Remove unused imports in browser and desktop (#14875) Removes unused imports from browser and desktop. These were missed in #14795. --- .../autofill/popup/fido2/fido2-cipher-row.component.ts | 4 ---- .../autofill/popup/settings/notifications.component.ts | 2 -- .../src/platform/popup/layout/popup-layout.stories.ts | 3 --- .../popup/generator/credential-generator.component.ts | 2 -- .../send-v2/send-created/send-created.component.ts | 3 +-- .../open-attachments/open-attachments.component.ts | 10 ++-------- .../autofill-vault-list-items.component.ts | 9 +-------- .../vault-list-items-container.component.ts | 1 - .../popup/settings/download-bitwarden.component.ts | 2 -- .../trash-list-items-container.component.ts | 1 - .../popup/settings/vault-settings-v2.component.ts | 2 -- .../src/platform/components/approve-ssh-request.ts | 2 -- 12 files changed, 4 insertions(+), 37 deletions(-) diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts index 98e73d2174c..02df3ffe9b3 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts @@ -10,8 +10,6 @@ import { ButtonModule, IconButtonModule, ItemModule, - SectionComponent, - SectionHeaderComponent, TypographyModule, } from "@bitwarden/components"; @@ -27,8 +25,6 @@ import { IconButtonModule, ItemModule, JslibModule, - SectionComponent, - SectionHeaderComponent, TypographyModule, ], }) diff --git a/apps/browser/src/autofill/popup/settings/notifications.component.ts b/apps/browser/src/autofill/popup/settings/notifications.component.ts index be447e3f885..476b601c4e5 100644 --- a/apps/browser/src/autofill/popup/settings/notifications.component.ts +++ b/apps/browser/src/autofill/popup/settings/notifications.component.ts @@ -18,7 +18,6 @@ import { } from "@bitwarden/components"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; -import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; @@ -31,7 +30,6 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co RouterModule, PopupPageComponent, PopupHeaderComponent, - PopupFooterComponent, PopOutComponent, ItemModule, CardComponent, diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index f20049f6cde..48940f5fa10 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -185,7 +185,6 @@ class MockVaultPageComponent {} PopupPageComponent, PopupHeaderComponent, MockAddButtonComponent, - MockPopoutButtonComponent, MockCurrentAccountComponent, VaultComponent, ], @@ -290,9 +289,7 @@ class MockSettingsPageComponent {} PopupHeaderComponent, PopupFooterComponent, ButtonModule, - MockAddButtonComponent, MockPopoutButtonComponent, - MockCurrentAccountComponent, VaultComponent, IconButtonModule, ], diff --git a/apps/browser/src/tools/popup/generator/credential-generator.component.ts b/apps/browser/src/tools/popup/generator/credential-generator.component.ts index 9c1af07efdd..a2ef4be6620 100644 --- a/apps/browser/src/tools/popup/generator/credential-generator.component.ts +++ b/apps/browser/src/tools/popup/generator/credential-generator.component.ts @@ -7,7 +7,6 @@ import { GeneratorModule } from "@bitwarden/generator-components"; import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; -import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; @@ -22,7 +21,6 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co PopOutComponent, PopupHeaderComponent, PopupPageComponent, - PopupFooterComponent, RouterModule, ItemModule, ], diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts index 7191040ac6f..7680e05dd5b 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts @@ -3,7 +3,7 @@ import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { ActivatedRoute, Router, RouterLink, RouterModule } from "@angular/router"; +import { ActivatedRoute, Router, RouterModule } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -31,7 +31,6 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page PopOutComponent, PopupHeaderComponent, PopupPageComponent, - RouterLink, RouterModule, PopupFooterComponent, IconModule, diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts index 9189ea51313..d44b54bcb96 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts @@ -18,13 +18,7 @@ import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { - BadgeModule, - CardComponent, - ItemModule, - ToastService, - TypographyModule, -} from "@bitwarden/components"; +import { BadgeModule, ItemModule, ToastService, TypographyModule } from "@bitwarden/components"; import BrowserPopupUtils from "../../../../../../platform/popup/browser-popup-utils"; import { FilePopoutUtilsService } from "../../../../../../tools/popup/services/file-popout-utils.service"; @@ -33,7 +27,7 @@ import { FilePopoutUtilsService } from "../../../../../../tools/popup/services/f standalone: true, selector: "app-open-attachments", templateUrl: "./open-attachments.component.html", - imports: [BadgeModule, CommonModule, ItemModule, JslibModule, TypographyModule, CardComponent], + imports: [BadgeModule, CommonModule, ItemModule, JslibModule, TypographyModule], }) export class OpenAttachmentsComponent implements OnInit { /** Cipher `id` */ diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts index 72d51776f7b..bdc0d7ae5bc 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts @@ -6,12 +6,7 @@ import { combineLatest, map, Observable } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { - IconButtonModule, - SectionComponent, - SectionHeaderComponent, - TypographyModule, -} from "@bitwarden/components"; +import { IconButtonModule, TypographyModule } from "@bitwarden/components"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; @@ -23,11 +18,9 @@ import { VaultListItemsContainerComponent } from "../vault-list-items-container/ standalone: true, imports: [ CommonModule, - SectionComponent, TypographyModule, VaultListItemsContainerComponent, JslibModule, - SectionHeaderComponent, IconButtonModule, ], selector: "app-autofill-vault-list-items", diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index 6df1bdf8ae5..073d49333b5 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -74,7 +74,6 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options ScrollingModule, DisclosureComponent, DisclosureTriggerForDirective, - DecryptionFailureDialogComponent, ], selector: "app-vault-list-items-container", templateUrl: "vault-list-items-container.component.html", diff --git a/apps/browser/src/vault/popup/settings/download-bitwarden.component.ts b/apps/browser/src/vault/popup/settings/download-bitwarden.component.ts index fa7efa87bda..0287b7d504f 100644 --- a/apps/browser/src/vault/popup/settings/download-bitwarden.component.ts +++ b/apps/browser/src/vault/popup/settings/download-bitwarden.component.ts @@ -9,7 +9,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CardComponent, LinkModule, TypographyModule } from "@bitwarden/components"; -import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; @@ -26,7 +25,6 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co PopOutComponent, CardComponent, TypographyModule, - CurrentAccountComponent, LinkModule, ], }) diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts index cbfc89bf922..b4899e12eda 100644 --- a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts @@ -49,7 +49,6 @@ import { PopupCipherView } from "../../views/popup-cipher.view"; IconButtonModule, OrgIconDirective, TypographyModule, - DecryptionFailureDialogComponent, ], changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts index c969f0436df..049e853d2cd 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts @@ -10,7 +10,6 @@ import { ItemModule, ToastOptions, ToastService } from "@bitwarden/components"; import { BrowserApi } from "../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; -import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; @@ -22,7 +21,6 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co JslibModule, RouterModule, PopupPageComponent, - PopupFooterComponent, PopupHeaderComponent, PopOutComponent, ItemModule, diff --git a/apps/desktop/src/platform/components/approve-ssh-request.ts b/apps/desktop/src/platform/components/approve-ssh-request.ts index c6c7388ecf1..515fd94ecd6 100644 --- a/apps/desktop/src/platform/components/approve-ssh-request.ts +++ b/apps/desktop/src/platform/components/approve-ssh-request.ts @@ -13,7 +13,6 @@ import { IconButtonModule, DialogService, } from "@bitwarden/components"; -import { CipherFormGeneratorComponent } from "@bitwarden/vault"; export interface ApproveSshRequestParams { cipherName: string; @@ -30,7 +29,6 @@ export interface ApproveSshRequestParams { DialogModule, CommonModule, JslibModule, - CipherFormGeneratorComponent, ButtonModule, IconButtonModule, ReactiveFormsModule, From 43c963032db7f8a92fc97e24f7044af22f5751cf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 16:25:30 +0200 Subject: [PATCH 143/163] [deps] Architecture: Update lint-staged to v16 (#14925) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Oscar Hinton --- package-lock.json | 160 ++++++---------------------------------------- package.json | 2 +- 2 files changed, 22 insertions(+), 140 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2e2623f89cc..88302755623 100644 --- a/package-lock.json +++ b/package-lock.json @@ -153,7 +153,7 @@ "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", "json5": "2.2.3", - "lint-staged": "15.5.1", + "lint-staged": "16.0.0", "mini-css-extract-plugin": "2.9.2", "nx": "20.8.0", "postcss": "8.5.3", @@ -25970,28 +25970,28 @@ } }, "node_modules/lint-staged": { - "version": "15.5.1", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.1.tgz", - "integrity": "sha512-6m7u8mue4Xn6wK6gZvSCQwBvMBR36xfY24nF5bMTf2MHDYG6S3yhJuOgdYVw99hsjyDt2d4z168b3naI8+NWtQ==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.0.0.tgz", + "integrity": "sha512-sUCprePs6/rbx4vKC60Hez6X10HPkpDJaGcy3D1NdwR7g1RcNkWL8q9mJMreOqmHBTs+1sNFp+wOiX9fr+hoOQ==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^5.4.1", "commander": "^13.1.0", "debug": "^4.4.0", - "execa": "^8.0.1", "lilconfig": "^3.1.3", - "listr2": "^8.2.5", + "listr2": "^8.3.3", "micromatch": "^4.0.8", + "nano-spawn": "^1.0.0", "pidtree": "^0.6.0", "string-argv": "^0.3.2", - "yaml": "^2.7.0" + "yaml": "^2.7.1" }, "bin": { "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": ">=18.12.0" + "node": ">=20.18" }, "funding": { "url": "https://opencollective.com/lint-staged" @@ -26077,53 +26077,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lint-staged/node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/lint-staged/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, "node_modules/lint-staged/node_modules/is-fullwidth-code-point": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", @@ -26137,19 +26090,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lint-staged/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lint-staged/node_modules/listr2": { "version": "8.3.3", "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", @@ -26168,64 +26108,6 @@ "node": ">=18.0.0" } }, - "node_modules/lint-staged/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lint-staged/node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", @@ -26277,19 +26159,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/lint-staged/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lint-staged/node_modules/wrap-ansi": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", @@ -28632,6 +28501,19 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nano-spawn": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", + "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", diff --git a/package.json b/package.json index ce6adf3009d..8a8e80bd632 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", "json5": "2.2.3", - "lint-staged": "15.5.1", + "lint-staged": "16.0.0", "mini-css-extract-plugin": "2.9.2", "nx": "20.8.0", "postcss": "8.5.3", From beb00a206b7b48f4f053b394b06160b5dd2c3f2c Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 26 May 2025 17:02:28 +0200 Subject: [PATCH 144/163] Add UUID helpers to the SDK (#14939) * Add UUID helpers to the SDK * Address review feedback --- .../platform/abstractions/sdk/sdk.service.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/libs/common/src/platform/abstractions/sdk/sdk.service.ts b/libs/common/src/platform/abstractions/sdk/sdk.service.ts index 3adf3291bbf..07dfb2aa0df 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk.service.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk.service.ts @@ -1,9 +1,10 @@ import { Observable } from "rxjs"; -import { BitwardenClient } from "@bitwarden/sdk-internal"; +import { BitwardenClient, Uuid } from "@bitwarden/sdk-internal"; import { UserId } from "../../../types/guid"; import { Rc } from "../../misc/reference-counting/rc"; +import { Utils } from "../../misc/utils"; export class UserNotLoggedInError extends Error { constructor(userId: UserId) { @@ -11,6 +12,30 @@ export class UserNotLoggedInError extends Error { } } +export class InvalidUuid extends Error { + constructor(uuid: string) { + super(`Invalid UUID: ${uuid}`); + } +} + +/** + * Converts a string to UUID. Will throw an error if the UUID is non valid. + */ +export function asUuid(uuid: string): T { + if (Utils.isGuid(uuid)) { + return uuid as T; + } + + throw new InvalidUuid(uuid); +} + +/** + * Converts a UUID to the string representation. + */ +export function uuidToString(uuid: T): string { + return uuid as unknown as string; +} + export abstract class SdkService { /** * Retrieve the version of the SDK. From a6e2012087f1dc8e49c4b2d70a2f04a6a8ddd37c Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 27 May 2025 10:03:54 +0200 Subject: [PATCH 145/163] [PM-21600] Migrate account and security to standalone (#14762) Migrates account and security settings components to standalone and removing them from the `LooseComponentsModule`. --- .../settings/account/account.component.ts | 15 ++++++-- .../account/change-avatar-dialog.component.ts | 7 +++- .../account/change-email.component.spec.ts | 3 +- .../account/change-email.component.ts | 5 ++- .../settings/account/danger-zone.component.ts | 4 +-- .../account/deauthorize-sessions.component.ts | 7 ++-- .../delete-account-dialog.component.ts | 6 +++- .../settings/account/profile.component.ts | 7 +++- .../account/selectable-avatar.component.ts | 6 +++- .../settings/security/api-key.component.ts | 7 ++-- .../security/security-keys.component.ts | 7 ++-- .../settings/security/security.component.ts | 7 ++-- .../src/app/shared/loose-components.module.ts | 35 ------------------- 13 files changed, 62 insertions(+), 54 deletions(-) diff --git a/apps/web/src/app/auth/settings/account/account.component.ts b/apps/web/src/app/auth/settings/account/account.component.ts index cfc01f17674..c06df56e386 100644 --- a/apps/web/src/app/auth/settings/account/account.component.ts +++ b/apps/web/src/app/auth/settings/account/account.component.ts @@ -8,16 +8,27 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { DialogService } from "@bitwarden/components"; +import { HeaderModule } from "../../../layouts/header/header.module"; +import { SharedModule } from "../../../shared"; import { PurgeVaultComponent } from "../../../vault/settings/purge-vault.component"; +import { ChangeEmailComponent } from "./change-email.component"; +import { DangerZoneComponent } from "./danger-zone.component"; import { DeauthorizeSessionsComponent } from "./deauthorize-sessions.component"; import { DeleteAccountDialogComponent } from "./delete-account-dialog.component"; +import { ProfileComponent } from "./profile.component"; import { SetAccountVerifyDevicesDialogComponent } from "./set-account-verify-devices-dialog.component"; @Component({ - selector: "app-account", templateUrl: "account.component.html", - standalone: false, + standalone: true, + imports: [ + SharedModule, + HeaderModule, + ProfileComponent, + ChangeEmailComponent, + DangerZoneComponent, + ], }) export class AccountComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); diff --git a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts index 5d71333c0de..80fdb20954f 100644 --- a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts @@ -24,6 +24,10 @@ import { ToastService, } from "@bitwarden/components"; +import { SharedModule } from "../../../shared"; + +import { SelectableAvatarComponent } from "./selectable-avatar.component"; + type ChangeAvatarDialogData = { profile: ProfileResponse; }; @@ -31,7 +35,8 @@ type ChangeAvatarDialogData = { @Component({ templateUrl: "change-avatar-dialog.component.html", encapsulation: ViewEncapsulation.None, - standalone: false, + standalone: true, + imports: [SharedModule, SelectableAvatarComponent], }) export class ChangeAvatarDialogComponent implements OnInit, OnDestroy { profile: ProfileResponse; diff --git a/apps/web/src/app/auth/settings/account/change-email.component.spec.ts b/apps/web/src/app/auth/settings/account/change-email.component.spec.ts index 838a50b5c2e..f5c0733e5b0 100644 --- a/apps/web/src/app/auth/settings/account/change-email.component.spec.ts +++ b/apps/web/src/app/auth/settings/account/change-email.component.spec.ts @@ -33,8 +33,7 @@ describe("ChangeEmailComponent", () => { accountService = mockAccountServiceWith("UserId" as UserId); await TestBed.configureTestingModule({ - declarations: [ChangeEmailComponent], - imports: [ReactiveFormsModule, SharedModule], + imports: [ReactiveFormsModule, SharedModule, ChangeEmailComponent], providers: [ { provide: AccountService, useValue: accountService }, { provide: ApiService, useValue: apiService }, diff --git a/apps/web/src/app/auth/settings/account/change-email.component.ts b/apps/web/src/app/auth/settings/account/change-email.component.ts index c86c8c2f4f7..98f704d6044 100644 --- a/apps/web/src/app/auth/settings/account/change-email.component.ts +++ b/apps/web/src/app/auth/settings/account/change-email.component.ts @@ -14,10 +14,13 @@ import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; +import { SharedModule } from "../../../shared"; + @Component({ selector: "app-change-email", templateUrl: "change-email.component.html", - standalone: false, + standalone: true, + imports: [SharedModule], }) export class ChangeEmailComponent implements OnInit { tokenSent = false; diff --git a/apps/web/src/app/auth/settings/account/danger-zone.component.ts b/apps/web/src/app/auth/settings/account/danger-zone.component.ts index 91f22c7d08f..e07b6e6b8db 100644 --- a/apps/web/src/app/auth/settings/account/danger-zone.component.ts +++ b/apps/web/src/app/auth/settings/account/danger-zone.component.ts @@ -3,8 +3,8 @@ import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; -import { JslibModule } from "@bitwarden/angular/jslib.module"; import { TypographyModule } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; /** * Component for the Danger Zone section of the Account/Organization Settings page. @@ -13,6 +13,6 @@ import { TypographyModule } from "@bitwarden/components"; selector: "app-danger-zone", templateUrl: "danger-zone.component.html", standalone: true, - imports: [TypographyModule, JslibModule, CommonModule], + imports: [CommonModule, TypographyModule, I18nPipe], }) export class DangerZoneComponent {} diff --git a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts index da4d2dce9d7..a48e968ab3e 100644 --- a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts +++ b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts @@ -1,6 +1,7 @@ import { Component } from "@angular/core"; import { FormBuilder } from "@angular/forms"; +import { UserVerificationFormInputComponent } from "@bitwarden/auth/angular"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { Verification } from "@bitwarden/common/auth/types/verification"; @@ -9,10 +10,12 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { DialogService, ToastService } from "@bitwarden/components"; +import { SharedModule } from "../../../shared"; + @Component({ - selector: "app-deauthorize-sessions", templateUrl: "deauthorize-sessions.component.html", - standalone: false, + standalone: true, + imports: [SharedModule, UserVerificationFormInputComponent], }) export class DeauthorizeSessionsComponent { deauthForm = this.formBuilder.group({ diff --git a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts index 8a3575af5ba..0fc2276b779 100644 --- a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts @@ -3,15 +3,19 @@ import { Component } from "@angular/core"; import { FormBuilder } from "@angular/forms"; +import { UserVerificationFormInputComponent } from "@bitwarden/auth/angular"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; import { Verification } from "@bitwarden/common/auth/types/verification"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; +import { SharedModule } from "../../../shared"; + @Component({ templateUrl: "delete-account-dialog.component.html", - standalone: false, + standalone: true, + imports: [SharedModule, UserVerificationFormInputComponent], }) export class DeleteAccountDialogComponent { deleteForm = this.formBuilder.group({ diff --git a/apps/web/src/app/auth/settings/account/profile.component.ts b/apps/web/src/app/auth/settings/account/profile.component.ts index dc3997f58bb..a33efd742aa 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.ts +++ b/apps/web/src/app/auth/settings/account/profile.component.ts @@ -14,12 +14,17 @@ import { ProfileResponse } from "@bitwarden/common/models/response/profile.respo import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogService, ToastService } from "@bitwarden/components"; +import { DynamicAvatarComponent } from "../../../components/dynamic-avatar.component"; +import { SharedModule } from "../../../shared"; +import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component"; + import { ChangeAvatarDialogComponent } from "./change-avatar-dialog.component"; @Component({ selector: "app-profile", templateUrl: "profile.component.html", - standalone: false, + standalone: true, + imports: [SharedModule, DynamicAvatarComponent, AccountFingerprintComponent], }) export class ProfileComponent implements OnInit, OnDestroy { loading = true; diff --git a/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts b/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts index 33c307882c5..a53e3990090 100644 --- a/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts +++ b/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts @@ -1,7 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgClass } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { AvatarModule } from "@bitwarden/components"; + @Component({ selector: "selectable-avatar", template: ` `, - standalone: false, + standalone: true, + imports: [NgClass, AvatarModule], }) export class SelectableAvatarComponent { @Input() id: string; diff --git a/apps/web/src/app/auth/settings/security/api-key.component.ts b/apps/web/src/app/auth/settings/security/api-key.component.ts index 4f87c082881..5e61b4b4584 100644 --- a/apps/web/src/app/auth/settings/security/api-key.component.ts +++ b/apps/web/src/app/auth/settings/security/api-key.component.ts @@ -3,12 +3,15 @@ import { Component, Inject } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; +import { UserVerificationFormInputComponent } from "@bitwarden/auth/angular"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; import { ApiKeyResponse } from "@bitwarden/common/auth/models/response/api-key.response"; import { Verification } from "@bitwarden/common/auth/types/verification"; import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components"; +import { SharedModule } from "../../../shared"; + export type ApiKeyDialogData = { keyType: string; isRotation?: boolean; @@ -21,9 +24,9 @@ export type ApiKeyDialogData = { apiKeyDescription: string; }; @Component({ - selector: "app-api-key", templateUrl: "api-key.component.html", - standalone: false, + standalone: true, + imports: [SharedModule, UserVerificationFormInputComponent], }) export class ApiKeyComponent { clientId: string; diff --git a/apps/web/src/app/auth/settings/security/security-keys.component.ts b/apps/web/src/app/auth/settings/security/security-keys.component.ts index 98e743f57dc..6d33193cdde 100644 --- a/apps/web/src/app/auth/settings/security/security-keys.component.ts +++ b/apps/web/src/app/auth/settings/security/security-keys.component.ts @@ -8,12 +8,15 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { DialogService } from "@bitwarden/components"; +import { SharedModule } from "../../../shared"; + import { ApiKeyComponent } from "./api-key.component"; +import { ChangeKdfModule } from "./change-kdf/change-kdf.module"; @Component({ - selector: "app-security-keys", templateUrl: "security-keys.component.html", - standalone: false, + standalone: true, + imports: [SharedModule, ChangeKdfModule], }) export class SecurityKeysComponent implements OnInit { showChangeKdf = true; diff --git a/apps/web/src/app/auth/settings/security/security.component.ts b/apps/web/src/app/auth/settings/security/security.component.ts index 41b1af17abb..95733d693e2 100644 --- a/apps/web/src/app/auth/settings/security/security.component.ts +++ b/apps/web/src/app/auth/settings/security/security.component.ts @@ -4,10 +4,13 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { HeaderModule } from "../../../layouts/header/header.module"; +import { SharedModule } from "../../../shared"; + @Component({ - selector: "app-security", templateUrl: "security.component.html", - standalone: false, + standalone: true, + imports: [SharedModule, HeaderModule], }) export class SecurityComponent implements OnInit { showChangePassword = true; diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index e59633ee499..44323614f17 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -15,23 +15,12 @@ import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations import { RecoverDeleteComponent } from "../auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component"; import { SetPasswordComponent } from "../auth/set-password.component"; -import { AccountComponent } from "../auth/settings/account/account.component"; -import { ChangeAvatarDialogComponent } from "../auth/settings/account/change-avatar-dialog.component"; -import { ChangeEmailComponent } from "../auth/settings/account/change-email.component"; import { DangerZoneComponent } from "../auth/settings/account/danger-zone.component"; -import { DeauthorizeSessionsComponent } from "../auth/settings/account/deauthorize-sessions.component"; -import { DeleteAccountDialogComponent } from "../auth/settings/account/delete-account-dialog.component"; -import { ProfileComponent } from "../auth/settings/account/profile.component"; -import { SelectableAvatarComponent } from "../auth/settings/account/selectable-avatar.component"; import { EmergencyAccessConfirmComponent } from "../auth/settings/emergency-access/confirm/emergency-access-confirm.component"; import { EmergencyAccessAddEditComponent } from "../auth/settings/emergency-access/emergency-access-add-edit.component"; import { EmergencyAccessComponent } from "../auth/settings/emergency-access/emergency-access.component"; import { EmergencyAccessTakeoverComponent } from "../auth/settings/emergency-access/takeover/emergency-access-takeover.component"; import { EmergencyAccessViewComponent } from "../auth/settings/emergency-access/view/emergency-access-view.component"; -import { ApiKeyComponent } from "../auth/settings/security/api-key.component"; -import { ChangeKdfModule } from "../auth/settings/security/change-kdf/change-kdf.module"; -import { SecurityKeysComponent } from "../auth/settings/security/security-keys.component"; -import { SecurityComponent } from "../auth/settings/security/security.component"; import { UserVerificationModule } from "../auth/shared/components/user-verification"; import { UpdatePasswordComponent } from "../auth/update-password.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; @@ -39,7 +28,6 @@ import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component" import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.component"; import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-families.component"; import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component"; -import { DynamicAvatarComponent } from "../components/dynamic-avatar.component"; // eslint-disable-next-line no-restricted-imports -- Temporarily disabled until DIRT refactors these out of this module import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../dirt/reports/pages/organizations/exposed-passwords-report.component"; // eslint-disable-next-line no-restricted-imports -- Temporarily disabled until DIRT refactors these out of this module @@ -68,8 +56,6 @@ import { SharedModule } from "./shared.module"; imports: [ SharedModule, UserVerificationModule, - ChangeKdfModule, - DynamicAvatarComponent, AccountFingerprintComponent, OrganizationBadgeModule, PipesModule, @@ -85,11 +71,6 @@ import { SharedModule } from "./shared.module"; ], declarations: [ AcceptFamilySponsorshipComponent, - AccountComponent, - ApiKeyComponent, - ChangeEmailComponent, - DeauthorizeSessionsComponent, - DeleteAccountDialogComponent, EmergencyAccessAddEditComponent, EmergencyAccessComponent, EmergencyAccessConfirmComponent, @@ -104,15 +85,10 @@ import { SharedModule } from "./shared.module"; OrgUserConfirmComponent, OrgWeakPasswordsReportComponent, PremiumBadgeComponent, - ProfileComponent, - ChangeAvatarDialogComponent, PurgeVaultComponent, RecoverDeleteComponent, RecoverTwoFactorComponent, RemovePasswordComponent, - SecurityComponent, - SecurityKeysComponent, - SelectableAvatarComponent, SetPasswordComponent, SponsoredFamiliesComponent, FreeBitwardenFamiliesComponent, @@ -125,12 +101,6 @@ import { SharedModule } from "./shared.module"; exports: [ UserVerificationModule, PremiumBadgeComponent, - AccountComponent, - ApiKeyComponent, - ChangeEmailComponent, - DeauthorizeSessionsComponent, - DeleteAccountDialogComponent, - DynamicAvatarComponent, EmergencyAccessAddEditComponent, EmergencyAccessComponent, EmergencyAccessConfirmComponent, @@ -146,15 +116,10 @@ import { SharedModule } from "./shared.module"; OrgUserConfirmComponent, OrgWeakPasswordsReportComponent, PremiumBadgeComponent, - ProfileComponent, - ChangeAvatarDialogComponent, PurgeVaultComponent, RecoverDeleteComponent, RecoverTwoFactorComponent, RemovePasswordComponent, - SecurityComponent, - SecurityKeysComponent, - SelectableAvatarComponent, SetPasswordComponent, SponsoredFamiliesComponent, FreeBitwardenFamiliesComponent, From 45f2104fd8905ff28fe4c6fe570cdf59ae9cb1d3 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 27 May 2025 14:31:27 +0200 Subject: [PATCH 146/163] fix: broken SDK interface (#14959) --- .../src/platform/services/sdk/default-sdk.service.ts | 12 +++++++++++- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index 7027b5134a0..8e84642fb99 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -152,7 +152,15 @@ export class DefaultSdkService implements SdkService { const settings = this.toSettings(env); const client = await this.sdkClientFactory.createSdkClient(settings); - await this.initializeClient(client, account, kdfParams, privateKey, userKey, orgKeys); + await this.initializeClient( + userId, + client, + account, + kdfParams, + privateKey, + userKey, + orgKeys, + ); return client; }; @@ -182,6 +190,7 @@ export class DefaultSdkService implements SdkService { } private async initializeClient( + userId: UserId, client: BitwardenClient, account: AccountInfo, kdfParams: KdfConfig, @@ -190,6 +199,7 @@ export class DefaultSdkService implements SdkService { orgKeys: Record | null, ) { await client.crypto().initialize_user_crypto({ + userId, email: account.email, method: { decryptedKey: { decrypted_user_key: userKey.keyB64 } }, kdfParams: diff --git a/package-lock.json b/package-lock.json index 88302755623..ff21acbc20b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.168", + "@bitwarden/sdk-internal": "0.2.0-main.177", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.1.0", @@ -4803,9 +4803,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.168", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.168.tgz", - "integrity": "sha512-NU10oqw+GI9oHrh8/i/IC8/7oaYmswqC2E/0Zc56xC3jY7uNgFZgpae7JhyMU6UxzrAjiEqdmGnm+AGWFiPG8w==", + "version": "0.2.0-main.177", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.177.tgz", + "integrity": "sha512-2fp/g0WJDPPrIqrU88QrwoJsZTzoi7S7eCf+Qq0/8x3ImqQyoYJEdHdz06YHjUdS0CzucPrwTo5zJ/ZvcLNOmQ==", "license": "GPL-3.0" }, "node_modules/@bitwarden/send-ui": { diff --git a/package.json b/package.json index 8a8e80bd632..d27d919fb16 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.168", + "@bitwarden/sdk-internal": "0.2.0-main.177", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.1.0", From 888e2031a702f3e12c61b666aa87fa8fc62d3cd4 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 27 May 2025 08:24:53 -0500 Subject: [PATCH 147/163] [PM-21090] Vault - Repeated Syncs (#14740) * move `fullSync` contents to private methods in prep to storing the respective promise * store in-flight sync so multiple calls to the sync service are avoided * Revert "store in-flight sync so multiple calls to the sync service are avoided" This reverts commit 233c8e9d4b92d448f63e157b01c168d840c7e531. * Revert "move `fullSync` contents to private methods in prep to storing the respective promise" This reverts commit 3f686ac6a4b41e332180478f269d9fde85f562a1. * store inflight API calls for sync service - This avoids duplicate network requests in a relatively short amount of time but still allows consumers to call `fullSync` if needed * add debug log for duplicate sync --- .../sync/default-sync.service.spec.ts | 65 +++++++++++++++---- .../src/platform/sync/default-sync.service.ts | 32 ++++++++- 2 files changed, 82 insertions(+), 15 deletions(-) diff --git a/libs/common/src/platform/sync/default-sync.service.spec.ts b/libs/common/src/platform/sync/default-sync.service.spec.ts index 29543672dbe..fc6b9481bd5 100644 --- a/libs/common/src/platform/sync/default-sync.service.spec.ts +++ b/libs/common/src/platform/sync/default-sync.service.spec.ts @@ -130,23 +130,23 @@ describe("DefaultSyncService", () => { const user1 = "user1" as UserId; + const emptySyncResponse = new SyncResponse({ + profile: { + id: user1, + }, + folders: [], + collections: [], + ciphers: [], + sends: [], + domains: [], + policies: [], + }); + describe("fullSync", () => { beforeEach(() => { accountService.activeAccount$ = of({ id: user1 } as Account); Matrix.autoMockMethod(authService.authStatusFor$, () => of(AuthenticationStatus.Unlocked)); - apiService.getSync.mockResolvedValue( - new SyncResponse({ - profile: { - id: user1, - }, - folders: [], - collections: [], - ciphers: [], - sends: [], - domains: [], - policies: [], - }), - ); + apiService.getSync.mockResolvedValue(emptySyncResponse); Matrix.autoMockMethod(userDecryptionOptionsService.userDecryptionOptionsById$, () => of({ hasMasterPassword: true } satisfies UserDecryptionOptions), ); @@ -201,5 +201,44 @@ describe("DefaultSyncService", () => { expect(apiService.refreshIdentityToken).toHaveBeenCalledTimes(1); expect(apiService.getSync).toHaveBeenCalledTimes(1); }); + + describe("in-flight syncs", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("does not call getSync when one is already in progress", async () => { + const fullSyncPromises = [sut.fullSync(true), sut.fullSync(false), sut.fullSync(false)]; + + jest.advanceTimersByTime(100); + + await Promise.all(fullSyncPromises); + + expect(apiService.getSync).toHaveBeenCalledTimes(1); + }); + + it("does not call refreshIdentityToken when one is already in progress", async () => { + const fullSyncPromises = [sut.fullSync(true), sut.fullSync(false), sut.fullSync(false)]; + + jest.advanceTimersByTime(100); + + await Promise.all(fullSyncPromises); + + expect(apiService.refreshIdentityToken).toHaveBeenCalledTimes(1); + }); + + it("resets the in-flight properties when the complete", async () => { + const fullSyncPromises = [sut.fullSync(true), sut.fullSync(true)]; + + await Promise.all(fullSyncPromises); + + expect(sut["inFlightApiCalls"].refreshToken).toBeNull(); + expect(sut["inFlightApiCalls"].sync).toBeNull(); + }); + }); }); }); diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index 6e1a8f28443..47ac3784c33 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -58,11 +58,21 @@ import { MessageSender } from "../messaging"; import { StateProvider } from "../state"; import { CoreSyncService } from "./core-sync.service"; +import { SyncResponse } from "./sync.response"; import { SyncOptions } from "./sync.service"; export class DefaultSyncService extends CoreSyncService { syncInProgress = false; + /** The promises associated with any in-flight api calls. */ + private inFlightApiCalls: { + refreshToken: Promise | null; + sync: Promise | null; + } = { + refreshToken: null, + sync: null, + }; + constructor( private masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, @@ -141,9 +151,24 @@ export class DefaultSyncService extends CoreSyncService { try { if (!skipTokenRefresh) { - await this.apiService.refreshIdentityToken(); + // Store the promise so multiple calls to refresh the token are not made + if (this.inFlightApiCalls.refreshToken === null) { + this.inFlightApiCalls.refreshToken = this.apiService.refreshIdentityToken(); + } + + await this.inFlightApiCalls.refreshToken; } - const response = await this.apiService.getSync(); + + // Store the promise so multiple calls to sync are not made + if (this.inFlightApiCalls.sync === null) { + this.inFlightApiCalls.sync = this.apiService.getSync(); + } else { + this.logService.debug( + "Sync: Sync network call already in progress, returning existing promise", + ); + } + + const response = await this.inFlightApiCalls.sync; await this.syncProfile(response.profile); await this.syncFolders(response.folders, response.profile.id); @@ -162,6 +187,9 @@ export class DefaultSyncService extends CoreSyncService { } else { return this.syncCompleted(false, userId); } + } finally { + this.inFlightApiCalls.refreshToken = null; + this.inFlightApiCalls.sync = null; } } From f4f659c52a0703d9f1e868185925185799f34b5e Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 27 May 2025 15:38:33 +0200 Subject: [PATCH 148/163] Increase allowed memory for chromatic (#14964) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d27d919fb16..f63db41e1f3 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "lint:dep-ownership": "tsc --project ./scripts/tsconfig.json && node ./scripts/dist/dep-ownership.js", "docs:json": "compodoc -p ./tsconfig.json -e json -d . --disableRoutesGraph", "storybook": "ng run components:storybook", - "build-storybook": "ng run components:build-storybook", - "build-storybook:ci": "ng run components:build-storybook --webpack-stats-json", + "build-storybook": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" ng run components:build-storybook", + "build-storybook:ci": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" ng run components:build-storybook --webpack-stats-json", "test-stories": "test-storybook --url http://localhost:6006", "test-stories:watch": "test-stories --watch", "postinstall": "patch-package" From 97a591e738b3d062cdf8b75efd8b1ccef8934547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Tue, 27 May 2025 09:51:14 -0400 Subject: [PATCH 149/163] [PM-16793] port credential generator service to providers (#14071) * introduce extension service * deprecate legacy forwarder types * eliminate repeat algorithm emissions * extend logging to preference management * align forwarder ids with vendor ids * fix duplicate policy emissions; debugging required logger enhancements ----- Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- apps/browser/src/_locales/en/messages.json | 3 + apps/desktop/src/locales/en/messages.json | 39 +- .../policies/password-generator.component.ts | 26 +- apps/web/src/locales/en/messages.json | 27 +- .../tools/extension/extension.service.spec.ts | 1 + libs/common/src/tools/log/disabled-logger.ts | 24 + .../src/tools/log/disabled-semantic-logger.ts | 22 - libs/common/src/tools/log/factory.ts | 79 +- libs/common/src/tools/log/index.ts | 2 + libs/common/src/tools/log/types.ts | 11 + libs/common/src/tools/log/util.ts | 12 + libs/common/src/tools/private-classifier.ts | 2 +- libs/common/src/tools/public-classifier.ts | 2 +- libs/common/src/tools/rx.rxjs.ts | 13 + libs/common/src/tools/rx.spec.ts | 1504 +++++++++-------- libs/common/src/tools/rx.ts | 82 +- .../user-state-subject-dependency-provider.ts | 5 + .../tools/state/user-state-subject.spec.ts | 1 + .../src/tools/state/user-state-subject.ts | 7 +- libs/common/src/tools/types.ts | 5 + libs/common/src/tools/util.ts | 16 +- .../src/components/export.component.ts | 6 +- .../src/catchall-settings.component.ts | 19 +- .../credential-generator-history.component.ts | 19 +- .../src/credential-generator.component.html | 10 +- .../src/credential-generator.component.ts | 300 ++-- .../src/forwarder-settings.component.ts | 52 +- .../src/generator-services.module.ts | 144 +- .../src/passphrase-settings.component.ts | 88 +- .../src/password-generator.component.html | 5 +- .../src/password-generator.component.ts | 142 +- .../src/password-settings.component.ts | 86 +- .../src/subaddress-settings.component.ts | 20 +- .../src/username-generator.component.html | 6 +- .../src/username-generator.component.ts | 286 ++-- .../src/username-settings.component.ts | 19 +- libs/tools/generator/components/src/util.ts | 103 +- ...redential-generator-service.abstraction.ts | 104 ++ .../generator.service.abstraction.ts | 1 + .../generator/core/src/abstractions/index.ts | 1 + .../core/src/data/default-addy-io-options.ts | 8 - .../data/default-credential-preferences.ts | 18 - .../src/data/default-duck-duck-go-options.ts | 6 - .../core/src/data/default-fastmail-options.ts | 8 - .../src/data/default-firefox-relay-options.ts | 6 - .../src/data/default-forward-email-options.ts | 7 - .../src/data/default-simple-login-options.ts | 7 - .../core/src/data/generator-types.ts | 15 - .../generator/core/src/data/generators.ts | 422 ----- libs/tools/generator/core/src/data/index.ts | 12 - .../tools/generator/core/src/data/policies.ts | 23 - .../core/src/data/username-digits.ts | 4 - .../core/src/engine/email-randomizer.spec.ts | 21 +- .../core/src/engine/email-randomizer.ts | 5 +- .../generator/core/src/engine/forwarder.ts | 4 +- .../src/engine/password-randomizer.spec.ts | 11 +- .../core/src/engine/password-randomizer.ts | 5 +- .../src/engine/username-randomizer.spec.ts | 8 +- libs/tools/generator/core/src/index.ts | 23 +- .../generator/core/src/integration/addy-io.ts | 3 +- .../core/src/integration/duck-duck-go.ts | 3 +- .../core/src/integration/fastmail.ts | 3 +- .../core/src/integration/firefox-relay.ts | 3 +- .../core/src/integration/forward-email.ts | 3 +- .../core/src/integration/simple-login.ts | 3 +- .../core/src/metadata/algorithm-metadata.ts | 10 +- .../core/src/metadata/email/catchall.spec.ts | 3 +- .../core/src/metadata/email/catchall.ts | 9 +- .../core/src/metadata/email/forwarder.ts | 17 +- .../src/metadata/email/plus-address.spec.ts | 3 +- .../core/src/metadata/email/plus-address.ts | 9 +- .../core/src/metadata/generator-metadata.ts | 3 +- .../generator/core/src/metadata/index.ts | 42 +- .../metadata/password/eff-word-list.spec.ts | 3 +- .../src/metadata/password/eff-word-list.ts | 11 +- .../metadata/password/random-password.spec.ts | 3 +- .../src/metadata/password/random-password.ts | 9 +- .../metadata/username/eff-word-list.spec.ts | 3 +- .../src/metadata/username/eff-word-list.ts | 9 +- .../tools/generator/core/src/metadata/util.ts | 8 +- .../available-algorithms-constraint.ts | 76 + .../available-algorithms-policy.spec.ts | 18 +- .../policies/available-algorithms-policy.ts | 43 +- ...ynamic-password-policy-constraints.spec.ts | 50 +- .../generator/core/src/policies/index.ts | 2 + ...phrase-generator-options-evaluator.spec.ts | 70 +- .../passphrase-least-privilege.spec.ts | 20 +- .../passphrase-policy-constraints.spec.ts | 20 +- ...ssword-generator-options-evaluator.spec.ts | 126 +- .../policies/password-least-privilege.spec.ts | 24 +- .../credential-generator-providers.ts | 14 + .../providers/credential-preferences.spec.ts | 105 ++ .../src/providers/credential-preferences.ts | 28 + .../generator-dependency-provider.ts | 12 + .../generator-metadata-provider.spec.ts | 5 +- .../generator-metadata-provider.ts | 56 +- .../generator-profile-provider.spec.ts | 1 + .../generator-profile-provider.ts | 6 +- .../generator/core/src/providers/index.ts | 4 + libs/tools/generator/core/src/rx.ts | 14 - .../credential-generator.service.spec.ts | 1050 ------------ .../services/credential-generator.service.ts | 296 ---- .../services/credential-preferences.spec.ts | 53 - .../src/services/credential-preferences.ts | 30 - ...fault-credential-generator.service.spec.ts | 356 ++++ .../default-credential-generator.service.ts | 219 +++ .../default-generator.service.spec.ts | 12 +- .../generator/core/src/services/index.ts | 2 +- .../eff-username-generator-strategy.ts | 7 +- .../passphrase-generator-strategy.spec.ts | 31 +- .../passphrase-generator-strategy.ts | 14 +- .../password-generator-strategy.spec.ts | 53 +- .../strategies/password-generator-strategy.ts | 18 +- .../core/src/types/algorithm-info.ts | 50 + .../credential-generator-configuration.ts | 153 -- .../core/src/types/credential-preference.ts | 10 + .../core/src/types/forwarder-options.ts | 65 +- .../core/src/types/generate-request.ts | 11 +- .../src/types/generated-credential.spec.ts | 20 +- .../core/src/types/generated-credential.ts | 8 +- .../core/src/types/generator-type.ts | 83 - libs/tools/generator/core/src/types/index.ts | 16 +- .../core/src/types/policy-configuration.ts | 13 - libs/tools/generator/core/src/util.spec.ts | 90 +- libs/tools/generator/core/src/util.ts | 33 +- libs/tools/generator/core/tsconfig.json | 6 +- .../history/src/generated-credential.spec.ts | 22 +- .../history/src/generated-credential.ts | 4 +- .../src/generator-history.abstraction.ts | 4 +- .../local-generator-history.service.spec.ts | 54 +- .../src/local-generator-history.service.ts | 9 +- .../legacy/src}/forwarders.ts | 16 +- ...legacy-password-generation.service.spec.ts | 89 +- ...legacy-username-generation.service.spec.ts | 103 +- .../src/legacy-username-generation.service.ts | 19 +- .../generator-navigation-evaluator.spec.ts | 4 +- .../src/generator-navigation-evaluator.ts | 4 +- .../src/generator-navigation-policy.ts | 4 +- .../navigation/src/generator-navigation.ts | 7 +- .../options/send-options.component.ts | 6 +- 140 files changed, 3720 insertions(+), 4085 deletions(-) create mode 100644 libs/common/src/tools/log/disabled-logger.ts delete mode 100644 libs/common/src/tools/log/disabled-semantic-logger.ts create mode 100644 libs/common/src/tools/log/types.ts create mode 100644 libs/common/src/tools/log/util.ts create mode 100644 libs/common/src/tools/rx.rxjs.ts create mode 100644 libs/tools/generator/core/src/abstractions/credential-generator-service.abstraction.ts delete mode 100644 libs/tools/generator/core/src/data/default-addy-io-options.ts delete mode 100644 libs/tools/generator/core/src/data/default-credential-preferences.ts delete mode 100644 libs/tools/generator/core/src/data/default-duck-duck-go-options.ts delete mode 100644 libs/tools/generator/core/src/data/default-fastmail-options.ts delete mode 100644 libs/tools/generator/core/src/data/default-firefox-relay-options.ts delete mode 100644 libs/tools/generator/core/src/data/default-forward-email-options.ts delete mode 100644 libs/tools/generator/core/src/data/default-simple-login-options.ts delete mode 100644 libs/tools/generator/core/src/data/generator-types.ts delete mode 100644 libs/tools/generator/core/src/data/generators.ts delete mode 100644 libs/tools/generator/core/src/data/policies.ts delete mode 100644 libs/tools/generator/core/src/data/username-digits.ts create mode 100644 libs/tools/generator/core/src/policies/available-algorithms-constraint.ts create mode 100644 libs/tools/generator/core/src/providers/credential-generator-providers.ts create mode 100644 libs/tools/generator/core/src/providers/credential-preferences.spec.ts create mode 100644 libs/tools/generator/core/src/providers/credential-preferences.ts create mode 100644 libs/tools/generator/core/src/providers/generator-dependency-provider.ts rename libs/tools/generator/core/src/{services => providers}/generator-metadata-provider.spec.ts (98%) rename libs/tools/generator/core/src/{services => providers}/generator-metadata-provider.ts (87%) rename libs/tools/generator/core/src/{services => providers}/generator-profile-provider.spec.ts (99%) rename libs/tools/generator/core/src/{services => providers}/generator-profile-provider.ts (94%) create mode 100644 libs/tools/generator/core/src/providers/index.ts delete mode 100644 libs/tools/generator/core/src/services/credential-generator.service.spec.ts delete mode 100644 libs/tools/generator/core/src/services/credential-generator.service.ts delete mode 100644 libs/tools/generator/core/src/services/credential-preferences.spec.ts delete mode 100644 libs/tools/generator/core/src/services/credential-preferences.ts create mode 100644 libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts create mode 100644 libs/tools/generator/core/src/services/default-credential-generator.service.ts create mode 100644 libs/tools/generator/core/src/types/algorithm-info.ts delete mode 100644 libs/tools/generator/core/src/types/credential-generator-configuration.ts create mode 100644 libs/tools/generator/core/src/types/credential-preference.ts delete mode 100644 libs/tools/generator/core/src/types/generator-type.ts rename libs/tools/generator/{core/src/data => extensions/legacy/src}/forwarders.ts (73%) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 4775d1f7af0..feb5a7706f3 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2204,6 +2204,9 @@ "useThisPassword": { "message": "Use this password" }, + "useThisPassphrase": { + "message": "Use this passphrase" + }, "useThisUsername": { "message": "Use this username" }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 631f7d571f6..9d668d464ae 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -351,12 +351,6 @@ "other": { "message": "Other" }, - "generatePassword": { - "message": "Generate password" - }, - "generatePassphrase": { - "message": "Generate passphrase" - }, "type": { "message": "Type" }, @@ -2633,6 +2627,24 @@ "usernameGenerator": { "message": "Username generator" }, + "generatePassword": { + "message": "Generate password" + }, + "generatePassphrase": { + "message": "Generate passphrase" + }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "spinboxBoundariesHint": { "message": "Value must be between $MIN$ and $MAX$.", "description": "Explains spin box minimum and maximum values to the user", @@ -2686,6 +2698,15 @@ "useThisEmail": { "message": "Use this email" }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisPassphrase": { + "message": "Use this passphrase" + }, + "useThisUsername": { + "message": "Use this username" + }, "random": { "message": "Random" }, @@ -3051,12 +3072,6 @@ "weakAndBreachedMasterPasswordDesc": { "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, - "useThisPassword": { - "message": "Use this password" - }, - "useThisUsername": { - "message": "Use this username" - }, "checkForBreaches": { "message": "Check known data breaches for this password" }, diff --git a/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts b/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts index f11b14aea38..26f87f333eb 100644 --- a/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts @@ -7,7 +7,7 @@ import { BehaviorSubject, map } from "rxjs"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Generators } from "@bitwarden/generator-core"; +import { BuiltIn, Profile } from "@bitwarden/generator-core"; import { BasePolicy, BasePolicyComponent } from "./base-policy.component"; @@ -26,14 +26,22 @@ export class PasswordGeneratorPolicy extends BasePolicy { export class PasswordGeneratorPolicyComponent extends BasePolicyComponent { // these properties forward the application default settings to the UI // for HTML attribute bindings - protected readonly minLengthMin = Generators.password.settings.constraints.length.min; - protected readonly minLengthMax = Generators.password.settings.constraints.length.max; - protected readonly minNumbersMin = Generators.password.settings.constraints.minNumber.min; - protected readonly minNumbersMax = Generators.password.settings.constraints.minNumber.max; - protected readonly minSpecialMin = Generators.password.settings.constraints.minSpecial.min; - protected readonly minSpecialMax = Generators.password.settings.constraints.minSpecial.max; - protected readonly minNumberWordsMin = Generators.passphrase.settings.constraints.numWords.min; - protected readonly minNumberWordsMax = Generators.passphrase.settings.constraints.numWords.max; + protected readonly minLengthMin = + BuiltIn.password.profiles[Profile.account].constraints.default.length.min; + protected readonly minLengthMax = + BuiltIn.password.profiles[Profile.account].constraints.default.length.max; + protected readonly minNumbersMin = + BuiltIn.password.profiles[Profile.account].constraints.default.minNumber.min; + protected readonly minNumbersMax = + BuiltIn.password.profiles[Profile.account].constraints.default.minNumber.max; + protected readonly minSpecialMin = + BuiltIn.password.profiles[Profile.account].constraints.default.minSpecial.min; + protected readonly minSpecialMax = + BuiltIn.password.profiles[Profile.account].constraints.default.minSpecial.max; + protected readonly minNumberWordsMin = + BuiltIn.passphrase.profiles[Profile.account].constraints.default.numWords.min; + protected readonly minNumberWordsMax = + BuiltIn.passphrase.profiles[Profile.account].constraints.default.numWords.max; data = this.formBuilder.group({ overridePasswordType: [null], diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 3c17baabbb9..a170612fab2 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -547,12 +547,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "generatePassword": { - "message": "Generate password" - }, - "generatePassphrase": { - "message": "Generate passphrase" - }, "checkPassword": { "message": "Check if password has been exposed." }, @@ -6825,6 +6819,24 @@ "generateEmail": { "message": "Generate email" }, + "generatePassword": { + "message": "Generate password" + }, + "generatePassphrase": { + "message": "Generate passphrase" + }, + "passwordGenerated": { + "message": "Password generated" + }, + "passphraseGenerated": { + "message": "Passphrase generated" + }, + "usernameGenerated": { + "message": "Username generated" + }, + "emailGenerated": { + "message": "Email generated" + }, "spinboxBoundariesHint": { "message": "Value must be between $MIN$ and $MAX$.", "description": "Explains spin box minimum and maximum values to the user", @@ -6888,6 +6900,9 @@ "useThisPassword": { "message": "Use this password" }, + "useThisPassphrase": { + "message": "Use this passphrase" + }, "useThisUsername": { "message": "Use this username" }, diff --git a/libs/common/src/tools/extension/extension.service.spec.ts b/libs/common/src/tools/extension/extension.service.spec.ts index dad5684b523..9959488feca 100644 --- a/libs/common/src/tools/extension/extension.service.spec.ts +++ b/libs/common/src/tools/extension/extension.service.spec.ts @@ -60,6 +60,7 @@ const SomeProvider = { } as LegacyEncryptorProvider, state: SomeStateProvider, log: disabledSemanticLoggerProvider, + now: Date.now, } as UserStateSubjectDependencyProvider; const SomeExtension: ExtensionMetadata = { diff --git a/libs/common/src/tools/log/disabled-logger.ts b/libs/common/src/tools/log/disabled-logger.ts new file mode 100644 index 00000000000..53feb0c8b39 --- /dev/null +++ b/libs/common/src/tools/log/disabled-logger.ts @@ -0,0 +1,24 @@ +import { Jsonify } from "type-fest"; + +import { deepFreeze } from "../util"; + +import { SemanticLogger } from "./semantic-logger.abstraction"; + +/** All disabled loggers emitted by this module are `===` to this logger. */ +export const DISABLED_LOGGER: SemanticLogger = deepFreeze({ + debug(_content: Jsonify, _message?: string): void {}, + + info(_content: Jsonify, _message?: string): void {}, + + warn(_content: Jsonify, _message?: string): void {}, + + error(_content: Jsonify, _message?: string): void {}, + + panic(content: Jsonify, message?: string): never { + if (typeof content === "string" && !message) { + throw new Error(content); + } else { + throw new Error(message); + } + }, +}); diff --git a/libs/common/src/tools/log/disabled-semantic-logger.ts b/libs/common/src/tools/log/disabled-semantic-logger.ts deleted file mode 100644 index 21ea48bbe51..00000000000 --- a/libs/common/src/tools/log/disabled-semantic-logger.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Jsonify } from "type-fest"; - -import { SemanticLogger } from "./semantic-logger.abstraction"; - -/** Disables semantic logs. Still panics. */ -export class DisabledSemanticLogger implements SemanticLogger { - debug(_content: Jsonify, _message?: string): void {} - - info(_content: Jsonify, _message?: string): void {} - - warn(_content: Jsonify, _message?: string): void {} - - error(_content: Jsonify, _message?: string): void {} - - panic(content: Jsonify, message?: string): never { - if (typeof content === "string" && !message) { - throw new Error(content); - } else { - throw new Error(message); - } - } -} diff --git a/libs/common/src/tools/log/factory.ts b/libs/common/src/tools/log/factory.ts index f8abc4d2240..5f0f2d74e91 100644 --- a/libs/common/src/tools/log/factory.ts +++ b/libs/common/src/tools/log/factory.ts @@ -3,11 +3,10 @@ import { Jsonify } from "type-fest"; import { LogService } from "../../platform/abstractions/log.service"; import { DefaultSemanticLogger } from "./default-semantic-logger"; -import { DisabledSemanticLogger } from "./disabled-semantic-logger"; +import { DISABLED_LOGGER } from "./disabled-logger"; import { SemanticLogger } from "./semantic-logger.abstraction"; - -/** A type for injection of a log provider */ -export type LogProvider = (context: Jsonify) => SemanticLogger; +import { LogProvider } from "./types"; +import { warnLoggingEnabled } from "./util"; /** Instantiates a semantic logger that emits nothing when a message * is logged. @@ -18,38 +17,72 @@ export type LogProvider = (context: Jsonify) => SemanticLogger export function disabledSemanticLoggerProvider( _context: Jsonify, ): SemanticLogger { - return new DisabledSemanticLogger(); + return DISABLED_LOGGER; } /** Instantiates a semantic logger that emits logs to the console. - * @param context a static payload that is cloned when the logger - * logs a message. The `messages`, `level`, and `content` fields - * are reserved for use by loggers. - * @param settings specializes how the semantic logger functions. - * If this is omitted, the logger suppresses debug messages. + * @param logService writes semantic logs to the console */ -export function consoleSemanticLoggerProvider( - logger: LogService, - context: Jsonify, -): SemanticLogger { - return new DefaultSemanticLogger(logger, context); +export function consoleSemanticLoggerProvider(logService: LogService): LogProvider { + function provider(context: Jsonify) { + const logger = new DefaultSemanticLogger(logService, context); + + warnLoggingEnabled(logService, "consoleSemanticLoggerProvider", context); + return logger; + } + + return provider; } -/** Instantiates a semantic logger that emits logs to the console. +/** Instantiates a semantic logger that emits logs to the console when the + * context's `type` matches its values. + * @param logService writes semantic logs to the console + * @param types the values to match against + */ +export function enableLogForTypes(logService: LogService, types: string[]): LogProvider { + if (types.length) { + warnLoggingEnabled(logService, "enableLogForTypes", { types }); + } + + function provider(context: Jsonify) { + const { type } = context as { type?: unknown }; + if (typeof type === "string" && types.includes(type)) { + const logger = new DefaultSemanticLogger(logService, context); + + warnLoggingEnabled(logService, "enableLogForTypes", { + targetType: type, + available: types, + loggerContext: context, + }); + return logger; + } else { + return DISABLED_LOGGER; + } + } + + return provider; +} + +/** Instantiates a semantic logger that emits logs to the console when its enabled. + * @param enable logs are emitted when this is true + * @param logService writes semantic logs to the console * @param context a static payload that is cloned when the logger - * logs a message. The `messages`, `level`, and `content` fields - * are reserved for use by loggers. - * @param settings specializes how the semantic logger functions. - * If this is omitted, the logger suppresses debug messages. + * logs a message. + * + * @remarks The `message`, `level`, `provider`, and `content` fields + * are reserved for use by the semantic logging system. */ export function ifEnabledSemanticLoggerProvider( enable: boolean, - logger: LogService, + logService: LogService, context: Jsonify, ) { if (enable) { - return consoleSemanticLoggerProvider(logger, context); + const logger = new DefaultSemanticLogger(logService, context); + + warnLoggingEnabled(logService, "ifEnabledSemanticLoggerProvider", context); + return logger; } else { - return disabledSemanticLoggerProvider(context); + return DISABLED_LOGGER; } } diff --git a/libs/common/src/tools/log/index.ts b/libs/common/src/tools/log/index.ts index 22444d23e27..1c93a6ce63f 100644 --- a/libs/common/src/tools/log/index.ts +++ b/libs/common/src/tools/log/index.ts @@ -1,2 +1,4 @@ export * from "./factory"; +export * from "./disabled-logger"; +export { LogProvider } from "./types"; export { SemanticLogger } from "./semantic-logger.abstraction"; diff --git a/libs/common/src/tools/log/types.ts b/libs/common/src/tools/log/types.ts new file mode 100644 index 00000000000..ca887af4225 --- /dev/null +++ b/libs/common/src/tools/log/types.ts @@ -0,0 +1,11 @@ +import { Jsonify } from "type-fest"; + +import { SemanticLogger } from "./semantic-logger.abstraction"; + +/** Creates a semantic logger. + * @param context all logs emitted by the logger are extended with + * these fields. + * @remarks The `message`, `level`, `provider`, and `content` fields + * are reserved for use by the semantic logging system. + */ +export type LogProvider = (context: Jsonify) => SemanticLogger; diff --git a/libs/common/src/tools/log/util.ts b/libs/common/src/tools/log/util.ts new file mode 100644 index 00000000000..cf1c39230e1 --- /dev/null +++ b/libs/common/src/tools/log/util.ts @@ -0,0 +1,12 @@ +import { LogService } from "../../platform/abstractions/log.service"; + +// show our GRIT - these functions implement generalized logging +// controls and should return DISABLED_LOGGER in production. +export function warnLoggingEnabled(logService: LogService, method: string, context?: any) { + logService.warning({ + method, + context, + provider: "tools/log", + message: "Semantic logging enabled. 🦟 Please report this bug if you see it 🦟", + }); +} diff --git a/libs/common/src/tools/private-classifier.ts b/libs/common/src/tools/private-classifier.ts index e2406d314c0..58244ae9906 100644 --- a/libs/common/src/tools/private-classifier.ts +++ b/libs/common/src/tools/private-classifier.ts @@ -17,7 +17,7 @@ export class PrivateClassifier implements Classifier; - return { disclosed: {}, secret }; + return { disclosed: null, secret }; } declassify(_disclosed: Jsonify>, secret: Jsonify) { diff --git a/libs/common/src/tools/public-classifier.ts b/libs/common/src/tools/public-classifier.ts index 136bee555ac..e036ebd1c42 100644 --- a/libs/common/src/tools/public-classifier.ts +++ b/libs/common/src/tools/public-classifier.ts @@ -16,7 +16,7 @@ export class PublicClassifier implements Classifier; - return { disclosed, secret: "" }; + return { disclosed, secret: null }; } declassify(disclosed: Jsonify, _secret: Jsonify>) { diff --git a/libs/common/src/tools/rx.rxjs.ts b/libs/common/src/tools/rx.rxjs.ts new file mode 100644 index 00000000000..7c11d658f19 --- /dev/null +++ b/libs/common/src/tools/rx.rxjs.ts @@ -0,0 +1,13 @@ +import { Observable } from "rxjs"; + +/** + * Used to infer types from arguments to functions like {@link withLatestReady}. + * So that you can have `forkJoin([Observable, PromiseLike]): Observable<[A, B]>` + * et al. + * @remarks this type definition is derived from rxjs' {@link ObservableInputTuple}. + * The difference is it *only* works with observables, while the rx version works + * with any thing that can become an observable. + */ +export type ObservableTuple = { + [K in keyof T]: Observable; +}; diff --git a/libs/common/src/tools/rx.spec.ts b/libs/common/src/tools/rx.spec.ts index ee1de1c9118..5177dcaa2a5 100644 --- a/libs/common/src/tools/rx.spec.ts +++ b/libs/common/src/tools/rx.spec.ts @@ -3,7 +3,7 @@ * @jest-environment ../../../../shared/test.environment.ts */ // @ts-strict-ignore this file explicitly tests what happens when types are ignored -import { of, firstValueFrom, Subject, tap, EmptyError } from "rxjs"; +import { of, firstValueFrom, Subject, tap, EmptyError, BehaviorSubject } from "rxjs"; import { awaitAsync, trackEmissions } from "../../spec"; @@ -16,733 +16,825 @@ import { reduceCollection, withLatestReady, pin, + memoizedMap, } from "./rx"; -describe("errorOnChange", () => { - it("emits a single value when the input emits only once", async () => { - const source$ = new Subject(); - const results: number[] = []; - source$.pipe(errorOnChange()).subscribe((v) => results.push(v)); +describe("tools rx utilites", () => { + describe("errorOnChange", () => { + it("emits a single value when the input emits only once", async () => { + const source$ = new Subject(); + const results: number[] = []; + source$.pipe(errorOnChange()).subscribe((v) => results.push(v)); - source$.next(1); + source$.next(1); - expect(results).toEqual([1]); + expect(results).toEqual([1]); + }); + + it("emits when the input emits", async () => { + const source$ = new Subject(); + const results: number[] = []; + source$.pipe(errorOnChange()).subscribe((v) => results.push(v)); + + source$.next(1); + source$.next(1); + + expect(results).toEqual([1, 1]); + }); + + it("errors when the input errors", async () => { + const source$ = new Subject(); + const expected = {}; + let error: any = null; + source$.pipe(errorOnChange()).subscribe({ error: (v: unknown) => (error = v) }); + + source$.error(expected); + + expect(error).toBe(expected); + }); + + it("completes when the input completes", async () => { + const source$ = new Subject(); + let complete: boolean = false; + source$.pipe(errorOnChange()).subscribe({ complete: () => (complete = true) }); + + source$.complete(); + + expect(complete).toBe(true); + }); + + it("errors when the input changes", async () => { + const source$ = new Subject(); + let error: any = null; + source$.pipe(errorOnChange()).subscribe({ error: (v: unknown) => (error = v) }); + + source$.next(1); + source$.next(2); + + expect(error).toEqual({ expectedValue: 1, actualValue: 2 }); + }); + + it("emits when the extracted value remains constant", async () => { + type Foo = { foo: string }; + const source$ = new Subject(); + const results: Foo[] = []; + source$.pipe(errorOnChange((v) => v.foo)).subscribe((v) => results.push(v)); + + source$.next({ foo: "bar" }); + source$.next({ foo: "bar" }); + + expect(results).toEqual([{ foo: "bar" }, { foo: "bar" }]); + }); + + it("errors when an extracted value changes", async () => { + type Foo = { foo: string }; + const source$ = new Subject(); + let error: any = null; + source$.pipe(errorOnChange((v) => v.foo)).subscribe({ error: (v: unknown) => (error = v) }); + + source$.next({ foo: "bar" }); + source$.next({ foo: "baz" }); + + expect(error).toEqual({ expectedValue: "bar", actualValue: "baz" }); + }); + + it("constructs an error when the extracted value changes", async () => { + type Foo = { foo: string }; + const source$ = new Subject(); + let error: any = null; + source$ + .pipe( + errorOnChange( + (v) => v.foo, + (expected, actual) => ({ expected, actual }), + ), + ) + .subscribe({ error: (v: unknown) => (error = v) }); + + source$.next({ foo: "bar" }); + source$.next({ foo: "baz" }); + + expect(error).toEqual({ expected: "bar", actual: "baz" }); + }); }); - it("emits when the input emits", async () => { - const source$ = new Subject(); - const results: number[] = []; - source$.pipe(errorOnChange()).subscribe((v) => results.push(v)); + describe("reduceCollection", () => { + it.each([[null], [undefined], [[]]])( + "should return the default value when the collection is %p", + async (value: number[]) => { + const reduce = (acc: number, value: number) => acc + value; + const source$ = of(value); - source$.next(1); - source$.next(1); + const result$ = source$.pipe(reduceCollection(reduce, 100)); + const result = await firstValueFrom(result$); - expect(results).toEqual([1, 1]); - }); + expect(result).toEqual(100); + }, + ); - it("errors when the input errors", async () => { - const source$ = new Subject(); - const expected = {}; - let error: any = null; - source$.pipe(errorOnChange()).subscribe({ error: (v: unknown) => (error = v) }); - - source$.error(expected); - - expect(error).toBe(expected); - }); - - it("completes when the input completes", async () => { - const source$ = new Subject(); - let complete: boolean = false; - source$.pipe(errorOnChange()).subscribe({ complete: () => (complete = true) }); - - source$.complete(); - - expect(complete).toBe(true); - }); - - it("errors when the input changes", async () => { - const source$ = new Subject(); - let error: any = null; - source$.pipe(errorOnChange()).subscribe({ error: (v: unknown) => (error = v) }); - - source$.next(1); - source$.next(2); - - expect(error).toEqual({ expectedValue: 1, actualValue: 2 }); - }); - - it("emits when the extracted value remains constant", async () => { - type Foo = { foo: string }; - const source$ = new Subject(); - const results: Foo[] = []; - source$.pipe(errorOnChange((v) => v.foo)).subscribe((v) => results.push(v)); - - source$.next({ foo: "bar" }); - source$.next({ foo: "bar" }); - - expect(results).toEqual([{ foo: "bar" }, { foo: "bar" }]); - }); - - it("errors when an extracted value changes", async () => { - type Foo = { foo: string }; - const source$ = new Subject(); - let error: any = null; - source$.pipe(errorOnChange((v) => v.foo)).subscribe({ error: (v: unknown) => (error = v) }); - - source$.next({ foo: "bar" }); - source$.next({ foo: "baz" }); - - expect(error).toEqual({ expectedValue: "bar", actualValue: "baz" }); - }); - - it("constructs an error when the extracted value changes", async () => { - type Foo = { foo: string }; - const source$ = new Subject(); - let error: any = null; - source$ - .pipe( - errorOnChange( - (v) => v.foo, - (expected, actual) => ({ expected, actual }), - ), - ) - .subscribe({ error: (v: unknown) => (error = v) }); - - source$.next({ foo: "bar" }); - source$.next({ foo: "baz" }); - - expect(error).toEqual({ expected: "bar", actual: "baz" }); - }); -}); - -describe("reduceCollection", () => { - it.each([[null], [undefined], [[]]])( - "should return the default value when the collection is %p", - async (value: number[]) => { + it("should reduce the collection to a single value", async () => { const reduce = (acc: number, value: number) => acc + value; - const source$ = of(value); + const source$ = of([1, 2, 3]); - const result$ = source$.pipe(reduceCollection(reduce, 100)); + const result$ = source$.pipe(reduceCollection(reduce, 0)); const result = await firstValueFrom(result$); - expect(result).toEqual(100); - }, - ); - - it("should reduce the collection to a single value", async () => { - const reduce = (acc: number, value: number) => acc + value; - const source$ = of([1, 2, 3]); - - const result$ = source$.pipe(reduceCollection(reduce, 0)); - const result = await firstValueFrom(result$); - - expect(result).toEqual(6); - }); -}); - -describe("distinctIfShallowMatch", () => { - it("emits a single value", async () => { - const source$ = of({ foo: true }); - const pipe$ = source$.pipe(distinctIfShallowMatch()); - - const result = trackEmissions(pipe$); - await awaitAsync(); - - expect(result).toEqual([{ foo: true }]); - }); - - it("emits different values", async () => { - const source$ = of({ foo: true }, { foo: false }); - const pipe$ = source$.pipe(distinctIfShallowMatch()); - - const result = trackEmissions(pipe$); - await awaitAsync(); - - expect(result).toEqual([{ foo: true }, { foo: false }]); - }); - - it("emits new keys", async () => { - const source$ = of({ foo: true }, { foo: true, bar: true }); - const pipe$ = source$.pipe(distinctIfShallowMatch()); - - const result = trackEmissions(pipe$); - await awaitAsync(); - - expect(result).toEqual([{ foo: true }, { foo: true, bar: true }]); - }); - - it("suppresses identical values", async () => { - const source$ = of({ foo: true }, { foo: true }); - const pipe$ = source$.pipe(distinctIfShallowMatch()); - - const result = trackEmissions(pipe$); - await awaitAsync(); - - expect(result).toEqual([{ foo: true }]); - }); - - it("suppresses removed keys", async () => { - const source$ = of({ foo: true, bar: true }, { foo: true }); - const pipe$ = source$.pipe(distinctIfShallowMatch()); - - const result = trackEmissions(pipe$); - await awaitAsync(); - - expect(result).toEqual([{ foo: true, bar: true }]); - }); -}); - -describe("anyComplete", () => { - it("emits true when its input completes", () => { - const input$ = new Subject(); - - const emissions: boolean[] = []; - anyComplete(input$).subscribe((e) => emissions.push(e)); - input$.complete(); - - expect(emissions).toEqual([true]); - }); - - it("completes when its input is already complete", () => { - const input = new Subject(); - input.complete(); - - let completed = false; - anyComplete(input).subscribe({ complete: () => (completed = true) }); - - expect(completed).toBe(true); - }); - - it("completes when any input completes", () => { - const input$ = new Subject(); - const completing$ = new Subject(); - - let completed = false; - anyComplete([input$, completing$]).subscribe({ complete: () => (completed = true) }); - completing$.complete(); - - expect(completed).toBe(true); - }); - - it("ignores emissions", () => { - const input$ = new Subject(); - - const emissions: boolean[] = []; - anyComplete(input$).subscribe((e) => emissions.push(e)); - input$.next(1); - input$.next(2); - input$.complete(); - - expect(emissions).toEqual([true]); - }); - - it("forwards errors", () => { - const input$ = new Subject(); - const expected = { some: "error" }; - - let error = null; - anyComplete(input$).subscribe({ error: (e: unknown) => (error = e) }); - input$.error(expected); - - expect(error).toEqual(expected); - }); -}); - -describe("ready", () => { - it("connects when subscribed", () => { - const watch$ = new Subject(); - let connected = false; - const source$ = new Subject().pipe(tap({ subscribe: () => (connected = true) })); - - // precondition: ready$ should be cold - const ready$ = source$.pipe(ready(watch$)); - expect(connected).toBe(false); - - ready$.subscribe(); - - expect(connected).toBe(true); - }); - - it("suppresses source emissions until its watch emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - const results: number[] = []; - ready$.subscribe((n) => results.push(n)); - - // precondition: no emissions - source$.next(1); - expect(results).toEqual([]); - - watch$.next(); - - expect(results).toEqual([1]); - }); - - it("suppresses source emissions until all watches emit", () => { - const watchA$ = new Subject(); - const watchB$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready([watchA$, watchB$])); - const results: number[] = []; - ready$.subscribe((n) => results.push(n)); - - // preconditions: no emissions - source$.next(1); - expect(results).toEqual([]); - watchA$.next(); - expect(results).toEqual([]); - - watchB$.next(); - - expect(results).toEqual([1]); - }); - - it("emits the last source emission when its watch emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - const results: number[] = []; - ready$.subscribe((n) => results.push(n)); - - // precondition: no emissions - source$.next(1); - expect(results).toEqual([]); - - source$.next(2); - watch$.next(); - - expect(results).toEqual([2]); - }); - - it("emits all source emissions after its watch emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - const results: number[] = []; - ready$.subscribe((n) => results.push(n)); - - watch$.next(); - source$.next(1); - source$.next(2); - - expect(results).toEqual([1, 2]); - }); - - it("ignores repeated watch emissions", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - const results: number[] = []; - ready$.subscribe((n) => results.push(n)); - - watch$.next(); - source$.next(1); - watch$.next(); - source$.next(2); - watch$.next(); - - expect(results).toEqual([1, 2]); - }); - - it("completes when its source completes", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - let completed = false; - ready$.subscribe({ complete: () => (completed = true) }); - - source$.complete(); - - expect(completed).toBeTruthy(); - }); - - it("errors when its source errors", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - const expected = { some: "error" }; - let error = null; - ready$.subscribe({ error: (e: unknown) => (error = e) }); - - source$.error(expected); - - expect(error).toEqual(expected); - }); - - it("errors when its watch errors", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - const expected = { some: "error" }; - let error = null; - ready$.subscribe({ error: (e: unknown) => (error = e) }); - - watch$.error(expected); - - expect(error).toEqual(expected); - }); - - it("errors when its watch completes before emitting", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - let error = null; - ready$.subscribe({ error: (e: unknown) => (error = e) }); - - watch$.complete(); - - expect(error).toBeInstanceOf(EmptyError); - }); -}); - -describe("withLatestReady", () => { - it("connects when subscribed", () => { - const watch$ = new Subject(); - let connected = false; - const source$ = new Subject().pipe(tap({ subscribe: () => (connected = true) })); - - // precondition: ready$ should be cold - const ready$ = source$.pipe(withLatestReady(watch$)); - expect(connected).toBe(false); - - ready$.subscribe(); - - expect(connected).toBe(true); - }); - - it("suppresses source emissions until its watch emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(withLatestReady(watch$)); - const results: [number, string][] = []; - ready$.subscribe((n) => results.push(n)); - - // precondition: no emissions - source$.next(1); - expect(results).toEqual([]); - - watch$.next("watch"); - - expect(results).toEqual([[1, "watch"]]); - }); - - it("emits the last source emission when its watch emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(withLatestReady(watch$)); - const results: [number, string][] = []; - ready$.subscribe((n) => results.push(n)); - - // precondition: no emissions - source$.next(1); - expect(results).toEqual([]); - - source$.next(2); - watch$.next("watch"); - - expect(results).toEqual([[2, "watch"]]); - }); - - it("emits all source emissions after its watch emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(withLatestReady(watch$)); - const results: [number, string][] = []; - ready$.subscribe((n) => results.push(n)); - - watch$.next("watch"); - source$.next(1); - source$.next(2); - - expect(results).toEqual([ - [1, "watch"], - [2, "watch"], - ]); - }); - - it("appends the latest watch emission", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(withLatestReady(watch$)); - const results: [number, string][] = []; - ready$.subscribe((n) => results.push(n)); - - watch$.next("ignored"); - watch$.next("watch"); - source$.next(1); - watch$.next("ignored"); - watch$.next("watch"); - source$.next(2); - - expect(results).toEqual([ - [1, "watch"], - [2, "watch"], - ]); - }); - - it("completes when its source completes", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(withLatestReady(watch$)); - let completed = false; - ready$.subscribe({ complete: () => (completed = true) }); - - source$.complete(); - - expect(completed).toBeTruthy(); - }); - - it("errors when its source errors", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(withLatestReady(watch$)); - const expected = { some: "error" }; - let error = null; - ready$.subscribe({ error: (e: unknown) => (error = e) }); - - source$.error(expected); - - expect(error).toEqual(expected); - }); - - it("errors when its watch errors", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(withLatestReady(watch$)); - const expected = { some: "error" }; - let error = null; - ready$.subscribe({ error: (e: unknown) => (error = e) }); - - watch$.error(expected); - - expect(error).toEqual(expected); - }); - - it("errors when its watch completes before emitting", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(withLatestReady(watch$)); - let error = null; - ready$.subscribe({ error: (e: unknown) => (error = e) }); - - watch$.complete(); - - expect(error).toBeInstanceOf(EmptyError); - }); -}); - -describe("on", () => { - it("connects when subscribed", () => { - const watch$ = new Subject(); - let connected = false; - const source$ = new Subject().pipe(tap({ subscribe: () => (connected = true) })); - - // precondition: on$ should be cold - const on$ = source$.pipe(on(watch$)); - expect(connected).toBeFalsy(); - - on$.subscribe(); - - expect(connected).toBeTruthy(); - }); - - it("suppresses source emissions until `on` emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const results: number[] = []; - source$.pipe(on(watch$)).subscribe((n) => results.push(n)); - - // precondition: on$ should be cold - source$.next(1); - expect(results).toEqual([]); - - watch$.next(); - - expect(results).toEqual([1]); - }); - - it("repeats source emissions when `on` emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const results: number[] = []; - source$.pipe(on(watch$)).subscribe((n) => results.push(n)); - source$.next(1); - - watch$.next(); - watch$.next(); - - expect(results).toEqual([1, 1]); - }); - - it("updates source emissions when `on` emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const results: number[] = []; - source$.pipe(on(watch$)).subscribe((n) => results.push(n)); - - source$.next(1); - watch$.next(); - source$.next(2); - watch$.next(); - - expect(results).toEqual([1, 2]); - }); - - it("emits a value when `on` emits before the source is ready", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const results: number[] = []; - source$.pipe(on(watch$)).subscribe((n) => results.push(n)); - - watch$.next(); - source$.next(1); - - expect(results).toEqual([1]); - }); - - it("ignores repeated `on` emissions before the source is ready", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const results: number[] = []; - source$.pipe(on(watch$)).subscribe((n) => results.push(n)); - - watch$.next(); - watch$.next(); - source$.next(1); - - expect(results).toEqual([1]); - }); - - it("emits only the latest source emission when `on` emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const results: number[] = []; - source$.pipe(on(watch$)).subscribe((n) => results.push(n)); - source$.next(1); - - watch$.next(); - - source$.next(2); - source$.next(3); - watch$.next(); - - expect(results).toEqual([1, 3]); - }); - - it("completes when its source completes", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - let complete: boolean = false; - source$.pipe(on(watch$)).subscribe({ complete: () => (complete = true) }); - - source$.complete(); - - expect(complete).toBeTruthy(); - }); - - it("completes when its watch completes", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - let complete: boolean = false; - source$.pipe(on(watch$)).subscribe({ complete: () => (complete = true) }); - - watch$.complete(); - - expect(complete).toBeTruthy(); - }); - - it("errors when its source errors", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const expected = { some: "error" }; - let error = null; - source$.pipe(on(watch$)).subscribe({ error: (e: unknown) => (error = e) }); - - source$.error(expected); - - expect(error).toEqual(expected); - }); - - it("errors when its watch errors", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const expected = { some: "error" }; - let error = null; - source$.pipe(on(watch$)).subscribe({ error: (e: unknown) => (error = e) }); - - watch$.error(expected); - - expect(error).toEqual(expected); - }); -}); - -describe("pin", () => { - it("emits the first value", async () => { - const input = new Subject(); - const result: unknown[] = []; - - input.pipe(pin()).subscribe((v) => result.push(v)); - input.next(1); - - expect(result).toEqual([1]); - }); - - it("filters repeated emissions", async () => { - const input = new Subject(); - const result: unknown[] = []; - - input.pipe(pin({ distinct: (p, c) => p == c })).subscribe((v) => result.push(v)); - input.next(1); - input.next(1); - - expect(result).toEqual([1]); - }); - - it("errors if multiple emissions occur", async () => { - const input = new Subject(); - let error: any = null!; - - input.pipe(pin()).subscribe({ - error: (e: unknown) => { - error = e; - }, + expect(result).toEqual(6); }); - input.next(1); - input.next(1); - - expect(error).toBeInstanceOf(Error); - expect(error.message).toMatch(/^unknown/); }); - it("names the pinned observables if multiple emissions occur", async () => { - const input = new Subject(); - let error: any = null!; + describe("distinctIfShallowMatch", () => { + it("emits a single value", async () => { + const source$ = of({ foo: true }); + const pipe$ = source$.pipe(distinctIfShallowMatch()); - input.pipe(pin({ name: () => "example" })).subscribe({ - error: (e: unknown) => { - error = e; - }, + const result = trackEmissions(pipe$); + await awaitAsync(); + + expect(result).toEqual([{ foo: true }]); }); - input.next(1); - input.next(1); - expect(error).toBeInstanceOf(Error); - expect(error.message).toMatch(/^example/); + it("emits different values", async () => { + const source$ = of({ foo: true }, { foo: false }); + const pipe$ = source$.pipe(distinctIfShallowMatch()); + + const result = trackEmissions(pipe$); + await awaitAsync(); + + expect(result).toEqual([{ foo: true }, { foo: false }]); + }); + + it("emits new keys", async () => { + const source$ = of({ foo: true }, { foo: true, bar: true }); + const pipe$ = source$.pipe(distinctIfShallowMatch()); + + const result = trackEmissions(pipe$); + await awaitAsync(); + + expect(result).toEqual([{ foo: true }, { foo: true, bar: true }]); + }); + + it("suppresses identical values", async () => { + const source$ = of({ foo: true }, { foo: true }); + const pipe$ = source$.pipe(distinctIfShallowMatch()); + + const result = trackEmissions(pipe$); + await awaitAsync(); + + expect(result).toEqual([{ foo: true }]); + }); + + it("suppresses removed keys", async () => { + const source$ = of({ foo: true, bar: true }, { foo: true }); + const pipe$ = source$.pipe(distinctIfShallowMatch()); + + const result = trackEmissions(pipe$); + await awaitAsync(); + + expect(result).toEqual([{ foo: true, bar: true }]); + }); }); - it("errors if indistinct emissions occur", async () => { - const input = new Subject(); - let error: any = null!; + describe("anyComplete", () => { + it("emits true when its input completes", () => { + const input$ = new Subject(); - input - .pipe(pin({ distinct: (p, c) => p == c })) - .subscribe({ error: (e: unknown) => (error = e) }); - input.next(1); - input.next(2); + const emissions: boolean[] = []; + anyComplete(input$).subscribe((e) => emissions.push(e)); + input$.complete(); - expect(error).toBeInstanceOf(Error); - expect(error.message).toMatch(/^unknown/); + expect(emissions).toEqual([true]); + }); + + it("completes when its input is already complete", () => { + const input = new Subject(); + input.complete(); + + let completed = false; + anyComplete(input).subscribe({ complete: () => (completed = true) }); + + expect(completed).toBe(true); + }); + + it("completes when any input completes", () => { + const input$ = new Subject(); + const completing$ = new Subject(); + + let completed = false; + anyComplete([input$, completing$]).subscribe({ complete: () => (completed = true) }); + completing$.complete(); + + expect(completed).toBe(true); + }); + + it("ignores emissions", () => { + const input$ = new Subject(); + + const emissions: boolean[] = []; + anyComplete(input$).subscribe((e) => emissions.push(e)); + input$.next(1); + input$.next(2); + input$.complete(); + + expect(emissions).toEqual([true]); + }); + + it("forwards errors", () => { + const input$ = new Subject(); + const expected = { some: "error" }; + + let error = null; + anyComplete(input$).subscribe({ error: (e: unknown) => (error = e) }); + input$.error(expected); + + expect(error).toEqual(expected); + }); + }); + + describe("ready", () => { + it("connects when subscribed", () => { + const watch$ = new Subject(); + let connected = false; + const source$ = new Subject().pipe(tap({ subscribe: () => (connected = true) })); + + // precondition: ready$ should be cold + const ready$ = source$.pipe(ready(watch$)); + expect(connected).toBe(false); + + ready$.subscribe(); + + expect(connected).toBe(true); + }); + + it("suppresses source emissions until its watch emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + const results: number[] = []; + ready$.subscribe((n) => results.push(n)); + + // precondition: no emissions + source$.next(1); + expect(results).toEqual([]); + + watch$.next(); + + expect(results).toEqual([1]); + }); + + it("suppresses source emissions until all watches emit", () => { + const watchA$ = new Subject(); + const watchB$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready([watchA$, watchB$])); + const results: number[] = []; + ready$.subscribe((n) => results.push(n)); + + // preconditions: no emissions + source$.next(1); + expect(results).toEqual([]); + watchA$.next(); + expect(results).toEqual([]); + + watchB$.next(); + + expect(results).toEqual([1]); + }); + + it("emits the last source emission when its watch emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + const results: number[] = []; + ready$.subscribe((n) => results.push(n)); + + // precondition: no emissions + source$.next(1); + expect(results).toEqual([]); + + source$.next(2); + watch$.next(); + + expect(results).toEqual([2]); + }); + + it("emits all source emissions after its watch emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + const results: number[] = []; + ready$.subscribe((n) => results.push(n)); + + watch$.next(); + source$.next(1); + source$.next(2); + + expect(results).toEqual([1, 2]); + }); + + it("ignores repeated watch emissions", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + const results: number[] = []; + ready$.subscribe((n) => results.push(n)); + + watch$.next(); + source$.next(1); + watch$.next(); + source$.next(2); + watch$.next(); + + expect(results).toEqual([1, 2]); + }); + + it("completes when its source completes", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + let completed = false; + ready$.subscribe({ complete: () => (completed = true) }); + + source$.complete(); + + expect(completed).toBeTruthy(); + }); + + it("errors when its source errors", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + const expected = { some: "error" }; + let error = null; + ready$.subscribe({ error: (e: unknown) => (error = e) }); + + source$.error(expected); + + expect(error).toEqual(expected); + }); + + it("errors when its watch errors", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + const expected = { some: "error" }; + let error = null; + ready$.subscribe({ error: (e: unknown) => (error = e) }); + + watch$.error(expected); + + expect(error).toEqual(expected); + }); + + it("errors when its watch completes before emitting", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + let error = null; + ready$.subscribe({ error: (e: unknown) => (error = e) }); + + watch$.complete(); + + expect(error).toBeInstanceOf(EmptyError); + }); + }); + + describe("withLatestReady", () => { + it("connects when subscribed", () => { + const watch$ = new Subject(); + let connected = false; + const source$ = new Subject().pipe(tap({ subscribe: () => (connected = true) })); + + // precondition: ready$ should be cold + const ready$ = source$.pipe(withLatestReady(watch$)); + expect(connected).toBe(false); + + ready$.subscribe(); + + expect(connected).toBe(true); + }); + + it("suppresses source emissions until its watch emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + const results: [number, string][] = []; + ready$.subscribe((n) => results.push(n)); + + // precondition: no emissions + source$.next(1); + expect(results).toEqual([]); + + watch$.next("watch"); + + expect(results).toEqual([[1, "watch"]]); + }); + + it("emits the last source emission when its watch emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + const results: [number, string][] = []; + ready$.subscribe((n) => results.push(n)); + + // precondition: no emissions + source$.next(1); + expect(results).toEqual([]); + + source$.next(2); + watch$.next("watch"); + + expect(results).toEqual([[2, "watch"]]); + }); + + it("emits all source emissions after its watch emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + const results: [number, string][] = []; + ready$.subscribe((n) => results.push(n)); + + watch$.next("watch"); + source$.next(1); + source$.next(2); + + expect(results).toEqual([ + [1, "watch"], + [2, "watch"], + ]); + }); + + it("appends the latest watch emission", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + const results: [number, string][] = []; + ready$.subscribe((n) => results.push(n)); + + watch$.next("ignored"); + watch$.next("watch"); + source$.next(1); + watch$.next("ignored"); + watch$.next("watch"); + source$.next(2); + + expect(results).toEqual([ + [1, "watch"], + [2, "watch"], + ]); + }); + + it("completes when its source completes", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + let completed = false; + ready$.subscribe({ complete: () => (completed = true) }); + + source$.complete(); + + expect(completed).toBeTruthy(); + }); + + it("errors when its source errors", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + const expected = { some: "error" }; + let error = null; + ready$.subscribe({ error: (e: unknown) => (error = e) }); + + source$.error(expected); + + expect(error).toEqual(expected); + }); + + it("errors when its watch errors", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + const expected = { some: "error" }; + let error = null; + ready$.subscribe({ error: (e: unknown) => (error = e) }); + + watch$.error(expected); + + expect(error).toEqual(expected); + }); + + it("errors when its watch completes before emitting", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + let error = null; + ready$.subscribe({ error: (e: unknown) => (error = e) }); + + watch$.complete(); + + expect(error).toBeInstanceOf(EmptyError); + }); + }); + + describe("on", () => { + it("connects when subscribed", () => { + const watch$ = new Subject(); + let connected = false; + const source$ = new Subject().pipe(tap({ subscribe: () => (connected = true) })); + + // precondition: on$ should be cold + const on$ = source$.pipe(on(watch$)); + expect(connected).toBeFalsy(); + + on$.subscribe(); + + expect(connected).toBeTruthy(); + }); + + it("suppresses source emissions until `on` emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const results: number[] = []; + source$.pipe(on(watch$)).subscribe((n) => results.push(n)); + + // precondition: on$ should be cold + source$.next(1); + expect(results).toEqual([]); + + watch$.next(); + + expect(results).toEqual([1]); + }); + + it("repeats source emissions when `on` emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const results: number[] = []; + source$.pipe(on(watch$)).subscribe((n) => results.push(n)); + source$.next(1); + + watch$.next(); + watch$.next(); + + expect(results).toEqual([1, 1]); + }); + + it("updates source emissions when `on` emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const results: number[] = []; + source$.pipe(on(watch$)).subscribe((n) => results.push(n)); + + source$.next(1); + watch$.next(); + source$.next(2); + watch$.next(); + + expect(results).toEqual([1, 2]); + }); + + it("emits a value when `on` emits before the source is ready", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const results: number[] = []; + source$.pipe(on(watch$)).subscribe((n) => results.push(n)); + + watch$.next(); + source$.next(1); + + expect(results).toEqual([1]); + }); + + it("ignores repeated `on` emissions before the source is ready", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const results: number[] = []; + source$.pipe(on(watch$)).subscribe((n) => results.push(n)); + + watch$.next(); + watch$.next(); + source$.next(1); + + expect(results).toEqual([1]); + }); + + it("emits only the latest source emission when `on` emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const results: number[] = []; + source$.pipe(on(watch$)).subscribe((n) => results.push(n)); + source$.next(1); + + watch$.next(); + + source$.next(2); + source$.next(3); + watch$.next(); + + expect(results).toEqual([1, 3]); + }); + + it("completes when its source completes", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + let complete: boolean = false; + source$.pipe(on(watch$)).subscribe({ complete: () => (complete = true) }); + + source$.complete(); + + expect(complete).toBeTruthy(); + }); + + it("completes when its watch completes", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + let complete: boolean = false; + source$.pipe(on(watch$)).subscribe({ complete: () => (complete = true) }); + + watch$.complete(); + + expect(complete).toBeTruthy(); + }); + + it("errors when its source errors", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const expected = { some: "error" }; + let error = null; + source$.pipe(on(watch$)).subscribe({ error: (e: unknown) => (error = e) }); + + source$.error(expected); + + expect(error).toEqual(expected); + }); + + it("errors when its watch errors", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const expected = { some: "error" }; + let error = null; + source$.pipe(on(watch$)).subscribe({ error: (e: unknown) => (error = e) }); + + watch$.error(expected); + + expect(error).toEqual(expected); + }); + }); + + describe("pin", () => { + it("emits the first value", async () => { + const input = new Subject(); + const result: unknown[] = []; + + input.pipe(pin()).subscribe((v) => result.push(v)); + input.next(1); + + expect(result).toEqual([1]); + }); + + it("filters repeated emissions", async () => { + const input = new Subject(); + const result: unknown[] = []; + + input.pipe(pin({ distinct: (p, c) => p == c })).subscribe((v) => result.push(v)); + input.next(1); + input.next(1); + + expect(result).toEqual([1]); + }); + + it("errors if multiple emissions occur", async () => { + const input = new Subject(); + let error: any = null!; + + input.pipe(pin()).subscribe({ + error: (e: unknown) => { + error = e; + }, + }); + input.next(1); + input.next(1); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatch(/^unknown/); + }); + + it("names the pinned observables if multiple emissions occur", async () => { + const input = new Subject(); + let error: any = null!; + + input.pipe(pin({ name: () => "example" })).subscribe({ + error: (e: unknown) => { + error = e; + }, + }); + input.next(1); + input.next(1); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatch(/^example/); + }); + + it("errors if indistinct emissions occur", async () => { + const input = new Subject(); + let error: any = null!; + + input + .pipe(pin({ distinct: (p, c) => p == c })) + .subscribe({ error: (e: unknown) => (error = e) }); + input.next(1); + input.next(2); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatch(/^unknown/); + }); + }); + + describe("memoizedMap", () => { + it("maps a value", () => { + const source$ = new Subject(); + const result$ = new BehaviorSubject({}); + const expectedResult = {}; + source$.pipe(memoizedMap(() => expectedResult)).subscribe(result$); + + source$.next("foo"); + + expect(result$.value).toEqual(expectedResult); + }); + + it("caches a mapped result", () => { + const source$ = new Subject(); + const result$ = new BehaviorSubject({}); + const map = jest.fn(() => ({})); + source$.pipe(memoizedMap(map)).subscribe(result$); + + source$.next("foo"); + source$.next("foo"); + + expect(map).toHaveBeenCalledTimes(1); + }); + + it("caches the last mapped result", () => { + const source$ = new Subject(); + const result$ = new BehaviorSubject({}); + const map = jest.fn(() => ({})); + source$.pipe(memoizedMap(map)).subscribe(result$); + + source$.next("foo"); + source$.next("foo"); + source$.next("bar"); + source$.next("foo"); + + expect(map).toHaveBeenCalledTimes(3); + }); + + it("caches multiple mapped results", () => { + const source$ = new Subject(); + const result$ = new BehaviorSubject({}); + const map = jest.fn(() => ({})); + source$.pipe(memoizedMap(map, { size: 2 })).subscribe(result$); + + source$.next("foo"); + source$.next("bar"); + source$.next("foo"); + source$.next("bar"); + + expect(map).toHaveBeenCalledTimes(2); + }); + + it("caches a result by key", () => { + const source$ = new Subject<{ key: string }>(); + const result$ = new BehaviorSubject({}); + const map = jest.fn(() => ({})); + source$.pipe(memoizedMap(map, { key: (s) => s.key })).subscribe(result$); + + // the messages are not equal; the keys are + source$.next({ key: "foo" }); + source$.next({ key: "foo" }); + source$.next({ key: "bar" }); + source$.next({ key: "bar" }); + + expect(map).toHaveBeenCalledTimes(2); + }); + }); + + it("errors", () => { + const source$ = new Subject(); + let error: unknown = null; + source$.pipe(memoizedMap(() => {})).subscribe({ error: (e: unknown) => (error = e) }); + const expectedError = {}; + + source$.error(expectedError); + + expect(error).toEqual(expectedError); + }); + + it("completes", () => { + const source$ = new Subject(); + let completed = false; + source$.pipe(memoizedMap(() => {})).subscribe({ complete: () => (completed = true) }); + + source$.complete(); + + expect(completed).toEqual(true); }); }); diff --git a/libs/common/src/tools/rx.ts b/libs/common/src/tools/rx.ts index ea397135581..4c9e547d151 100644 --- a/libs/common/src/tools/rx.ts +++ b/libs/common/src/tools/rx.ts @@ -20,8 +20,13 @@ import { startWith, pairwise, MonoTypeOperatorFunction, + Cons, + scan, + filter, } from "rxjs"; +import { ObservableTuple } from "./rx.rxjs"; + /** Returns its input. */ function identity(value: any): any { return value; @@ -164,26 +169,30 @@ export function ready(watch$: Observable | Observable[]) { ); } -export function withLatestReady( - watch$: Observable, -): OperatorFunction { +export function withLatestReady( + ...watches$: [...ObservableTuple] +): OperatorFunction> { return connect((source$) => { // these subscriptions are safe because `source$` connects only after there // is an external subscriber. const source = new ReplaySubject(1); source$.subscribe(source); - const watch = new ReplaySubject(1); - watch$.subscribe(watch); + + const watches = watches$.map((w) => { + const watch$ = new ReplaySubject(1); + w.subscribe(watch$); + return watch$; + }) as [...ObservableTuple]; // `concat` is subscribed immediately after it's returned, at which point - // `zip` blocks until all items in `watching$` are ready. If that occurs + // `zip` blocks until all items in `watches` are ready. If that occurs // after `source$` is hot, then the replay subject sends the last-captured - // emission through immediately. Otherwise, `ready` waits for the next - // emission - return concat(zip(watch).pipe(first(), ignoreElements()), source).pipe( - withLatestFrom(watch), + // emission through immediately. Otherwise, `withLatestFrom` waits for the + // next emission + return concat(zip(watches).pipe(first(), ignoreElements()), source).pipe( + withLatestFrom(...watches), takeUntil(anyComplete(source)), - ); + ) as Observable>; }); } @@ -238,3 +247,54 @@ export function pin(options?: { }), ); } + +/** maps a value to a result and keeps a cache of the mapping + * @param mapResult - maps the stream to a result; this function must return + * a value. It must not return null or undefined. + * @param options.size - the number of entries in the cache + * @param options.key - maps the source to a cache key + * @remarks this method is useful for optimization of expensive + * `mapResult` calls. It's also useful when an interned reference type + * is needed. + */ +export function memoizedMap>( + mapResult: (source: Source) => Result, + options?: { size?: number; key?: (source: Source) => unknown }, +): OperatorFunction { + return pipe( + // scan's accumulator contains the cache + scan( + ([cache], source) => { + const key: unknown = options?.key?.(source) ?? source; + + // cache hit? + let result = cache?.get(key); + if (result) { + return [cache, result] as const; + } + + // cache miss + result = mapResult(source); + cache?.set(key, result); + + // trim cache + const overage = cache.size - (options?.size ?? 1); + if (overage > 0) { + Array.from(cache?.keys() ?? []) + .slice(0, overage) + .forEach((k) => cache?.delete(k)); + } + + return [cache, result] as const; + }, + // FIXME: upgrade to a least-recently-used cache + [new Map(), null] as [Map, Source | null], + ), + + // encapsulate cache + map(([, result]) => result), + + // preserve `NonNullable` constraint on `Result` + filter((result): result is Result => !!result), + ); +} diff --git a/libs/common/src/tools/state/user-state-subject-dependency-provider.ts b/libs/common/src/tools/state/user-state-subject-dependency-provider.ts index 2763aac4830..89b6d7e7270 100644 --- a/libs/common/src/tools/state/user-state-subject-dependency-provider.ts +++ b/libs/common/src/tools/state/user-state-subject-dependency-provider.ts @@ -15,4 +15,9 @@ export abstract class UserStateSubjectDependencyProvider { // FIXME: remove `log` and inject the system provider into the USS instead /** Provides semantic logging */ abstract log: (_context: Jsonify) => SemanticLogger; + + /** Get the system time as a number of seconds since the unix epoch + * @remarks this can be turned into a date using `new Date(provider.now())` + */ + abstract now: () => number; } diff --git a/libs/common/src/tools/state/user-state-subject.spec.ts b/libs/common/src/tools/state/user-state-subject.spec.ts index 20acff17877..a6d452d37fd 100644 --- a/libs/common/src/tools/state/user-state-subject.spec.ts +++ b/libs/common/src/tools/state/user-state-subject.spec.ts @@ -90,6 +90,7 @@ const SomeProvider = { } as LegacyEncryptorProvider, state: SomeStateProvider, log: disabledSemanticLoggerProvider, + now: () => 100, }; function fooMaxLength(maxLength: number): StateConstraints { diff --git a/libs/common/src/tools/state/user-state-subject.ts b/libs/common/src/tools/state/user-state-subject.ts index dd88ec2fb20..118b0069c84 100644 --- a/libs/common/src/tools/state/user-state-subject.ts +++ b/libs/common/src/tools/state/user-state-subject.ts @@ -477,7 +477,12 @@ export class UserStateSubject< * @returns the subscription */ subscribe(observer?: Partial> | ((value: State) => void) | null): Subscription { - return this.output.pipe(map((wc) => wc.state)).subscribe(observer); + return this.output + .pipe( + map((wc) => wc.state), + distinctUntilChanged(), + ) + .subscribe(observer); } // using subjects to ensure the right semantics are followed; diff --git a/libs/common/src/tools/types.ts b/libs/common/src/tools/types.ts index 83f451351c2..6123b1f1dea 100644 --- a/libs/common/src/tools/types.ts +++ b/libs/common/src/tools/types.ts @@ -2,6 +2,11 @@ import { Simplify } from "type-fest"; import { IntegrationId } from "./integration"; +/** When this is a string, it contains the i18n key. When it is an object, the `literal` member + * contains text that should not be translated. + */ +export type I18nKeyOrLiteral = string | { literal: string }; + /** Constraints that are shared by all primitive field types */ type PrimitiveConstraint = { /** `true` indicates the field is required; otherwise the field is optional */ diff --git a/libs/common/src/tools/util.ts b/libs/common/src/tools/util.ts index 9a3a14c1c83..0fcb5a972af 100644 --- a/libs/common/src/tools/util.ts +++ b/libs/common/src/tools/util.ts @@ -1,3 +1,5 @@ +import { I18nKeyOrLiteral } from "./types"; + /** Recursively freeze an object's own keys * @param value the value to freeze * @returns `value` @@ -10,10 +12,22 @@ export function deepFreeze(value: T): Readonly { for (const key of keys) { const own = value[key]; - if ((own && typeof own === "object") || typeof own === "function") { + if (own && typeof own === "object") { deepFreeze(own); } } return Object.freeze(value); } + +/** Type guard that returns `true` when the value is an i18n key. */ +export function isI18nKey(value: I18nKeyOrLiteral): value is string { + return typeof value === "string"; +} + +/** Type guard that returns `true` when the value requires no translation. + * @remarks the literal value can be accessed using the `.literal` property. + */ +export function isLiteral(value: I18nKeyOrLiteral): value is { literal: string } { + return typeof value === "object" && "literal" in value; +} diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 0512b56e20d..956cc611c2e 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -57,7 +57,7 @@ import { ToastService, } from "@bitwarden/components"; import { GeneratorServicesModule } from "@bitwarden/generator-components"; -import { CredentialGeneratorService, GenerateRequest, Generators } from "@bitwarden/generator-core"; +import { CredentialGeneratorService, GenerateRequest, Type } from "@bitwarden/generator-core"; import { ExportedVault, VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; import { EncryptedExportType } from "../enums/encrypted-export-type.enum"; @@ -248,7 +248,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { }), ); this.generatorService - .generate$(Generators.password, { on$: this.onGenerate$, account$ }) + .generate$({ on$: this.onGenerate$, account$ }) .pipe(takeUntil(this.destroy$)) .subscribe((generated) => { this.exportForm.patchValue({ @@ -379,7 +379,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { } generatePassword = async () => { - this.onGenerate$.next({ source: "export" }); + this.onGenerate$.next({ source: "export", type: Type.password }); }; submit = async () => { diff --git a/libs/tools/generator/components/src/catchall-settings.component.ts b/libs/tools/generator/components/src/catchall-settings.component.ts index 3bf586c5a29..a836b26f98b 100644 --- a/libs/tools/generator/components/src/catchall-settings.component.ts +++ b/libs/tools/generator/components/src/catchall-settings.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component, EventEmitter, @@ -17,7 +15,7 @@ import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { CatchallGenerationOptions, CredentialGeneratorService, - Generators, + BuiltIn, } from "@bitwarden/generator-core"; /** Options group for catchall emails */ @@ -28,7 +26,6 @@ import { }) export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges { /** Instantiates the component - * @param accountService queries user availability * @param generatorService settings and policy logic * @param formBuilder reactive form controls */ @@ -37,24 +34,26 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges { private generatorService: CredentialGeneratorService, ) {} - /** Binds the component to a specific user's settings. + /** Binds the component to a specific user's settings.\ + * @remarks this is initialized to null but since it's a required input it'll + * never have that value in practice. */ @Input({ required: true }) - account: Account; + account!: Account; private account$ = new ReplaySubject(1); /** Emits settings updates and completes if the settings become unavailable. * @remarks this does not emit the initial settings. If you would like * to receive live settings updates including the initial update, - * use `CredentialGeneratorService.settings$(...)` instead. + * use `CredentialGeneratorService.settings(...)` instead. */ @Output() readonly onUpdated = new EventEmitter(); /** The template's control bindings */ protected settings = this.formBuilder.group({ - catchallDomain: [Generators.catchall.settings.initial.catchallDomain], + catchallDomain: [""], }); async ngOnChanges(changes: SimpleChanges) { @@ -64,7 +63,7 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges { } async ngOnInit() { - const settings = await this.generatorService.settings(Generators.catchall, { + const settings = await this.generatorService.settings(BuiltIn.catchall, { account$: this.account$, }); @@ -79,7 +78,7 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges { this.saveSettings .pipe( withLatestFrom(this.settings.valueChanges), - map(([, settings]) => settings), + map(([, settings]) => settings as CatchallGenerationOptions), takeUntil(this.destroyed$), ) .subscribe(settings); diff --git a/libs/tools/generator/components/src/credential-generator-history.component.ts b/libs/tools/generator/components/src/credential-generator-history.component.ts index e7dadb3da71..76dfbaea867 100644 --- a/libs/tools/generator/components/src/credential-generator-history.component.ts +++ b/libs/tools/generator/components/src/credential-generator-history.component.ts @@ -6,6 +6,7 @@ import { BehaviorSubject, ReplaySubject, Subject, map, switchMap, takeUntil, tap import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SemanticLogger, @@ -19,10 +20,11 @@ import { ItemModule, NoItemsModule, } from "@bitwarden/components"; -import { CredentialGeneratorService } from "@bitwarden/generator-core"; +import { AlgorithmsByType, CredentialGeneratorService } from "@bitwarden/generator-core"; import { GeneratedCredential, GeneratorHistoryService } from "@bitwarden/generator-history"; import { GeneratorModule } from "./generator.module"; +import { translate } from "./util"; @Component({ standalone: true, @@ -45,6 +47,7 @@ export class CredentialGeneratorHistoryComponent implements OnChanges, OnInit, O constructor( private generatorService: CredentialGeneratorService, private history: GeneratorHistoryService, + private i18nService: I18nService, private logService: LogService, ) {} @@ -94,13 +97,19 @@ export class CredentialGeneratorHistoryComponent implements OnChanges, OnInit, O } protected getCopyText(credential: GeneratedCredential) { - const info = this.generatorService.algorithm(credential.category); - return info.copy; + // there isn't a way way to look up category metadata so + // bodge it by looking up algorithm metadata + const [id] = AlgorithmsByType[credential.category]; + const info = this.generatorService.algorithm(id); + return translate(info.i18nKeys.copyCredential, this.i18nService); } protected getGeneratedValueText(credential: GeneratedCredential) { - const info = this.generatorService.algorithm(credential.category); - return info.credentialType; + // there isn't a way way to look up category metadata so + // bodge it by looking up algorithm metadata + const [id] = AlgorithmsByType[credential.category]; + const info = this.generatorService.algorithm(id); + return translate(info.i18nKeys.credentialType, this.i18nService); } ngOnDestroy() { diff --git a/libs/tools/generator/components/src/credential-generator.component.html b/libs/tools/generator/components/src/credential-generator.component.html index 24146968456..559554b7ddb 100644 --- a/libs/tools/generator/components/src/credential-generator.component.html +++ b/libs/tools/generator/components/src/credential-generator.component.html @@ -42,13 +42,13 @@ @@ -84,7 +84,7 @@ @@ -94,12 +94,12 @@ [forwarder]="forwarderId$ | async" /> diff --git a/libs/tools/generator/components/src/credential-generator.component.ts b/libs/tools/generator/components/src/credential-generator.component.ts index 0b48b4cf0f7..78b803392df 100644 --- a/libs/tools/generator/components/src/credential-generator.component.ts +++ b/libs/tools/generator/components/src/credential-generator.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { LiveAnnouncer } from "@angular/cdk/a11y"; import { Component, @@ -24,15 +22,15 @@ import { map, ReplaySubject, Subject, - switchMap, takeUntil, + tap, withLatestFrom, } from "rxjs"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { IntegrationId } from "@bitwarden/common/tools/integration"; +import { VendorId } from "@bitwarden/common/tools/extension"; import { SemanticLogger, disabledSemanticLoggerProvider, @@ -41,23 +39,25 @@ import { import { UserId } from "@bitwarden/common/types/guid"; import { ToastService, Option } from "@bitwarden/components"; import { - AlgorithmInfo, - CredentialAlgorithm, - CredentialCategory, + CredentialType, CredentialGeneratorService, GenerateRequest, GeneratedCredential, - Generators, - getForwarderConfiguration, - isEmailAlgorithm, - isForwarderIntegration, - isPasswordAlgorithm, + isForwarderExtensionId, isSameAlgorithm, + isEmailAlgorithm, isUsernameAlgorithm, - toCredentialGeneratorConfiguration, + isPasswordAlgorithm, + CredentialAlgorithm, + AlgorithmMetadata, + Algorithm, + AlgorithmsByType, + Type, } from "@bitwarden/generator-core"; import { GeneratorHistoryService } from "@bitwarden/generator-history"; +import { translate } from "./util"; + // constants used to identify navigation selections that are not // generator algorithms const IDENTIFIER = "identifier"; @@ -84,11 +84,14 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro private ariaLive: LiveAnnouncer, ) {} + /** exports algorithm symbols to the template */ + protected readonly Algorithm = Algorithm; + /** Binds the component to a specific user's settings. When this input is not provided, * the form binds to the active user */ @Input() - account: Account | null; + account: Account | null = null; /** Send structured debug logs from the credential generator component * to the debugger console. @@ -127,7 +130,7 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro @Output() readonly onGenerated = new EventEmitter(); - protected root$ = new BehaviorSubject<{ nav: string }>({ + protected root$ = new BehaviorSubject<{ nav: string | null }>({ nav: null, }); @@ -141,11 +144,11 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro } protected username = this.formBuilder.group({ - nav: [null as string], + nav: [null as string | null], }); protected forwarder = this.formBuilder.group({ - nav: [null as string], + nav: [null as string | null], }); async ngOnInit() { @@ -154,33 +157,52 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro }); if (!this.account) { - this.account = await firstValueFrom(this.accountService.activeAccount$); - this.log.info( - { userId: this.account.id }, - "account not specified; using active account settings", - ); - this.account$.next(this.account); + const account = await firstValueFrom(this.accountService.activeAccount$); + if (!account) { + this.log.panic("active account cannot be `null`."); + } + + this.log.info({ userId: account.id }, "account not specified; using active account settings"); + this.account$.next(account); } - this.generatorService - .algorithms$(["email", "username"], { account$: this.account$ }) + combineLatest([ + this.generatorService.algorithms$("email", { account$: this.account$ }), + this.generatorService.algorithms$("username", { account$: this.account$ }), + ]) .pipe( + map((algorithms) => algorithms.flat()), map((algorithms) => { - const usernames = algorithms.filter((a) => !isForwarderIntegration(a.id)); + // construct options for username and email algorithms; replace forwarder + // entry with a virtual entry for drill-down + const usernames = algorithms.filter((a) => !isForwarderExtensionId(a.id)); + usernames.sort((a, b) => a.weight - b.weight); const usernameOptions = this.toOptions(usernames); - usernameOptions.push({ value: FORWARDER, label: this.i18nService.t("forwardedEmail") }); + usernameOptions.splice(-1, 0, { + value: FORWARDER, + label: this.i18nService.t("forwardedEmail"), + }); - const forwarders = algorithms.filter((a) => isForwarderIntegration(a.id)); + // construct options for forwarder algorithms; they get their own selection box + const forwarders = algorithms.filter((a) => isForwarderExtensionId(a.id)); + forwarders.sort((a, b) => a.weight - b.weight); const forwarderOptions = this.toOptions(forwarders); forwarderOptions.unshift({ value: NONE_SELECTED, label: this.i18nService.t("select") }); return [usernameOptions, forwarderOptions] as const; }), + tap((algorithms) => + this.log.debug({ algorithms: algorithms as object }, "algorithms loaded"), + ), takeUntil(this.destroyed), ) .subscribe(([usernames, forwarders]) => { - this.usernameOptions$.next(usernames); - this.forwarderOptions$.next(forwarders); + // update subjects within the angular zone so that the + // template bindings refresh immediately + this.zone.run(() => { + this.usernameOptions$.next(usernames); + this.forwarderOptions$.next(forwarders); + }); }); this.generatorService @@ -195,9 +217,15 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro ) .subscribe(this.rootOptions$); - this.algorithm$ + this.maybeAlgorithm$ .pipe( - map((a) => a?.description), + map((a) => { + if (a?.i18nKeys?.description) { + return translate(a.i18nKeys.description, this.i18nService); + } else { + return ""; + } + }), takeUntil(this.destroyed), ) .subscribe((hint) => { @@ -208,9 +236,9 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro }); }); - this.algorithm$ + this.maybeAlgorithm$ .pipe( - map((a) => a?.category), + map((a) => a?.type), distinctUntilChanged(), takeUntil(this.destroyed), ) @@ -223,10 +251,12 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro }); // wire up the generator - this.algorithm$ + this.generatorService + .generate$({ + on$: this.generate$, + account$: this.account$, + }) .pipe( - filter((algorithm) => !!algorithm), - switchMap((algorithm) => this.typeToGenerator$(algorithm.id)), catchError((error: unknown, generator) => { if (typeof error === "string") { this.toastService.showToast({ @@ -241,11 +271,14 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro // continue with origin stream return generator; }), - withLatestFrom(this.account$, this.algorithm$), + withLatestFrom(this.account$, this.maybeAlgorithm$), takeUntil(this.destroyed), ) .subscribe(([generated, account, algorithm]) => { - this.log.debug({ source: generated.source }, "credential generated"); + this.log.debug( + { source: generated.source ?? null, algorithm: algorithm?.id ?? null }, + "credential generated", + ); this.generatorHistoryService .track(account.id, generated.credential, generated.category, generated.generationDate) @@ -256,8 +289,8 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { - if (generated.source === this.USER_REQUEST) { - this.announce(algorithm.onGeneratedMessage); + if (algorithm && generated.source === this.USER_REQUEST) { + this.announce(translate(algorithm.i18nKeys.credentialGenerated, this.i18nService)); } this.generatedCredential$.next(generated); @@ -274,36 +307,46 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro this.root$ .pipe( - map( - (root): CascadeValue => - root.nav === IDENTIFIER - ? { nav: root.nav } - : { nav: root.nav, algorithm: JSON.parse(root.nav) }, - ), + map((root): CascadeValue => { + if (root.nav === IDENTIFIER) { + return { nav: root.nav }; + } else if (root.nav) { + return { nav: root.nav, algorithm: JSON.parse(root.nav) }; + } else { + return { nav: IDENTIFIER }; + } + }), takeUntil(this.destroyed), ) .subscribe(activeRoot$); this.username.valueChanges .pipe( - map( - (username): CascadeValue => - username.nav === FORWARDER - ? { nav: username.nav } - : { nav: username.nav, algorithm: JSON.parse(username.nav) }, - ), + map((username): CascadeValue => { + if (username.nav === FORWARDER) { + return { nav: username.nav }; + } else if (username.nav) { + return { nav: username.nav, algorithm: JSON.parse(username.nav) }; + } else { + const [algorithm] = AlgorithmsByType[Type.username]; + return { nav: JSON.stringify(algorithm), algorithm }; + } + }), takeUntil(this.destroyed), ) .subscribe(activeIdentifier$); this.forwarder.valueChanges .pipe( - map( - (forwarder): CascadeValue => - forwarder.nav === NONE_SELECTED - ? { nav: forwarder.nav } - : { nav: forwarder.nav, algorithm: JSON.parse(forwarder.nav) }, - ), + map((forwarder): CascadeValue => { + if (forwarder.nav === NONE_SELECTED) { + return { nav: forwarder.nav }; + } else if (forwarder.nav) { + return { nav: forwarder.nav, algorithm: JSON.parse(forwarder.nav) }; + } else { + return { nav: NONE_SELECTED }; + } + }), takeUntil(this.destroyed), ) .subscribe(activeForwarder$); @@ -314,7 +357,7 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro map(([root, username, forwarder]) => { const showForwarder = !root.algorithm && !username.algorithm; const forwarderId = - showForwarder && isForwarderIntegration(forwarder.algorithm) + showForwarder && forwarder.algorithm && isForwarderExtensionId(forwarder.algorithm) ? forwarder.algorithm.forwarder : null; return [showForwarder, forwarderId] as const; @@ -344,47 +387,51 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro return null; } }), - distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)), + distinctUntilChanged((prev, next) => { + if (prev === null || next === null) { + return false; + } else { + return isSameAlgorithm(prev.id, next.id); + } + }), takeUntil(this.destroyed), ) .subscribe((algorithm) => { - this.log.debug(algorithm, "algorithm selected"); + this.log.debug({ algorithm: algorithm?.id ?? null }, "algorithm selected"); // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { - this.algorithm$.next(algorithm); + this.maybeAlgorithm$.next(algorithm); }); }); // assume the last-selected generator algorithm is the user's preferred one const preferences = await this.generatorService.preferences({ account$: this.account$ }); this.algorithm$ - .pipe( - filter((algorithm) => !!algorithm), - withLatestFrom(preferences), - takeUntil(this.destroyed), - ) + .pipe(withLatestFrom(preferences), takeUntil(this.destroyed)) .subscribe(([algorithm, preference]) => { - function setPreference(category: CredentialCategory, log: SemanticLogger) { - const p = preference[category]; + function setPreference(type: CredentialType) { + const p = preference[type]; p.algorithm = algorithm.id; p.updated = new Date(); - - log.info({ algorithm, category }, "algorithm preferences updated"); } // `is*Algorithm` decides `algorithm`'s type, which flows into `setPreference` if (isEmailAlgorithm(algorithm.id)) { - setPreference("email", this.log); + setPreference("email"); } else if (isUsernameAlgorithm(algorithm.id)) { - setPreference("username", this.log); + setPreference("username"); } else if (isPasswordAlgorithm(algorithm.id)) { - setPreference("password", this.log); + setPreference("password"); } else { return; } + this.log.info( + { algorithm: algorithm.id, type: algorithm.type }, + "algorithm preferences updated", + ); preferences.next(preference); }); @@ -392,10 +439,12 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro preferences .pipe( map(({ email, username, password }) => { - const forwarderPref = isForwarderIntegration(email.algorithm) ? email : null; const usernamePref = email.updated > username.updated ? email : username; + const forwarderPref = isForwarderExtensionId(usernamePref.algorithm) + ? usernamePref + : null; - // inject drilldown flags + // inject drill-down flags const forwarderNav = !forwarderPref ? NONE_SELECTED : JSON.stringify(forwarderPref.algorithm); @@ -411,14 +460,14 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro selection: { nav: rootNav }, active: { nav: rootNav, - algorithm: rootNav === IDENTIFIER ? null : password.algorithm, + algorithm: rootNav === IDENTIFIER ? undefined : password.algorithm, } as CascadeValue, }, username: { selection: { nav: userNav }, active: { nav: userNav, - algorithm: forwarderPref ? null : usernamePref.algorithm, + algorithm: forwarderPref ? undefined : usernamePref.algorithm, }, }, forwarder: { @@ -435,6 +484,15 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro takeUntil(this.destroyed), ) .subscribe(({ root, username, forwarder }) => { + this.log.debug( + { + root: root.selection, + username: username.selection, + forwarder: forwarder.selection, + }, + "navigation updated", + ); + // update navigation; break subscription loop this.onRootChanged(root.selection); this.username.setValue(username.selection, { emitEvent: false }); @@ -448,16 +506,16 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro // automatically regenerate when the algorithm switches if the algorithm // allows it; otherwise set a placeholder - this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => { + this.maybeAlgorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => { this.zone.run(() => { - if (!a || a.onlyOnRequest) { - this.log.debug("autogeneration disabled; clearing generated credential"); - this.generatedCredential$.next(null); - } else { + if (a?.capabilities?.autogenerate) { this.log.debug("autogeneration enabled"); this.generate("autogenerate").catch((e: unknown) => { this.log.error(e as object, "a failure occurred during autogeneration"); }); + } else { + this.log.debug("autogeneration disabled; clearing generated credential"); + this.generatedCredential$.next(undefined); } }); }); @@ -469,41 +527,6 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro this.ariaLive.announce(message).catch((e) => this.logService.error(e)); } - private typeToGenerator$(algorithm: CredentialAlgorithm) { - const dependencies = { - on$: this.generate$, - account$: this.account$, - }; - - this.log.debug({ algorithm }, "constructing generation stream"); - - switch (algorithm) { - case "catchall": - return this.generatorService.generate$(Generators.catchall, dependencies); - - case "subaddress": - return this.generatorService.generate$(Generators.subaddress, dependencies); - - case "username": - return this.generatorService.generate$(Generators.username, dependencies); - - case "password": - return this.generatorService.generate$(Generators.password, dependencies); - - case "passphrase": - return this.generatorService.generate$(Generators.passphrase, dependencies); - } - - if (isForwarderIntegration(algorithm)) { - const forwarder = getForwarderConfiguration(algorithm.forwarder); - const configuration = toCredentialGeneratorConfiguration(forwarder); - const generator = this.generatorService.generate$(configuration, dependencies); - return generator; - } - - this.log.panic({ algorithm }, `Invalid generator type: "${algorithm}"`); - } - /** Lists the top-level credential types supported by the component. * @remarks This is string-typed because angular doesn't support * structural equality for objects, which prevents `CredentialAlgorithm` @@ -519,15 +542,20 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro protected forwarderOptions$ = new BehaviorSubject[]>([]); /** Tracks the currently selected forwarder. */ - protected forwarderId$ = new BehaviorSubject(null); + protected forwarderId$ = new BehaviorSubject(null); /** Tracks forwarder control visibility */ protected showForwarder$ = new BehaviorSubject(false); /** tracks the currently selected credential type */ - protected algorithm$ = new ReplaySubject(1); + protected maybeAlgorithm$ = new ReplaySubject(1); - protected showAlgorithm$ = this.algorithm$.pipe( + /** tracks the last valid algorithm selection */ + protected algorithm$ = this.maybeAlgorithm$.pipe( + filter((algorithm): algorithm is AlgorithmMetadata => !!algorithm), + ); + + protected showAlgorithm$ = this.maybeAlgorithm$.pipe( combineLatestWith(this.showForwarder$), map(([algorithm, showForwarder]) => (showForwarder ? null : algorithm)), ); @@ -536,33 +564,32 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro * Emits the copy button aria-label respective of the selected credential type */ protected credentialTypeCopyLabel$ = this.algorithm$.pipe( - filter((algorithm) => !!algorithm), - map(({ copy }) => copy), + map(({ i18nKeys: { copyCredential } }) => translate(copyCredential, this.i18nService)), ); /** * Emits the generate button aria-label respective of the selected credential type */ protected credentialTypeGenerateLabel$ = this.algorithm$.pipe( - filter((algorithm) => !!algorithm), - map(({ generate }) => generate), + map(({ i18nKeys: { generateCredential } }) => translate(generateCredential, this.i18nService)), ); /** * Emits the copy credential toast respective of the selected credential type */ protected credentialTypeLabel$ = this.algorithm$.pipe( - filter((algorithm) => !!algorithm), - map(({ credentialType }) => credentialType), + map(({ i18nKeys: { credentialType } }) => translate(credentialType, this.i18nService)), ); /** Emits hint key for the currently selected credential type */ - protected credentialTypeHint$ = new ReplaySubject(1); + protected credentialTypeHint$ = new ReplaySubject(1); /** tracks the currently selected credential category */ - protected category$ = new ReplaySubject(1); + protected category$ = new ReplaySubject(1); - private readonly generatedCredential$ = new BehaviorSubject(null); + private readonly generatedCredential$ = new BehaviorSubject( + undefined, + ); /** Emits the last generated value. */ protected readonly value$ = this.generatedCredential$.pipe( @@ -580,15 +607,20 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro * origin in the debugger. */ protected async generate(source: string) { - const request = { source, website: this.website }; + const algorithm = await firstValueFrom(this.algorithm$); + const request: GenerateRequest = { source, algorithm: algorithm.id }; + if (this.website) { + request.website = this.website; + } + this.log.debug(request, "generation requested"); this.generate$.next(request); } - private toOptions(algorithms: AlgorithmInfo[]) { + private toOptions(algorithms: AlgorithmMetadata[]) { const options: Option[] = algorithms.map((algorithm) => ({ value: JSON.stringify(algorithm.id), - label: algorithm.name, + label: translate(algorithm.i18nKeys.name, this.i18nService), })); return options; diff --git a/libs/tools/generator/components/src/forwarder-settings.component.ts b/libs/tools/generator/components/src/forwarder-settings.component.ts index 689cc7e258c..c961cd5bb7a 100644 --- a/libs/tools/generator/components/src/forwarder-settings.component.ts +++ b/libs/tools/generator/components/src/forwarder-settings.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component, EventEmitter, @@ -14,13 +12,11 @@ import { FormBuilder } from "@angular/forms"; import { map, ReplaySubject, skip, Subject, switchAll, takeUntil, withLatestFrom } from "rxjs"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; -import { IntegrationId } from "@bitwarden/common/tools/integration"; +import { VendorId } from "@bitwarden/common/tools/extension"; import { - CredentialGeneratorConfiguration, CredentialGeneratorService, - getForwarderConfiguration, - NoPolicy, - toCredentialGeneratorConfiguration, + ForwarderOptions, + GeneratorMetadata, } from "@bitwarden/generator-core"; const Controls = Object.freeze({ @@ -37,7 +33,6 @@ const Controls = Object.freeze({ }) export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy { /** Instantiates the component - * @param accountService queries user availability * @param generatorService settings and policy logic * @param formBuilder reactive form controls */ @@ -47,14 +42,16 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy ) {} /** Binds the component to a specific user's settings. + * @remarks this is initialized to null but since it's a required input it'll + * never have that value in practice. */ @Input({ required: true }) - account: Account; + account: Account = null!; protected account$ = new ReplaySubject(1); @Input({ required: true }) - forwarder: IntegrationId; + forwarder: VendorId = null!; /** Emits settings updates and completes if the settings become unavailable. * @remarks this does not emit the initial settings. If you would like @@ -71,24 +68,19 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy [Controls.baseUrl]: [""], }); - private forwarderId$ = new ReplaySubject(1); + private vendor = new ReplaySubject(1); async ngOnInit() { - const forwarder$ = new ReplaySubject>(1); - this.forwarderId$ + const forwarder$ = new ReplaySubject>(1); + this.vendor .pipe( - map((id) => getForwarderConfiguration(id)), - // type erasure necessary because the configuration properties are - // determined dynamically at runtime - // FIXME: this can be eliminated by unifying the forwarder settings types; - // see `ForwarderConfiguration<...>` for details. - map((forwarder) => toCredentialGeneratorConfiguration(forwarder)), + map((vendor) => this.generatorService.forwarder(vendor)), takeUntil(this.destroyed$), ) .subscribe((forwarder) => { - this.displayDomain = forwarder.request.includes("domain"); - this.displayToken = forwarder.request.includes("token"); - this.displayBaseUrl = forwarder.request.includes("baseUrl"); + this.displayDomain = forwarder.capabilities.fields.includes("domain"); + this.displayToken = forwarder.capabilities.fields.includes("token"); + this.displayBaseUrl = forwarder.capabilities.fields.includes("baseUrl"); forwarder$.next(forwarder); }); @@ -107,10 +99,10 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy forwarder$.pipe(takeUntil(this.destroyed$)).subscribe((forwarder) => { for (const name in Controls) { const control = this.settings.get(name); - if (forwarder.request.includes(name as any)) { - control.enable({ emitEvent: false }); + if (forwarder.capabilities.fields.includes(name)) { + control?.enable({ emitEvent: false }); } else { - control.disable({ emitEvent: false }); + control?.disable({ emitEvent: false }); } } }); @@ -128,7 +120,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy this.saveSettings .pipe(withLatestFrom(this.settings.valueChanges, settings$), takeUntil(this.destroyed$)) .subscribe(([, value, settings]) => { - settings.next(value); + settings.next(value as ForwarderOptions); }); } @@ -140,7 +132,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy async ngOnChanges(changes: SimpleChanges) { this.refresh$.complete(); if ("forwarder" in changes) { - this.forwarderId$.next(this.forwarder); + this.vendor.next(this.forwarder); } if ("account" in changes) { @@ -148,9 +140,9 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy } } - protected displayDomain: boolean; - protected displayToken: boolean; - protected displayBaseUrl: boolean; + protected displayDomain: boolean = false; + protected displayToken: boolean = false; + protected displayBaseUrl: boolean = false; private readonly refresh$ = new Subject(); diff --git a/libs/tools/generator/components/src/generator-services.module.ts b/libs/tools/generator/components/src/generator-services.module.ts index 214abbd0ac2..3a7b771a25d 100644 --- a/libs/tools/generator/components/src/generator-services.module.ts +++ b/libs/tools/generator/components/src/generator-services.module.ts @@ -7,19 +7,40 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateProvider } from "@bitwarden/common/platform/state"; import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider"; import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider"; -import { disabledSemanticLoggerProvider } from "@bitwarden/common/tools/log"; +import { Site } from "@bitwarden/common/tools/extension"; +import { ExtensionRegistry } from "@bitwarden/common/tools/extension/extension-registry.abstraction"; +import { ExtensionService } from "@bitwarden/common/tools/extension/extension.service"; +import { DefaultFields, DefaultSites, Extension } from "@bitwarden/common/tools/extension/metadata"; +import { RuntimeExtensionRegistry } from "@bitwarden/common/tools/extension/runtime-extension-registry"; +import { VendorExtensions, Vendors } from "@bitwarden/common/tools/extension/vendor"; +import { RestClient } from "@bitwarden/common/tools/integration/rpc"; +import { + LogProvider, + disabledSemanticLoggerProvider, + enableLogForTypes, +} from "@bitwarden/common/tools/log"; +import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider"; import { + BuiltIn, createRandomizer, CredentialGeneratorService, Randomizer, + providers, + DefaultCredentialGeneratorService, } from "@bitwarden/generator-core"; import { KeyService } from "@bitwarden/key-management"; export const RANDOMIZER = new SafeInjectionToken("Randomizer"); +const GENERATOR_SERVICE_PROVIDER = new SafeInjectionToken( + "CredentialGeneratorProviders", +); +const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken("SystemServices"); /** Shared module containing generator component dependencies */ @NgModule({ @@ -35,6 +56,116 @@ export const RANDOMIZER = new SafeInjectionToken("Randomizer"); useClass: KeyServiceLegacyEncryptorProvider, deps: [EncryptService, KeyService], }), + safeProvider({ + provide: ExtensionRegistry, + useFactory: () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + + registry.registerSite(Extension[Site.forwarder]); + for (const vendor of Vendors) { + registry.registerVendor(vendor); + } + for (const extension of VendorExtensions) { + registry.registerExtension(extension); + } + registry.setPermission({ all: true }, "default"); + + return registry; + }, + deps: [], + }), + safeProvider({ + provide: SYSTEM_SERVICE_PROVIDER, + useFactory: ( + encryptor: LegacyEncryptorProvider, + state: StateProvider, + policy: PolicyService, + registry: ExtensionRegistry, + logger: LogService, + environment: PlatformUtilsService, + ) => { + let log: LogProvider; + if (environment.isDev()) { + log = enableLogForTypes(logger, []); + } else { + log = disabledSemanticLoggerProvider; + } + + const extension = new ExtensionService(registry, { + encryptor, + state, + log, + now: Date.now, + }); + + return { + policy, + extension, + log, + }; + }, + deps: [ + LegacyEncryptorProvider, + StateProvider, + PolicyService, + ExtensionRegistry, + LogService, + PlatformUtilsService, + ], + }), + safeProvider({ + provide: GENERATOR_SERVICE_PROVIDER, + useFactory: ( + system: SystemServiceProvider, + random: Randomizer, + encryptor: LegacyEncryptorProvider, + state: StateProvider, + i18n: I18nService, + api: ApiService, + ) => { + const userStateDeps = { + encryptor, + state, + log: system.log, + now: Date.now, + } satisfies UserStateSubjectDependencyProvider; + + const metadata = new providers.GeneratorMetadataProvider( + userStateDeps, + system, + Object.values(BuiltIn), + ); + const profile = new providers.GeneratorProfileProvider(userStateDeps, system.policy); + + const generator: providers.GeneratorDependencyProvider = { + randomizer: random, + client: new RestClient(api, i18n), + i18nService: i18n, + }; + + const userState: UserStateSubjectDependencyProvider = { + encryptor, + state, + log: system.log, + now: Date.now, + }; + + return { + userState, + generator, + profile, + metadata, + } satisfies providers.CredentialGeneratorProviders; + }, + deps: [ + SYSTEM_SERVICE_PROVIDER, + RANDOMIZER, + LegacyEncryptorProvider, + StateProvider, + I18nService, + ApiService, + ], + }), safeProvider({ provide: UserStateSubjectDependencyProvider, useFactory: (encryptor: LegacyEncryptorProvider, state: StateProvider) => @@ -42,19 +173,14 @@ export const RANDOMIZER = new SafeInjectionToken("Randomizer"); encryptor, state, log: disabledSemanticLoggerProvider, + now: Date.now, }), deps: [LegacyEncryptorProvider, StateProvider], }), safeProvider({ provide: CredentialGeneratorService, - useClass: CredentialGeneratorService, - deps: [ - RANDOMIZER, - PolicyService, - ApiService, - I18nService, - UserStateSubjectDependencyProvider, - ], + useClass: DefaultCredentialGeneratorService, + deps: [GENERATOR_SERVICE_PROVIDER, SYSTEM_SERVICE_PROVIDER], }), ], }) diff --git a/libs/tools/generator/components/src/passphrase-settings.component.ts b/libs/tools/generator/components/src/passphrase-settings.component.ts index 405914977c5..b3525251392 100644 --- a/libs/tools/generator/components/src/passphrase-settings.component.ts +++ b/libs/tools/generator/components/src/passphrase-settings.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { OnInit, @@ -12,14 +10,20 @@ import { OnChanges, } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { skip, takeUntil, Subject, map, withLatestFrom, ReplaySubject } from "rxjs"; +import { skip, takeUntil, Subject, map, withLatestFrom, ReplaySubject, tap } from "rxjs"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { + SemanticLogger, + disabledSemanticLoggerProvider, + ifEnabledSemanticLoggerProvider, +} from "@bitwarden/common/tools/log"; import { - Generators, CredentialGeneratorService, PassphraseGenerationOptions, + BuiltIn, } from "@bitwarden/generator-core"; const Controls = Object.freeze({ @@ -45,12 +49,26 @@ export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy private formBuilder: FormBuilder, private generatorService: CredentialGeneratorService, private i18nService: I18nService, + private logService: LogService, ) {} + /** Send structured debug logs from the credential generator component + * to the debugger console. + * + * @warning this may reveal sensitive information in plaintext. + */ + @Input() + debug: boolean = false; + + // this `log` initializer is overridden in `ngOnInit` + private log: SemanticLogger = disabledSemanticLoggerProvider({}); + /** Binds the component to a specific user's settings. + * @remarks this is initialized to null but since it's a required input it'll + * never have that value in practice. */ @Input({ required: true }) - account: Account; + account: Account = null!; protected account$ = new ReplaySubject(1); @@ -70,53 +88,66 @@ export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy /** Emits settings updates and completes if the settings become unavailable. * @remarks this does not emit the initial settings. If you would like * to receive live settings updates including the initial update, - * use `CredentialGeneratorService.settings$(...)` instead. + * use {@link CredentialGeneratorService.settings} instead. */ @Output() readonly onUpdated = new EventEmitter(); protected settings = this.formBuilder.group({ - [Controls.numWords]: [Generators.passphrase.settings.initial.numWords], - [Controls.wordSeparator]: [Generators.passphrase.settings.initial.wordSeparator], - [Controls.capitalize]: [Generators.passphrase.settings.initial.capitalize], - [Controls.includeNumber]: [Generators.passphrase.settings.initial.includeNumber], + [Controls.numWords]: [0], + [Controls.wordSeparator]: [""], + [Controls.capitalize]: [false], + [Controls.includeNumber]: [false], }); async ngOnInit() { - const settings = await this.generatorService.settings(Generators.passphrase, { + this.log = ifEnabledSemanticLoggerProvider(this.debug, this.logService, { + type: "PassphraseSettingsComponent", + }); + + const settings = await this.generatorService.settings(BuiltIn.passphrase, { account$: this.account$, }); // skips reactive event emissions to break a subscription cycle settings.withConstraints$ - .pipe(takeUntil(this.destroyed$)) + .pipe( + tap((content) => this.log.debug(content, "passphrase settings loaded with constraints")), + takeUntil(this.destroyed$), + ) .subscribe(({ state, constraints }) => { this.settings.patchValue(state, { emitEvent: false }); let boundariesHint = this.i18nService.t( "spinboxBoundariesHint", - constraints.numWords.min?.toString(), - constraints.numWords.max?.toString(), + constraints.numWords?.min?.toString(), + constraints.numWords?.max?.toString(), ); - if (state.numWords <= (constraints.numWords.recommendation ?? 0)) { + if ((state.numWords ?? 0) <= (constraints.numWords?.recommendation ?? 0)) { boundariesHint += this.i18nService.t( "passphraseNumWordsRecommendationHint", - constraints.numWords.recommendation?.toString(), + constraints.numWords?.recommendation?.toString(), ); } this.numWordsBoundariesHint.next(boundariesHint); }); // the first emission is the current value; subsequent emissions are updates - settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated); + settings + .pipe( + skip(1), + tap((settings) => this.log.debug(settings, "passphrase settings onUpdate event")), + takeUntil(this.destroyed$), + ) + .subscribe(this.onUpdated); // explain policy & disable policy-overridden fields this.generatorService - .policy$(Generators.passphrase, { account$: this.account$ }) + .policy$(BuiltIn.passphrase, { account$: this.account$ }) .pipe(takeUntil(this.destroyed$)) .subscribe(({ constraints }) => { - this.wordSeparatorMaxLength = constraints.wordSeparator.maxLength; - this.policyInEffect = constraints.policyInEffect; + this.wordSeparatorMaxLength = constraints.wordSeparator?.maxLength ?? 0; + this.policyInEffect = constraints.policyInEffect ?? false; this.toggleEnabled(Controls.capitalize, !constraints.capitalize?.readonly); this.toggleEnabled(Controls.includeNumber, !constraints.includeNumber?.readonly); @@ -126,22 +157,25 @@ export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy this.saveSettings .pipe( withLatestFrom(this.settings.valueChanges), - map(([, settings]) => settings), + tap(([source, form]) => + this.log.debug({ source, form }, "save passphrase settings request"), + ), + map(([, settings]) => settings as PassphraseGenerationOptions), takeUntil(this.destroyed$), ) .subscribe(settings); } /** attribute binding for wordSeparator[maxlength] */ - protected wordSeparatorMaxLength: number; + protected wordSeparatorMaxLength: number = 0; private saveSettings = new Subject(); - save(site: string = "component api call") { - this.saveSettings.next(site); + save(source: string = "component api call") { + this.saveSettings.next(source); } /** display binding for enterprise policy notice */ - protected policyInEffect: boolean; + protected policyInEffect: boolean = false; private numWordsBoundariesHint = new ReplaySubject(1); @@ -150,9 +184,9 @@ export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy private toggleEnabled(setting: keyof typeof Controls, enabled: boolean) { if (enabled) { - this.settings.get(setting).enable({ emitEvent: false }); + this.settings.get(setting)?.enable({ emitEvent: false }); } else { - this.settings.get(setting).disable({ emitEvent: false }); + this.settings.get(setting)?.disable({ emitEvent: false }); } } diff --git a/libs/tools/generator/components/src/password-generator.component.html b/libs/tools/generator/components/src/password-generator.component.html index 41ed982a490..cc5bdba6062 100644 --- a/libs/tools/generator/components/src/password-generator.component.html +++ b/libs/tools/generator/components/src/password-generator.component.html @@ -3,6 +3,7 @@ class="tw-mb-4" [selected]="credentialType$ | async" (selectedChange)="onCredentialTypeChanged($event)" + *ngIf="showCredentialTypes$ | async" attr.aria-label="{{ 'type' | i18n }}" > @@ -38,14 +39,14 @@ (1); @@ -87,7 +94,11 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy async ngOnChanges(changes: SimpleChanges) { const account = changes?.account; - if (account?.previousValue?.id !== account?.currentValue?.id) { + if ( + account && + account.currentValue.id && + account.previousValue.id !== account.currentValue.id + ) { this.log.debug( { previousUserId: account?.previousValue?.id as UserId, @@ -95,15 +106,19 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy }, "account input change detected", ); - this.account$.next(this.account); + this.account$.next(account.currentValue.id); } } + @Input() + profile: GeneratorProfile = Profile.account; + /** Removes bottom margin, passed to downstream components */ - @Input({ transform: coerceBooleanProperty }) disableMargin = false; + @Input({ transform: coerceBooleanProperty }) + disableMargin = false; /** tracks the currently selected credential type */ - protected credentialType$ = new BehaviorSubject(null); + protected credentialType$ = new BehaviorSubject(null); /** Emits the last generated value. */ protected readonly value$ = new BehaviorSubject(""); @@ -119,9 +134,11 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy * origin in the debugger. */ protected async generate(source: string) { - this.log.debug({ source }, "generation requested"); + const algorithm = await firstValueFrom(this.algorithm$); + const request: GenerateRequest = { source, algorithm: algorithm.id, profile: this.profile }; - this.generate$.next({ source }); + this.log.debug(request, "generation requested"); + this.generate$.next(request); } /** Tracks changes to the selected credential type @@ -146,16 +163,17 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy async ngOnInit() { this.log = ifEnabledSemanticLoggerProvider(this.debug, this.logService, { - type: "UsernameGeneratorComponent", + type: "PasswordGeneratorComponent", }); if (!this.account) { - this.account = await firstValueFrom(this.accountService.activeAccount$); - this.log.info( - { userId: this.account.id }, - "account not specified; using active account settings", - ); - this.account$.next(this.account); + const account = await firstValueFrom(this.accountService.activeAccount$); + if (!account) { + this.log.panic("active account cannot be `null`."); + } + + this.log.info({ userId: account.id }, "account not specified; using active account settings"); + this.account$.next(account); } this.generatorService @@ -167,10 +185,9 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy .subscribe(this.passwordOptions$); // wire up the generator - this.algorithm$ + this.generatorService + .generate$({ on$: this.generate$, account$: this.account$ }) .pipe( - filter((algorithm) => !!algorithm), - switchMap((algorithm) => this.typeToGenerator$(algorithm.id)), catchError((error: unknown, generator) => { if (typeof error === "string") { this.toastService.showToast({ @@ -189,7 +206,7 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy takeUntil(this.destroyed), ) .subscribe(([generated, account, algorithm]) => { - this.log.debug({ source: generated.source }, "credential generated"); + this.log.debug({ source: generated.source ?? null }, "credential generated"); this.generatorHistoryService .track(account.id, generated.credential, generated.category, generated.generationDate) @@ -201,7 +218,7 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy // template bindings refresh immediately this.zone.run(() => { if (generated.source === this.USER_REQUEST) { - this.announce(algorithm.onGeneratedMessage); + this.announce(translate(algorithm.i18nKeys.credentialGenerated, this.i18nService)); } this.onGenerated.next(generated); @@ -219,10 +236,7 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy ) .subscribe(([algorithm, preference]) => { if (isPasswordAlgorithm(algorithm)) { - this.log.info( - { algorithm, category: CredentialCategories.password }, - "algorithm preferences updated", - ); + this.log.info({ algorithm, type: Type.password }, "algorithm preferences updated"); preference.password.algorithm = algorithm; preference.password.updated = new Date(); } else { @@ -236,11 +250,17 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy preferences .pipe( map(({ password }) => this.generatorService.algorithm(password.algorithm)), - distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)), + distinctUntilChanged((prev, next) => { + if (prev === null || next === null) { + return false; + } else { + return isSameAlgorithm(prev.id, next.id); + } + }), takeUntil(this.destroyed), ) .subscribe((algorithm) => { - this.log.debug(algorithm, "algorithm selected"); + this.log.debug({ algorithm: algorithm.id }, "algorithm selected"); // update navigation this.onCredentialTypeChanged(algorithm.id); @@ -248,22 +268,22 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { - this.algorithm$.next(algorithm); - this.onAlgorithm.next(algorithm); + this.maybeAlgorithm$.next(algorithm); + this.onAlgorithm.next(toAlgorithmInfo(algorithm, this.i18nService)); }); }); // generate on load unless the generator prohibits it - this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => { + this.maybeAlgorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => { this.zone.run(() => { - if (!a || a.onlyOnRequest) { - this.log.debug("autogeneration disabled; clearing generated credential"); - this.value$.next("-"); - } else { + if (a?.capabilities?.autogenerate) { this.log.debug("autogeneration enabled"); this.generate("autogenerate").catch((e: unknown) => { this.log.error(e as object, "a failure occurred during autogeneration"); }); + } else { + this.log.debug("autogeneration disabled; clearing generated credential"); + this.value$.next("-"); } }); }); @@ -275,59 +295,45 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy this.ariaLive.announce(message).catch((e) => this.logService.error(e)); } - private typeToGenerator$(algorithm: CredentialAlgorithm) { - const dependencies = { - on$: this.generate$, - account$: this.account$, - }; - - this.log.debug({ algorithm }, "constructing generation stream"); - - switch (algorithm) { - case "password": - return this.generatorService.generate$(Generators.password, dependencies); - - case "passphrase": - return this.generatorService.generate$(Generators.passphrase, dependencies); - default: - this.log.panic({ algorithm }, `Invalid generator type: "${algorithm}"`); - } - } - /** Lists the credential types supported by the component. */ protected passwordOptions$ = new BehaviorSubject[]>([]); + /** Determines when the password/passphrase selector is visible. */ + protected showCredentialTypes$ = this.passwordOptions$.pipe(map((options) => options.length > 1)); + /** tracks the currently selected credential type */ - protected algorithm$ = new ReplaySubject(1); + protected maybeAlgorithm$ = new ReplaySubject(1); + + /** tracks the last valid algorithm selection */ + protected algorithm$ = this.maybeAlgorithm$.pipe( + filter((algorithm): algorithm is AlgorithmMetadata => !!algorithm), + ); /** * Emits the copy button aria-label respective of the selected credential type */ protected credentialTypeCopyLabel$ = this.algorithm$.pipe( - filter((algorithm) => !!algorithm), - map(({ copy }) => copy), + map(({ i18nKeys: { copyCredential } }) => translate(copyCredential, this.i18nService)), ); /** * Emits the generate button aria-label respective of the selected credential type */ protected credentialTypeGenerateLabel$ = this.algorithm$.pipe( - filter((algorithm) => !!algorithm), - map(({ generate }) => generate), + map(({ i18nKeys: { generateCredential } }) => translate(generateCredential, this.i18nService)), ); /** * Emits the copy credential toast respective of the selected credential type */ protected credentialTypeLabel$ = this.algorithm$.pipe( - filter((algorithm) => !!algorithm), - map(({ credentialType }) => credentialType), + map(({ i18nKeys: { credentialType } }) => translate(credentialType, this.i18nService)), ); - private toOptions(algorithms: AlgorithmInfo[]) { + private toOptions(algorithms: AlgorithmMetadata[]) { const options: Option[] = algorithms.map((algorithm) => ({ value: algorithm.id, - label: algorithm.name, + label: translate(algorithm.i18nKeys.name, this.i18nService), })); return options; diff --git a/libs/tools/generator/components/src/password-settings.component.ts b/libs/tools/generator/components/src/password-settings.component.ts index 346e9549cd8..965ada38146 100644 --- a/libs/tools/generator/components/src/password-settings.component.ts +++ b/libs/tools/generator/components/src/password-settings.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { OnInit, @@ -17,11 +15,13 @@ import { takeUntil, Subject, map, filter, tap, skip, ReplaySubject, withLatestFr import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { - Generators, CredentialGeneratorService, PasswordGenerationOptions, + BuiltIn, } from "@bitwarden/generator-core"; +import { hasRangeOfValues } from "./util"; + const Controls = Object.freeze({ length: "length", uppercase: "uppercase", @@ -52,9 +52,11 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy { ) {} /** Binds the component to a specific user's settings. + * @remarks this is initialized to null but since it's a required input it'll + * never have that value in practice. */ @Input({ required: true }) - account: Account; + account: Account = null!; protected account$ = new ReplaySubject(1); @@ -78,40 +80,40 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy { /** Emits settings updates and completes if the settings become unavailable. * @remarks this does not emit the initial settings. If you would like * to receive live settings updates including the initial update, - * use `CredentialGeneratorService.settings$(...)` instead. + * use `CredentialGeneratorService.settings(...)` instead. */ @Output() readonly onUpdated = new EventEmitter(); protected settings = this.formBuilder.group({ - [Controls.length]: [Generators.password.settings.initial.length], - [Controls.uppercase]: [Generators.password.settings.initial.uppercase], - [Controls.lowercase]: [Generators.password.settings.initial.lowercase], - [Controls.number]: [Generators.password.settings.initial.number], - [Controls.special]: [Generators.password.settings.initial.special], - [Controls.minNumber]: [Generators.password.settings.initial.minNumber], - [Controls.minSpecial]: [Generators.password.settings.initial.minSpecial], - [Controls.avoidAmbiguous]: [!Generators.password.settings.initial.ambiguous], + [Controls.length]: [0], + [Controls.uppercase]: [false], + [Controls.lowercase]: [false], + [Controls.number]: [false], + [Controls.special]: [false], + [Controls.minNumber]: [0], + [Controls.minSpecial]: [0], + [Controls.avoidAmbiguous]: [false], }); private get numbers() { - return this.settings.get(Controls.number); + return this.settings.get(Controls.number)!; } private get special() { - return this.settings.get(Controls.special); + return this.settings.get(Controls.special)!; } private get minNumber() { - return this.settings.get(Controls.minNumber); + return this.settings.get(Controls.minNumber)!; } private get minSpecial() { - return this.settings.get(Controls.minSpecial); + return this.settings.get(Controls.minSpecial)!; } async ngOnInit() { - const settings = await this.generatorService.settings(Generators.password, { + const settings = await this.generatorService.settings(BuiltIn.password, { account$: this.account$, }); @@ -130,13 +132,13 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy { .subscribe(([state, constraints]) => { let boundariesHint = this.i18nService.t( "spinboxBoundariesHint", - constraints.length.min?.toString(), - constraints.length.max?.toString(), + constraints.length?.min?.toString(), + constraints.length?.max?.toString(), ); - if (state.length <= (constraints.length.recommendation ?? 0)) { + if (state.length <= (constraints.length?.recommendation ?? 0)) { boundariesHint += this.i18nService.t( "passwordLengthRecommendationHint", - constraints.length.recommendation?.toString(), + constraints.length?.recommendation?.toString(), ); } this.lengthBoundariesHint.next(boundariesHint); @@ -147,19 +149,25 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy { // explain policy & disable policy-overridden fields this.generatorService - .policy$(Generators.password, { account$: this.account$ }) + .policy$(BuiltIn.password, { account$: this.account$ }) .pipe(takeUntil(this.destroyed$)) .subscribe(({ constraints }) => { - this.policyInEffect = constraints.policyInEffect; + this.policyInEffect = constraints.policyInEffect ?? false; const toggles = [ - [Controls.length, constraints.length.min < constraints.length.max], + [Controls.length, hasRangeOfValues(constraints.length?.min, constraints.length?.max)], [Controls.uppercase, !constraints.uppercase?.readonly], [Controls.lowercase, !constraints.lowercase?.readonly], [Controls.number, !constraints.number?.readonly], [Controls.special, !constraints.special?.readonly], - [Controls.minNumber, constraints.minNumber.min < constraints.minNumber.max], - [Controls.minSpecial, constraints.minSpecial.min < constraints.minSpecial.max], + [ + Controls.minNumber, + hasRangeOfValues(constraints.minNumber?.min, constraints.minNumber?.max), + ], + [ + Controls.minSpecial, + hasRangeOfValues(constraints.minSpecial?.min, constraints.minSpecial?.max), + ], ] as [keyof typeof Controls, boolean][]; for (const [control, enabled] of toggles) { @@ -172,7 +180,7 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy { let lastMinNumber = 1; this.numbers.valueChanges .pipe( - filter((checked) => !(checked && this.minNumber.value > 0)), + filter((checked) => !(checked && (this.minNumber.value ?? 0) > 0)), map((checked) => (checked ? lastMinNumber : 0)), takeUntil(this.destroyed$), ) @@ -180,8 +188,11 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy { this.minNumber.valueChanges .pipe( - map((value) => [value, value > 0] as const), - tap(([value, checkNumbers]) => (lastMinNumber = checkNumbers ? value : lastMinNumber)), + map((value) => [value, (value ?? 0) > 0] as const), + tap( + ([value, checkNumbers]) => + (lastMinNumber = checkNumbers && value ? value : lastMinNumber), + ), takeUntil(this.destroyed$), ) .subscribe(([, checkNumbers]) => this.numbers.setValue(checkNumbers, { emitEvent: false })); @@ -189,7 +200,7 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy { let lastMinSpecial = 1; this.special.valueChanges .pipe( - filter((checked) => !(checked && this.minSpecial.value > 0)), + filter((checked) => !(checked && (this.minSpecial.value ?? 0) > 0)), map((checked) => (checked ? lastMinSpecial : 0)), takeUntil(this.destroyed$), ) @@ -197,8 +208,11 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy { this.minSpecial.valueChanges .pipe( - map((value) => [value, value > 0] as const), - tap(([value, checkSpecial]) => (lastMinSpecial = checkSpecial ? value : lastMinSpecial)), + map((value) => [value, (value ?? 0) > 0] as const), + tap( + ([value, checkSpecial]) => + (lastMinSpecial = checkSpecial && value ? value : lastMinSpecial), + ), takeUntil(this.destroyed$), ) .subscribe(([, checkSpecial]) => this.special.setValue(checkSpecial, { emitEvent: false })); @@ -230,7 +244,7 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy { } /** display binding for enterprise policy notice */ - protected policyInEffect: boolean; + protected policyInEffect: boolean = false; private lengthBoundariesHint = new ReplaySubject(1); @@ -239,9 +253,9 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy { private toggleEnabled(setting: keyof typeof Controls, enabled: boolean) { if (enabled) { - this.settings.get(setting).enable({ emitEvent: false }); + this.settings.get(setting)?.enable({ emitEvent: false }); } else { - this.settings.get(setting).disable({ emitEvent: false }); + this.settings.get(setting)?.disable({ emitEvent: false }); } } diff --git a/libs/tools/generator/components/src/subaddress-settings.component.ts b/libs/tools/generator/components/src/subaddress-settings.component.ts index b09ecc86f9e..27ed6d5f9f3 100644 --- a/libs/tools/generator/components/src/subaddress-settings.component.ts +++ b/libs/tools/generator/components/src/subaddress-settings.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component, EventEmitter, @@ -13,10 +11,10 @@ import { import { FormBuilder } from "@angular/forms"; import { map, ReplaySubject, skip, Subject, takeUntil, withLatestFrom } from "rxjs"; -import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { CredentialGeneratorService, - Generators, + BuiltIn, SubaddressGenerationOptions, } from "@bitwarden/generator-core"; @@ -28,20 +26,20 @@ import { }) export class SubaddressSettingsComponent implements OnInit, OnChanges, OnDestroy { /** Instantiates the component - * @param accountService queries user availability * @param generatorService settings and policy logic * @param formBuilder reactive form controls */ constructor( private formBuilder: FormBuilder, private generatorService: CredentialGeneratorService, - private accountService: AccountService, ) {} /** Binds the component to a specific user's settings. + * @remarks this is initialized to null but since it's a required input it'll + * never have that value in practice. */ @Input({ required: true }) - account: Account; + account: Account = null!; protected account$ = new ReplaySubject(1); @@ -54,18 +52,18 @@ export class SubaddressSettingsComponent implements OnInit, OnChanges, OnDestroy /** Emits settings updates and completes if the settings become unavailable. * @remarks this does not emit the initial settings. If you would like * to receive live settings updates including the initial update, - * use `CredentialGeneratorService.settings$(...)` instead. + * use `CredentialGeneratorService.settings(...)` instead. */ @Output() readonly onUpdated = new EventEmitter(); /** The template's control bindings */ protected settings = this.formBuilder.group({ - subaddressEmail: [Generators.subaddress.settings.initial.subaddressEmail], + subaddressEmail: [""], }); async ngOnInit() { - const settings = await this.generatorService.settings(Generators.subaddress, { + const settings = await this.generatorService.settings(BuiltIn.plusAddress, { account$: this.account$, }); @@ -79,7 +77,7 @@ export class SubaddressSettingsComponent implements OnInit, OnChanges, OnDestroy this.saveSettings .pipe( withLatestFrom(this.settings.valueChanges), - map(([, settings]) => settings), + map(([, settings]) => settings as SubaddressGenerationOptions), takeUntil(this.destroyed$), ) .subscribe(settings); diff --git a/libs/tools/generator/components/src/username-generator.component.html b/libs/tools/generator/components/src/username-generator.component.html index 533a7a0e543..51b998f1d56 100644 --- a/libs/tools/generator/components/src/username-generator.component.html +++ b/libs/tools/generator/components/src/username-generator.component.html @@ -59,7 +59,7 @@ @@ -69,12 +69,12 @@ [account]="account$ | async" /> diff --git a/libs/tools/generator/components/src/username-generator.component.ts b/libs/tools/generator/components/src/username-generator.component.ts index de48a9bd6b1..6227bcd3f7c 100644 --- a/libs/tools/generator/components/src/username-generator.component.ts +++ b/libs/tools/generator/components/src/username-generator.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { LiveAnnouncer } from "@angular/cdk/a11y"; import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { @@ -25,15 +23,15 @@ import { map, ReplaySubject, Subject, - switchMap, takeUntil, + tap, withLatestFrom, } from "rxjs"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { IntegrationId } from "@bitwarden/common/tools/integration"; +import { VendorId } from "@bitwarden/common/tools/extension"; import { SemanticLogger, disabledSemanticLoggerProvider, @@ -43,21 +41,23 @@ import { UserId } from "@bitwarden/common/types/guid"; import { ToastService, Option } from "@bitwarden/components"; import { AlgorithmInfo, - CredentialAlgorithm, - CredentialCategories, CredentialGeneratorService, GenerateRequest, GeneratedCredential, - Generators, - getForwarderConfiguration, + isForwarderExtensionId, isEmailAlgorithm, - isForwarderIntegration, - isSameAlgorithm, isUsernameAlgorithm, - toCredentialGeneratorConfiguration, + isSameAlgorithm, + CredentialAlgorithm, + AlgorithmMetadata, + AlgorithmsByType, + Type, + Algorithm, } from "@bitwarden/generator-core"; import { GeneratorHistoryService } from "@bitwarden/generator-history"; +import { toAlgorithmInfo, translate } from "./util"; + // constants used to identify navigation selections that are not // generator algorithms const FORWARDER = "forwarder"; @@ -89,11 +89,14 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy private ariaLive: LiveAnnouncer, ) {} + /** exports algorithm symbols to the template */ + protected readonly Algorithm = Algorithm; + /** Binds the component to a specific user's settings. When this input is not provided, * the form binds to the active user */ @Input() - account: Account | null; + account: Account | null = null; protected account$ = new ReplaySubject(1); @@ -110,7 +113,11 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy async ngOnChanges(changes: SimpleChanges) { const account = changes?.account; - if (account?.previousValue?.id !== account?.currentValue?.id) { + if ( + account && + account.currentValue.id && + account.previousValue.id !== account.currentValue.id + ) { this.log.debug( { previousUserId: account?.previousValue?.id as UserId, @@ -118,7 +125,7 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy }, "account input change detected", ); - this.account$.next(this.account); + this.account$.next(account.currentValue.id); } } @@ -134,18 +141,18 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy /** emits algorithm info when the selected algorithm changes */ @Output() - readonly onAlgorithm = new EventEmitter(); + readonly onAlgorithm = new EventEmitter(); /** Removes bottom margin from internal elements */ @Input({ transform: coerceBooleanProperty }) disableMargin = false; /** Tracks the selected generation algorithm */ protected username = this.formBuilder.group({ - nav: [null as string], + nav: [null as string | null], }); protected forwarder = this.formBuilder.group({ - nav: [null as string], + nav: [null as string | null], }); async ngOnInit() { @@ -154,38 +161,63 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy }); if (!this.account) { - this.account = await firstValueFrom(this.accountService.activeAccount$); - this.log.info( - { userId: this.account.id }, - "account not specified; using active account settings", - ); - this.account$.next(this.account); + const account = await firstValueFrom(this.accountService.activeAccount$); + if (!account) { + this.log.panic("active account cannot be `null`."); + } + + this.log.info({ userId: account.id }, "account not specified; using active account settings"); + this.account$.next(account); } - this.generatorService - .algorithms$(["email", "username"], { account$: this.account$ }) + combineLatest([ + this.generatorService.algorithms$("email", { account$: this.account$ }), + this.generatorService.algorithms$("username", { account$: this.account$ }), + ]) .pipe( + map((algorithms) => algorithms.flat()), map((algorithms) => { - const usernames = algorithms.filter((a) => !isForwarderIntegration(a.id)); + // construct options for username and email algorithms; replace forwarder + // entry with a virtual entry for drill-down + const usernames = algorithms.filter((a) => !isForwarderExtensionId(a.id)); + usernames.sort((a, b) => a.weight - b.weight); const usernameOptions = this.toOptions(usernames); - usernameOptions.push({ value: FORWARDER, label: this.i18nService.t("forwardedEmail") }); + usernameOptions.splice(-1, 0, { + value: FORWARDER, + label: this.i18nService.t("forwardedEmail"), + }); - const forwarders = algorithms.filter((a) => isForwarderIntegration(a.id)); + // construct options for forwarder algorithms; they get their own selection box + const forwarders = algorithms.filter((a) => isForwarderExtensionId(a.id)); + forwarders.sort((a, b) => a.weight - b.weight); const forwarderOptions = this.toOptions(forwarders); forwarderOptions.unshift({ value: NONE_SELECTED, label: this.i18nService.t("select") }); return [usernameOptions, forwarderOptions] as const; }), + tap((algorithms) => + this.log.debug({ algorithms: algorithms as object }, "algorithms loaded"), + ), takeUntil(this.destroyed), ) .subscribe(([usernames, forwarders]) => { - this.typeOptions$.next(usernames); - this.forwarderOptions$.next(forwarders); + // update subjects within the angular zone so that the + // template bindings refresh immediately + this.zone.run(() => { + this.typeOptions$.next(usernames); + this.forwarderOptions$.next(forwarders); + }); }); - this.algorithm$ + this.maybeAlgorithm$ .pipe( - map((a) => a?.description), + map((a) => { + if (a?.i18nKeys?.description) { + return translate(a.i18nKeys.description, this.i18nService); + } else { + return ""; + } + }), takeUntil(this.destroyed), ) .subscribe((hint) => { @@ -197,10 +229,12 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy }); // wire up the generator - this.algorithm$ + this.generatorService + .generate$({ + on$: this.generate$, + account$: this.account$, + }) .pipe( - filter((algorithm) => !!algorithm), - switchMap((algorithm) => this.typeToGenerator$(algorithm.id)), catchError((error: unknown, generator) => { if (typeof error === "string") { this.toastService.showToast({ @@ -215,11 +249,14 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy // continue with origin stream return generator; }), - withLatestFrom(this.account$, this.algorithm$), + withLatestFrom(this.account$, this.maybeAlgorithm$), takeUntil(this.destroyed), ) .subscribe(([generated, account, algorithm]) => { - this.log.debug({ source: generated.source }, "credential generated"); + this.log.debug( + { source: generated.source ?? null, algorithm: algorithm?.id ?? null }, + "credential generated", + ); this.generatorHistoryService .track(account.id, generated.credential, generated.category, generated.generationDate) @@ -230,12 +267,12 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { - if (generated.source === this.USER_REQUEST) { - this.announce(algorithm.onGeneratedMessage); + if (algorithm && generated.source === this.USER_REQUEST) { + this.announce(translate(algorithm.i18nKeys.credentialGenerated, this.i18nService)); } + this.generatedCredential$.next(generated); this.onGenerated.next(generated); - this.value$.next(generated.credential); }); }); @@ -248,24 +285,31 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy this.username.valueChanges .pipe( - map( - (username): CascadeValue => - username.nav === FORWARDER - ? { nav: username.nav } - : { nav: username.nav, algorithm: JSON.parse(username.nav) }, - ), + map((username): CascadeValue => { + if (username.nav === FORWARDER) { + return { nav: username.nav }; + } else if (username.nav) { + return { nav: username.nav, algorithm: JSON.parse(username.nav) }; + } else { + const [algorithm] = AlgorithmsByType[Type.username]; + return { nav: JSON.stringify(algorithm), algorithm }; + } + }), takeUntil(this.destroyed), ) .subscribe(activeIdentifier$); this.forwarder.valueChanges .pipe( - map( - (forwarder): CascadeValue => - forwarder.nav === NONE_SELECTED - ? { nav: forwarder.nav } - : { nav: forwarder.nav, algorithm: JSON.parse(forwarder.nav) }, - ), + map((forwarder): CascadeValue => { + if (forwarder.nav === NONE_SELECTED) { + return { nav: forwarder.nav }; + } else if (forwarder.nav) { + return { nav: forwarder.nav, algorithm: JSON.parse(forwarder.nav) }; + } else { + return { nav: NONE_SELECTED }; + } + }), takeUntil(this.destroyed), ) .subscribe(activeForwarder$); @@ -276,7 +320,7 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy map(([username, forwarder]) => { const showForwarder = !username.algorithm; const forwarderId = - showForwarder && isForwarderIntegration(forwarder.algorithm) + showForwarder && forwarder.algorithm && isForwarderExtensionId(forwarder.algorithm) ? forwarder.algorithm.forwarder : null; return [showForwarder, forwarderId] as const; @@ -306,57 +350,61 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy return null; } }), - distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)), + distinctUntilChanged((prev, next) => { + if (prev === null || next === null) { + return false; + } else { + return isSameAlgorithm(prev.id, next.id); + } + }), takeUntil(this.destroyed), ) .subscribe((algorithm) => { - this.log.debug(algorithm, "algorithm selected"); + this.log.debug({ algorithm: algorithm?.id ?? null }, "algorithm selected"); // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { - this.algorithm$.next(algorithm); - this.onAlgorithm.next(algorithm); + this.maybeAlgorithm$.next(algorithm); + if (algorithm) { + this.onAlgorithm.next(toAlgorithmInfo(algorithm, this.i18nService)); + } else { + this.onAlgorithm.next(null); + } }); }); // assume the last-visible generator algorithm is the user's preferred one const preferences = await this.generatorService.preferences({ account$: this.account$ }); this.algorithm$ - .pipe( - filter((algorithm) => !!algorithm), - withLatestFrom(preferences), - takeUntil(this.destroyed), - ) + .pipe(withLatestFrom(preferences), takeUntil(this.destroyed)) .subscribe(([algorithm, preference]) => { if (isEmailAlgorithm(algorithm.id)) { - this.log.info( - { algorithm, category: CredentialCategories.email }, - "algorithm preferences updated", - ); preference.email.algorithm = algorithm.id; preference.email.updated = new Date(); } else if (isUsernameAlgorithm(algorithm.id)) { - this.log.info( - { algorithm, category: CredentialCategories.username }, - "algorithm preferences updated", - ); preference.username.algorithm = algorithm.id; preference.username.updated = new Date(); } else { return; } + this.log.info( + { algorithm: algorithm.id, type: algorithm.type }, + "algorithm preferences updated", + ); preferences.next(preference); }); preferences .pipe( map(({ email, username }) => { - const forwarderPref = isForwarderIntegration(email.algorithm) ? email : null; const usernamePref = email.updated > username.updated ? email : username; + const forwarderPref = isForwarderExtensionId(usernamePref.algorithm) + ? usernamePref + : null; - // inject drilldown flags + // inject drill-down flags const forwarderNav = !forwarderPref ? NONE_SELECTED : JSON.stringify(forwarderPref.algorithm); @@ -368,7 +416,7 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy selection: { nav: userNav }, active: { nav: userNav, - algorithm: forwarderPref ? null : usernamePref.algorithm, + algorithm: forwarderPref ? undefined : usernamePref.algorithm, }, }, forwarder: { @@ -385,6 +433,14 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy takeUntil(this.destroyed), ) .subscribe(({ username, forwarder }) => { + this.log.debug( + { + username: username.selection, + forwarder: forwarder.selection, + }, + "navigation updated", + ); + // update navigation; break subscription loop this.username.setValue(username.selection, { emitEvent: false }); this.forwarder.setValue(forwarder.selection, { emitEvent: false }); @@ -396,17 +452,16 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy // automatically regenerate when the algorithm switches if the algorithm // allows it; otherwise set a placeholder - this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => { + this.maybeAlgorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => { this.zone.run(() => { - if (!a || a.onlyOnRequest) { - this.log.debug("autogeneration disabled; clearing generated credential"); - this.value$.next("-"); - } else { + if (a?.capabilities?.autogenerate) { this.log.debug("autogeneration enabled"); - this.generate("autogenerate").catch((e: unknown) => { this.log.error(e as object, "a failure occurred during autogeneration"); }); + } else { + this.log.debug("autogeneration disabled; clearing generated credential"); + this.generatedCredential$.next(undefined); } }); }); @@ -414,34 +469,6 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy this.log.debug("component initialized"); } - private typeToGenerator$(algorithm: CredentialAlgorithm) { - const dependencies = { - on$: this.generate$, - account$: this.account$, - }; - - this.log.debug({ algorithm }, "constructing generation stream"); - - switch (algorithm) { - case "catchall": - return this.generatorService.generate$(Generators.catchall, dependencies); - - case "subaddress": - return this.generatorService.generate$(Generators.subaddress, dependencies); - - case "username": - return this.generatorService.generate$(Generators.username, dependencies); - } - - if (isForwarderIntegration(algorithm)) { - const forwarder = getForwarderConfiguration(algorithm.forwarder); - const configuration = toCredentialGeneratorConfiguration(forwarder); - return this.generatorService.generate$(configuration, dependencies); - } - - this.log.panic({ algorithm }, `Invalid generator type: "${algorithm}"`); - } - private announce(message: string) { this.ariaLive.announce(message).catch((e) => this.logService.error(e)); } @@ -450,7 +477,7 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy protected typeOptions$ = new BehaviorSubject[]>([]); /** Tracks the currently selected forwarder. */ - protected forwarderId$ = new BehaviorSubject(null); + protected forwarderId$ = new BehaviorSubject(null); /** Lists the credential types supported by the component. */ protected forwarderOptions$ = new BehaviorSubject[]>([]); @@ -458,19 +485,30 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy /** Tracks forwarder control visibility */ protected showForwarder$ = new BehaviorSubject(false); - /** tracks the currently selected credential type */ - protected algorithm$ = new ReplaySubject(1); + /** tracks the currently selected algorithm; emits `null` when no algorithm selected */ + protected maybeAlgorithm$ = new ReplaySubject(1); + + /** tracks the last valid algorithm selection */ + protected algorithm$ = this.maybeAlgorithm$.pipe( + filter((algorithm): algorithm is AlgorithmMetadata => !!algorithm), + ); /** Emits hint key for the currently selected credential type */ protected credentialTypeHint$ = new ReplaySubject(1); + private readonly generatedCredential$ = new BehaviorSubject( + undefined, + ); + /** Emits the last generated value. */ - protected readonly value$ = new BehaviorSubject(""); + protected readonly value$ = this.generatedCredential$.pipe( + map((generated) => generated?.credential ?? "-"), + ); /** Emits when a new credential is requested */ private readonly generate$ = new Subject(); - protected showAlgorithm$ = this.algorithm$.pipe( + protected showAlgorithm$ = this.maybeAlgorithm$.pipe( combineLatestWith(this.showForwarder$), map(([algorithm, showForwarder]) => (showForwarder ? null : algorithm)), ); @@ -479,24 +517,21 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy * Emits the copy button aria-label respective of the selected credential type */ protected credentialTypeCopyLabel$ = this.algorithm$.pipe( - filter((algorithm) => !!algorithm), - map(({ copy }) => copy), + map(({ i18nKeys: { copyCredential } }) => translate(copyCredential, this.i18nService)), ); /** * Emits the generate button aria-label respective of the selected credential type */ protected credentialTypeGenerateLabel$ = this.algorithm$.pipe( - filter((algorithm) => !!algorithm), - map(({ generate }) => generate), + map(({ i18nKeys: { generateCredential } }) => translate(generateCredential, this.i18nService)), ); /** * Emits the copy credential toast respective of the selected credential type */ protected credentialTypeLabel$ = this.algorithm$.pipe( - filter((algorithm) => !!algorithm), - map(({ credentialType }) => credentialType), + map(({ i18nKeys: { credentialType } }) => translate(credentialType, this.i18nService)), ); /** Identifies generator requests that were requested by the user */ @@ -507,15 +542,20 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy * origin in the debugger. */ protected async generate(source: string) { - const request = { source, website: this.website }; + const algorithm = await firstValueFrom(this.algorithm$); + const request: GenerateRequest = { source, algorithm: algorithm.id }; + if (this.website) { + request.website = this.website; + } + this.log.debug(request, "generation requested"); this.generate$.next(request); } - private toOptions(algorithms: AlgorithmInfo[]) { + private toOptions(algorithms: AlgorithmMetadata[]) { const options: Option[] = algorithms.map((algorithm) => ({ value: JSON.stringify(algorithm.id), - label: algorithm.name, + label: translate(algorithm.i18nKeys.name, this.i18nService), })); return options; @@ -528,9 +568,11 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy // finalize subjects this.generate$.complete(); - this.value$.complete(); + this.generatedCredential$.complete(); // finalize component bindings this.onGenerated.complete(); + + this.log.debug("component destroyed"); } } diff --git a/libs/tools/generator/components/src/username-settings.component.ts b/libs/tools/generator/components/src/username-settings.component.ts index ea3cfbd35fb..7a12957f906 100644 --- a/libs/tools/generator/components/src/username-settings.component.ts +++ b/libs/tools/generator/components/src/username-settings.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component, EventEmitter, @@ -17,7 +15,7 @@ import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { CredentialGeneratorService, EffUsernameGenerationOptions, - Generators, + BuiltIn, } from "@bitwarden/generator-core"; /** Options group for usernames */ @@ -28,7 +26,6 @@ import { }) export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy { /** Instantiates the component - * @param accountService queries user availability * @param generatorService settings and policy logic * @param formBuilder reactive form controls */ @@ -38,9 +35,11 @@ export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy { ) {} /** Binds the component to a specific user's settings. + * @remarks this is initialized to null but since it's a required input it'll + * never have that value in practice. */ @Input({ required: true }) - account: Account; + account: Account = null!; protected account$ = new ReplaySubject(1); @@ -53,19 +52,19 @@ export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy { /** Emits settings updates and completes if the settings become unavailable. * @remarks this does not emit the initial settings. If you would like * to receive live settings updates including the initial update, - * use `CredentialGeneratorService.settings$(...)` instead. + * use `CredentialGeneratorService.settings(...)` instead. */ @Output() readonly onUpdated = new EventEmitter(); /** The template's control bindings */ protected settings = this.formBuilder.group({ - wordCapitalize: [Generators.username.settings.initial.wordCapitalize], - wordIncludeNumber: [Generators.username.settings.initial.wordIncludeNumber], + wordCapitalize: [false], + wordIncludeNumber: [false], }); async ngOnInit() { - const settings = await this.generatorService.settings(Generators.username, { + const settings = await this.generatorService.settings(BuiltIn.effWordList, { account$: this.account$, }); @@ -79,7 +78,7 @@ export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy { this.saveSettings .pipe( withLatestFrom(this.settings.valueChanges), - map(([, settings]) => settings), + map(([, settings]) => settings as EffUsernameGenerationOptions), takeUntil(this.destroyed$), ) .subscribe(settings); diff --git a/libs/tools/generator/components/src/util.ts b/libs/tools/generator/components/src/util.ts index 95e55d816ce..4b0ce4383de 100644 --- a/libs/tools/generator/components/src/util.ts +++ b/libs/tools/generator/components/src/util.ts @@ -1,71 +1,46 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { ValidatorFn, Validators } from "@angular/forms"; -import { distinctUntilChanged, map, pairwise, pipe, skipWhile, startWith, takeWhile } from "rxjs"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { I18nKeyOrLiteral } from "@bitwarden/common/tools/types"; +import { isI18nKey } from "@bitwarden/common/tools/util"; +import { AlgorithmInfo, AlgorithmMetadata } from "@bitwarden/generator-core"; -import { AnyConstraint, Constraints } from "@bitwarden/common/tools/types"; -import { UserId } from "@bitwarden/common/types/guid"; -import { CredentialGeneratorConfiguration } from "@bitwarden/generator-core"; +/** Adapts {@link AlgorithmMetadata} to legacy {@link AlgorithmInfo} structure. */ +export function toAlgorithmInfo(metadata: AlgorithmMetadata, i18n: I18nService) { + const info: AlgorithmInfo = { + id: metadata.id, + type: metadata.type, + name: translate(metadata.i18nKeys.name, i18n), + generate: translate(metadata.i18nKeys.generateCredential, i18n), + onGeneratedMessage: translate(metadata.i18nKeys.credentialGenerated, i18n), + credentialType: translate(metadata.i18nKeys.credentialType, i18n), + copy: translate(metadata.i18nKeys.copyCredential, i18n), + useGeneratedValue: translate(metadata.i18nKeys.useCredential, i18n), + onlyOnRequest: !metadata.capabilities.autogenerate, + request: metadata.capabilities.fields, + }; -export function completeOnAccountSwitch() { - return pipe( - map(({ id }: { id: UserId | null }) => id), - skipWhile((id) => !id), - startWith(null as UserId), - pairwise(), - takeWhile(([prev, next]) => (prev ?? next) === next), - map(([_, id]) => id), - distinctUntilChanged(), - ); + if (metadata.i18nKeys.description) { + info.description = translate(metadata.i18nKeys.description, i18n); + } + + return info; } -export function toValidators( - target: keyof Settings, - configuration: CredentialGeneratorConfiguration, - policy?: Constraints, -) { - const validators: Array = []; - - // widen the types to avoid typecheck issues - const config: AnyConstraint = configuration.settings.constraints[target]; - const runtime: AnyConstraint = policy[target]; - - const required = getConstraint("required", config, runtime) ?? false; - if (required) { - validators.push(Validators.required); - } - - const maxLength = getConstraint("maxLength", config, runtime); - if (maxLength !== undefined) { - validators.push(Validators.maxLength(maxLength)); - } - - const minLength = getConstraint("minLength", config, runtime); - if (minLength !== undefined) { - validators.push(Validators.minLength(config.minLength)); - } - - const min = getConstraint("min", config, runtime); - if (min !== undefined) { - validators.push(Validators.min(min)); - } - - const max = getConstraint("max", config, runtime); - if (max !== undefined) { - validators.push(Validators.max(max)); - } - - return validators; +/** Translates an internationalization key + * @param key the key to translate + * @param i18n the service providing translations + * @returns the translated key; if the key is a literal the literal + * is returned instead. + */ +export function translate(key: I18nKeyOrLiteral, i18n: I18nService) { + return isI18nKey(key) ? i18n.t(key) : key.literal; } -function getConstraint( - key: Key, - config: AnyConstraint, - policy?: AnyConstraint, -) { - if (policy && key in policy) { - return policy[key] ?? config[key]; - } else if (config && key in config) { - return config[key]; - } +/** Returns true when min < max + * @param min the minimum value to check; when this is nullish it becomes 0. + * @param max the maximum value to check; when this is nullish it becomes +Infinity. + */ +export function hasRangeOfValues(min?: number, max?: number) { + const minimum = min ?? 0; + const maximum = max ?? Number.POSITIVE_INFINITY; + return minimum < maximum; } diff --git a/libs/tools/generator/core/src/abstractions/credential-generator-service.abstraction.ts b/libs/tools/generator/core/src/abstractions/credential-generator-service.abstraction.ts new file mode 100644 index 00000000000..74f78514594 --- /dev/null +++ b/libs/tools/generator/core/src/abstractions/credential-generator-service.abstraction.ts @@ -0,0 +1,104 @@ +import { Observable } from "rxjs"; + +import { Account } from "@bitwarden/common/auth/abstractions/account.service"; +import { BoundDependency, OnDependency } from "@bitwarden/common/tools/dependencies"; +import { VendorId } from "@bitwarden/common/tools/extension"; +import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject"; + +import { + CredentialAlgorithm, + GeneratorMetadata, + GeneratorProfile, + CredentialType, +} from "../metadata"; +import { AlgorithmMetadata } from "../metadata/algorithm-metadata"; +import { + CredentialPreference, + ForwarderOptions, + GeneratedCredential, + GenerateRequest, +} from "../types"; +import { GeneratorConstraints } from "../types/generator-constraints"; + +/** Generates credentials used in identity and/or authentication flows. + */ +export abstract class CredentialGeneratorService { + /** Generates a stream of credentials + * @param dependencies.on$ Required. A new credential is emitted when this emits. + */ + abstract generate$: ( + dependencies: OnDependency & BoundDependency<"account", Account>, + ) => Observable; + + /** Emits metadata for the set of algorithms available to a user. + * @param type the set of algorithms + * @param dependencies.account$ algorithms are filtered to only + * those matching the provided account's policy. + * @returns An observable that emits algorithm metadata. + */ + abstract algorithms$: ( + type: CredentialType, + dependencies: BoundDependency<"account", Account>, + ) => Observable; + + /** Lists metadata for a set of algorithms. + * @param type the type or types of algorithms + * @returns A list containing the requested metadata. + * @remarks this is a raw data interface. To apply rules such as algorithm availability, + * use {@link algorithms$} instead. + */ + abstract algorithms: (type: CredentialType | CredentialType[]) => AlgorithmMetadata[]; + + /** Look up the metadata for a specific generator algorithm + * @param id identifies the algorithm + * @returns the requested metadata, or `null` if the metadata wasn't found. + */ + abstract algorithm: (id: CredentialAlgorithm) => AlgorithmMetadata; + + /** Look up the forwarder metadata for a vendor. + * @param id identifies the vendor proving the forwarder + */ + abstract forwarder: (id: VendorId) => GeneratorMetadata; + + /** Get a subject bound to credential generator preferences. + * @param dependencies.account$ identifies the account to which the preferences are bound + * @returns a subject bound to the user's preferences + * @remarks Preferences determine which algorithms are used when generating a + * credential from a credential category (e.g. `PassX` or `Username`). Preferences + * should not be used to hold navigation history. Use {@link @bitwarden/generator-navigation} + * instead. + */ + abstract preferences: ( + dependencies: BoundDependency<"account", Account>, + ) => UserStateSubject; + + /** Get a subject bound to a specific user's settings. the subject enforces policy for the + * settings by automatically updating incorrect values to those allowed by policy. + * @param metadata determines which generator's settings are loaded + * @param dependencies.account$ identifies the account to which the settings are bound + * @param profile identifies the generator profile to load; when this is not specified + * the user's account profile is loaded. + * @returns a subject bound to the requested user's generator settings + * @remarks Generator metadata can be looked up using {@link BuiltIn} and {@link forwarder}. + */ + abstract settings: ( + metadata: Readonly>, + dependencies: BoundDependency<"account", Account>, + profile?: GeneratorProfile, + ) => UserStateSubject; + + /** Get the policy constraints for the provided configuration + * @param metadata determines which generator's policy is loaded + * @param dependencies.account$ determines which user's policy is loaded + * @param profile identifies the generator profile to load; when this is not specified + * the user's account profile is loaded. + * @returns an observable that emits the policy once `dependencies.account$` + * and the policy become available. + * @remarks Generator metadata can be looked up using {@link BuiltIn} and {@link forwarder}. + */ + abstract policy$: ( + metadata: Readonly>, + dependencies: BoundDependency<"account", Account>, + profile?: GeneratorProfile, + ) => Observable>; +} diff --git a/libs/tools/generator/core/src/abstractions/generator.service.abstraction.ts b/libs/tools/generator/core/src/abstractions/generator.service.abstraction.ts index 221c0b8b007..9c12dba44c7 100644 --- a/libs/tools/generator/core/src/abstractions/generator.service.abstraction.ts +++ b/libs/tools/generator/core/src/abstractions/generator.service.abstraction.ts @@ -9,6 +9,7 @@ import { PolicyEvaluator } from "./policy-evaluator.abstraction"; /** Generates credentials used for user authentication * @typeParam Options the credential generation configuration * @typeParam Policy the policy enforced by the generator + * @deprecated Use {@link CredentialGeneratorService} instead. */ export abstract class GeneratorService { /** An observable monitoring the options saved to disk. diff --git a/libs/tools/generator/core/src/abstractions/index.ts b/libs/tools/generator/core/src/abstractions/index.ts index 471ec89ea32..4f45f985ef2 100644 --- a/libs/tools/generator/core/src/abstractions/index.ts +++ b/libs/tools/generator/core/src/abstractions/index.ts @@ -1,3 +1,4 @@ +export { CredentialGeneratorService } from "./credential-generator-service.abstraction"; export { GeneratorService } from "./generator.service.abstraction"; export { GeneratorStrategy } from "./generator-strategy.abstraction"; export { PolicyEvaluator } from "./policy-evaluator.abstraction"; diff --git a/libs/tools/generator/core/src/data/default-addy-io-options.ts b/libs/tools/generator/core/src/data/default-addy-io-options.ts deleted file mode 100644 index 2ebeefff6a8..00000000000 --- a/libs/tools/generator/core/src/data/default-addy-io-options.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { EmailDomainOptions, SelfHostedApiOptions } from "../types"; - -export const DefaultAddyIoOptions: SelfHostedApiOptions & EmailDomainOptions = Object.freeze({ - website: null, - baseUrl: "https://app.addy.io", - token: "", - domain: "", -}); diff --git a/libs/tools/generator/core/src/data/default-credential-preferences.ts b/libs/tools/generator/core/src/data/default-credential-preferences.ts deleted file mode 100644 index c26d44b3b79..00000000000 --- a/libs/tools/generator/core/src/data/default-credential-preferences.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CredentialPreference } from "../types"; - -import { EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from "./generator-types"; - -export const DefaultCredentialPreferences: CredentialPreference = Object.freeze({ - email: Object.freeze({ - algorithm: EmailAlgorithms[0], - updated: new Date(0), - }), - password: Object.freeze({ - algorithm: PasswordAlgorithms[0], - updated: new Date(0), - }), - username: Object.freeze({ - algorithm: UsernameAlgorithms[0], - updated: new Date(0), - }), -}); diff --git a/libs/tools/generator/core/src/data/default-duck-duck-go-options.ts b/libs/tools/generator/core/src/data/default-duck-duck-go-options.ts deleted file mode 100644 index c600e6e512a..00000000000 --- a/libs/tools/generator/core/src/data/default-duck-duck-go-options.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ApiOptions } from "../types"; - -export const DefaultDuckDuckGoOptions: ApiOptions = Object.freeze({ - website: null, - token: "", -}); diff --git a/libs/tools/generator/core/src/data/default-fastmail-options.ts b/libs/tools/generator/core/src/data/default-fastmail-options.ts deleted file mode 100644 index 18faefc4643..00000000000 --- a/libs/tools/generator/core/src/data/default-fastmail-options.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ApiOptions, EmailPrefixOptions } from "../types"; - -export const DefaultFastmailOptions: ApiOptions & EmailPrefixOptions = Object.freeze({ - website: "", - domain: "", - prefix: "", - token: "", -}); diff --git a/libs/tools/generator/core/src/data/default-firefox-relay-options.ts b/libs/tools/generator/core/src/data/default-firefox-relay-options.ts deleted file mode 100644 index 20433a3e12a..00000000000 --- a/libs/tools/generator/core/src/data/default-firefox-relay-options.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ApiOptions } from "../types"; - -export const DefaultFirefoxRelayOptions: ApiOptions = Object.freeze({ - website: null, - token: "", -}); diff --git a/libs/tools/generator/core/src/data/default-forward-email-options.ts b/libs/tools/generator/core/src/data/default-forward-email-options.ts deleted file mode 100644 index d5175534a05..00000000000 --- a/libs/tools/generator/core/src/data/default-forward-email-options.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ApiOptions, EmailDomainOptions } from "../types"; - -export const DefaultForwardEmailOptions: ApiOptions & EmailDomainOptions = Object.freeze({ - website: null, - token: "", - domain: "", -}); diff --git a/libs/tools/generator/core/src/data/default-simple-login-options.ts b/libs/tools/generator/core/src/data/default-simple-login-options.ts deleted file mode 100644 index 965b1222cd3..00000000000 --- a/libs/tools/generator/core/src/data/default-simple-login-options.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { SelfHostedApiOptions } from "../types"; - -export const DefaultSimpleLoginOptions: SelfHostedApiOptions = Object.freeze({ - website: null, - baseUrl: "https://app.simplelogin.io", - token: "", -}); diff --git a/libs/tools/generator/core/src/data/generator-types.ts b/libs/tools/generator/core/src/data/generator-types.ts deleted file mode 100644 index e54ec34e497..00000000000 --- a/libs/tools/generator/core/src/data/generator-types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** Types of passwords that may be generated by the credential generator */ -export const PasswordAlgorithms = Object.freeze(["password", "passphrase"] as const); - -/** Types of usernames that may be generated by the credential generator */ -export const UsernameAlgorithms = Object.freeze(["username"] as const); - -/** Types of email addresses that may be generated by the credential generator */ -export const EmailAlgorithms = Object.freeze(["catchall", "subaddress"] as const); - -/** All types of credentials that may be generated by the credential generator */ -export const CredentialAlgorithms = Object.freeze([ - ...PasswordAlgorithms, - ...UsernameAlgorithms, - ...EmailAlgorithms, -] as const); diff --git a/libs/tools/generator/core/src/data/generators.ts b/libs/tools/generator/core/src/data/generators.ts deleted file mode 100644 index da87c60f1f4..00000000000 --- a/libs/tools/generator/core/src/data/generators.ts +++ /dev/null @@ -1,422 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; -import { ApiSettings } from "@bitwarden/common/tools/integration/rpc"; -import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; -import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; -import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; - -import { - EmailRandomizer, - ForwarderConfiguration, - PasswordRandomizer, - UsernameRandomizer, -} from "../engine"; -import { Forwarder } from "../engine/forwarder"; -import { - DefaultPolicyEvaluator, - DynamicPasswordPolicyConstraints, - PassphraseGeneratorOptionsEvaluator, - passphraseLeastPrivilege, - PassphrasePolicyConstraints, - PasswordGeneratorOptionsEvaluator, - passwordLeastPrivilege, -} from "../policies"; -import { CatchallConstraints } from "../policies/catchall-constraints"; -import { SubaddressConstraints } from "../policies/subaddress-constraints"; -import { - CatchallGenerationOptions, - CredentialGenerator, - CredentialGeneratorConfiguration, - EffUsernameGenerationOptions, - GeneratorDependencyProvider, - NoPolicy, - PassphraseGenerationOptions, - PassphraseGeneratorPolicy, - PasswordGenerationOptions, - PasswordGeneratorPolicy, - SubaddressGenerationOptions, -} from "../types"; - -import { DefaultCatchallOptions } from "./default-catchall-options"; -import { DefaultEffUsernameOptions } from "./default-eff-username-options"; -import { DefaultPassphraseBoundaries } from "./default-passphrase-boundaries"; -import { DefaultPassphraseGenerationOptions } from "./default-passphrase-generation-options"; -import { DefaultPasswordBoundaries } from "./default-password-boundaries"; -import { DefaultPasswordGenerationOptions } from "./default-password-generation-options"; -import { DefaultSubaddressOptions } from "./default-subaddress-generator-options"; - -const PASSPHRASE: CredentialGeneratorConfiguration< - PassphraseGenerationOptions, - PassphraseGeneratorPolicy -> = Object.freeze({ - id: "passphrase", - category: "password", - nameKey: "passphrase", - generateKey: "generatePassphrase", - onGeneratedMessageKey: "passphraseGenerated", - credentialTypeKey: "passphrase", - copyKey: "copyPassphrase", - useGeneratedValueKey: "useThisPassword", - onlyOnRequest: false, - request: [], - engine: { - create( - dependencies: GeneratorDependencyProvider, - ): CredentialGenerator { - return new PasswordRandomizer(dependencies.randomizer); - }, - }, - settings: { - initial: DefaultPassphraseGenerationOptions, - constraints: { - numWords: { - min: DefaultPassphraseBoundaries.numWords.min, - max: DefaultPassphraseBoundaries.numWords.max, - recommendation: DefaultPassphraseGenerationOptions.numWords, - }, - wordSeparator: { maxLength: 1 }, - }, - account: { - key: "passphraseGeneratorSettings", - target: "object", - format: "plain", - classifier: new PublicClassifier([ - "numWords", - "wordSeparator", - "capitalize", - "includeNumber", - ]), - state: GENERATOR_DISK, - initial: DefaultPassphraseGenerationOptions, - options: { - deserializer: (value) => value, - clearOn: ["logout"], - }, - } satisfies ObjectKey, - }, - policy: { - type: PolicyType.PasswordGenerator, - disabledValue: Object.freeze({ - minNumberWords: 0, - capitalize: false, - includeNumber: false, - }), - combine: passphraseLeastPrivilege, - createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy), - toConstraints: (policy) => - new PassphrasePolicyConstraints(policy, PASSPHRASE.settings.constraints), - }, -}); - -const PASSWORD: CredentialGeneratorConfiguration< - PasswordGenerationOptions, - PasswordGeneratorPolicy -> = Object.freeze({ - id: "password", - category: "password", - nameKey: "password", - generateKey: "generatePassword", - onGeneratedMessageKey: "passwordGenerated", - credentialTypeKey: "password", - copyKey: "copyPassword", - useGeneratedValueKey: "useThisPassword", - onlyOnRequest: false, - request: [], - engine: { - create( - dependencies: GeneratorDependencyProvider, - ): CredentialGenerator { - return new PasswordRandomizer(dependencies.randomizer); - }, - }, - settings: { - initial: DefaultPasswordGenerationOptions, - constraints: { - length: { - min: DefaultPasswordBoundaries.length.min, - max: DefaultPasswordBoundaries.length.max, - recommendation: DefaultPasswordGenerationOptions.length, - }, - minNumber: { - min: DefaultPasswordBoundaries.minDigits.min, - max: DefaultPasswordBoundaries.minDigits.max, - }, - minSpecial: { - min: DefaultPasswordBoundaries.minSpecialCharacters.min, - max: DefaultPasswordBoundaries.minSpecialCharacters.max, - }, - }, - account: { - key: "passwordGeneratorSettings", - target: "object", - format: "plain", - classifier: new PublicClassifier([ - "length", - "ambiguous", - "uppercase", - "minUppercase", - "lowercase", - "minLowercase", - "number", - "minNumber", - "special", - "minSpecial", - ]), - state: GENERATOR_DISK, - initial: DefaultPasswordGenerationOptions, - options: { - deserializer: (value) => value, - clearOn: ["logout"], - }, - } satisfies ObjectKey, - }, - policy: { - type: PolicyType.PasswordGenerator, - disabledValue: Object.freeze({ - minLength: 0, - useUppercase: false, - useLowercase: false, - useNumbers: false, - numberCount: 0, - useSpecial: false, - specialCount: 0, - }), - combine: passwordLeastPrivilege, - createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy), - toConstraints: (policy) => - new DynamicPasswordPolicyConstraints(policy, PASSWORD.settings.constraints), - }, -}); - -const USERNAME: CredentialGeneratorConfiguration = - Object.freeze({ - id: "username", - category: "username", - nameKey: "randomWord", - generateKey: "generateUsername", - onGeneratedMessageKey: "usernameGenerated", - credentialTypeKey: "username", - copyKey: "copyUsername", - useGeneratedValueKey: "useThisUsername", - onlyOnRequest: false, - request: [], - engine: { - create( - dependencies: GeneratorDependencyProvider, - ): CredentialGenerator { - return new UsernameRandomizer(dependencies.randomizer); - }, - }, - settings: { - initial: DefaultEffUsernameOptions, - constraints: {}, - account: { - key: "effUsernameGeneratorSettings", - target: "object", - format: "plain", - classifier: new PublicClassifier([ - "wordCapitalize", - "wordIncludeNumber", - ]), - state: GENERATOR_DISK, - initial: DefaultEffUsernameOptions, - options: { - deserializer: (value) => value, - clearOn: ["logout"], - }, - } satisfies ObjectKey, - }, - policy: { - type: PolicyType.PasswordGenerator, - disabledValue: {}, - combine(_acc: NoPolicy, _policy: Policy) { - return {}; - }, - createEvaluator(_policy: NoPolicy) { - return new DefaultPolicyEvaluator(); - }, - toConstraints(_policy: NoPolicy) { - return new IdentityConstraint(); - }, - }, - }); - -const CATCHALL: CredentialGeneratorConfiguration = - Object.freeze({ - id: "catchall", - category: "email", - nameKey: "catchallEmail", - descriptionKey: "catchallEmailDesc", - generateKey: "generateEmail", - onGeneratedMessageKey: "emailGenerated", - credentialTypeKey: "email", - copyKey: "copyEmail", - useGeneratedValueKey: "useThisEmail", - onlyOnRequest: false, - request: [], - engine: { - create( - dependencies: GeneratorDependencyProvider, - ): CredentialGenerator { - return new EmailRandomizer(dependencies.randomizer); - }, - }, - settings: { - initial: DefaultCatchallOptions, - constraints: { catchallDomain: { minLength: 1 } }, - account: { - key: "catchallGeneratorSettings", - target: "object", - format: "plain", - classifier: new PublicClassifier([ - "catchallType", - "catchallDomain", - ]), - state: GENERATOR_DISK, - initial: { - catchallType: "random", - catchallDomain: "", - }, - options: { - deserializer: (value) => value, - clearOn: ["logout"], - }, - } satisfies ObjectKey, - }, - policy: { - type: PolicyType.PasswordGenerator, - disabledValue: {}, - combine(_acc: NoPolicy, _policy: Policy) { - return {}; - }, - createEvaluator(_policy: NoPolicy) { - return new DefaultPolicyEvaluator(); - }, - toConstraints(_policy: NoPolicy, email: string) { - return new CatchallConstraints(email); - }, - }, - }); - -const SUBADDRESS: CredentialGeneratorConfiguration = - Object.freeze({ - id: "subaddress", - category: "email", - nameKey: "plusAddressedEmail", - descriptionKey: "plusAddressedEmailDesc", - generateKey: "generateEmail", - onGeneratedMessageKey: "emailGenerated", - credentialTypeKey: "email", - copyKey: "copyEmail", - useGeneratedValueKey: "useThisEmail", - onlyOnRequest: false, - request: [], - engine: { - create( - dependencies: GeneratorDependencyProvider, - ): CredentialGenerator { - return new EmailRandomizer(dependencies.randomizer); - }, - }, - settings: { - initial: DefaultSubaddressOptions, - constraints: {}, - account: { - key: "subaddressGeneratorSettings", - target: "object", - format: "plain", - classifier: new PublicClassifier([ - "subaddressType", - "subaddressEmail", - ]), - state: GENERATOR_DISK, - initial: { - subaddressType: "random", - subaddressEmail: "", - }, - options: { - deserializer: (value) => value, - clearOn: ["logout"], - }, - } satisfies ObjectKey, - }, - policy: { - type: PolicyType.PasswordGenerator, - disabledValue: {}, - combine(_acc: NoPolicy, _policy: Policy) { - return {}; - }, - createEvaluator(_policy: NoPolicy) { - return new DefaultPolicyEvaluator(); - }, - toConstraints(_policy: NoPolicy, email: string) { - return new SubaddressConstraints(email); - }, - }, - }); - -export function toCredentialGeneratorConfiguration( - configuration: ForwarderConfiguration, -) { - const forwarder = Object.freeze({ - id: { forwarder: configuration.id }, - category: "email", - nameKey: configuration.name, - descriptionKey: "forwardedEmailDesc", - generateKey: "generateEmail", - onGeneratedMessageKey: "emailGenerated", - credentialTypeKey: "email", - copyKey: "copyEmail", - useGeneratedValueKey: "useThisEmail", - onlyOnRequest: true, - request: configuration.forwarder.request, - engine: { - create(dependencies: GeneratorDependencyProvider) { - // FIXME: figure out why `configuration` fails to typecheck - const config: any = configuration; - return new Forwarder(config, dependencies.client, dependencies.i18nService); - }, - }, - settings: { - initial: configuration.forwarder.defaultSettings, - constraints: configuration.forwarder.settingsConstraints, - account: configuration.forwarder.local.settings, - }, - policy: { - type: PolicyType.PasswordGenerator, - disabledValue: {}, - combine(_acc: NoPolicy, _policy: Policy) { - return {}; - }, - createEvaluator(_policy: NoPolicy) { - return new DefaultPolicyEvaluator(); - }, - toConstraints(_policy: NoPolicy) { - return new IdentityConstraint(); - }, - }, - } satisfies CredentialGeneratorConfiguration); - - return forwarder; -} - -/** Generator configurations */ -export const Generators = Object.freeze({ - /** Passphrase generator configuration */ - passphrase: PASSPHRASE, - - /** Password generator configuration */ - password: PASSWORD, - - /** Username generator configuration */ - username: USERNAME, - - /** Catchall email generator configuration */ - catchall: CATCHALL, - - /** Email subaddress generator configuration */ - subaddress: SUBADDRESS, -}); diff --git a/libs/tools/generator/core/src/data/index.ts b/libs/tools/generator/core/src/data/index.ts index 482703fd3c3..bcf57e98c9a 100644 --- a/libs/tools/generator/core/src/data/index.ts +++ b/libs/tools/generator/core/src/data/index.ts @@ -1,20 +1,8 @@ -export * from "./generators"; -export * from "./default-addy-io-options"; export * from "./default-catchall-options"; -export * from "./default-duck-duck-go-options"; -export * from "./default-fastmail-options"; -export * from "./default-forward-email-options"; export * from "./default-passphrase-boundaries"; export * from "./default-password-boundaries"; export * from "./default-eff-username-options"; -export * from "./default-firefox-relay-options"; export * from "./default-passphrase-generation-options"; export * from "./default-password-generation-options"; -export * from "./default-credential-preferences"; export * from "./default-subaddress-generator-options"; -export * from "./default-simple-login-options"; -export * from "./forwarders"; export * from "./integrations"; -export * from "./policies"; -export * from "./username-digits"; -export * from "./generator-types"; diff --git a/libs/tools/generator/core/src/data/policies.ts b/libs/tools/generator/core/src/data/policies.ts deleted file mode 100644 index 4e46718a395..00000000000 --- a/libs/tools/generator/core/src/data/policies.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { - PassphraseGenerationOptions, - PassphraseGeneratorPolicy, - PasswordGenerationOptions, - PasswordGeneratorPolicy, - PolicyConfiguration, -} from "../types"; - -import { Generators } from "./generators"; - -/** Policy configurations - * @deprecated use Generator.*.policy instead - */ -export const Policies = Object.freeze({ - Passphrase: Generators.passphrase.policy, - Password: Generators.password.policy, -} satisfies { - /** Passphrase policy configuration */ - Passphrase: PolicyConfiguration; - - /** Password policy configuration */ - Password: PolicyConfiguration; -}); diff --git a/libs/tools/generator/core/src/data/username-digits.ts b/libs/tools/generator/core/src/data/username-digits.ts deleted file mode 100644 index 99ef15cf1ca..00000000000 --- a/libs/tools/generator/core/src/data/username-digits.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const UsernameDigits = Object.freeze({ - enabled: 4, - disabled: 0, -}); diff --git a/libs/tools/generator/core/src/engine/email-randomizer.spec.ts b/libs/tools/generator/core/src/engine/email-randomizer.spec.ts index fb953af1659..2ebe50d12d4 100644 --- a/libs/tools/generator/core/src/engine/email-randomizer.spec.ts +++ b/libs/tools/generator/core/src/engine/email-randomizer.spec.ts @@ -2,6 +2,8 @@ import { mock } from "jest-mock-extended"; import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; +import { Algorithm, Type } from "../metadata"; + import { Randomizer } from "./abstractions"; import { EmailRandomizer } from "./email-randomizer"; @@ -41,7 +43,8 @@ describe("EmailRandomizer", () => { async (email) => { const emailRandomizer = new EmailRandomizer(randomizer); - const result = await emailRandomizer.randomAsciiSubaddress(email); + // this tests what happens when the type system is subverted + const result = await emailRandomizer.randomAsciiSubaddress(email!); expect(result).toEqual(""); }, @@ -100,7 +103,8 @@ describe("EmailRandomizer", () => { it.each([[null], [undefined], [""]])("returns null if the domain is %p", async (domain) => { const emailRandomizer = new EmailRandomizer(randomizer); - const result = await emailRandomizer.randomAsciiCatchall(domain); + // this tests what happens when the type system is subverted + const result = await emailRandomizer.randomAsciiCatchall(domain!); expect(result).toBeNull(); }); @@ -150,7 +154,8 @@ describe("EmailRandomizer", () => { it.each([[null], [undefined], [""]])("returns null if the domain is %p", async (domain) => { const emailRandomizer = new EmailRandomizer(randomizer); - const result = await emailRandomizer.randomWordsCatchall(domain); + // this tests what happens when the type system is subverted + const result = await emailRandomizer.randomWordsCatchall(domain!); expect(result).toBeNull(); }); @@ -214,32 +219,32 @@ describe("EmailRandomizer", () => { const email = new EmailRandomizer(randomizer); const result = await email.generate( - {}, + { algorithm: Algorithm.catchall }, { catchallDomain: "example.com", }, ); - expect(result.category).toEqual("catchall"); + expect(result.category).toEqual(Type.email); }); it("processes subaddress generation options", async () => { const email = new EmailRandomizer(randomizer); const result = await email.generate( - {}, + { algorithm: Algorithm.plusAddress }, { subaddressEmail: "foo@example.com", }, ); - expect(result.category).toEqual("subaddress"); + expect(result.category).toEqual(Type.email); }); it("throws when it cannot recognize the options type", async () => { const email = new EmailRandomizer(randomizer); - const result = email.generate({}, {}); + const result = email.generate({ algorithm: Algorithm.password }, {}); await expect(result).rejects.toBeInstanceOf(Error); }); diff --git a/libs/tools/generator/core/src/engine/email-randomizer.ts b/libs/tools/generator/core/src/engine/email-randomizer.ts index 0be95a975af..f673ba05fc0 100644 --- a/libs/tools/generator/core/src/engine/email-randomizer.ts +++ b/libs/tools/generator/core/src/engine/email-randomizer.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; +import { Type } from "../metadata"; import { CatchallGenerationOptions, CredentialGenerator, @@ -128,7 +129,7 @@ export class EmailRandomizer return new GeneratedCredential( email, - "catchall", + Type.email, Date.now(), request.source, request.website, @@ -138,7 +139,7 @@ export class EmailRandomizer return new GeneratedCredential( email, - "subaddress", + Type.email, Date.now(), request.source, request.website, diff --git a/libs/tools/generator/core/src/engine/forwarder.ts b/libs/tools/generator/core/src/engine/forwarder.ts index 6c6e574e873..5f41e35d21d 100644 --- a/libs/tools/generator/core/src/engine/forwarder.ts +++ b/libs/tools/generator/core/src/engine/forwarder.ts @@ -8,6 +8,7 @@ import { } from "@bitwarden/common/tools/integration/rpc"; import { GenerationRequest } from "@bitwarden/common/tools/types"; +import { Type } from "../metadata"; import { CredentialGenerator, GeneratedCredential } from "../types"; import { AccountRequest, ForwarderConfiguration } from "./forwarder-configuration"; @@ -40,9 +41,8 @@ export class Forwarder implements CredentialGenerator { const create = this.createForwardingAddress(this.configuration, settings); const result = await this.client.fetchJson(create, requestOptions); - const id = { forwarder: this.configuration.id }; - return new GeneratedCredential(result, id, Date.now()); + return new GeneratedCredential(result, Type.email, Date.now()); } private createContext( diff --git a/libs/tools/generator/core/src/engine/password-randomizer.spec.ts b/libs/tools/generator/core/src/engine/password-randomizer.spec.ts index fca98855fd5..a36c4bb5352 100644 --- a/libs/tools/generator/core/src/engine/password-randomizer.spec.ts +++ b/libs/tools/generator/core/src/engine/password-randomizer.spec.ts @@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended"; import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; import { Randomizer } from "../abstractions"; +import { Algorithm, Type } from "../metadata"; import { Ascii } from "./data"; import { PasswordRandomizer } from "./password-randomizer"; @@ -341,32 +342,32 @@ describe("PasswordRandomizer", () => { const password = new PasswordRandomizer(randomizer); const result = await password.generate( - {}, + { algorithm: Algorithm.password }, { length: 10, }, ); - expect(result.category).toEqual("password"); + expect(result.category).toEqual(Type.password); }); it("processes passphrase generation options", async () => { const password = new PasswordRandomizer(randomizer); const result = await password.generate( - {}, + { algorithm: Algorithm.passphrase }, { numWords: 10, }, ); - expect(result.category).toEqual("passphrase"); + expect(result.category).toEqual(Type.password); }); it("throws when it cannot recognize the options type", async () => { const password = new PasswordRandomizer(randomizer); - const result = password.generate({}, {}); + const result = password.generate({ algorithm: Algorithm.username }, {}); await expect(result).rejects.toBeInstanceOf(Error); }); diff --git a/libs/tools/generator/core/src/engine/password-randomizer.ts b/libs/tools/generator/core/src/engine/password-randomizer.ts index a9612d2fb45..dc61ee064e1 100644 --- a/libs/tools/generator/core/src/engine/password-randomizer.ts +++ b/libs/tools/generator/core/src/engine/password-randomizer.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; +import { Type } from "../metadata"; import { CredentialGenerator, GenerateRequest, @@ -86,7 +87,7 @@ export class PasswordRandomizer return new GeneratedCredential( password, - "password", + Type.password, Date.now(), request.source, request.website, @@ -97,7 +98,7 @@ export class PasswordRandomizer return new GeneratedCredential( passphrase, - "passphrase", + Type.password, Date.now(), request.source, request.website, diff --git a/libs/tools/generator/core/src/engine/username-randomizer.spec.ts b/libs/tools/generator/core/src/engine/username-randomizer.spec.ts index 54d140e4469..be0650fe16e 100644 --- a/libs/tools/generator/core/src/engine/username-randomizer.spec.ts +++ b/libs/tools/generator/core/src/engine/username-randomizer.spec.ts @@ -2,6 +2,8 @@ import { mock } from "jest-mock-extended"; import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; +import { Algorithm, Type } from "../metadata"; + import { Randomizer } from "./abstractions"; import { UsernameRandomizer } from "./username-randomizer"; @@ -108,19 +110,19 @@ describe("UsernameRandomizer", () => { const username = new UsernameRandomizer(randomizer); const result = await username.generate( - {}, + { algorithm: Algorithm.username }, { wordIncludeNumber: true, }, ); - expect(result.category).toEqual("username"); + expect(result.category).toEqual(Type.username); }); it("throws when it cannot recognize the options type", async () => { const username = new UsernameRandomizer(randomizer); - const result = username.generate({}, {}); + const result = username.generate({ algorithm: Algorithm.passphrase }, {}); await expect(result).rejects.toBeInstanceOf(Error); }); diff --git a/libs/tools/generator/core/src/index.ts b/libs/tools/generator/core/src/index.ts index 494d034b674..928e6786ff9 100644 --- a/libs/tools/generator/core/src/index.ts +++ b/libs/tools/generator/core/src/index.ts @@ -3,13 +3,34 @@ export * from "./abstractions"; export * from "./data"; export { createRandomizer } from "./factories"; export * from "./types"; -export { CredentialGeneratorService } from "./services"; +export { DefaultCredentialGeneratorService } from "./services"; +export { + CredentialType, + CredentialAlgorithm, + PasswordAlgorithm, + Algorithm, + BuiltIn, + Type, + Profile, + GeneratorMetadata, + GeneratorProfile, + AlgorithmMetadata, + AlgorithmsByType, +} from "./metadata"; +export { + isForwarderExtensionId, + isEmailAlgorithm, + isUsernameAlgorithm, + isPasswordAlgorithm, + isSameAlgorithm, +} from "./metadata/util"; // These internal interfacess are exposed for use by other generator modules // They are unstable and may change arbitrarily export * as engine from "./engine"; export * as integration from "./integration"; export * as policies from "./policies"; +export * as providers from "./providers"; export * as rx from "./rx"; export * as services from "./services"; export * as strategies from "./strategies"; diff --git a/libs/tools/generator/core/src/integration/addy-io.ts b/libs/tools/generator/core/src/integration/addy-io.ts index 93ffed3392a..bd1be0ee6ae 100644 --- a/libs/tools/generator/core/src/integration/addy-io.ts +++ b/libs/tools/generator/core/src/integration/addy-io.ts @@ -4,6 +4,7 @@ import { UserKeyDefinition, } from "@bitwarden/common/platform/state"; import { VendorId } from "@bitwarden/common/tools/extension"; +import { Vendor } from "@bitwarden/common/tools/extension/vendor/data"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, @@ -101,7 +102,7 @@ const forwarder = Object.freeze({ export const AddyIo = Object.freeze({ // integration - id: "anonaddy" as IntegrationId & VendorId, + id: Vendor.addyio as IntegrationId & VendorId, name: "Addy.io", extends: ["forwarder"], diff --git a/libs/tools/generator/core/src/integration/duck-duck-go.ts b/libs/tools/generator/core/src/integration/duck-duck-go.ts index d2bd6173a14..b88a7f77a33 100644 --- a/libs/tools/generator/core/src/integration/duck-duck-go.ts +++ b/libs/tools/generator/core/src/integration/duck-duck-go.ts @@ -4,6 +4,7 @@ import { UserKeyDefinition, } from "@bitwarden/common/platform/state"; import { VendorId } from "@bitwarden/common/tools/extension"; +import { Vendor } from "@bitwarden/common/tools/extension/vendor/data"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc"; import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier"; @@ -90,7 +91,7 @@ const forwarder = Object.freeze({ // integration-wide configuration export const DuckDuckGo = Object.freeze({ - id: "duckduckgo" as IntegrationId & VendorId, + id: Vendor.duckduckgo as IntegrationId & VendorId, name: "DuckDuckGo", baseUrl: "https://quack.duckduckgo.com/api", selfHost: "never", diff --git a/libs/tools/generator/core/src/integration/fastmail.ts b/libs/tools/generator/core/src/integration/fastmail.ts index bfde1aa70f5..a540807666e 100644 --- a/libs/tools/generator/core/src/integration/fastmail.ts +++ b/libs/tools/generator/core/src/integration/fastmail.ts @@ -6,6 +6,7 @@ import { UserKeyDefinition, } from "@bitwarden/common/platform/state"; import { VendorId } from "@bitwarden/common/tools/extension"; +import { Vendor } from "@bitwarden/common/tools/extension/vendor/data"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc"; import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier"; @@ -160,7 +161,7 @@ const forwarder = Object.freeze({ // integration-wide configuration export const Fastmail = Object.freeze({ - id: "fastmail" as IntegrationId & VendorId, + id: Vendor.fastmail as IntegrationId & VendorId, name: "Fastmail", baseUrl: "https://api.fastmail.com", selfHost: "maybe", diff --git a/libs/tools/generator/core/src/integration/firefox-relay.ts b/libs/tools/generator/core/src/integration/firefox-relay.ts index f80de0c95dd..9fbd56aa6ed 100644 --- a/libs/tools/generator/core/src/integration/firefox-relay.ts +++ b/libs/tools/generator/core/src/integration/firefox-relay.ts @@ -4,6 +4,7 @@ import { UserKeyDefinition, } from "@bitwarden/common/platform/state"; import { VendorId } from "@bitwarden/common/tools/extension"; +import { Vendor } from "@bitwarden/common/tools/extension/vendor/data"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc"; import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier"; @@ -98,7 +99,7 @@ const forwarder = Object.freeze({ // integration-wide configuration export const FirefoxRelay = Object.freeze({ - id: "firefoxrelay" as IntegrationId & VendorId, + id: Vendor.mozilla as IntegrationId & VendorId, name: "Firefox Relay", baseUrl: "https://relay.firefox.com/api", selfHost: "never", diff --git a/libs/tools/generator/core/src/integration/forward-email.ts b/libs/tools/generator/core/src/integration/forward-email.ts index 34b4602b94b..b53fc4ffab6 100644 --- a/libs/tools/generator/core/src/integration/forward-email.ts +++ b/libs/tools/generator/core/src/integration/forward-email.ts @@ -4,6 +4,7 @@ import { UserKeyDefinition, } from "@bitwarden/common/platform/state"; import { VendorId } from "@bitwarden/common/tools/extension"; +import { Vendor } from "@bitwarden/common/tools/extension/vendor/data"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc"; import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier"; @@ -102,7 +103,7 @@ const forwarder = Object.freeze({ export const ForwardEmail = Object.freeze({ // integration metadata - id: "forwardemail" as IntegrationId & VendorId, + id: Vendor.forwardemail as IntegrationId & VendorId, name: "Forward Email", extends: ["forwarder"], diff --git a/libs/tools/generator/core/src/integration/simple-login.ts b/libs/tools/generator/core/src/integration/simple-login.ts index efbac69cec2..f3cc776d401 100644 --- a/libs/tools/generator/core/src/integration/simple-login.ts +++ b/libs/tools/generator/core/src/integration/simple-login.ts @@ -4,6 +4,7 @@ import { UserKeyDefinition, } from "@bitwarden/common/platform/state"; import { VendorId } from "@bitwarden/common/tools/extension"; +import { Vendor } from "@bitwarden/common/tools/extension/vendor/data"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, @@ -104,7 +105,7 @@ const forwarder = Object.freeze({ // integration-wide configuration export const SimpleLogin = Object.freeze({ - id: "simplelogin" as IntegrationId & VendorId, + id: Vendor.simplelogin as IntegrationId & VendorId, name: "SimpleLogin", selfHost: "maybe", extends: ["forwarder"], diff --git a/libs/tools/generator/core/src/metadata/algorithm-metadata.ts b/libs/tools/generator/core/src/metadata/algorithm-metadata.ts index c07deef5535..8bffa630dd9 100644 --- a/libs/tools/generator/core/src/metadata/algorithm-metadata.ts +++ b/libs/tools/generator/core/src/metadata/algorithm-metadata.ts @@ -1,6 +1,6 @@ -import { CredentialAlgorithm, CredentialType } from "./type"; +import { I18nKeyOrLiteral } from "@bitwarden/common/tools/types"; -type I18nKeyOrLiteral = string | { literal: string }; +import { CredentialAlgorithm, CredentialType } from "./type"; /** Credential generator metadata common across credential generators */ export type AlgorithmMetadata = { @@ -14,7 +14,7 @@ export type AlgorithmMetadata = { id: CredentialAlgorithm; /** The kind of credential generated by this configuration */ - category: CredentialType; + type: CredentialType; /** Used to order credential algorithms for display purposes. * Items with lesser weights appear before entries with greater @@ -23,6 +23,10 @@ export type AlgorithmMetadata = { weight: number; /** Localization keys */ + // FIXME: in practice, keys like `credentialGenerated` all align + // with credential types and contain duplicate keys. Extract + // them into a "credential type metadata" type and integrate + // that type with the algorithm metadata instead. i18nKeys: { /** descriptive name of the algorithm */ name: I18nKeyOrLiteral; diff --git a/libs/tools/generator/core/src/metadata/email/catchall.spec.ts b/libs/tools/generator/core/src/metadata/email/catchall.spec.ts index d6cc1795e0b..1099a6d59ea 100644 --- a/libs/tools/generator/core/src/metadata/email/catchall.spec.ts +++ b/libs/tools/generator/core/src/metadata/email/catchall.spec.ts @@ -2,7 +2,8 @@ import { mock } from "jest-mock-extended"; import { EmailRandomizer } from "../../engine"; import { CatchallConstraints } from "../../policies/catchall-constraints"; -import { CatchallGenerationOptions, GeneratorDependencyProvider } from "../../types"; +import { GeneratorDependencyProvider } from "../../providers"; +import { CatchallGenerationOptions } from "../../types"; import { Profile } from "../data"; import { CoreProfileMetadata } from "../profile-metadata"; import { isCoreProfile } from "../util"; diff --git a/libs/tools/generator/core/src/metadata/email/catchall.ts b/libs/tools/generator/core/src/metadata/email/catchall.ts index 0711e5c3719..991f6b18b73 100644 --- a/libs/tools/generator/core/src/metadata/email/catchall.ts +++ b/libs/tools/generator/core/src/metadata/email/catchall.ts @@ -4,17 +4,14 @@ import { deepFreeze } from "@bitwarden/common/tools/util"; import { EmailRandomizer } from "../../engine"; import { CatchallConstraints } from "../../policies/catchall-constraints"; -import { - CatchallGenerationOptions, - CredentialGenerator, - GeneratorDependencyProvider, -} from "../../types"; +import { GeneratorDependencyProvider } from "../../providers"; +import { CatchallGenerationOptions, CredentialGenerator } from "../../types"; import { Algorithm, Type, Profile } from "../data"; import { GeneratorMetadata } from "../generator-metadata"; const catchall: GeneratorMetadata = deepFreeze({ id: Algorithm.catchall, - category: Type.email, + type: Type.email, weight: 210, i18nKeys: { name: "catchallEmail", diff --git a/libs/tools/generator/core/src/metadata/email/forwarder.ts b/libs/tools/generator/core/src/metadata/email/forwarder.ts index f4f150f33fa..1066f890ef8 100644 --- a/libs/tools/generator/core/src/metadata/email/forwarder.ts +++ b/libs/tools/generator/core/src/metadata/email/forwarder.ts @@ -1,19 +1,14 @@ import { ExtensionMetadata, ExtensionStorageKey } from "@bitwarden/common/tools/extension/type"; -import { SelfHostedApiSettings } from "@bitwarden/common/tools/integration/rpc"; import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; import { getForwarderConfiguration } from "../../data"; -import { EmailDomainSettings, EmailPrefixSettings } from "../../engine"; import { Forwarder } from "../../engine/forwarder"; -import { GeneratorDependencyProvider } from "../../types"; +import { GeneratorDependencyProvider } from "../../providers"; +import { ForwarderOptions } from "../../types"; import { Profile, Type } from "../data"; import { GeneratorMetadata } from "../generator-metadata"; import { ForwarderProfileMetadata } from "../profile-metadata"; -// These options are used by all forwarders; each forwarder uses a different set, -// as defined by `GeneratorMetadata.capabilities.fields`. -type ForwarderOptions = Partial; - // update the extension metadata export function toForwarderMetadata( extension: ExtensionMetadata, @@ -28,7 +23,7 @@ export function toForwarderMetadata( const generator: GeneratorMetadata = { id: { forwarder: extension.product.vendor.id }, - category: Type.email, + type: Type.email, weight: 300, i18nKeys: { name, @@ -56,6 +51,12 @@ export function toForwarderMetadata( storage: { key: "forwarder", frame: 512, + initial: { + token: "", + baseUrl: "", + domain: "", + prefix: "", + }, options: { deserializer: (value) => value, clearOn: ["logout"], diff --git a/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts b/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts index 063cb71c23a..befc900ceab 100644 --- a/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts +++ b/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts @@ -2,7 +2,8 @@ import { mock } from "jest-mock-extended"; import { EmailRandomizer } from "../../engine"; import { SubaddressConstraints } from "../../policies/subaddress-constraints"; -import { SubaddressGenerationOptions, GeneratorDependencyProvider } from "../../types"; +import { GeneratorDependencyProvider } from "../../providers"; +import { SubaddressGenerationOptions } from "../../types"; import { Profile } from "../data"; import { CoreProfileMetadata } from "../profile-metadata"; import { isCoreProfile } from "../util"; diff --git a/libs/tools/generator/core/src/metadata/email/plus-address.ts b/libs/tools/generator/core/src/metadata/email/plus-address.ts index 0db0acd415c..940d6599442 100644 --- a/libs/tools/generator/core/src/metadata/email/plus-address.ts +++ b/libs/tools/generator/core/src/metadata/email/plus-address.ts @@ -4,17 +4,14 @@ import { deepFreeze } from "@bitwarden/common/tools/util"; import { EmailRandomizer } from "../../engine"; import { SubaddressConstraints } from "../../policies/subaddress-constraints"; -import { - CredentialGenerator, - GeneratorDependencyProvider, - SubaddressGenerationOptions, -} from "../../types"; +import { GeneratorDependencyProvider } from "../../providers"; +import { CredentialGenerator, SubaddressGenerationOptions } from "../../types"; import { Algorithm, Profile, Type } from "../data"; import { GeneratorMetadata } from "../generator-metadata"; const plusAddress: GeneratorMetadata = deepFreeze({ id: Algorithm.plusAddress, - category: Type.email, + type: Type.email, weight: 200, i18nKeys: { name: "plusAddressedEmail", diff --git a/libs/tools/generator/core/src/metadata/generator-metadata.ts b/libs/tools/generator/core/src/metadata/generator-metadata.ts index 9296d30430e..704ce88b217 100644 --- a/libs/tools/generator/core/src/metadata/generator-metadata.ts +++ b/libs/tools/generator/core/src/metadata/generator-metadata.ts @@ -1,4 +1,5 @@ -import { CredentialGenerator, GeneratorDependencyProvider } from "../types"; +import { GeneratorDependencyProvider } from "../providers"; +import { CredentialGenerator } from "../types"; import { AlgorithmMetadata } from "./algorithm-metadata"; import { Profile } from "./data"; diff --git a/libs/tools/generator/core/src/metadata/index.ts b/libs/tools/generator/core/src/metadata/index.ts index d9437822270..17c02918705 100644 --- a/libs/tools/generator/core/src/metadata/index.ts +++ b/libs/tools/generator/core/src/metadata/index.ts @@ -3,7 +3,32 @@ import { AlgorithmsByType as AlgorithmsByTypeData, Type as TypeData, } from "./data"; +import catchall from "./email/catchall"; +import plusAddress from "./email/plus-address"; +import passphrase from "./password/eff-word-list"; +import password from "./password/random-password"; import { CredentialType, CredentialAlgorithm } from "./type"; +import effWordList from "./username/eff-word-list"; + +/** Credential generators hosted natively by the credential generator system. + * These are supplemented by generators from the {@link ExtensionService}. + */ +export const BuiltIn = Object.freeze({ + /** Catchall email address generator */ + catchall, + + /** plus-addressed email address generator */ + plusAddress, + + /** passphrase generator using the EFF word list */ + passphrase, + + /** password generator */ + password, + + /** username generator using the EFF word list */ + effWordList, +}); // `CredentialAlgorithm` is defined in terms of `ABT`; supplying // type information in the barrel file breaks a circular dependency. @@ -12,14 +37,29 @@ export const AlgorithmsByType: Record< CredentialType, ReadonlyArray > = AlgorithmsByTypeData; + +/** A list of all built-in algorithm identifiers + * @remarks this is useful when you need to filter invalid values + */ export const Algorithms: ReadonlyArray = Object.freeze( Object.values(AlgorithmData), ); + +/** A list of all built-in algorithm types + * @remarks this is useful when you need to filter invalid values + */ export const Types: ReadonlyArray = Object.freeze(Object.values(TypeData)); export { Profile, Type, Algorithm } from "./data"; export { toForwarderMetadata } from "./email/forwarder"; +export { AlgorithmMetadata } from "./algorithm-metadata"; export { GeneratorMetadata } from "./generator-metadata"; export { ProfileContext, CoreProfileMetadata, ProfileMetadata } from "./profile-metadata"; -export { GeneratorProfile, CredentialAlgorithm, CredentialType } from "./type"; +export { + GeneratorProfile, + CredentialAlgorithm, + PasswordAlgorithm, + CredentialType, + ForwarderExtensionId, +} from "./type"; export { isForwarderProfile, toVendorId, isForwarderExtensionId } from "./util"; diff --git a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts index e02d63d3d59..0c0693af272 100644 --- a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts +++ b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts @@ -5,7 +5,8 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { PasswordRandomizer } from "../../engine"; import { PassphrasePolicyConstraints } from "../../policies"; -import { PassphraseGenerationOptions, GeneratorDependencyProvider } from "../../types"; +import { GeneratorDependencyProvider } from "../../providers"; +import { PassphraseGenerationOptions } from "../../types"; import { Profile } from "../data"; import { CoreProfileMetadata } from "../profile-metadata"; import { isCoreProfile } from "../util"; diff --git a/libs/tools/generator/core/src/metadata/password/eff-word-list.ts b/libs/tools/generator/core/src/metadata/password/eff-word-list.ts index fc86032bf6b..021112f7c89 100644 --- a/libs/tools/generator/core/src/metadata/password/eff-word-list.ts +++ b/libs/tools/generator/core/src/metadata/password/eff-word-list.ts @@ -5,17 +5,14 @@ import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; import { PasswordRandomizer } from "../../engine"; import { passphraseLeastPrivilege, PassphrasePolicyConstraints } from "../../policies"; -import { - CredentialGenerator, - GeneratorDependencyProvider, - PassphraseGenerationOptions, -} from "../../types"; +import { GeneratorDependencyProvider } from "../../providers"; +import { CredentialGenerator, PassphraseGenerationOptions } from "../../types"; import { Algorithm, Profile, Type } from "../data"; import { GeneratorMetadata } from "../generator-metadata"; const passphrase: GeneratorMetadata = { id: Algorithm.passphrase, - category: Type.password, + type: Type.password, weight: 110, i18nKeys: { name: "passphrase", @@ -26,7 +23,7 @@ const passphrase: GeneratorMetadata = { useCredential: "useThisPassphrase", }, capabilities: { - autogenerate: false, + autogenerate: true, fields: [], }, engine: { diff --git a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts index 9e38c50ee2a..b22f3e9356d 100644 --- a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts +++ b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts @@ -5,7 +5,8 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { PasswordRandomizer } from "../../engine"; import { DynamicPasswordPolicyConstraints } from "../../policies"; -import { PasswordGenerationOptions, GeneratorDependencyProvider } from "../../types"; +import { GeneratorDependencyProvider } from "../../providers"; +import { PasswordGenerationOptions } from "../../types"; import { Profile } from "../data"; import { CoreProfileMetadata } from "../profile-metadata"; import { isCoreProfile } from "../util"; diff --git a/libs/tools/generator/core/src/metadata/password/random-password.ts b/libs/tools/generator/core/src/metadata/password/random-password.ts index 693236b0967..e446f1962a5 100644 --- a/libs/tools/generator/core/src/metadata/password/random-password.ts +++ b/libs/tools/generator/core/src/metadata/password/random-password.ts @@ -5,17 +5,14 @@ import { deepFreeze } from "@bitwarden/common/tools/util"; import { PasswordRandomizer } from "../../engine"; import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies"; -import { - CredentialGenerator, - GeneratorDependencyProvider, - PasswordGeneratorSettings, -} from "../../types"; +import { GeneratorDependencyProvider } from "../../providers"; +import { CredentialGenerator, PasswordGeneratorSettings } from "../../types"; import { Algorithm, Profile, Type } from "../data"; import { GeneratorMetadata } from "../generator-metadata"; const password: GeneratorMetadata = deepFreeze({ id: Algorithm.password, - category: Type.password, + type: Type.password, weight: 100, i18nKeys: { name: "password", diff --git a/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts index d47d5ec9fcb..beebb016504 100644 --- a/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts +++ b/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts @@ -3,7 +3,8 @@ import { mock } from "jest-mock-extended"; import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; import { UsernameRandomizer } from "../../engine"; -import { EffUsernameGenerationOptions, GeneratorDependencyProvider } from "../../types"; +import { GeneratorDependencyProvider } from "../../providers"; +import { EffUsernameGenerationOptions } from "../../types"; import { Profile } from "../data"; import { CoreProfileMetadata } from "../profile-metadata"; import { isCoreProfile } from "../util"; diff --git a/libs/tools/generator/core/src/metadata/username/eff-word-list.ts b/libs/tools/generator/core/src/metadata/username/eff-word-list.ts index 6373daf8ed5..2802eea2c08 100644 --- a/libs/tools/generator/core/src/metadata/username/eff-word-list.ts +++ b/libs/tools/generator/core/src/metadata/username/eff-word-list.ts @@ -4,17 +4,14 @@ import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state import { deepFreeze } from "@bitwarden/common/tools/util"; import { UsernameRandomizer } from "../../engine"; -import { - CredentialGenerator, - EffUsernameGenerationOptions, - GeneratorDependencyProvider, -} from "../../types"; +import { GeneratorDependencyProvider } from "../../providers"; +import { CredentialGenerator, EffUsernameGenerationOptions } from "../../types"; import { Algorithm, Profile, Type } from "../data"; import { GeneratorMetadata } from "../generator-metadata"; const effWordList: GeneratorMetadata = deepFreeze({ id: Algorithm.username, - category: Type.username, + type: Type.username, weight: 400, i18nKeys: { name: "randomWord", diff --git a/libs/tools/generator/core/src/metadata/util.ts b/libs/tools/generator/core/src/metadata/util.ts index 86b2742e86d..a8e8879b57c 100644 --- a/libs/tools/generator/core/src/metadata/util.ts +++ b/libs/tools/generator/core/src/metadata/util.ts @@ -12,23 +12,23 @@ import { /** Returns true when the input algorithm is a password algorithm. */ export function isPasswordAlgorithm( - algorithm: CredentialAlgorithm, + algorithm: CredentialAlgorithm | null, ): algorithm is PasswordAlgorithm { return AlgorithmsByType.password.includes(algorithm as any); } /** Returns true when the input algorithm is a username algorithm. */ export function isUsernameAlgorithm( - algorithm: CredentialAlgorithm, + algorithm: CredentialAlgorithm | null, ): algorithm is UsernameAlgorithm { return AlgorithmsByType.username.includes(algorithm as any); } /** Returns true when the input algorithm is a forwarder integration. */ export function isForwarderExtensionId( - algorithm: CredentialAlgorithm, + algorithm: CredentialAlgorithm | null, ): algorithm is ForwarderExtensionId { - return algorithm && typeof algorithm === "object" && "forwarder" in algorithm; + return !!(algorithm && typeof algorithm === "object" && "forwarder" in algorithm); } /** Extract a `VendorId` from a `CredentialAlgorithm`. diff --git a/libs/tools/generator/core/src/policies/available-algorithms-constraint.ts b/libs/tools/generator/core/src/policies/available-algorithms-constraint.ts new file mode 100644 index 00000000000..1824581664b --- /dev/null +++ b/libs/tools/generator/core/src/policies/available-algorithms-constraint.ts @@ -0,0 +1,76 @@ +import { SemanticLogger } from "@bitwarden/common/tools/log"; +import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider"; +import { Constraints, StateConstraints } from "@bitwarden/common/tools/types"; + +import { CredentialAlgorithm, CredentialType } from "../metadata"; +import { CredentialPreference } from "../types"; +import { TypeRequest } from "../types/metadata-request"; + +export class AvailableAlgorithmsConstraint implements StateConstraints { + /** Well-known constraints of `State` */ + readonly constraints: Readonly> = {}; + + /** Creates a password policy constraints + * @param algorithms loads the algorithms for an algorithm type + * @param isAvailable returns `true` when `algorithm` is enabled by policy + * @param system provides logging facilities + */ + constructor( + readonly algorithms: (request: TypeRequest) => CredentialAlgorithm[], + readonly isAvailable: (algorithm: CredentialAlgorithm) => boolean, + readonly system: UserStateSubjectDependencyProvider, + ) { + this.log = system.log({ type: "AvailableAlgorithmsConstraint" }); + } + private readonly log: SemanticLogger; + + adjust(preferences: CredentialPreference): CredentialPreference { + const result: any = {}; + + const types = Object.keys(preferences) as CredentialType[]; + for (const t of types) { + result[t] = this.adjustPreference(t, preferences[t]); + } + + return result; + } + + private adjustPreference(type: CredentialType, preference: { algorithm: CredentialAlgorithm }) { + if (this.isAvailable(preference.algorithm)) { + this.log.debug({ preference, type }, "using preferred algorithm"); + + return preference; + } + + // choose a default - this algorithm is arbitrary, but stable. + const algorithms = type ? this.algorithms({ type: type }) : []; + const defaultAlgorithm = algorithms.find(this.isAvailable) ?? null; + + // adjust the preference + let adjustedPreference; + if (defaultAlgorithm) { + adjustedPreference = { + ...preference, + algorithm: defaultAlgorithm, + updated: this.system.now(), + }; + this.log.debug( + { preference, defaultAlgorithm, type }, + "preference not available; defaulting the algorithm", + ); + } else { + // FIXME: hard-code a fallback in category metadata + this.log.warn( + { preference, type }, + "preference not available and default algorithm not found; continuing with preference", + ); + adjustedPreference = preference; + } + + return adjustedPreference; + } + + fix(preferences: CredentialPreference): CredentialPreference { + return preferences; + } +} diff --git a/libs/tools/generator/core/src/policies/available-algorithms-policy.spec.ts b/libs/tools/generator/core/src/policies/available-algorithms-policy.spec.ts index 1ef0adc1af4..5f699974fba 100644 --- a/libs/tools/generator/core/src/policies/available-algorithms-policy.spec.ts +++ b/libs/tools/generator/core/src/policies/available-algorithms-policy.spec.ts @@ -2,15 +2,15 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { PolicyId } from "@bitwarden/common/types/guid"; -import { CredentialAlgorithms, PasswordAlgorithms } from "../data"; +import { Algorithm, Algorithms, AlgorithmsByType } from "../metadata"; import { availableAlgorithms } from "./available-algorithms-policy"; -describe("availableAlgorithmsPolicy", () => { +describe("availableAlgorithms_vNextPolicy", () => { it("returns all algorithms", () => { const result = availableAlgorithms([]); - for (const expected of CredentialAlgorithms) { + for (const expected of Algorithms) { expect(result).toContain(expected); } }); @@ -30,7 +30,7 @@ describe("availableAlgorithmsPolicy", () => { expect(result).toContain(override); - for (const expected of PasswordAlgorithms.filter((a) => a !== override)) { + for (const expected of AlgorithmsByType[Algorithm.password].filter((a) => a !== override)) { expect(result).not.toContain(expected); } }); @@ -50,7 +50,7 @@ describe("availableAlgorithmsPolicy", () => { expect(result).toContain(override); - for (const expected of PasswordAlgorithms.filter((a) => a !== override)) { + for (const expected of AlgorithmsByType[Algorithm.password].filter((a) => a !== override)) { expect(result).not.toContain(expected); } }); @@ -79,7 +79,7 @@ describe("availableAlgorithmsPolicy", () => { expect(result).toContain("password"); - for (const expected of PasswordAlgorithms.filter((a) => a !== "password")) { + for (const expected of AlgorithmsByType[Algorithm.password].filter((a) => a !== "password")) { expect(result).not.toContain(expected); } }); @@ -97,7 +97,7 @@ describe("availableAlgorithmsPolicy", () => { const result = availableAlgorithms([policy]); - for (const expected of CredentialAlgorithms) { + for (const expected of Algorithms) { expect(result).toContain(expected); } }); @@ -115,7 +115,7 @@ describe("availableAlgorithmsPolicy", () => { const result = availableAlgorithms([policy]); - for (const expected of CredentialAlgorithms) { + for (const expected of Algorithms) { expect(result).toContain(expected); } }); @@ -133,7 +133,7 @@ describe("availableAlgorithmsPolicy", () => { const result = availableAlgorithms([policy]); - for (const expected of CredentialAlgorithms) { + for (const expected of Algorithms) { expect(result).toContain(expected); } }); diff --git a/libs/tools/generator/core/src/policies/available-algorithms-policy.ts b/libs/tools/generator/core/src/policies/available-algorithms-policy.ts index 0c44a1a0408..e63b648cf44 100644 --- a/libs/tools/generator/core/src/policies/available-algorithms-policy.ts +++ b/libs/tools/generator/core/src/policies/available-algorithms-policy.ts @@ -1,57 +1,30 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { PolicyType } from "@bitwarden/common/admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models // implement ADR-0002 import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { - CredentialAlgorithm as LegacyAlgorithm, - EmailAlgorithms, - PasswordAlgorithms, - UsernameAlgorithms, -} from ".."; -import { CredentialAlgorithm } from "../metadata"; +import { AlgorithmsByType, CredentialAlgorithm, Type } from "../metadata"; /** Reduces policies to a set of available algorithms * @param policies the policies to reduce * @returns the resulting `AlgorithmAvailabilityPolicy` */ -export function availableAlgorithms(policies: Policy[]): LegacyAlgorithm[] { +export function availableAlgorithms(policies: Policy[]): CredentialAlgorithm[] { const overridePassword = policies .filter((policy) => policy.type === PolicyType.PasswordGenerator && policy.enabled) .reduce( (type, policy) => (type === "password" ? type : (policy.data.overridePasswordType ?? type)), - null as LegacyAlgorithm, + null as CredentialAlgorithm | null, ); - const policy: LegacyAlgorithm[] = [...EmailAlgorithms, ...UsernameAlgorithms]; + const policy: CredentialAlgorithm[] = [ + ...AlgorithmsByType[Type.email], + ...AlgorithmsByType[Type.username], + ]; if (overridePassword) { policy.push(overridePassword); } else { - policy.push(...PasswordAlgorithms); - } - - return policy; -} - -/** Reduces policies to a set of available algorithms - * @param policies the policies to reduce - * @returns the resulting `AlgorithmAvailabilityPolicy` - */ -export function availableAlgorithms_vNext(policies: Policy[]): CredentialAlgorithm[] { - const overridePassword = policies - .filter((policy) => policy.type === PolicyType.PasswordGenerator && policy.enabled) - .reduce( - (type, policy) => (type === "password" ? type : (policy.data.overridePasswordType ?? type)), - null as CredentialAlgorithm, - ); - - const policy: CredentialAlgorithm[] = [...EmailAlgorithms, ...UsernameAlgorithms]; - if (overridePassword) { - policy.push(overridePassword); - } else { - policy.push(...PasswordAlgorithms); + policy.push(...AlgorithmsByType[Type.password]); } return policy; diff --git a/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts b/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts index c8ae02ef723..0bebb0825bf 100644 --- a/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts +++ b/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts @@ -1,15 +1,25 @@ import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; -import { Generators } from "../data"; +import { BuiltIn, Profile } from "../metadata"; import { PasswordGeneratorSettings } from "../types"; import { AtLeastOne, Zero } from "./constraints"; import { DynamicPasswordPolicyConstraints } from "./dynamic-password-policy-constraints"; -const accoutSettings = Generators.password.settings.account as ObjectKey; -const defaultOptions = accoutSettings.initial; -const disabledPolicy = Generators.password.policy.disabledValue; -const someConstraints = Generators.password.settings.constraints; +// non-null assertions used because these are always-defined constants +const accoutSettings = BuiltIn.password.profiles[Profile.account]! + .storage as ObjectKey; +const defaultOptions = accoutSettings.initial!; +const disabledPolicy = { + minLength: 0, + useUppercase: false, + useLowercase: false, + useNumbers: false, + numberCount: 0, + useSpecial: false, + specialCount: 0, +}; +const someConstraints = BuiltIn.password.profiles[Profile.account]!.constraints.default; describe("DynamicPasswordPolicyConstraints", () => { describe("constructor", () => { @@ -33,8 +43,8 @@ describe("DynamicPasswordPolicyConstraints", () => { const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints); expect(constraints.policyInEffect).toBeTruthy(); - expect(constraints.lowercase.readonly).toEqual(true); - expect(constraints.lowercase.requiredValue).toEqual(true); + expect(constraints.lowercase?.readonly).toEqual(true); + expect(constraints.lowercase?.requiredValue).toEqual(true); expect(constraints.minLowercase).toEqual({ min: 1 }); }); @@ -43,8 +53,8 @@ describe("DynamicPasswordPolicyConstraints", () => { const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints); expect(constraints.policyInEffect).toBeTruthy(); - expect(constraints.uppercase.readonly).toEqual(true); - expect(constraints.uppercase.requiredValue).toEqual(true); + expect(constraints.uppercase?.readonly).toEqual(true); + expect(constraints.uppercase?.requiredValue).toEqual(true); expect(constraints.minUppercase).toEqual({ min: 1 }); }); @@ -53,8 +63,8 @@ describe("DynamicPasswordPolicyConstraints", () => { const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints); expect(constraints.policyInEffect).toBeTruthy(); - expect(constraints.number.readonly).toEqual(true); - expect(constraints.number.requiredValue).toEqual(true); + expect(constraints.number?.readonly).toEqual(true); + expect(constraints.number?.requiredValue).toEqual(true); expect(constraints.minNumber).toEqual({ min: 1, max: 9 }); }); @@ -63,8 +73,8 @@ describe("DynamicPasswordPolicyConstraints", () => { const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints); expect(constraints.policyInEffect).toBeTruthy(); - expect(constraints.special.readonly).toEqual(true); - expect(constraints.special.requiredValue).toEqual(true); + expect(constraints.special?.readonly).toEqual(true); + expect(constraints.special?.requiredValue).toEqual(true); expect(constraints.minSpecial).toEqual({ min: 1, max: 9 }); }); @@ -73,8 +83,8 @@ describe("DynamicPasswordPolicyConstraints", () => { const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints); expect(constraints.policyInEffect).toBeTruthy(); - expect(constraints.number.readonly).toEqual(true); - expect(constraints.number.requiredValue).toEqual(true); + expect(constraints.number?.readonly).toEqual(true); + expect(constraints.number?.requiredValue).toEqual(true); expect(constraints.minNumber).toEqual({ min: 2, max: 9 }); }); @@ -83,8 +93,8 @@ describe("DynamicPasswordPolicyConstraints", () => { const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints); expect(constraints.policyInEffect).toBeTruthy(); - expect(constraints.special.readonly).toEqual(true); - expect(constraints.special.requiredValue).toEqual(true); + expect(constraints.special?.readonly).toEqual(true); + expect(constraints.special?.requiredValue).toEqual(true); expect(constraints.minSpecial).toEqual({ min: 2, max: 9 }); }); @@ -140,7 +150,8 @@ describe("DynamicPasswordPolicyConstraints", () => { const dynamic = new DynamicPasswordPolicyConstraints( { ...disabledPolicy, - useLowercase, + // the `undefined` case is testing behavior when the type system is bypassed + useLowercase: useLowercase!, }, someConstraints, ); @@ -185,7 +196,8 @@ describe("DynamicPasswordPolicyConstraints", () => { const dynamic = new DynamicPasswordPolicyConstraints( { ...disabledPolicy, - useUppercase, + // the `undefined` case is testing behavior when the type system is bypassed + useUppercase: useUppercase!, }, someConstraints, ); diff --git a/libs/tools/generator/core/src/policies/index.ts b/libs/tools/generator/core/src/policies/index.ts index 0d05e702306..893f0402d38 100644 --- a/libs/tools/generator/core/src/policies/index.ts +++ b/libs/tools/generator/core/src/policies/index.ts @@ -5,3 +5,5 @@ export { PassphrasePolicyConstraints } from "./passphrase-policy-constraints"; export { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator"; export { passphraseLeastPrivilege } from "./passphrase-least-privilege"; export { passwordLeastPrivilege } from "./password-least-privilege"; +export { AvailableAlgorithmsConstraint } from "./available-algorithms-constraint"; +export { availableAlgorithms } from "./available-algorithms-policy"; diff --git a/libs/tools/generator/core/src/policies/passphrase-generator-options-evaluator.spec.ts b/libs/tools/generator/core/src/policies/passphrase-generator-options-evaluator.spec.ts index 3b1eb799391..5fcf847c504 100644 --- a/libs/tools/generator/core/src/policies/passphrase-generator-options-evaluator.spec.ts +++ b/libs/tools/generator/core/src/policies/passphrase-generator-options-evaluator.spec.ts @@ -1,12 +1,32 @@ -import { Policies, DefaultPassphraseBoundaries } from "../data"; -import { PassphraseGenerationOptions } from "../types"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { deepFreeze } from "@bitwarden/common/tools/util"; + +import { DefaultPassphraseBoundaries } from "../data"; +import { + PassphraseGenerationOptions, + PassphraseGeneratorPolicy, + PolicyConfiguration, +} from "../types"; import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator"; +import { passphraseLeastPrivilege } from "./passphrase-least-privilege"; -describe("Password generator options builder", () => { +const Passphrase: PolicyConfiguration = + deepFreeze({ + type: PolicyType.PasswordGenerator, + disabledValue: { + minNumberWords: 0, + capitalize: false, + includeNumber: false, + }, + combine: passphraseLeastPrivilege, + createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy), + }); + +describe("Passphrase generator options builder", () => { describe("constructor()", () => { it("should set the policy object to a copy of the input policy", () => { - const policy: any = Object.assign({}, Policies.Passphrase.disabledValue); + const policy: any = Object.assign({}, Passphrase.disabledValue); policy.minNumberWords = 10; // arbitrary change for deep equality check const builder = new PassphraseGeneratorOptionsEvaluator(policy); @@ -16,7 +36,7 @@ describe("Password generator options builder", () => { }); it("should set default boundaries when a default policy is used", () => { - const policy = Object.assign({}, Policies.Passphrase.disabledValue); + const policy = Object.assign({}, Passphrase.disabledValue); const builder = new PassphraseGeneratorOptionsEvaluator(policy); expect(builder.numWords).toEqual(DefaultPassphraseBoundaries.numWords); @@ -25,7 +45,7 @@ describe("Password generator options builder", () => { it.each([1, 2])( "should use the default word boundaries when they are greater than `policy.minNumberWords` (= %i)", (minNumberWords) => { - const policy: any = Object.assign({}, Policies.Passphrase.disabledValue); + const policy: any = Object.assign({}, Passphrase.disabledValue); policy.minNumberWords = minNumberWords; const builder = new PassphraseGeneratorOptionsEvaluator(policy); @@ -37,7 +57,7 @@ describe("Password generator options builder", () => { it.each([8, 12, 18])( "should use `policy.minNumberWords` (= %i) when it is greater than the default minimum words", (minNumberWords) => { - const policy: any = Object.assign({}, Policies.Passphrase.disabledValue); + const policy: any = Object.assign({}, Passphrase.disabledValue); policy.minNumberWords = minNumberWords; const builder = new PassphraseGeneratorOptionsEvaluator(policy); @@ -50,7 +70,7 @@ describe("Password generator options builder", () => { it.each([150, 300, 9000])( "should use `policy.minNumberWords` (= %i) when it is greater than the default boundaries", (minNumberWords) => { - const policy: any = Object.assign({}, Policies.Passphrase.disabledValue); + const policy: any = Object.assign({}, Passphrase.disabledValue); policy.minNumberWords = minNumberWords; const builder = new PassphraseGeneratorOptionsEvaluator(policy); @@ -63,14 +83,14 @@ describe("Password generator options builder", () => { describe("policyInEffect", () => { it("should return false when the policy has no effect", () => { - const policy = Object.assign({}, Policies.Passphrase.disabledValue); + const policy = Object.assign({}, Passphrase.disabledValue); const builder = new PassphraseGeneratorOptionsEvaluator(policy); expect(builder.policyInEffect).toEqual(false); }); it("should return true when the policy has a numWords greater than the default boundary", () => { - const policy: any = Object.assign({}, Policies.Passphrase.disabledValue); + const policy: any = Object.assign({}, Passphrase.disabledValue); policy.minNumberWords = DefaultPassphraseBoundaries.numWords.min + 1; const builder = new PassphraseGeneratorOptionsEvaluator(policy); @@ -78,7 +98,7 @@ describe("Password generator options builder", () => { }); it("should return true when the policy has capitalize enabled", () => { - const policy: any = Object.assign({}, Policies.Passphrase.disabledValue); + const policy: any = Object.assign({}, Passphrase.disabledValue); policy.capitalize = true; const builder = new PassphraseGeneratorOptionsEvaluator(policy); @@ -86,7 +106,7 @@ describe("Password generator options builder", () => { }); it("should return true when the policy has includeNumber enabled", () => { - const policy: any = Object.assign({}, Policies.Passphrase.disabledValue); + const policy: any = Object.assign({}, Passphrase.disabledValue); policy.includeNumber = true; const builder = new PassphraseGeneratorOptionsEvaluator(policy); @@ -98,7 +118,7 @@ describe("Password generator options builder", () => { // All tests should freeze the options to ensure they are not modified it("should set `capitalize` to `false` when the policy does not override it", () => { - const policy = Object.assign({}, Policies.Passphrase.disabledValue); + const policy = Object.assign({}, Passphrase.disabledValue); const builder = new PassphraseGeneratorOptionsEvaluator(policy); const options = Object.freeze({}); @@ -108,7 +128,7 @@ describe("Password generator options builder", () => { }); it("should set `capitalize` to `true` when the policy overrides it", () => { - const policy: any = Object.assign({}, Policies.Passphrase.disabledValue); + const policy: any = Object.assign({}, Passphrase.disabledValue); policy.capitalize = true; const builder = new PassphraseGeneratorOptionsEvaluator(policy); const options = Object.freeze({ capitalize: false }); @@ -119,7 +139,7 @@ describe("Password generator options builder", () => { }); it("should set `includeNumber` to false when the policy does not override it", () => { - const policy = Object.assign({}, Policies.Passphrase.disabledValue); + const policy = Object.assign({}, Passphrase.disabledValue); const builder = new PassphraseGeneratorOptionsEvaluator(policy); const options = Object.freeze({}); @@ -129,7 +149,7 @@ describe("Password generator options builder", () => { }); it("should set `includeNumber` to true when the policy overrides it", () => { - const policy: any = Object.assign({}, Policies.Passphrase.disabledValue); + const policy: any = Object.assign({}, Passphrase.disabledValue); policy.includeNumber = true; const builder = new PassphraseGeneratorOptionsEvaluator(policy); const options = Object.freeze({ includeNumber: false }); @@ -140,7 +160,7 @@ describe("Password generator options builder", () => { }); it("should set `numWords` to the minimum value when it isn't supplied", () => { - const policy = Object.assign({}, Policies.Passphrase.disabledValue); + const policy = Object.assign({}, Passphrase.disabledValue); const builder = new PassphraseGeneratorOptionsEvaluator(policy); const options = Object.freeze({}); @@ -154,7 +174,7 @@ describe("Password generator options builder", () => { (numWords) => { expect(numWords).toBeLessThan(DefaultPassphraseBoundaries.numWords.min); - const policy = Object.assign({}, Policies.Passphrase.disabledValue); + const policy = Object.assign({}, Passphrase.disabledValue); const builder = new PassphraseGeneratorOptionsEvaluator(policy); const options = Object.freeze({ numWords }); @@ -170,7 +190,7 @@ describe("Password generator options builder", () => { expect(numWords).toBeGreaterThanOrEqual(DefaultPassphraseBoundaries.numWords.min); expect(numWords).toBeLessThanOrEqual(DefaultPassphraseBoundaries.numWords.max); - const policy = Object.assign({}, Policies.Passphrase.disabledValue); + const policy = Object.assign({}, Passphrase.disabledValue); const builder = new PassphraseGeneratorOptionsEvaluator(policy); const options = Object.freeze({ numWords }); @@ -185,7 +205,7 @@ describe("Password generator options builder", () => { (numWords) => { expect(numWords).toBeGreaterThan(DefaultPassphraseBoundaries.numWords.max); - const policy = Object.assign({}, Policies.Passphrase.disabledValue); + const policy = Object.assign({}, Passphrase.disabledValue); const builder = new PassphraseGeneratorOptionsEvaluator(policy); const options = Object.freeze({ numWords }); @@ -196,7 +216,7 @@ describe("Password generator options builder", () => { ); it("should preserve unknown properties", () => { - const policy = Object.assign({}, Policies.Passphrase.disabledValue); + const policy = Object.assign({}, Passphrase.disabledValue); const builder = new PassphraseGeneratorOptionsEvaluator(policy); const options = Object.freeze({ unknown: "property", @@ -214,7 +234,7 @@ describe("Password generator options builder", () => { // All tests should freeze the options to ensure they are not modified it("should return the input options without altering them", () => { - const policy = Object.assign({}, Policies.Passphrase.disabledValue); + const policy = Object.assign({}, Passphrase.disabledValue); const builder = new PassphraseGeneratorOptionsEvaluator(policy); const options = Object.freeze({ wordSeparator: "%" }); @@ -224,7 +244,7 @@ describe("Password generator options builder", () => { }); it("should set `wordSeparator` to '-' when it isn't supplied and there is no policy override", () => { - const policy = Object.assign({}, Policies.Passphrase.disabledValue); + const policy = Object.assign({}, Passphrase.disabledValue); const builder = new PassphraseGeneratorOptionsEvaluator(policy); const options = Object.freeze({}); @@ -234,7 +254,7 @@ describe("Password generator options builder", () => { }); it("should leave `wordSeparator` as the empty string '' when it is the empty string", () => { - const policy = Object.assign({}, Policies.Passphrase.disabledValue); + const policy = Object.assign({}, Passphrase.disabledValue); const builder = new PassphraseGeneratorOptionsEvaluator(policy); const options = Object.freeze({ wordSeparator: "" }); @@ -244,7 +264,7 @@ describe("Password generator options builder", () => { }); it("should preserve unknown properties", () => { - const policy = Object.assign({}, Policies.Passphrase.disabledValue); + const policy = Object.assign({}, Passphrase.disabledValue); const builder = new PassphraseGeneratorOptionsEvaluator(policy); const options = Object.freeze({ unknown: "property", diff --git a/libs/tools/generator/core/src/policies/passphrase-least-privilege.spec.ts b/libs/tools/generator/core/src/policies/passphrase-least-privilege.spec.ts index ecac3855987..0fbc1796e9e 100644 --- a/libs/tools/generator/core/src/policies/passphrase-least-privilege.spec.ts +++ b/libs/tools/generator/core/src/policies/passphrase-least-privilege.spec.ts @@ -4,8 +4,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { PolicyId } from "@bitwarden/common/types/guid"; -import { Policies } from "../data"; - import { passphraseLeastPrivilege } from "./passphrase-least-privilege"; function createPolicy( @@ -22,21 +20,27 @@ function createPolicy( }); } +const disabledValue = Object.freeze({ + minNumberWords: 0, + capitalize: false, + includeNumber: false, +}); + describe("passphraseLeastPrivilege", () => { it("should return the accumulator when the policy type does not apply", () => { const policy = createPolicy({}, PolicyType.RequireSso); - const result = passphraseLeastPrivilege(Policies.Passphrase.disabledValue, policy); + const result = passphraseLeastPrivilege(disabledValue, policy); - expect(result).toEqual(Policies.Passphrase.disabledValue); + expect(result).toEqual(disabledValue); }); it("should return the accumulator when the policy is not enabled", () => { const policy = createPolicy({}, PolicyType.PasswordGenerator, false); - const result = passphraseLeastPrivilege(Policies.Passphrase.disabledValue, policy); + const result = passphraseLeastPrivilege(disabledValue, policy); - expect(result).toEqual(Policies.Passphrase.disabledValue); + expect(result).toEqual(disabledValue); }); it.each([ @@ -46,8 +50,8 @@ describe("passphraseLeastPrivilege", () => { ])("should take the %p from the policy", (input, value) => { const policy = createPolicy({ [input]: value }); - const result = passphraseLeastPrivilege(Policies.Passphrase.disabledValue, policy); + const result = passphraseLeastPrivilege(disabledValue, policy); - expect(result).toEqual({ ...Policies.Passphrase.disabledValue, [input]: value }); + expect(result).toEqual({ ...disabledValue, [input]: value }); }); }); diff --git a/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts b/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts index d6e0a5615dc..6306382c84e 100644 --- a/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts +++ b/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts @@ -1,4 +1,4 @@ -import { Generators } from "../data"; +import { BuiltIn, Profile } from "../metadata"; import { PassphrasePolicyConstraints } from "./passphrase-policy-constraints"; @@ -9,8 +9,12 @@ const SomeSettings = { wordSeparator: "-", }; -const disabledPolicy = Generators.passphrase.policy.disabledValue; -const someConstraints = Generators.passphrase.settings.constraints; +const disabledPolicy = { + minNumberWords: 0, + capitalize: false, + includeNumber: false, +}; +const someConstraints = BuiltIn.passphrase.profiles[Profile.account]!.constraints.default; describe("PassphrasePolicyConstraints", () => { describe("constructor", () => { @@ -61,7 +65,7 @@ describe("PassphrasePolicyConstraints", () => { expect(constraints.policyInEffect).toBeTruthy(); expect(constraints.numWords).toMatchObject({ min: 10, - max: someConstraints.numWords.max, + max: someConstraints.numWords?.max, }); }); }); @@ -84,8 +88,8 @@ describe("PassphrasePolicyConstraints", () => { }); it.each([ - [1, someConstraints.numWords.min, 3, someConstraints.numWords.max], - [21, someConstraints.numWords.min, 20, someConstraints.numWords.max], + [1, someConstraints.numWords?.min, 3, someConstraints.numWords?.max], + [21, someConstraints.numWords?.min, 20, someConstraints.numWords?.max], ])( `fits numWords (=%p) within the default bounds (%p <= %p <= %p)`, (value, _, expected, __) => { @@ -98,8 +102,8 @@ describe("PassphrasePolicyConstraints", () => { ); it.each([ - [1, 6, 6, someConstraints.numWords.max], - [21, 20, 20, someConstraints.numWords.max], + [1, 6, 6, someConstraints.numWords?.max], + [21, 20, 20, someConstraints.numWords?.max], ])( "fits numWords (=%p) within the policy bounds (%p <= %p <= %p)", (value, minNumberWords, expected, _) => { diff --git a/libs/tools/generator/core/src/policies/password-generator-options-evaluator.spec.ts b/libs/tools/generator/core/src/policies/password-generator-options-evaluator.spec.ts index 91334f91f85..a088f93d3fe 100644 --- a/libs/tools/generator/core/src/policies/password-generator-options-evaluator.spec.ts +++ b/libs/tools/generator/core/src/policies/password-generator-options-evaluator.spec.ts @@ -1,14 +1,34 @@ -import { DefaultPasswordBoundaries, Policies } from "../data"; -import { PasswordGenerationOptions } from "../types"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { deepFreeze } from "@bitwarden/common/tools/util"; + +import { DefaultPasswordBoundaries } from "../data"; +import { PasswordGenerationOptions, PasswordGeneratorPolicy, PolicyConfiguration } from "../types"; import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator"; +import { passwordLeastPrivilege } from "./password-least-privilege"; + +const Password: PolicyConfiguration = + deepFreeze({ + type: PolicyType.PasswordGenerator, + disabledValue: { + minLength: 0, + useUppercase: false, + useLowercase: false, + useNumbers: false, + numberCount: 0, + useSpecial: false, + specialCount: 0, + }, + combine: passwordLeastPrivilege, + createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy), + }); describe("Password generator options builder", () => { const defaultOptions = Object.freeze({ minLength: 0 }); describe("constructor()", () => { it("should set the policy object to a copy of the input policy", () => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.minLength = 10; // arbitrary change for deep equality check const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -18,7 +38,7 @@ describe("Password generator options builder", () => { }); it("should set default boundaries when a default policy is used", () => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -32,7 +52,7 @@ describe("Password generator options builder", () => { (minLength) => { expect(minLength).toBeLessThan(DefaultPasswordBoundaries.length.min); - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.minLength = minLength; const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -47,7 +67,7 @@ describe("Password generator options builder", () => { expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.min); expect(expectedLength).toBeLessThanOrEqual(DefaultPasswordBoundaries.length.max); - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.minLength = expectedLength; const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -62,7 +82,7 @@ describe("Password generator options builder", () => { (expectedLength) => { expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.max); - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.minLength = expectedLength; const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -78,7 +98,7 @@ describe("Password generator options builder", () => { expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.min); expect(expectedMinDigits).toBeLessThanOrEqual(DefaultPasswordBoundaries.minDigits.max); - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.numberCount = expectedMinDigits; const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -93,7 +113,7 @@ describe("Password generator options builder", () => { (expectedMinDigits) => { expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.max); - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.numberCount = expectedMinDigits; const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -113,7 +133,7 @@ describe("Password generator options builder", () => { DefaultPasswordBoundaries.minSpecialCharacters.max, ); - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.specialCount = expectedSpecialCharacters; const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -132,7 +152,7 @@ describe("Password generator options builder", () => { DefaultPasswordBoundaries.minSpecialCharacters.max, ); - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.specialCount = expectedSpecialCharacters; const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -151,7 +171,7 @@ describe("Password generator options builder", () => { (expectedLength, numberCount, specialCount) => { expect(expectedLength).toBeGreaterThanOrEqual(DefaultPasswordBoundaries.length.min); - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.numberCount = numberCount; policy.specialCount = specialCount; @@ -164,14 +184,14 @@ describe("Password generator options builder", () => { describe("policyInEffect", () => { it("should return false when the policy has no effect", () => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); expect(builder.policyInEffect).toEqual(false); }); it("should return true when the policy has a minlength greater than the default boundary", () => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.minLength = DefaultPasswordBoundaries.length.min + 1; const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -179,7 +199,7 @@ describe("Password generator options builder", () => { }); it("should return true when the policy has a number count greater than the default boundary", () => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.numberCount = DefaultPasswordBoundaries.minDigits.min + 1; const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -187,7 +207,7 @@ describe("Password generator options builder", () => { }); it("should return true when the policy has a special character count greater than the default boundary", () => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.specialCount = DefaultPasswordBoundaries.minSpecialCharacters.min + 1; const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -195,7 +215,7 @@ describe("Password generator options builder", () => { }); it("should return true when the policy has uppercase enabled", () => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.useUppercase = true; const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -203,7 +223,7 @@ describe("Password generator options builder", () => { }); it("should return true when the policy has lowercase enabled", () => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.useLowercase = true; const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -211,7 +231,7 @@ describe("Password generator options builder", () => { }); it("should return true when the policy has numbers enabled", () => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.useNumbers = true; const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -219,7 +239,7 @@ describe("Password generator options builder", () => { }); it("should return true when the policy has special characters enabled", () => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.useSpecial = true; const builder = new PasswordGeneratorOptionsEvaluator(policy); @@ -237,7 +257,7 @@ describe("Password generator options builder", () => { ])( "should set `options.uppercase` to '%s' when `policy.useUppercase` is false and `options.uppercase` is '%s'", (expectedUppercase, uppercase) => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.useUppercase = false; const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ ...defaultOptions, uppercase }); @@ -251,7 +271,7 @@ describe("Password generator options builder", () => { it.each([false, true, undefined])( "should set `options.uppercase` (= %s) to true when `policy.useUppercase` is true", (uppercase) => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.useUppercase = true; const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ ...defaultOptions, uppercase }); @@ -269,7 +289,7 @@ describe("Password generator options builder", () => { ])( "should set `options.lowercase` to '%s' when `policy.useLowercase` is false and `options.lowercase` is '%s'", (expectedLowercase, lowercase) => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.useLowercase = false; const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ ...defaultOptions, lowercase }); @@ -283,7 +303,7 @@ describe("Password generator options builder", () => { it.each([false, true, undefined])( "should set `options.lowercase` (= %s) to true when `policy.useLowercase` is true", (lowercase) => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.useLowercase = true; const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ ...defaultOptions, lowercase }); @@ -301,7 +321,7 @@ describe("Password generator options builder", () => { ])( "should set `options.number` to '%s' when `policy.useNumbers` is false and `options.number` is '%s'", (expectedNumber, number) => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.useNumbers = false; const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ ...defaultOptions, number }); @@ -315,7 +335,7 @@ describe("Password generator options builder", () => { it.each([false, true, undefined])( "should set `options.number` (= %s) to true when `policy.useNumbers` is true", (number) => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.useNumbers = true; const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ ...defaultOptions, number }); @@ -333,7 +353,7 @@ describe("Password generator options builder", () => { ])( "should set `options.special` to '%s' when `policy.useSpecial` is false and `options.special` is '%s'", (expectedSpecial, special) => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.useSpecial = false; const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ ...defaultOptions, special }); @@ -347,7 +367,7 @@ describe("Password generator options builder", () => { it.each([false, true, undefined])( "should set `options.special` (= %s) to true when `policy.useSpecial` is true", (special) => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.useSpecial = true; const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ ...defaultOptions, special }); @@ -361,7 +381,7 @@ describe("Password generator options builder", () => { it.each([1, 2, 3, 4])( "should set `options.length` (= %i) to the minimum it is less than the minimum length", (length) => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); expect(length).toBeLessThan(builder.length.min); @@ -376,7 +396,7 @@ describe("Password generator options builder", () => { it.each([5, 10, 50, 100, 128])( "should not change `options.length` (= %i) when it is within the boundaries", (length) => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); expect(length).toBeGreaterThanOrEqual(builder.length.min); expect(length).toBeLessThanOrEqual(builder.length.max); @@ -392,7 +412,7 @@ describe("Password generator options builder", () => { it.each([129, 500, 9000])( "should set `options.length` (= %i) to the maximum length when it is exceeded", (length) => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); expect(length).toBeGreaterThan(builder.length.max); @@ -414,7 +434,7 @@ describe("Password generator options builder", () => { ])( "should set `options.number === %s` when `options.minNumber` (= %i) is set to a value greater than 0", (expectedNumber, minNumber) => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ ...defaultOptions, minNumber }); @@ -425,7 +445,7 @@ describe("Password generator options builder", () => { ); it("should set `options.minNumber` to the minimum value when `options.number` is true", () => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ ...defaultOptions, number: true }); @@ -435,7 +455,7 @@ describe("Password generator options builder", () => { }); it("should set `options.minNumber` to 0 when `options.number` is false", () => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ ...defaultOptions, number: false }); @@ -447,7 +467,7 @@ describe("Password generator options builder", () => { it.each([1, 2, 3, 4])( "should set `options.minNumber` (= %i) to the minimum it is less than the minimum number", (minNumber) => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.numberCount = 5; // arbitrary value greater than minNumber expect(minNumber).toBeLessThan(policy.numberCount); @@ -463,7 +483,7 @@ describe("Password generator options builder", () => { it.each([1, 3, 5, 7, 9])( "should not change `options.minNumber` (= %i) when it is within the boundaries", (minNumber) => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); expect(minNumber).toBeGreaterThanOrEqual(builder.minDigits.min); expect(minNumber).toBeLessThanOrEqual(builder.minDigits.max); @@ -479,7 +499,7 @@ describe("Password generator options builder", () => { it.each([10, 20, 400])( "should set `options.minNumber` (= %i) to the maximum digit boundary when it is exceeded", (minNumber) => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); expect(minNumber).toBeGreaterThan(builder.minDigits.max); @@ -501,7 +521,7 @@ describe("Password generator options builder", () => { ])( "should set `options.special === %s` when `options.minSpecial` (= %i) is set to a value greater than 0", (expectedSpecial, minSpecial) => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ ...defaultOptions, minSpecial }); @@ -512,7 +532,7 @@ describe("Password generator options builder", () => { ); it("should set `options.minSpecial` to the minimum value when `options.special` is true", () => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ ...defaultOptions, special: true }); @@ -522,7 +542,7 @@ describe("Password generator options builder", () => { }); it("should set `options.minSpecial` to 0 when `options.special` is false", () => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ ...defaultOptions, special: false }); @@ -534,7 +554,7 @@ describe("Password generator options builder", () => { it.each([1, 2, 3, 4])( "should set `options.minSpecial` (= %i) to the minimum it is less than the minimum special characters", (minSpecial) => { - const policy: any = Object.assign({}, Policies.Password.disabledValue); + const policy: any = Object.assign({}, Password.disabledValue); policy.specialCount = 5; // arbitrary value greater than minSpecial expect(minSpecial).toBeLessThan(policy.specialCount); @@ -550,7 +570,7 @@ describe("Password generator options builder", () => { it.each([1, 3, 5, 7, 9])( "should not change `options.minSpecial` (= %i) when it is within the boundaries", (minSpecial) => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); expect(minSpecial).toBeGreaterThanOrEqual(builder.minSpecialCharacters.min); expect(minSpecial).toBeLessThanOrEqual(builder.minSpecialCharacters.max); @@ -566,7 +586,7 @@ describe("Password generator options builder", () => { it.each([10, 20, 400])( "should set `options.minSpecial` (= %i) to the maximum special character boundary when it is exceeded", (minSpecial) => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); expect(minSpecial).toBeGreaterThan(builder.minSpecialCharacters.max); @@ -579,7 +599,7 @@ describe("Password generator options builder", () => { ); it("should preserve unknown properties", () => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ unknown: "property", @@ -602,7 +622,7 @@ describe("Password generator options builder", () => { ])( "should output `options.minLowercase === %i` when `options.lowercase` is %s", (expectedMinLowercase, lowercase) => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ lowercase, ...defaultOptions }); @@ -618,7 +638,7 @@ describe("Password generator options builder", () => { ])( "should output `options.minUppercase === %i` when `options.uppercase` is %s", (expectedMinUppercase, uppercase) => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ uppercase, ...defaultOptions }); @@ -634,7 +654,7 @@ describe("Password generator options builder", () => { ])( "should output `options.minNumber === %i` when `options.number` is %s and `options.minNumber` is not set", (expectedMinNumber, number) => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ number, ...defaultOptions }); @@ -652,7 +672,7 @@ describe("Password generator options builder", () => { ])( "should output `options.number === %s` when `options.minNumber` is %i and `options.number` is not set", (expectedNumber, minNumber) => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ minNumber, ...defaultOptions }); @@ -668,7 +688,7 @@ describe("Password generator options builder", () => { ])( "should output `options.minSpecial === %i` when `options.special` is %s and `options.minSpecial` is not set", (special, expectedMinSpecial) => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ special, ...defaultOptions }); @@ -686,7 +706,7 @@ describe("Password generator options builder", () => { ])( "should output `options.special === %s` when `options.minSpecial` is %i and `options.special` is not set", (minSpecial, expectedSpecial) => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ minSpecial, ...defaultOptions }); @@ -707,7 +727,7 @@ describe("Password generator options builder", () => { const sumOfMinimums = minLowercase + minUppercase + minNumber + minSpecial; expect(sumOfMinimums).toBeLessThan(DefaultPasswordBoundaries.length.min); - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ minLowercase, @@ -732,7 +752,7 @@ describe("Password generator options builder", () => { (expectedMinLength, minLowercase, minUppercase, minNumber, minSpecial) => { expect(expectedMinLength).toBeGreaterThanOrEqual(DefaultPasswordBoundaries.length.min); - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ minLowercase, @@ -749,7 +769,7 @@ describe("Password generator options builder", () => { ); it("should preserve unknown properties", () => { - const policy = Object.assign({}, Policies.Password.disabledValue); + const policy = Object.assign({}, Password.disabledValue); const builder = new PasswordGeneratorOptionsEvaluator(policy); const options = Object.freeze({ unknown: "property", diff --git a/libs/tools/generator/core/src/policies/password-least-privilege.spec.ts b/libs/tools/generator/core/src/policies/password-least-privilege.spec.ts index 5d5430b8cad..7f8dce19b15 100644 --- a/libs/tools/generator/core/src/policies/password-least-privilege.spec.ts +++ b/libs/tools/generator/core/src/policies/password-least-privilege.spec.ts @@ -4,8 +4,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { PolicyId } from "@bitwarden/common/types/guid"; -import { Policies } from "../data"; - import { passwordLeastPrivilege } from "./password-least-privilege"; function createPolicy( @@ -22,21 +20,31 @@ function createPolicy( }); } +const disabledValue = Object.freeze({ + minLength: 0, + useUppercase: false, + useLowercase: false, + useNumbers: false, + numberCount: 0, + useSpecial: false, + specialCount: 0, +}); + describe("passwordLeastPrivilege", () => { it("should return the accumulator when the policy type does not apply", () => { const policy = createPolicy({}, PolicyType.RequireSso); - const result = passwordLeastPrivilege(Policies.Password.disabledValue, policy); + const result = passwordLeastPrivilege(disabledValue, policy); - expect(result).toEqual(Policies.Password.disabledValue); + expect(result).toEqual(disabledValue); }); it("should return the accumulator when the policy is not enabled", () => { const policy = createPolicy({}, PolicyType.PasswordGenerator, false); - const result = passwordLeastPrivilege(Policies.Password.disabledValue, policy); + const result = passwordLeastPrivilege(disabledValue, policy); - expect(result).toEqual(Policies.Password.disabledValue); + expect(result).toEqual(disabledValue); }); it.each([ @@ -50,8 +58,8 @@ describe("passwordLeastPrivilege", () => { ])("should take the %p from the policy", (input, value, expected) => { const policy = createPolicy({ [input]: value }); - const result = passwordLeastPrivilege(Policies.Password.disabledValue, policy); + const result = passwordLeastPrivilege(disabledValue, policy); - expect(result).toEqual({ ...Policies.Password.disabledValue, [expected]: value }); + expect(result).toEqual({ ...disabledValue, [expected]: value }); }); }); diff --git a/libs/tools/generator/core/src/providers/credential-generator-providers.ts b/libs/tools/generator/core/src/providers/credential-generator-providers.ts new file mode 100644 index 00000000000..1e1a8345f34 --- /dev/null +++ b/libs/tools/generator/core/src/providers/credential-generator-providers.ts @@ -0,0 +1,14 @@ +import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider"; + +import { GeneratorDependencyProvider } from "./generator-dependency-provider"; +import { GeneratorMetadataProvider } from "./generator-metadata-provider"; +import { GeneratorProfileProvider } from "./generator-profile-provider"; + +// FIXME: find a better way to manage common dependencies than smashing them all +// together into a mega-type. +export type CredentialGeneratorProviders = { + readonly userState: UserStateSubjectDependencyProvider; + readonly generator: GeneratorDependencyProvider; + readonly profile: GeneratorProfileProvider; + readonly metadata: GeneratorMetadataProvider; +}; diff --git a/libs/tools/generator/core/src/providers/credential-preferences.spec.ts b/libs/tools/generator/core/src/providers/credential-preferences.spec.ts new file mode 100644 index 00000000000..6fd747f3823 --- /dev/null +++ b/libs/tools/generator/core/src/providers/credential-preferences.spec.ts @@ -0,0 +1,105 @@ +import { AlgorithmsByType, Type } from "../metadata"; +import { CredentialPreference } from "../types"; + +import { PREFERENCES } from "./credential-preferences"; + +const SomeCredentialPreferences: CredentialPreference = Object.freeze({ + email: Object.freeze({ + algorithm: AlgorithmsByType[Type.email][0], + updated: new Date(0), + }), + password: Object.freeze({ + algorithm: AlgorithmsByType[Type.password][0], + updated: new Date(0), + }), + username: Object.freeze({ + algorithm: AlgorithmsByType[Type.username][0], + updated: new Date(0), + }), +}); + +describe("PREFERENCES", () => { + describe("deserializer", () => { + it.each([[null], [undefined]])("creates new preferences (= %p)", (value) => { + // this case tests what happens when the type system is bypassed + const result = PREFERENCES.deserializer(value!); + + expect(result).toMatchObject({ + email: { + algorithm: AlgorithmsByType[Type.email][0], + }, + password: { + algorithm: AlgorithmsByType[Type.password][0], + }, + username: { + algorithm: AlgorithmsByType[Type.username][0], + }, + }); + }); + + it("fills missing password preferences", () => { + const input: any = structuredClone(SomeCredentialPreferences); + delete input.password; + + const result = PREFERENCES.deserializer(input); + + expect(result).toMatchObject({ + password: { + algorithm: AlgorithmsByType[Type.password][0], + }, + }); + }); + + it("fills missing email preferences", () => { + const input: any = structuredClone(SomeCredentialPreferences); + delete input.email; + + const result = PREFERENCES.deserializer(input); + + expect(result).toMatchObject({ + email: { + algorithm: AlgorithmsByType[Type.email][0], + }, + }); + }); + + it("fills missing username preferences", () => { + const input: any = structuredClone(SomeCredentialPreferences); + delete input.username; + + const result = PREFERENCES.deserializer(input); + + expect(result).toMatchObject({ + username: { + algorithm: AlgorithmsByType[Type.username][0], + }, + }); + }); + + it("converts string fields to Dates", () => { + const input: any = structuredClone(SomeCredentialPreferences); + input.email.updated = "1970-01-01T00:00:00.100Z"; + input.password.updated = "1970-01-01T00:00:00.200Z"; + input.username.updated = "1970-01-01T00:00:00.300Z"; + + const result = PREFERENCES.deserializer(input); + + expect(result?.email.updated).toEqual(new Date(100)); + expect(result?.password.updated).toEqual(new Date(200)); + expect(result?.username.updated).toEqual(new Date(300)); + }); + + it("converts number fields to Dates", () => { + const input: any = structuredClone(SomeCredentialPreferences); + input.email.updated = 100; + input.password.updated = 200; + input.username.updated = 300; + + const result = PREFERENCES.deserializer(input); + + expect(result?.email.updated).toEqual(new Date(100)); + expect(result?.password.updated).toEqual(new Date(200)); + expect(result?.username.updated).toEqual(new Date(300)); + }); + }); +}); diff --git a/libs/tools/generator/core/src/providers/credential-preferences.ts b/libs/tools/generator/core/src/providers/credential-preferences.ts new file mode 100644 index 00000000000..5c6efd6008b --- /dev/null +++ b/libs/tools/generator/core/src/providers/credential-preferences.ts @@ -0,0 +1,28 @@ +import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; + +import { AlgorithmsByType, CredentialType } from "../metadata"; +import { CredentialPreference } from "../types"; + +/** plaintext password generation options */ +export const PREFERENCES = new UserKeyDefinition( + GENERATOR_DISK, + "credentialPreferences", + { + deserializer: (value) => { + const result = (value as any) ?? {}; + + for (const key in AlgorithmsByType) { + const type = key as CredentialType; + if (result[type]) { + result[type].updated = new Date(result[type].updated); + } else { + const [algorithm] = AlgorithmsByType[type]; + result[type] = { algorithm, updated: new Date() }; + } + } + + return result; + }, + clearOn: ["logout"], + }, +); diff --git a/libs/tools/generator/core/src/providers/generator-dependency-provider.ts b/libs/tools/generator/core/src/providers/generator-dependency-provider.ts new file mode 100644 index 00000000000..14942698cdb --- /dev/null +++ b/libs/tools/generator/core/src/providers/generator-dependency-provider.ts @@ -0,0 +1,12 @@ +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { RestClient } from "@bitwarden/common/tools/integration/rpc"; + +import { Randomizer } from "../abstractions"; + +export type GeneratorDependencyProvider = { + randomizer: Randomizer; + client: RestClient; + // FIXME: introduce `I18nKeyOrLiteral` into forwarder + // structures and remove this dependency + i18nService: I18nService; +}; diff --git a/libs/tools/generator/core/src/services/generator-metadata-provider.spec.ts b/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts similarity index 98% rename from libs/tools/generator/core/src/services/generator-metadata-provider.spec.ts rename to libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts index 37a987f88bc..71fced46fa6 100644 --- a/libs/tools/generator/core/src/services/generator-metadata-provider.spec.ts +++ b/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts @@ -75,6 +75,7 @@ const SystemProvider = { } as LegacyEncryptorProvider, state: SomeStateProvider, log: disabledSemanticLoggerProvider, + now: Date.now, } as UserStateSubjectDependencyProvider; const SomeSiteId: SiteId = Site.forwarder; @@ -415,14 +416,14 @@ describe("GeneratorMetadataProvider", () => { await expect(firstValueFrom(result)).resolves.toEqual(plusAddress.id); }); - it("emits undefined when the user's preference is unavailable and there is no metadata", async () => { + it("emits the original preference when the user's preference is unavailable and there is no metadata", async () => { SomePolicyService.policiesByType$.mockReturnValue(new BehaviorSubject([])); const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []); const result = new ReplaySubject(1); provider.preference$(Type.email, { account$: SomeAccount$ }).subscribe(result); - await expect(firstValueFrom(result)).resolves.toBeUndefined(); + await expect(firstValueFrom(result)).resolves.toEqual(preferences[Type.email].algorithm); }); }); diff --git a/libs/tools/generator/core/src/services/generator-metadata-provider.ts b/libs/tools/generator/core/src/providers/generator-metadata-provider.ts similarity index 87% rename from libs/tools/generator/core/src/services/generator-metadata-provider.ts rename to libs/tools/generator/core/src/providers/generator-metadata-provider.ts index 161f7192c39..52901545023 100644 --- a/libs/tools/generator/core/src/services/generator-metadata-provider.ts +++ b/libs/tools/generator/core/src/providers/generator-metadata-provider.ts @@ -1,12 +1,4 @@ -import { - Observable, - combineLatestWith, - distinctUntilChanged, - map, - shareReplay, - switchMap, - takeUntil, -} from "rxjs"; +import { Observable, distinctUntilChanged, map, shareReplay, switchMap, takeUntil } from "rxjs"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; @@ -14,7 +6,7 @@ import { BoundDependency } from "@bitwarden/common/tools/dependencies"; import { ExtensionSite } from "@bitwarden/common/tools/extension"; import { SemanticLogger } from "@bitwarden/common/tools/log"; import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; -import { anyComplete, pin } from "@bitwarden/common/tools/rx"; +import { anyComplete, memoizedMap, pin } from "@bitwarden/common/tools/rx"; import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject"; import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider"; @@ -29,7 +21,7 @@ import { Algorithms, Types, } from "../metadata"; -import { availableAlgorithms_vNext } from "../policies/available-algorithms-policy"; +import { AvailableAlgorithmsConstraint, availableAlgorithms } from "../policies"; import { CredentialPreference } from "../types"; import { AlgorithmRequest, @@ -148,8 +140,15 @@ export class GeneratorMetadataProvider { const policies$ = this.application.policy .policiesByType$(PolicyType.PasswordGenerator, id) .pipe( - map((p) => availableAlgorithms_vNext(p).filter((a) => this._metadata.has(a))), - map((p) => new Set(p)), + map((p) => + availableAlgorithms(p) + .filter((a) => this._metadata.has(a)) + .sort(), + ), + // interning the set transformation lets `distinctUntilChanged()` eliminate + // repeating policy emissions using reference equality + memoizedMap((a) => new Set(a), { key: (a) => a.join(":") }), + distinctUntilChanged(), // complete policy emissions otherwise `switchMap` holds `available$` open indefinitely takeUntil(anyComplete(id$)), ); @@ -211,24 +210,7 @@ export class GeneratorMetadataProvider { const account$ = dependencies.account$.pipe(shareReplay({ bufferSize: 1, refCount: true })); const algorithm$ = this.preferences({ account$ }).pipe( - combineLatestWith(this.isAvailable$({ account$ })), - map(([preferences, isAvailable]) => { - const algorithm: CredentialAlgorithm = preferences[type].algorithm; - if (isAvailable(algorithm)) { - return algorithm; - } - - const algorithms = type ? this.algorithms({ type: type }) : []; - // `?? null` because logging types must be `Jsonify` - const defaultAlgorithm = algorithms.find(isAvailable) ?? null; - this.log.debug( - { algorithm, defaultAlgorithm, credentialType: type }, - "preference not available; defaulting the generator algorithm", - ); - - // `?? undefined` so that interface is ADR-14 compliant - return defaultAlgorithm ?? undefined; - }), + map((preferences) => preferences[type].algorithm), distinctUntilChanged(), ); @@ -246,8 +228,16 @@ export class GeneratorMetadataProvider { preferences( dependencies: BoundDependency<"account", Account>, ): UserStateSubject { - // FIXME: enforce policy - const subject = new UserStateSubject(PREFERENCES, this.system, dependencies); + const account$ = dependencies.account$.pipe(shareReplay({ bufferSize: 1, refCount: true })); + + const constraints$ = this.isAvailable$({ account$ }).pipe( + map( + (isAvailable) => + new AvailableAlgorithmsConstraint(this.algorithms.bind(this), isAvailable, this.system), + ), + ); + + const subject = new UserStateSubject(PREFERENCES, this.system, { account$, constraints$ }); return subject; } diff --git a/libs/tools/generator/core/src/services/generator-profile-provider.spec.ts b/libs/tools/generator/core/src/providers/generator-profile-provider.spec.ts similarity index 99% rename from libs/tools/generator/core/src/services/generator-profile-provider.spec.ts rename to libs/tools/generator/core/src/providers/generator-profile-provider.spec.ts index aeb1a648a14..1053834eca7 100644 --- a/libs/tools/generator/core/src/services/generator-profile-provider.spec.ts +++ b/libs/tools/generator/core/src/providers/generator-profile-provider.spec.ts @@ -67,6 +67,7 @@ const dependencyProvider: UserStateSubjectDependencyProvider = { encryptor: encryptorProvider, state: stateProvider, log: disabledSemanticLoggerProvider, + now: Date.now, }; // settings storage location diff --git a/libs/tools/generator/core/src/services/generator-profile-provider.ts b/libs/tools/generator/core/src/providers/generator-profile-provider.ts similarity index 94% rename from libs/tools/generator/core/src/services/generator-profile-provider.ts rename to libs/tools/generator/core/src/providers/generator-profile-provider.ts index 7088e23d3fe..4117d1f2a78 100644 --- a/libs/tools/generator/core/src/services/generator-profile-provider.ts +++ b/libs/tools/generator/core/src/providers/generator-profile-provider.ts @@ -19,6 +19,7 @@ import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/stat import { ProfileContext, CoreProfileMetadata, ProfileMetadata } from "../metadata"; import { GeneratorConstraints } from "../types/generator-constraints"; +import { equivalent } from "../util"; /** Surfaces contextual information to credential generators */ export class GeneratorProfileProvider { @@ -99,7 +100,10 @@ export class GeneratorProfileProvider { const constraints$ = policies$.pipe( map((policies) => profile.constraints.create(policies, context)), - tap(() => this.log.debug("constraints created")), + distinctUntilChanged((previous, next) => { + return equivalent(previous, next); + }), + tap((constraints) => this.log.debug(constraints as object, "constraints updated")), ); return constraints$; diff --git a/libs/tools/generator/core/src/providers/index.ts b/libs/tools/generator/core/src/providers/index.ts new file mode 100644 index 00000000000..bad56b746b6 --- /dev/null +++ b/libs/tools/generator/core/src/providers/index.ts @@ -0,0 +1,4 @@ +export { CredentialGeneratorProviders } from "./credential-generator-providers"; +export { GeneratorMetadataProvider } from "./generator-metadata-provider"; +export { GeneratorProfileProvider } from "./generator-profile-provider"; +export { GeneratorDependencyProvider } from "./generator-dependency-provider"; diff --git a/libs/tools/generator/core/src/rx.ts b/libs/tools/generator/core/src/rx.ts index 44d23ef1c5c..ab907b6455f 100644 --- a/libs/tools/generator/core/src/rx.ts +++ b/libs/tools/generator/core/src/rx.ts @@ -18,20 +18,6 @@ export function mapPolicyToEvaluator( ); } -/** Maps an administrative console policy to constraints using the provided configuration. - * @param configuration the configuration that constructs the constraints. - */ -export function mapPolicyToConstraints( - configuration: PolicyConfiguration, - email: string, -) { - return pipe( - reduceCollection(configuration.combine, configuration.disabledValue), - distinctIfShallowMatch(), - map((policy) => configuration.toConstraints(policy, email)), - ); -} - /** Constructs a method that maps a policy to the default (no-op) policy. */ export function newDefaultEvaluator() { return () => { diff --git a/libs/tools/generator/core/src/services/credential-generator.service.spec.ts b/libs/tools/generator/core/src/services/credential-generator.service.spec.ts deleted file mode 100644 index 2bc8d514873..00000000000 --- a/libs/tools/generator/core/src/services/credential-generator.service.spec.ts +++ /dev/null @@ -1,1050 +0,0 @@ -// FIXME: remove ts-strict-ignore once `FakeAccountService` implements ts strict support -// @ts-strict-ignore -import { mock } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom, map, Subject } from "rxjs"; - -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"; -import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; -import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider"; -import { UserEncryptor } from "@bitwarden/common/tools/cryptography/user-encryptor.abstraction"; -import { disabledSemanticLoggerProvider } from "@bitwarden/common/tools/log"; -import { StateConstraints } from "@bitwarden/common/tools/types"; -import { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid"; - -import { - FakeStateProvider, - FakeAccountService, - awaitAsync, - ObservableTracker, -} from "../../../../../common/spec"; -import { Randomizer } from "../abstractions"; -import { Generators } from "../data"; -import { - CredentialGeneratorConfiguration, - GeneratedCredential, - GenerateRequest, - GeneratorConstraints, -} from "../types"; - -import { CredentialGeneratorService } from "./credential-generator.service"; - -// arbitrary settings types -type SomeSettings = { foo: string }; -type SomePolicy = { fooPolicy: boolean }; - -// settings storage location -const SettingsKey = new UserKeyDefinition(GENERATOR_DISK, "SomeSettings", { - deserializer: (value) => value, - clearOn: [], -}); - -// fake policies -const policyService = mock(); -const somePolicy = new Policy({ - data: { fooPolicy: true }, - type: PolicyType.PasswordGenerator, - id: "" as PolicyId, - organizationId: "" as OrganizationId, - enabled: true, -}); -const passwordOverridePolicy = new Policy({ - id: "" as PolicyId, - organizationId: "", - type: PolicyType.PasswordGenerator, - data: { - overridePasswordType: "password", - }, - enabled: true, -}); - -const passphraseOverridePolicy = new Policy({ - id: "" as PolicyId, - organizationId: "", - type: PolicyType.PasswordGenerator, - data: { - overridePasswordType: "passphrase", - }, - enabled: true, -}); - -const SomeTime = new Date(1); -const SomeAlgorithm = "passphrase"; -const SomeCategory = "password"; -const SomeNameKey = "passphraseKey"; -const SomeGenerateKey = "generateKey"; -const SomeCredentialTypeKey = "credentialTypeKey"; -const SomeOnGeneratedMessageKey = "onGeneratedMessageKey"; -const SomeCopyKey = "copyKey"; -const SomeUseGeneratedValueKey = "useGeneratedValueKey"; - -// fake the configuration -const SomeConfiguration: CredentialGeneratorConfiguration = { - id: SomeAlgorithm, - category: SomeCategory, - nameKey: SomeNameKey, - generateKey: SomeGenerateKey, - onGeneratedMessageKey: SomeOnGeneratedMessageKey, - credentialTypeKey: SomeCredentialTypeKey, - copyKey: SomeCopyKey, - useGeneratedValueKey: SomeUseGeneratedValueKey, - onlyOnRequest: false, - request: [], - engine: { - create: (_randomizer) => { - return { - generate: (request, settings) => { - const result = new GeneratedCredential( - settings.foo, - SomeAlgorithm, - SomeTime, - request.source, - request.website, - ); - return Promise.resolve(result); - }, - }; - }, - }, - settings: { - initial: { foo: "initial" }, - constraints: { foo: {} }, - account: SettingsKey, - }, - policy: { - type: PolicyType.PasswordGenerator, - disabledValue: { - fooPolicy: false, - }, - combine: (acc, policy) => { - return { fooPolicy: acc.fooPolicy || policy.data.fooPolicy }; - }, - createEvaluator: () => { - throw new Error("this should never be called"); - }, - toConstraints: (policy) => { - if (policy.fooPolicy) { - return { - constraints: { - policyInEffect: true, - }, - calibrate(state: SomeSettings) { - return { - constraints: {}, - adjust(state: SomeSettings) { - return { foo: `adjusted(${state.foo})` }; - }, - fix(state: SomeSettings) { - return { foo: `fixed(${state.foo})` }; - }, - } satisfies StateConstraints; - }, - } satisfies GeneratorConstraints; - } else { - return { - constraints: { - policyInEffect: false, - }, - adjust(state: SomeSettings) { - return state; - }, - fix(state: SomeSettings) { - return state; - }, - } satisfies GeneratorConstraints; - } - }, - }, -}; - -// fake user information -const SomeUser = "SomeUser" as UserId; -const AnotherUser = "SomeOtherUser" as UserId; -const accounts = { - [SomeUser]: { - id: SomeUser, - name: "some user", - email: "some.user@example.com", - emailVerified: true, - }, - [AnotherUser]: { - id: AnotherUser, - name: "some other user", - email: "some.other.user@example.com", - emailVerified: true, - }, -}; -const accountService = new FakeAccountService(accounts); - -// fake state -const stateProvider = new FakeStateProvider(accountService); - -// fake randomizer -const randomizer = mock(); - -const i18nService = mock(); - -const apiService = mock(); - -const encryptor = mock(); -const encryptorProvider = mock({ - userEncryptor$(_, dependencies) { - return dependencies.singleUserId$.pipe(map((userId) => ({ userId, encryptor }))); - }, -}); - -const account$ = new BehaviorSubject(accounts[SomeUser]); - -const providers = { - encryptor: encryptorProvider, - state: stateProvider, - log: disabledSemanticLoggerProvider, -}; - -describe("CredentialGeneratorService", () => { - beforeEach(async () => { - await accountService.switchAccount(SomeUser); - policyService.policiesByType$.mockImplementation(() => new BehaviorSubject([]).asObservable()); - i18nService.t.mockImplementation((key: string) => key); - apiService.fetch.mockImplementation(() => Promise.resolve(mock())); - jest.clearAllMocks(); - }); - - describe("generate$", () => { - it("completes when `on$` completes", async () => { - await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const on$ = new Subject(); - let complete = false; - - // confirm no emission during subscription - generator.generate$(SomeConfiguration, { on$, account$ }).subscribe({ - complete: () => { - complete = true; - }, - }); - on$.complete(); - await awaitAsync(); - - expect(complete).toBeTruthy(); - }); - - it("includes request.source in the generated credential", async () => { - const settings = { foo: "value" }; - await stateProvider.setUserState(SettingsKey, settings, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const on$ = new BehaviorSubject({ source: "some source" }); - const generated = new ObservableTracker( - generator.generate$(SomeConfiguration, { on$, account$ }), - ); - - const result = await generated.expectEmission(); - - expect(result.source).toEqual("some source"); - }); - - it("includes request.website in the generated credential", async () => { - const settings = { foo: "value" }; - await stateProvider.setUserState(SettingsKey, settings, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const on$ = new BehaviorSubject({ website: "some website" }); - const generated = new ObservableTracker( - generator.generate$(SomeConfiguration, { on$, account$ }), - ); - - const result = await generated.expectEmission(); - - expect(result.website).toEqual("some website"); - }); - - // FIXME: test these when the fake state provider can create the required emissions - it.todo("errors when the settings error"); - it.todo("completes when the settings complete"); - - it("emits a generation for a specific user when `user$` supplied", async () => { - await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); - await stateProvider.setUserState(SettingsKey, { foo: "another" }, AnotherUser); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const account$ = new BehaviorSubject(accounts[AnotherUser]).asObservable(); - const on$ = new Subject(); - const generated = new ObservableTracker( - generator.generate$(SomeConfiguration, { on$, account$ }), - ); - on$.next({}); - - const result = await generated.expectEmission(); - - expect(result).toEqual(new GeneratedCredential("another", SomeAlgorithm, SomeTime)); - }); - - it("errors when `user$` errors", async () => { - await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const on$ = new Subject(); - const account$ = new BehaviorSubject(accounts[SomeUser]); - let error = null; - - generator.generate$(SomeConfiguration, { on$, account$ }).subscribe({ - error: (e: unknown) => { - error = e; - }, - }); - account$.error({ some: "error" }); - await awaitAsync(); - - expect(error).toEqual({ some: "error" }); - }); - - it("completes when `user$` completes", async () => { - await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const on$ = new Subject(); - const account$ = new BehaviorSubject(accounts[SomeUser]); - let completed = false; - - generator.generate$(SomeConfiguration, { on$, account$ }).subscribe({ - complete: () => { - completed = true; - }, - }); - account$.complete(); - await awaitAsync(); - - expect(completed).toBeTruthy(); - }); - - it("emits a generation only when `on$` emits", async () => { - // This test breaks from arrange/act/assert because it is testing causality - await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const on$ = new Subject(); - const results: any[] = []; - - // confirm no emission during subscription - const sub = generator - .generate$(SomeConfiguration, { on$, account$ }) - .subscribe((result) => results.push(result)); - await awaitAsync(); - expect(results.length).toEqual(0); - - // confirm forwarded emission - on$.next({}); - await awaitAsync(); - expect(results).toEqual([new GeneratedCredential("value", SomeAlgorithm, SomeTime)]); - - // confirm updating settings does not cause an emission - await stateProvider.setUserState(SettingsKey, { foo: "next" }, SomeUser); - await awaitAsync(); - expect(results.length).toBe(1); - - // confirm forwarded emission takes latest value - on$.next({}); - await awaitAsync(); - sub.unsubscribe(); - - expect(results).toEqual([ - new GeneratedCredential("value", SomeAlgorithm, SomeTime), - new GeneratedCredential("next", SomeAlgorithm, SomeTime), - ]); - }); - - it("errors when `on$` errors", async () => { - await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const on$ = new Subject(); - let error: any = null; - - // confirm no emission during subscription - generator.generate$(SomeConfiguration, { on$, account$ }).subscribe({ - error: (e: unknown) => { - error = e; - }, - }); - on$.error({ some: "error" }); - await awaitAsync(); - - expect(error).toEqual({ some: "error" }); - }); - - // FIXME: test these when the fake state provider can delay its first emission - it.todo("emits when settings$ become available if on$ is called before they're ready."); - }); - - describe("algorithms", () => { - it("outputs password generation metadata", () => { - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - - const result = generator.algorithms("password"); - - expect(result.some((a) => a.id === Generators.password.id)).toBeTruthy(); - expect(result.some((a) => a.id === Generators.passphrase.id)).toBeTruthy(); - - // this test shouldn't contain entries outside of the current category - expect(result.some((a) => a.id === Generators.username.id)).toBeFalsy(); - expect(result.some((a) => a.id === Generators.catchall.id)).toBeFalsy(); - }); - - it("outputs username generation metadata", () => { - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - - const result = generator.algorithms("username"); - - expect(result.some((a) => a.id === Generators.username.id)).toBeTruthy(); - - // this test shouldn't contain entries outside of the current category - expect(result.some((a) => a.id === Generators.catchall.id)).toBeFalsy(); - expect(result.some((a) => a.id === Generators.password.id)).toBeFalsy(); - }); - - it("outputs email generation metadata", () => { - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - - const result = generator.algorithms("email"); - - expect(result.some((a) => a.id === Generators.catchall.id)).toBeTruthy(); - expect(result.some((a) => a.id === Generators.subaddress.id)).toBeTruthy(); - - // this test shouldn't contain entries outside of the current category - expect(result.some((a) => a.id === Generators.username.id)).toBeFalsy(); - expect(result.some((a) => a.id === Generators.password.id)).toBeFalsy(); - }); - - it("combines metadata across categories", () => { - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - - const result = generator.algorithms(["username", "email"]); - - expect(result.some((a) => a.id === Generators.username.id)).toBeTruthy(); - expect(result.some((a) => a.id === Generators.catchall.id)).toBeTruthy(); - expect(result.some((a) => a.id === Generators.subaddress.id)).toBeTruthy(); - - // this test shouldn't contain entries outside of the current categories - expect(result.some((a) => a.id === Generators.password.id)).toBeFalsy(); - }); - }); - - describe("algorithms$", () => { - // these tests cannot use the observable tracker because they return - // data that cannot be cloned - it("returns password metadata", async () => { - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - - const result = await firstValueFrom(generator.algorithms$("password", { account$ })); - - expect(result.some((a) => a.id === Generators.password.id)).toBeTruthy(); - expect(result.some((a) => a.id === Generators.passphrase.id)).toBeTruthy(); - }); - - it("returns username metadata", async () => { - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - - const result = await firstValueFrom(generator.algorithms$("username", { account$ })); - - expect(result.some((a) => a.id === Generators.username.id)).toBeTruthy(); - }); - - it("returns email metadata", async () => { - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - - const result = await firstValueFrom(generator.algorithms$("email", { account$ })); - - expect(result.some((a) => a.id === Generators.catchall.id)).toBeTruthy(); - expect(result.some((a) => a.id === Generators.subaddress.id)).toBeTruthy(); - }); - - it("returns username and email metadata", async () => { - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - - const result = await firstValueFrom( - generator.algorithms$(["username", "email"], { account$ }), - ); - - expect(result.some((a) => a.id === Generators.username.id)).toBeTruthy(); - expect(result.some((a) => a.id === Generators.catchall.id)).toBeTruthy(); - expect(result.some((a) => a.id === Generators.subaddress.id)).toBeTruthy(); - }); - - // Subsequent tests focus on passwords and passphrases as an example of policy - // awareness; they exercise the logic without being comprehensive - it("enforces the active user's policy", async () => { - const policy$ = new BehaviorSubject([passwordOverridePolicy]); - policyService.policiesByType$.mockReturnValue(policy$); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - - const result = await firstValueFrom(generator.algorithms$(["password"], { account$ })); - - expect(policyService.policiesByType$).toHaveBeenCalledWith( - PolicyType.PasswordGenerator, - SomeUser, - ); - expect(result.some((a) => a.id === Generators.password.id)).toBeTruthy(); - expect(result.some((a) => a.id === Generators.passphrase.id)).toBeFalsy(); - }); - - it("follows changes to the active user", async () => { - const account$ = new BehaviorSubject(accounts[SomeUser]); - policyService.policiesByType$.mockReturnValueOnce( - new BehaviorSubject([passwordOverridePolicy]), - ); - policyService.policiesByType$.mockReturnValueOnce( - new BehaviorSubject([passphraseOverridePolicy]), - ); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const results: any = []; - const sub = generator.algorithms$("password", { account$ }).subscribe((r) => results.push(r)); - - account$.next(accounts[AnotherUser]); - await awaitAsync(); - sub.unsubscribe(); - - const [someResult, anotherResult] = results; - - expect(policyService.policiesByType$).toHaveBeenNthCalledWith( - 1, - PolicyType.PasswordGenerator, - SomeUser, - ); - expect(someResult.some((a: any) => a.id === Generators.password.id)).toBeTruthy(); - expect(someResult.some((a: any) => a.id === Generators.passphrase.id)).toBeFalsy(); - - expect(policyService.policiesByType$).toHaveBeenNthCalledWith( - 2, - PolicyType.PasswordGenerator, - AnotherUser, - ); - expect(anotherResult.some((a: any) => a.id === Generators.passphrase.id)).toBeTruthy(); - expect(anotherResult.some((a: any) => a.id === Generators.password.id)).toBeFalsy(); - }); - - it("reads an arbitrary user's settings", async () => { - policyService.policiesByType$.mockReturnValueOnce( - new BehaviorSubject([passwordOverridePolicy]), - ); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const account$ = new BehaviorSubject(accounts[AnotherUser]).asObservable(); - - const result = await firstValueFrom(generator.algorithms$("password", { account$ })); - - expect(policyService.policiesByType$).toHaveBeenCalledWith( - PolicyType.PasswordGenerator, - AnotherUser, - ); - expect(result.some((a: any) => a.id === Generators.password.id)).toBeTruthy(); - expect(result.some((a: any) => a.id === Generators.passphrase.id)).toBeFalsy(); - }); - - it("follows changes to the arbitrary user", async () => { - policyService.policiesByType$.mockReturnValueOnce( - new BehaviorSubject([passwordOverridePolicy]), - ); - policyService.policiesByType$.mockReturnValueOnce( - new BehaviorSubject([passphraseOverridePolicy]), - ); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const account = new BehaviorSubject(accounts[SomeUser]); - const account$ = account.asObservable(); - const results: any = []; - const sub = generator.algorithms$("password", { account$ }).subscribe((r) => results.push(r)); - - account.next(accounts[AnotherUser]); - await awaitAsync(); - sub.unsubscribe(); - - const [someResult, anotherResult] = results; - expect(policyService.policiesByType$).toHaveBeenCalledWith( - PolicyType.PasswordGenerator, - SomeUser, - ); - expect(someResult.some((a: any) => a.id === Generators.password.id)).toBeTruthy(); - expect(someResult.some((a: any) => a.id === Generators.passphrase.id)).toBeFalsy(); - - expect(policyService.policiesByType$).toHaveBeenCalledWith( - PolicyType.PasswordGenerator, - AnotherUser, - ); - expect(anotherResult.some((a: any) => a.id === Generators.passphrase.id)).toBeTruthy(); - expect(anotherResult.some((a: any) => a.id === Generators.password.id)).toBeFalsy(); - }); - - it("errors when the arbitrary user's stream errors", async () => { - policyService.policiesByType$.mockReturnValueOnce( - new BehaviorSubject([passwordOverridePolicy]), - ); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const account = new BehaviorSubject(accounts[SomeUser]); - const account$ = account.asObservable(); - let error = null; - - generator.algorithms$("password", { account$ }).subscribe({ - error: (e: unknown) => { - error = e; - }, - }); - account.error({ some: "error" }); - await awaitAsync(); - - expect(error).toEqual({ some: "error" }); - }); - - it("completes when the arbitrary user's stream completes", async () => { - policyService.policiesByType$.mockReturnValueOnce( - new BehaviorSubject([passwordOverridePolicy]), - ); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const account = new BehaviorSubject(accounts[SomeUser]); - const account$ = account.asObservable(); - let completed = false; - - generator.algorithms$("password", { account$ }).subscribe({ - complete: () => { - completed = true; - }, - }); - account.complete(); - await awaitAsync(); - - expect(completed).toBeTruthy(); - }); - - it("ignores repeated arbitrary user emissions", async () => { - policyService.policiesByType$.mockReturnValueOnce( - new BehaviorSubject([passwordOverridePolicy]), - ); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const account = new BehaviorSubject(accounts[SomeUser]); - const account$ = account.asObservable(); - let count = 0; - - const sub = generator.algorithms$("password", { account$ }).subscribe({ - next: () => { - count++; - }, - }); - await awaitAsync(); - account.next(accounts[SomeUser]); - await awaitAsync(); - account.next(accounts[SomeUser]); - await awaitAsync(); - sub.unsubscribe(); - - expect(count).toEqual(1); - }); - }); - - describe("settings$", () => { - it("defaults to the configuration's initial settings if settings aren't found", async () => { - await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - - const result = await firstValueFrom(generator.settings$(SomeConfiguration, { account$ })); - - expect(result).toEqual(SomeConfiguration.settings.initial); - }); - - it("reads from the active user's configuration-defined storage", async () => { - const settings = { foo: "value" }; - await stateProvider.setUserState(SettingsKey, settings, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - - const result = await firstValueFrom(generator.settings$(SomeConfiguration, { account$ })); - - expect(result).toEqual(settings); - }); - - it("applies policy to the loaded settings", async () => { - const settings = { foo: "value" }; - await stateProvider.setUserState(SettingsKey, settings, SomeUser); - const policy$ = new BehaviorSubject([somePolicy]); - policyService.policiesByType$.mockReturnValue(policy$); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - - const result = await firstValueFrom(generator.settings$(SomeConfiguration, { account$ })); - - expect(result).toEqual({ foo: "adjusted(value)" }); - }); - - it("reads an arbitrary user's settings", async () => { - await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); - const anotherSettings = { foo: "another" }; - await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const account$ = new BehaviorSubject(accounts[AnotherUser]).asObservable(); - - const result = await firstValueFrom(generator.settings$(SomeConfiguration, { account$ })); - - expect(result).toEqual(anotherSettings); - }); - - it("errors when the arbitrary user's stream errors", async () => { - await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const account = new BehaviorSubject(accounts[SomeUser]); - const account$ = account.asObservable(); - let error = null; - - generator.settings$(SomeConfiguration, { account$ }).subscribe({ - error: (e: unknown) => { - error = e; - }, - }); - account.error({ some: "error" }); - await awaitAsync(); - - expect(error).toEqual({ some: "error" }); - }); - - it("completes when the arbitrary user's stream completes", async () => { - await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const account = new BehaviorSubject(accounts[SomeUser]); - const account$ = account.asObservable(); - let completed = false; - - generator.settings$(SomeConfiguration, { account$ }).subscribe({ - complete: () => { - completed = true; - }, - }); - account.complete(); - await awaitAsync(); - - expect(completed).toBeTruthy(); - }); - }); - - describe("settings", () => { - it("writes to the user's state", async () => { - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const subject = generator.settings(SomeConfiguration, { account$ }); - - subject.next({ foo: "next value" }); - await awaitAsync(); - const result = await firstValueFrom(stateProvider.getUserState$(SettingsKey, SomeUser)); - - expect(result).toEqual({ - foo: "next value", - }); - }); - }); - - describe("policy$", () => { - it("creates constraints without policy in effect when there is no policy", async () => { - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const account$ = new BehaviorSubject(accounts[SomeUser]).asObservable(); - - const result = await firstValueFrom(generator.policy$(SomeConfiguration, { account$ })); - - expect(result.constraints.policyInEffect).toBeFalsy(); - }); - - it("creates constraints with policy in effect when there is a policy", async () => { - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const account$ = new BehaviorSubject(accounts[SomeUser]).asObservable(); - const policy$ = new BehaviorSubject([somePolicy]); - policyService.policiesByType$.mockReturnValue(policy$); - - const result = await firstValueFrom(generator.policy$(SomeConfiguration, { account$ })); - - expect(result.constraints.policyInEffect).toBeTruthy(); - }); - - it("follows policy emissions", async () => { - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const account = new BehaviorSubject(accounts[SomeUser]); - const account$ = account.asObservable(); - const somePolicySubject = new BehaviorSubject([somePolicy]); - policyService.policiesByType$.mockReturnValueOnce(somePolicySubject.asObservable()); - const emissions: GeneratorConstraints[] = []; - const sub = generator - .policy$(SomeConfiguration, { account$ }) - .subscribe((policy) => emissions.push(policy)); - - // swap the active policy for an inactive policy - somePolicySubject.next([]); - await awaitAsync(); - sub.unsubscribe(); - const [someResult, anotherResult] = emissions; - - expect(someResult.constraints.policyInEffect).toBeTruthy(); - expect(anotherResult.constraints.policyInEffect).toBeFalsy(); - }); - - it("follows user emissions", async () => { - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const account = new BehaviorSubject(accounts[SomeUser]); - const account$ = account.asObservable(); - const somePolicy$ = new BehaviorSubject([somePolicy]).asObservable(); - const anotherPolicy$ = new BehaviorSubject([]).asObservable(); - policyService.policiesByType$ - .mockReturnValueOnce(somePolicy$) - .mockReturnValueOnce(anotherPolicy$); - const emissions: GeneratorConstraints[] = []; - const sub = generator - .policy$(SomeConfiguration, { account$ }) - .subscribe((policy) => emissions.push(policy)); - - // swapping the user invokes the return for `anotherPolicy$` - account.next(accounts[AnotherUser]); - await awaitAsync(); - sub.unsubscribe(); - const [someResult, anotherResult] = emissions; - - expect(someResult.constraints.policyInEffect).toBeTruthy(); - expect(anotherResult.constraints.policyInEffect).toBeFalsy(); - }); - - it("errors when the user errors", async () => { - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const account = new BehaviorSubject(accounts[SomeUser]); - const account$ = account.asObservable(); - const expectedError = { some: "error" }; - - let actualError: any = null; - generator.policy$(SomeConfiguration, { account$ }).subscribe({ - error: (e: unknown) => { - actualError = e; - }, - }); - account.error(expectedError); - await awaitAsync(); - - expect(actualError).toEqual(expectedError); - }); - - it("completes when the user completes", async () => { - const generator = new CredentialGeneratorService( - randomizer, - policyService, - apiService, - i18nService, - providers, - ); - const account = new BehaviorSubject(accounts[SomeUser]); - const account$ = account.asObservable(); - - let completed = false; - generator.policy$(SomeConfiguration, { account$ }).subscribe({ - complete: () => { - completed = true; - }, - }); - account.complete(); - await awaitAsync(); - - expect(completed).toBeTruthy(); - }); - }); -}); diff --git a/libs/tools/generator/core/src/services/credential-generator.service.ts b/libs/tools/generator/core/src/services/credential-generator.service.ts deleted file mode 100644 index eacc2ca6fc5..00000000000 --- a/libs/tools/generator/core/src/services/credential-generator.service.ts +++ /dev/null @@ -1,296 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { concatMap, distinctUntilChanged, map, Observable, switchMap, takeUntil } from "rxjs"; -import { Simplify } from "type-fest"; - -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"; -import { Account } from "@bitwarden/common/auth/abstractions/account.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { BoundDependency, OnDependency } from "@bitwarden/common/tools/dependencies"; -import { IntegrationMetadata } from "@bitwarden/common/tools/integration"; -import { RestClient } from "@bitwarden/common/tools/integration/rpc"; -import { anyComplete, withLatestReady } from "@bitwarden/common/tools/rx"; -import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject"; -import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider"; - -import { Randomizer } from "../abstractions"; -import { - Generators, - getForwarderConfiguration, - Integrations, - toCredentialGeneratorConfiguration, -} from "../data"; -import { availableAlgorithms } from "../policies/available-algorithms-policy"; -import { mapPolicyToConstraints } from "../rx"; -import { - CredentialAlgorithm, - CredentialCategories, - CredentialCategory, - AlgorithmInfo, - CredentialPreference, - isForwarderIntegration, - ForwarderIntegration, - GenerateRequest, -} from "../types"; -import { - CredentialGeneratorConfiguration as Configuration, - CredentialGeneratorInfo, - GeneratorDependencyProvider, -} from "../types/credential-generator-configuration"; -import { GeneratorConstraints } from "../types/generator-constraints"; - -import { PREFERENCES } from "./credential-preferences"; - -type Generate$Dependencies = Simplify< - OnDependency & BoundDependency<"account", Account> ->; - -export class CredentialGeneratorService { - constructor( - private readonly randomizer: Randomizer, - private readonly policyService: PolicyService, - private readonly apiService: ApiService, - private readonly i18nService: I18nService, - private readonly providers: UserStateSubjectDependencyProvider, - ) {} - - private getDependencyProvider(): GeneratorDependencyProvider { - return { - client: new RestClient(this.apiService, this.i18nService), - i18nService: this.i18nService, - randomizer: this.randomizer, - }; - } - - // FIXME: the rxjs methods of this service can be a lot more resilient if - // `Subjects` are introduced where sharing occurs - - /** Generates a stream of credentials - * @param configuration determines which generator's settings are loaded - * @param dependencies.on$ Required. A new credential is emitted when this emits. - */ - generate$( - configuration: Readonly>, - dependencies: Generate$Dependencies, - ) { - const engine = configuration.engine.create(this.getDependencyProvider()); - const settings$ = this.settings$(configuration, dependencies); - - // generation proper - const generate$ = dependencies.on$.pipe( - withLatestReady(settings$), - concatMap(([request, settings]) => engine.generate(request, settings)), - takeUntil(anyComplete([settings$])), - ); - - return generate$; - } - - /** Emits metadata concerning the provided generation algorithms - * @param category the category or categories of interest - * @param dependences.account$ algorithms are filtered to only - * those matching the provided account's policy. - * @returns An observable that emits algorithm metadata. - */ - algorithms$( - category: CredentialCategory, - dependencies: BoundDependency<"account", Account>, - ): Observable; - algorithms$( - category: CredentialCategory[], - dependencies: BoundDependency<"account", Account>, - ): Observable; - algorithms$( - category: CredentialCategory | CredentialCategory[], - dependencies: BoundDependency<"account", Account>, - ) { - // any cast required here because TypeScript fails to bind `category` - // to the union-typed overload of `algorithms`. - const algorithms = this.algorithms(category as any); - - // apply policy - const algorithms$ = dependencies.account$.pipe( - distinctUntilChanged(), - switchMap((account) => { - const policies$ = this.policyService - .policiesByType$(PolicyType.PasswordGenerator, account.id) - .pipe( - map((p) => new Set(availableAlgorithms(p))), - // complete policy emissions otherwise `switchMap` holds `algorithms$` open indefinitely - takeUntil(anyComplete(dependencies.account$)), - ); - return policies$; - }), - map((available) => { - const filtered = algorithms.filter( - (c) => isForwarderIntegration(c.id) || available.has(c.id), - ); - return filtered; - }), - ); - - return algorithms$; - } - - /** Lists metadata for the algorithms in a credential category - * @param category the category or categories of interest - * @returns A list containing the requested metadata. - */ - algorithms(category: CredentialCategory): AlgorithmInfo[]; - algorithms(category: CredentialCategory[]): AlgorithmInfo[]; - algorithms(category: CredentialCategory | CredentialCategory[]): AlgorithmInfo[] { - const categories: CredentialCategory[] = Array.isArray(category) ? category : [category]; - - const algorithms = categories - .flatMap((c) => CredentialCategories[c] as CredentialAlgorithm[]) - .map((id) => this.algorithm(id)) - .filter((info) => info !== null); - - const forwarders = Object.keys(Integrations) - .map((key: keyof typeof Integrations) => { - const forwarder: ForwarderIntegration = { forwarder: Integrations[key].id }; - return this.algorithm(forwarder); - }) - .filter((forwarder) => categories.includes(forwarder.category)); - - return algorithms.concat(forwarders); - } - - /** Look up the metadata for a specific generator algorithm - * @param id identifies the algorithm - * @returns the requested metadata, or `null` if the metadata wasn't found. - */ - algorithm(id: CredentialAlgorithm): AlgorithmInfo { - let generator: CredentialGeneratorInfo = null; - let integration: IntegrationMetadata = null; - - if (isForwarderIntegration(id)) { - const forwarderConfig = getForwarderConfiguration(id.forwarder); - integration = forwarderConfig; - - if (forwarderConfig) { - generator = toCredentialGeneratorConfiguration(forwarderConfig); - } - } else { - generator = Generators[id]; - } - - if (!generator) { - throw new Error(`Invalid credential algorithm: ${JSON.stringify(id)}`); - } - - const info: AlgorithmInfo = { - id: generator.id, - category: generator.category, - name: integration ? integration.name : this.i18nService.t(generator.nameKey), - generate: this.i18nService.t(generator.generateKey), - onGeneratedMessage: this.i18nService.t(generator.onGeneratedMessageKey), - credentialType: this.i18nService.t(generator.credentialTypeKey), - copy: this.i18nService.t(generator.copyKey), - useGeneratedValue: this.i18nService.t(generator.useGeneratedValueKey), - onlyOnRequest: generator.onlyOnRequest, - request: generator.request, - }; - - if (generator.descriptionKey) { - info.description = this.i18nService.t(generator.descriptionKey); - } - - return info; - } - - /** Get the settings for the provided configuration - * @param configuration determines which generator's settings are loaded - * @param dependencies.account$ identifies the account to which the settings are bound. - * @returns an observable that emits settings - * @remarks the observable enforces policies on the settings - */ - settings$( - configuration: Configuration, - dependencies: BoundDependency<"account", Account>, - ) { - const constraints$ = this.policy$(configuration, dependencies); - - const settings = new UserStateSubject(configuration.settings.account, this.providers, { - constraints$, - account$: dependencies.account$, - }); - - const settings$ = settings.pipe( - map((settings) => settings ?? structuredClone(configuration.settings.initial)), - ); - - return settings$; - } - - /** Get a subject bound to credential generator preferences. - * @param dependencies.account$ identifies the account to which the preferences are bound - * @returns a subject bound to the user's preferences - * @remarks Preferences determine which algorithms are used when generating a - * credential from a credential category (e.g. `PassX` or `Username`). Preferences - * should not be used to hold navigation history. Use @bitwarden/generator-navigation - * instead. - */ - preferences( - dependencies: BoundDependency<"account", Account>, - ): UserStateSubject { - // FIXME: enforce policy - const subject = new UserStateSubject(PREFERENCES, this.providers, dependencies); - - return subject; - } - - /** Get a subject bound to a specific user's settings - * @param configuration determines which generator's settings are loaded - * @param dependencies.account$ identifies the account to which the settings are bound - * @returns a subject bound to the requested user's generator settings - * @remarks the subject enforces policy for the settings - */ - settings( - configuration: Readonly>, - dependencies: BoundDependency<"account", Account>, - ) { - const constraints$ = this.policy$(configuration, dependencies); - - const subject = new UserStateSubject(configuration.settings.account, this.providers, { - constraints$, - account$: dependencies.account$, - }); - - return subject; - } - - /** Get the policy constraints for the provided configuration - * @param dependencies.account$ determines which user's policy is loaded - * @returns an observable that emits the policy once `dependencies.account$` - * and the policy become available. - */ - policy$( - configuration: Configuration, - dependencies: BoundDependency<"account", Account>, - ): Observable> { - const constraints$ = dependencies.account$.pipe( - map((account) => { - if (account.emailVerified) { - return { userId: account.id, email: account.email }; - } - - return { userId: account.id, email: null }; - }), - switchMap(({ userId, email }) => { - // complete policy emissions otherwise `switchMap` holds `policies$` open indefinitely - const policies$ = this.policyService - .policiesByType$(configuration.policy.type, userId) - .pipe( - mapPolicyToConstraints(configuration.policy, email), - takeUntil(anyComplete(dependencies.account$)), - ); - return policies$; - }), - ); - - return constraints$; - } -} diff --git a/libs/tools/generator/core/src/services/credential-preferences.spec.ts b/libs/tools/generator/core/src/services/credential-preferences.spec.ts deleted file mode 100644 index fc7c3e1bbc6..00000000000 --- a/libs/tools/generator/core/src/services/credential-preferences.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { DefaultCredentialPreferences } from "../data"; - -import { PREFERENCES } from "./credential-preferences"; - -describe("PREFERENCES", () => { - describe("deserializer", () => { - it.each([[null], [undefined]])("creates new preferences (= %p)", (value) => { - const result = PREFERENCES.deserializer(value); - - expect(result).toEqual(DefaultCredentialPreferences); - }); - - it("fills missing password preferences", () => { - const input = { ...DefaultCredentialPreferences }; - delete input.password; - - const result = PREFERENCES.deserializer(input as any); - - expect(result).toEqual(DefaultCredentialPreferences); - }); - - it("fills missing email preferences", () => { - const input = { ...DefaultCredentialPreferences }; - delete input.email; - - const result = PREFERENCES.deserializer(input as any); - - expect(result).toEqual(DefaultCredentialPreferences); - }); - - it("fills missing username preferences", () => { - const input = { ...DefaultCredentialPreferences }; - delete input.username; - - const result = PREFERENCES.deserializer(input as any); - - expect(result).toEqual(DefaultCredentialPreferences); - }); - - it("converts updated fields to Dates", () => { - const input = structuredClone(DefaultCredentialPreferences); - input.email.updated = "1970-01-01T00:00:00.100Z" as any; - input.password.updated = "1970-01-01T00:00:00.200Z" as any; - input.username.updated = "1970-01-01T00:00:00.300Z" as any; - - const result = PREFERENCES.deserializer(input as any); - - expect(result.email.updated).toEqual(new Date(100)); - expect(result.password.updated).toEqual(new Date(200)); - expect(result.username.updated).toEqual(new Date(300)); - }); - }); -}); diff --git a/libs/tools/generator/core/src/services/credential-preferences.ts b/libs/tools/generator/core/src/services/credential-preferences.ts deleted file mode 100644 index 3f6a6c1e1bd..00000000000 --- a/libs/tools/generator/core/src/services/credential-preferences.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; - -import { DefaultCredentialPreferences } from "../data"; -import { CredentialPreference } from "../types"; - -/** plaintext password generation options */ -export const PREFERENCES = new UserKeyDefinition( - GENERATOR_DISK, - "credentialPreferences", - { - deserializer: (value) => { - const result = (value as any) ?? {}; - - for (const key in DefaultCredentialPreferences) { - // bind `key` to `category` to transmute the type - const category: keyof typeof DefaultCredentialPreferences = key as any; - - const preference = result[category] ?? { ...DefaultCredentialPreferences[category] }; - if (typeof preference.updated === "string") { - preference.updated = new Date(preference.updated); - } - - result[category] = preference; - } - - return result; - }, - clearOn: ["logout"], - }, -); diff --git a/libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts b/libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts new file mode 100644 index 00000000000..81e7ae6ac63 --- /dev/null +++ b/libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts @@ -0,0 +1,356 @@ +import { BehaviorSubject, Subject, firstValueFrom, of } from "rxjs"; + +import { Account } from "@bitwarden/common/auth/abstractions/account.service"; +import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; +import { Site, VendorId } from "@bitwarden/common/tools/extension"; +import { Bitwarden } from "@bitwarden/common/tools/extension/vendor/bitwarden"; +import { Vendor } from "@bitwarden/common/tools/extension/vendor/data"; +import { SemanticLogger, ifEnabledSemanticLoggerProvider } from "@bitwarden/common/tools/log"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { awaitAsync } from "../../../../../common/spec"; +import { + Algorithm, + CredentialAlgorithm, + CredentialType, + ForwarderExtensionId, + GeneratorMetadata, + Profile, + Type, +} from "../metadata"; +import { CredentialGeneratorProviders } from "../providers"; +import { GenerateRequest, GeneratedCredential } from "../types"; + +import { DefaultCredentialGeneratorService } from "./default-credential-generator.service"; + +// Custom type for jest.fn() mocks to preserve their type +type JestMockFunction any> = jest.Mock, Parameters>; + +// two-level partial that preserves jest.fn() mock types +type MockTwoLevelPartial = { + [K in keyof T]?: T[K] extends object + ? { + [P in keyof T[K]]?: T[K][P] extends (...args: any) => any + ? JestMockFunction + : T[K][P]; + } + : T[K]; +}; + +describe("DefaultCredentialGeneratorService", () => { + let service: DefaultCredentialGeneratorService; + let providers: MockTwoLevelPartial; + let system: any; + let log: SemanticLogger; + let mockExtension: { settings: jest.Mock }; + let account: Account; + let createService: (overrides?: any) => DefaultCredentialGeneratorService; + + beforeEach(() => { + log = ifEnabledSemanticLoggerProvider(false, new ConsoleLogService(true), { + from: "DefaultCredentialGeneratorService tests", + }); + + mockExtension = { settings: jest.fn() }; + + // Use a hard-coded value for mockAccount + account = { + id: "test-account-id" as UserId, + emailVerified: true, + email: "test@example.com", + name: "Test User", + }; + + system = { + log: jest.fn().mockReturnValue(log), + extension: mockExtension, + }; + + providers = { + metadata: { + metadata: jest.fn(), + preference$: jest.fn(), + algorithms$: jest.fn(), + algorithms: jest.fn(), + preferences: jest.fn(), + }, + profile: { + settings: jest.fn(), + constraints$: jest.fn(), + }, + generator: {}, + }; + + // Creating the service instance with a cast to the expected type + createService = (overrides = {}) => { + // Force cast the incomplete providers to the required type + // similar to how the overrides are applied + const providersCast = providers as unknown as CredentialGeneratorProviders; + + const instance = new DefaultCredentialGeneratorService(providersCast, system); + Object.assign(instance, overrides); + return instance; + }; + + service = createService(); + }); + + describe("generate$", () => { + it("should generate credentials when provided a specific algorithm", async () => { + const mockEngine = { + generate: jest + .fn() + .mockReturnValue( + of( + new GeneratedCredential("generatedPassword", Type.password, Date.now(), "unit test"), + ), + ), + }; + const mockMetadata = { + id: Algorithm.password, + engine: { create: jest.fn().mockReturnValue(mockEngine) }, + } as unknown as GeneratorMetadata; + const mockSettings = new BehaviorSubject({ length: 12 }); + providers.metadata!.metadata = jest.fn().mockReturnValue(mockMetadata); + service = createService({ + settings: () => mockSettings as any, + }); + const on$ = new Subject(); + const account$ = new BehaviorSubject(account); + const result$ = new BehaviorSubject(null); + + service.generate$({ on$, account$ }).subscribe(result$); + on$.next({ algorithm: Algorithm.password }); + await awaitAsync(); + + expect(result$.value?.credential).toEqual("generatedPassword"); + expect(providers.metadata!.metadata).toHaveBeenCalledWith(Algorithm.password); + expect(mockMetadata.engine.create).toHaveBeenCalled(); + expect(mockEngine.generate).toHaveBeenCalled(); + }); + + it("should determine preferred algorithm from credential type and generate credentials", async () => { + const mockEngine = { + generate: jest + .fn() + .mockReturnValue( + of(new GeneratedCredential("generatedPassword", "password", Date.now(), "unit test")), + ), + }; + const mockMetadata = { + id: "testAlgorithm", + engine: { create: jest.fn().mockReturnValue(mockEngine) }, + } as unknown as GeneratorMetadata; + const mockSettings = new BehaviorSubject({ length: 12 }); + + providers.metadata!.preference$ = jest + .fn() + .mockReturnValue(of("testAlgorithm" as CredentialAlgorithm)); + providers.metadata!.metadata = jest.fn().mockReturnValue(mockMetadata); + service = createService({ + settings: () => mockSettings as any, + }); + + const on$ = new Subject(); + const account$ = new BehaviorSubject(account); + const result$ = new BehaviorSubject(null); + + service.generate$({ on$, account$ }).subscribe(result$); + on$.next({ type: Type.password }); + await awaitAsync(); + + expect(result$.value?.credential).toBe("generatedPassword"); + expect(result$.value?.category).toBe(Type.password); + expect(providers.metadata!.metadata).toHaveBeenCalledWith("testAlgorithm"); + }); + }); + + describe("algorithms$", () => { + it("should retrieve and map available algorithms for a credential type", async () => { + const mockAlgorithms = [Algorithm.password, Algorithm.passphrase] as CredentialAlgorithm[]; + const mockMetadata1 = { id: Algorithm.password } as GeneratorMetadata; + const mockMetadata2 = { id: Algorithm.passphrase } as GeneratorMetadata; + + providers.metadata!.algorithms$ = jest.fn().mockReturnValue(of(mockAlgorithms)); + providers.metadata!.metadata = jest + .fn() + .mockReturnValueOnce(mockMetadata1) + .mockReturnValueOnce(mockMetadata2); + + const result = await firstValueFrom( + service.algorithms$("password" as CredentialType, { account$: of(account) }), + ); + + expect(result).toEqual([mockMetadata1, mockMetadata2]); + }); + }); + + describe("algorithms", () => { + it("should list algorithm metadata for a single credential type", () => { + providers.metadata!.algorithms = jest + .fn() + .mockReturnValue([Algorithm.password, Algorithm.passphrase] as CredentialAlgorithm[]); + service = createService({ + algorithm: (id: CredentialAlgorithm) => ({ id }) as GeneratorMetadata, + }); + + const result = service.algorithms("password" as CredentialType); + + expect(result).toEqual([{ id: Algorithm.password }, { id: Algorithm.passphrase }]); + expect(providers.metadata!.algorithms).toHaveBeenCalledWith({ type: "password" }); + }); + + it("should list combined algorithm metadata for multiple credential types", () => { + providers.metadata!.algorithms = jest + .fn() + .mockReturnValueOnce([Algorithm.password] as CredentialAlgorithm[]) + .mockReturnValueOnce([Algorithm.username] as CredentialAlgorithm[]); + + service = createService({ + algorithm: (id: CredentialAlgorithm) => ({ id }) as GeneratorMetadata, + }); + + const result = service.algorithms(["password", "username"] as CredentialType[]); + + expect(result).toEqual([{ id: Algorithm.password }, { id: Algorithm.username }]); + expect(providers.metadata!.algorithms).toHaveBeenCalledWith({ type: "password" }); + expect(providers.metadata!.algorithms).toHaveBeenCalledWith({ type: "username" }); + }); + }); + + describe("algorithm", () => { + it("should retrieve metadata for a specific generator algorithm", () => { + const mockMetadata = { id: Algorithm.password } as GeneratorMetadata; + providers.metadata!.metadata = jest.fn().mockReturnValue(mockMetadata); + + const result = service.algorithm(Algorithm.password); + + expect(result).toBe(mockMetadata); + expect(providers.metadata!.metadata).toHaveBeenCalledWith(Algorithm.password); + }); + + it("should log a panic when algorithm ID is invalid", () => { + providers.metadata!.metadata = jest.fn().mockReturnValue(null); + + expect(() => service.algorithm("invalidAlgo" as CredentialAlgorithm)).toThrow( + "invalid credential algorithm", + ); + }); + }); + + describe("forwarder", () => { + it("should retrieve forwarder metadata for a specific vendor", () => { + const vendorId = Vendor.bitwarden; + const forwarderExtensionId: ForwarderExtensionId = { forwarder: vendorId }; + const mockMetadata = { + id: forwarderExtensionId, + type: "email" as CredentialType, + } as GeneratorMetadata; + + providers.metadata!.metadata = jest.fn().mockReturnValue(mockMetadata); + + const result = service.forwarder(vendorId); + + expect(result).toBe(mockMetadata); + expect(providers.metadata!.metadata).toHaveBeenCalledWith(forwarderExtensionId); + }); + + it("should log a panic when vendor ID is invalid", () => { + const invalidVendorId = "invalid-vendor" as VendorId; + providers.metadata!.metadata = jest.fn().mockReturnValue(null); + + expect(() => service.forwarder(invalidVendorId)).toThrow("invalid vendor"); + }); + }); + + describe("preferences", () => { + it("should retrieve credential preferences bound to the user's account", () => { + const mockPreferences = { defaultType: "password" }; + providers.metadata!.preferences = jest.fn().mockReturnValue(mockPreferences); + + const result = service.preferences({ account$: of(account) }); + + expect(result).toBe(mockPreferences); + }); + }); + + describe("settings", () => { + it("should load user settings for account-bound profiles", () => { + const mockSettings = { value: { length: 12 } }; + const mockMetadata = { + id: "test", + profiles: { + [Profile.account]: { id: "accountProfile" }, + }, + } as unknown as GeneratorMetadata; + + providers.profile!.settings = jest.fn().mockReturnValue(mockSettings); + + const result = service.settings(mockMetadata, { account$: of(account) }); + + expect(result).toBe(mockSettings); + }); + + it("should load user settings for extension-bound profiles", () => { + const mockSettings = new BehaviorSubject({ value: { length: 12 } }); + const vendorId = Vendor.bitwarden; + const forwarderProfile = { + id: { forwarder: Bitwarden.id }, + site: Site.forwarder, + type: "extension", + }; + const mockMetadata = { + id: { forwarder: vendorId } as ForwarderExtensionId, + profiles: { + [Profile.account]: forwarderProfile, + }, + } as unknown as GeneratorMetadata; + + mockExtension.settings.mockReturnValue(mockSettings); + + const result = service.settings(mockMetadata, { account$: of(account) }); + + expect(result).toBe(mockSettings); + }); + + it("should log a panic when profile metadata is not found", () => { + const mockMetadata = { + id: "test", + profiles: {}, + } as unknown as GeneratorMetadata; + + expect(() => service.settings(mockMetadata, { account$: of(account) })).toThrow( + "failed to load settings; profile metadata not found", + ); + }); + }); + + describe("policy$", () => { + it("should retrieve policy constraints for a specific profile", async () => { + const mockConstraints = { minLength: 8 }; + const mockMetadata = { + id: "test", + profiles: { + [Profile.account]: { id: "accountProfile" }, + }, + } as unknown as GeneratorMetadata; + + providers.profile!.constraints$ = jest.fn().mockReturnValue(of(mockConstraints)); + + const result = await firstValueFrom(service.policy$(mockMetadata, { account$: of(account) })); + + expect(result).toEqual(mockConstraints); + }); + + it("should log a panic when profile metadata is not found for policy retrieval", () => { + const mockMetadata = { + id: "test", + profiles: {}, + } as unknown as GeneratorMetadata; + + expect(() => service.policy$(mockMetadata, { account$: of(account) })).toThrow( + "failed to load policy; profile metadata not found", + ); + }); + }); +}); diff --git a/libs/tools/generator/core/src/services/default-credential-generator.service.ts b/libs/tools/generator/core/src/services/default-credential-generator.service.ts new file mode 100644 index 00000000000..453139d284c --- /dev/null +++ b/libs/tools/generator/core/src/services/default-credential-generator.service.ts @@ -0,0 +1,219 @@ +import { + ReplaySubject, + concatMap, + filter, + first, + map, + of, + share, + shareReplay, + switchAll, + switchMap, + takeUntil, + tap, + timer, + zip, +} from "rxjs"; + +import { Account } from "@bitwarden/common/auth/abstractions/account.service"; +import { BoundDependency, OnDependency } from "@bitwarden/common/tools/dependencies"; +import { VendorId } from "@bitwarden/common/tools/extension"; +import { SemanticLogger } from "@bitwarden/common/tools/log"; +import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; +import { anyComplete, memoizedMap } from "@bitwarden/common/tools/rx"; +import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject"; + +import { CredentialGeneratorService } from "../abstractions"; +import { + CredentialAlgorithm, + Profile, + GeneratorMetadata, + GeneratorProfile, + isForwarderProfile, + toVendorId, + CredentialType, +} from "../metadata"; +import { CredentialGeneratorProviders } from "../providers"; +import { GenerateRequest } from "../types"; +import { isAlgorithmRequest, isTypeRequest } from "../types/metadata-request"; + +const ALGORITHM_CACHE_SIZE = 10; +const THREE_MINUTES = 3 * 60 * 1000; + +export class DefaultCredentialGeneratorService implements CredentialGeneratorService { + /** Instantiate the `DefaultCredentialGeneratorService`. + * @param provide application services required by the credential generator. + * @param system low-level services required by the credential generator. + */ + constructor( + private readonly provide: CredentialGeneratorProviders, + private readonly system: SystemServiceProvider, + ) { + this.log = system.log({ type: "DefaultCredentialGeneratorService" }); + } + + private readonly log: SemanticLogger; + + generate$(dependencies: OnDependency & BoundDependency<"account", Account>) { + const request$ = dependencies.on$.pipe(shareReplay({ refCount: true, bufferSize: 1 })); + const account$ = dependencies.account$.pipe(shareReplay({ refCount: true, bufferSize: 1 })); + + // load algorithm metadata + const metadata$ = request$.pipe( + switchMap((request) => { + if (isAlgorithmRequest(request)) { + return of(request.algorithm); + } else if (isTypeRequest(request)) { + return this.provide.metadata.preference$(request.type, { account$ }).pipe(first()); + } else { + this.log.panic(request, "algorithm or category required"); + } + }), + filter((algorithm): algorithm is CredentialAlgorithm => !!algorithm), + memoizedMap((algorithm) => this.provide.metadata.metadata(algorithm), { + size: ALGORITHM_CACHE_SIZE, + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + + // load the active profile's settings + const settings$ = zip(request$, metadata$).pipe( + map( + ([request, metadata]) => + [{ ...request, profile: request.profile ?? Profile.account }, metadata] as const, + ), + memoizedMap( + ([request, metadata]) => { + const [profile, algorithm] = [request.profile, metadata.id]; + + // settings$ stays hot and buffers the most recent value in the cache + // for the next `request` + const settings$ = this.settings(metadata, { account$ }, profile).pipe( + tap(() => this.log.debug({ algorithm, profile }, "settings update received")), + share({ + connector: () => new ReplaySubject(1, THREE_MINUTES), + resetOnRefCountZero: () => timer(THREE_MINUTES), + }), + tap({ + subscribe: () => this.log.debug({ algorithm, profile }, "settings hot"), + complete: () => this.log.debug({ algorithm, profile }, "settings cold"), + }), + first(), + ); + + this.log.debug({ algorithm, profile }, "settings cached"); + return settings$; + }, + { key: ([request, metadata]) => `${metadata.id}:${request.profile}` }, + ), + switchAll(), + ); + + // load the algorithm's engine + const engine$ = metadata$.pipe( + memoizedMap( + (metadata) => { + const engine = metadata.engine.create(this.provide.generator); + + this.log.debug({ algorithm: metadata.id }, "engine cached"); + return engine; + }, + { size: ALGORITHM_CACHE_SIZE }, + ), + ); + + // generation proper + const generate$ = zip([request$, settings$, engine$]).pipe( + tap(([request]) => this.log.debug(request, "generating credential")), + concatMap(([request, settings, engine]) => engine.generate(request, settings)), + takeUntil(anyComplete([settings$])), + ); + + return generate$; + } + + algorithms$(type: CredentialType, dependencies: BoundDependency<"account", Account>) { + return this.provide.metadata + .algorithms$({ type }, dependencies) + .pipe(map((algorithms) => algorithms.map((a) => this.algorithm(a)))); + } + + algorithms(type: CredentialType | CredentialType[]) { + const types: CredentialType[] = Array.isArray(type) ? type : [type]; + const algorithms = types + .flatMap((type) => this.provide.metadata.algorithms({ type })) + .map((algorithm) => this.algorithm(algorithm)); + return algorithms; + } + + algorithm(id: CredentialAlgorithm) { + const metadata = this.provide.metadata.metadata(id); + if (!metadata) { + this.log.panic({ algorithm: id }, "invalid credential algorithm"); + } + + return metadata; + } + + forwarder(id: VendorId) { + const metadata = this.provide.metadata.metadata({ forwarder: id }); + if (!metadata) { + this.log.panic({ algorithm: id }, "invalid vendor"); + } + + return metadata; + } + + preferences(dependencies: BoundDependency<"account", Account>) { + return this.provide.metadata.preferences(dependencies); + } + + settings( + metadata: Readonly>, + dependencies: BoundDependency<"account", Account>, + profile: GeneratorProfile = Profile.account, + ) { + const activeProfile = metadata.profiles[profile]; + if (!activeProfile) { + this.log.panic( + { algorithm: metadata.id, profile }, + "failed to load settings; profile metadata not found", + ); + } + + let settings: UserStateSubject; + if (isForwarderProfile(activeProfile)) { + const vendor = toVendorId(metadata.id); + if (!vendor) { + this.log.panic( + { algorithm: metadata.id, profile }, + "failed to load extension profile; vendor not specified", + ); + } + + this.log.info({ profile, vendor, site: activeProfile.site }, "loading extension profile"); + settings = this.system.extension.settings(activeProfile, vendor, dependencies); + } else { + this.log.info({ profile, algorithm: metadata.id }, "loading generator profile"); + settings = this.provide.profile.settings(activeProfile, dependencies); + } + + return settings; + } + + policy$( + metadata: Readonly>, + dependencies: BoundDependency<"account", Account>, + profile: GeneratorProfile = Profile.account, + ) { + const activeProfile = metadata.profiles[profile]; + if (!activeProfile) { + this.log.panic( + { algorithm: metadata.id, profile }, + "failed to load policy; profile metadata not found", + ); + } + + return this.provide.profile.constraints$(activeProfile, dependencies); + } +} diff --git a/libs/tools/generator/core/src/services/default-generator.service.spec.ts b/libs/tools/generator/core/src/services/default-generator.service.spec.ts index eb9642a9417..a0f9342fa28 100644 --- a/libs/tools/generator/core/src/services/default-generator.service.spec.ts +++ b/libs/tools/generator/core/src/services/default-generator.service.spec.ts @@ -18,7 +18,7 @@ import { DefaultGeneratorService } from "./default-generator.service"; function mockPolicyService(config?: { state?: BehaviorSubject }) { const service = mock(); - const stateValue = config?.state ?? new BehaviorSubject([null]); + const stateValue = config?.state ?? new BehaviorSubject([]); service.policiesByType$.mockReturnValue(stateValue); return service; @@ -119,22 +119,22 @@ describe("Password generator service", () => { it("should update the evaluator when the password generator policy changes", async () => { // set up dependencies - const state = new BehaviorSubject([null]); + const state = new BehaviorSubject([]); const policy = mockPolicyService({ state }); const strategy = mockGeneratorStrategy(); const service = new DefaultGeneratorService(strategy, policy); // model responses for the observable update. The map is called multiple times, // and the array shift ensures reference equality is maintained. - const firstEvaluator = mock>(); - const secondEvaluator = mock>(); + const firstEvaluator: PolicyEvaluator = mock>(); + const secondEvaluator: PolicyEvaluator = mock>(); const evaluators = [firstEvaluator, secondEvaluator]; - strategy.toEvaluator.mockReturnValueOnce(pipe(map(() => evaluators.shift()))); + strategy.toEvaluator.mockReturnValueOnce(pipe(map(() => evaluators.shift()!))); // act const evaluator$ = service.evaluator$(SomeUser); const firstResult = await firstValueFrom(evaluator$); - state.next([null]); + state.next([]); const secondResult = await firstValueFrom(evaluator$); // assert diff --git a/libs/tools/generator/core/src/services/index.ts b/libs/tools/generator/core/src/services/index.ts index d7184f684ae..57094e28ddd 100644 --- a/libs/tools/generator/core/src/services/index.ts +++ b/libs/tools/generator/core/src/services/index.ts @@ -1,2 +1,2 @@ export { DefaultGeneratorService } from "./default-generator.service"; -export { CredentialGeneratorService } from "./credential-generator.service"; +export { DefaultCredentialGeneratorService } from "./default-credential-generator.service"; diff --git a/libs/tools/generator/core/src/strategies/eff-username-generator-strategy.ts b/libs/tools/generator/core/src/strategies/eff-username-generator-strategy.ts index 83ed3b0d14e..ebeacef81e8 100644 --- a/libs/tools/generator/core/src/strategies/eff-username-generator-strategy.ts +++ b/libs/tools/generator/core/src/strategies/eff-username-generator-strategy.ts @@ -2,7 +2,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { StateProvider } from "@bitwarden/common/platform/state"; import { GeneratorStrategy } from "../abstractions"; -import { DefaultEffUsernameOptions, UsernameDigits } from "../data"; +import { DefaultEffUsernameOptions } from "../data"; import { UsernameRandomizer } from "../engine"; import { newDefaultEvaluator } from "../rx"; import { EffUsernameGenerationOptions, NoPolicy } from "../types"; @@ -10,6 +10,11 @@ import { observe$PerUserId, sharedStateByUserId } from "../util"; import { EFF_USERNAME_SETTINGS } from "./storage"; +const UsernameDigits = Object.freeze({ + enabled: 4, + disabled: 0, +}); + /** Strategy for creating usernames from the EFF wordlist */ export class EffUsernameGeneratorStrategy implements GeneratorStrategy diff --git a/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts index 5abcea82493..ee521d753ae 100644 --- a/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts @@ -8,7 +8,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { StateProvider } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; -import { DefaultPassphraseGenerationOptions, Policies } from "../data"; +import { DefaultPassphraseGenerationOptions } from "../data"; import { PasswordRandomizer } from "../engine"; import { PassphraseGeneratorOptionsEvaluator } from "../policies"; @@ -20,7 +20,7 @@ const SomeUser = "some user" as UserId; describe("Passphrase generation strategy", () => { describe("toEvaluator()", () => { it("should map to the policy evaluator", async () => { - const strategy = new PassphraseGeneratorStrategy(null, null); + const strategy = new PassphraseGeneratorStrategy(null!, null!); const policy = mock({ type: PolicyType.PasswordGenerator, data: { @@ -44,13 +44,18 @@ describe("Passphrase generation strategy", () => { it.each([[[]], [null], [undefined]])( "should map `%p` to a disabled password policy evaluator", async (policies) => { - const strategy = new PassphraseGeneratorStrategy(null, null); + const strategy = new PassphraseGeneratorStrategy(null!, null!); - const evaluator$ = of(policies).pipe(strategy.toEvaluator()); + // this case tests when the type system is subverted + const evaluator$ = of(policies!).pipe(strategy.toEvaluator()); const evaluator = await firstValueFrom(evaluator$); expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator); - expect(evaluator.policy).toMatchObject(Policies.Passphrase.disabledValue); + expect(evaluator.policy).toMatchObject({ + minNumberWords: 0, + capitalize: false, + includeNumber: false, + }); }, ); }); @@ -58,7 +63,7 @@ describe("Passphrase generation strategy", () => { describe("durableState", () => { it("should use password settings key", () => { const provider = mock(); - const strategy = new PassphraseGeneratorStrategy(null, provider); + const strategy = new PassphraseGeneratorStrategy(null!, provider); strategy.durableState(SomeUser); @@ -68,7 +73,7 @@ describe("Passphrase generation strategy", () => { describe("defaults$", () => { it("should return the default subaddress options", async () => { - const strategy = new PassphraseGeneratorStrategy(null, null); + const strategy = new PassphraseGeneratorStrategy(null!, null!); const result = await firstValueFrom(strategy.defaults$(SomeUser)); @@ -78,7 +83,7 @@ describe("Passphrase generation strategy", () => { describe("policy", () => { it("should use password generator policy", () => { - const strategy = new PassphraseGeneratorStrategy(null, null); + const strategy = new PassphraseGeneratorStrategy(null!, null!); expect(strategy.policy).toBe(PolicyType.PasswordGenerator); }); @@ -95,7 +100,7 @@ describe("Passphrase generation strategy", () => { }); it("should map options", async () => { - const strategy = new PassphraseGeneratorStrategy(randomizer, null); + const strategy = new PassphraseGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ numWords: 6, @@ -114,7 +119,7 @@ describe("Passphrase generation strategy", () => { }); it("should default numWords", async () => { - const strategy = new PassphraseGeneratorStrategy(randomizer, null); + const strategy = new PassphraseGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ capitalize: true, @@ -132,7 +137,7 @@ describe("Passphrase generation strategy", () => { }); it("should default capitalize", async () => { - const strategy = new PassphraseGeneratorStrategy(randomizer, null); + const strategy = new PassphraseGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ numWords: 6, @@ -150,7 +155,7 @@ describe("Passphrase generation strategy", () => { }); it("should default includeNumber", async () => { - const strategy = new PassphraseGeneratorStrategy(randomizer, null); + const strategy = new PassphraseGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ numWords: 6, @@ -168,7 +173,7 @@ describe("Passphrase generation strategy", () => { }); it("should default wordSeparator", async () => { - const strategy = new PassphraseGeneratorStrategy(randomizer, null); + const strategy = new PassphraseGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ numWords: 6, diff --git a/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.ts b/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.ts index 759f065b2ff..374df84a5bd 100644 --- a/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.ts +++ b/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.ts @@ -4,8 +4,9 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { StateProvider } from "@bitwarden/common/platform/state"; import { GeneratorStrategy } from "../abstractions"; -import { DefaultPassphraseGenerationOptions, Policies } from "../data"; +import { DefaultPassphraseGenerationOptions } from "../data"; import { PasswordRandomizer } from "../engine"; +import { PassphraseGeneratorOptionsEvaluator, passphraseLeastPrivilege } from "../policies"; import { mapPolicyToEvaluator } from "../rx"; import { PassphraseGenerationOptions, PassphraseGeneratorPolicy } from "../types"; import { observe$PerUserId, optionsToEffWordListRequest, sharedStateByUserId } from "../util"; @@ -30,7 +31,16 @@ export class PassphraseGeneratorStrategy defaults$ = observe$PerUserId(() => DefaultPassphraseGenerationOptions); readonly policy = PolicyType.PasswordGenerator; toEvaluator() { - return mapPolicyToEvaluator(Policies.Passphrase); + return mapPolicyToEvaluator({ + type: PolicyType.PasswordGenerator, + disabledValue: Object.freeze({ + minNumberWords: 0, + capitalize: false, + includeNumber: false, + }), + combine: passphraseLeastPrivilege, + createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy), + }); } // algorithm diff --git a/libs/tools/generator/core/src/strategies/password-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/password-generator-strategy.spec.ts index 928e0b0dc8b..94e7c16be28 100644 --- a/libs/tools/generator/core/src/strategies/password-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/password-generator-strategy.spec.ts @@ -8,7 +8,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { StateProvider } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; -import { DefaultPasswordGenerationOptions, Policies } from "../data"; +import { DefaultPasswordGenerationOptions } from "../data"; import { PasswordRandomizer } from "../engine"; import { PasswordGeneratorOptionsEvaluator } from "../policies"; @@ -20,7 +20,7 @@ const SomeUser = "some user" as UserId; describe("Password generation strategy", () => { describe("toEvaluator()", () => { it("should map to a password policy evaluator", async () => { - const strategy = new PasswordGeneratorStrategy(null, null); + const strategy = new PasswordGeneratorStrategy(null!, null!); const policy = mock({ type: PolicyType.PasswordGenerator, data: { @@ -52,13 +52,22 @@ describe("Password generation strategy", () => { it.each([[[]], [null], [undefined]])( "should map `%p` to a disabled password policy evaluator", async (policies) => { - const strategy = new PasswordGeneratorStrategy(null, null); + const strategy = new PasswordGeneratorStrategy(null!, null!); - const evaluator$ = of(policies).pipe(strategy.toEvaluator()); + // this case tests when the type system is subverted + const evaluator$ = of(policies!).pipe(strategy.toEvaluator()); const evaluator = await firstValueFrom(evaluator$); expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator); - expect(evaluator.policy).toMatchObject(Policies.Password.disabledValue); + expect(evaluator.policy).toMatchObject({ + minLength: 0, + useUppercase: false, + useLowercase: false, + useNumbers: false, + numberCount: 0, + useSpecial: false, + specialCount: 0, + }); }, ); }); @@ -66,7 +75,7 @@ describe("Password generation strategy", () => { describe("durableState", () => { it("should use password settings key", () => { const provider = mock(); - const strategy = new PasswordGeneratorStrategy(null, provider); + const strategy = new PasswordGeneratorStrategy(null!, provider); strategy.durableState(SomeUser); @@ -76,7 +85,7 @@ describe("Password generation strategy", () => { describe("defaults$", () => { it("should return the default subaddress options", async () => { - const strategy = new PasswordGeneratorStrategy(null, null); + const strategy = new PasswordGeneratorStrategy(null!, null!); const result = await firstValueFrom(strategy.defaults$(SomeUser)); @@ -86,7 +95,7 @@ describe("Password generation strategy", () => { describe("policy", () => { it("should use password generator policy", () => { - const strategy = new PasswordGeneratorStrategy(null, null); + const strategy = new PasswordGeneratorStrategy(null!, null!); expect(strategy.policy).toBe(PolicyType.PasswordGenerator); }); @@ -103,7 +112,7 @@ describe("Password generation strategy", () => { }); it("should map options", async () => { - const strategy = new PasswordGeneratorStrategy(randomizer, null); + const strategy = new PasswordGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ length: 20, @@ -130,7 +139,7 @@ describe("Password generation strategy", () => { }); it("should disable uppercase", async () => { - const strategy = new PasswordGeneratorStrategy(randomizer, null); + const strategy = new PasswordGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ length: 3, @@ -157,7 +166,7 @@ describe("Password generation strategy", () => { }); it("should disable lowercase", async () => { - const strategy = new PasswordGeneratorStrategy(randomizer, null); + const strategy = new PasswordGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ length: 3, @@ -184,7 +193,7 @@ describe("Password generation strategy", () => { }); it("should disable digits", async () => { - const strategy = new PasswordGeneratorStrategy(randomizer, null); + const strategy = new PasswordGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ length: 3, @@ -211,7 +220,7 @@ describe("Password generation strategy", () => { }); it("should disable special", async () => { - const strategy = new PasswordGeneratorStrategy(randomizer, null); + const strategy = new PasswordGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ length: 3, @@ -238,7 +247,7 @@ describe("Password generation strategy", () => { }); it("should override length with minimums", async () => { - const strategy = new PasswordGeneratorStrategy(randomizer, null); + const strategy = new PasswordGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ length: 20, @@ -265,7 +274,7 @@ describe("Password generation strategy", () => { }); it("should default uppercase", async () => { - const strategy = new PasswordGeneratorStrategy(randomizer, null); + const strategy = new PasswordGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ length: 2, @@ -291,7 +300,7 @@ describe("Password generation strategy", () => { }); it("should default lowercase", async () => { - const strategy = new PasswordGeneratorStrategy(randomizer, null); + const strategy = new PasswordGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ length: 0, @@ -317,7 +326,7 @@ describe("Password generation strategy", () => { }); it("should default number", async () => { - const strategy = new PasswordGeneratorStrategy(randomizer, null); + const strategy = new PasswordGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ length: 0, @@ -343,7 +352,7 @@ describe("Password generation strategy", () => { }); it("should default special", async () => { - const strategy = new PasswordGeneratorStrategy(randomizer, null); + const strategy = new PasswordGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ length: 0, @@ -369,7 +378,7 @@ describe("Password generation strategy", () => { }); it("should default minUppercase", async () => { - const strategy = new PasswordGeneratorStrategy(randomizer, null); + const strategy = new PasswordGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ length: 0, @@ -395,7 +404,7 @@ describe("Password generation strategy", () => { }); it("should default minLowercase", async () => { - const strategy = new PasswordGeneratorStrategy(randomizer, null); + const strategy = new PasswordGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ length: 0, @@ -421,7 +430,7 @@ describe("Password generation strategy", () => { }); it("should default minNumber", async () => { - const strategy = new PasswordGeneratorStrategy(randomizer, null); + const strategy = new PasswordGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ length: 0, @@ -447,7 +456,7 @@ describe("Password generation strategy", () => { }); it("should default minSpecial", async () => { - const strategy = new PasswordGeneratorStrategy(randomizer, null); + const strategy = new PasswordGeneratorStrategy(randomizer, null!); const result = await strategy.generate({ length: 0, diff --git a/libs/tools/generator/core/src/strategies/password-generator-strategy.ts b/libs/tools/generator/core/src/strategies/password-generator-strategy.ts index 9ff8a3d88b0..1a5070901c2 100644 --- a/libs/tools/generator/core/src/strategies/password-generator-strategy.ts +++ b/libs/tools/generator/core/src/strategies/password-generator-strategy.ts @@ -2,8 +2,9 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { StateProvider } from "@bitwarden/common/platform/state"; import { GeneratorStrategy } from "../abstractions"; -import { Policies, DefaultPasswordGenerationOptions } from "../data"; +import { DefaultPasswordGenerationOptions } from "../data"; import { PasswordRandomizer } from "../engine"; +import { PasswordGeneratorOptionsEvaluator, passwordLeastPrivilege } from "../policies"; import { mapPolicyToEvaluator } from "../rx"; import { PasswordGenerationOptions, PasswordGeneratorPolicy } from "../types"; import { observe$PerUserId, optionsToRandomAsciiRequest, sharedStateByUserId } from "../util"; @@ -27,7 +28,20 @@ export class PasswordGeneratorStrategy defaults$ = observe$PerUserId(() => DefaultPasswordGenerationOptions); readonly policy = PolicyType.PasswordGenerator; toEvaluator() { - return mapPolicyToEvaluator(Policies.Password); + return mapPolicyToEvaluator({ + type: PolicyType.PasswordGenerator, + disabledValue: { + minLength: 0, + useUppercase: false, + useLowercase: false, + useNumbers: false, + numberCount: 0, + useSpecial: false, + specialCount: 0, + }, + combine: passwordLeastPrivilege, + createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy), + }); } // algorithm diff --git a/libs/tools/generator/core/src/types/algorithm-info.ts b/libs/tools/generator/core/src/types/algorithm-info.ts new file mode 100644 index 00000000000..a3db03600b6 --- /dev/null +++ b/libs/tools/generator/core/src/types/algorithm-info.ts @@ -0,0 +1,50 @@ +import { CredentialAlgorithm, CredentialType } from "../metadata"; + +// FIXME: deprecate or delete `AlgorithmInfo` once a better translation +// strategy is identified. +export type AlgorithmInfo = { + /** Uniquely identifies the credential configuration + * @example + * // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)` + * // to pattern test whether the credential describes a forwarder algorithm + * const meta : CredentialGeneratorInfo = // ... + * const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {}; + */ + id: CredentialAlgorithm; + + /** The kind of credential generated by this configuration */ + type: CredentialType; + + /** Localized algorithm name */ + name: string; + + /* Localized generate button label */ + generate: string; + + /** Localized "credential generated" informational message */ + onGeneratedMessage: string; + + /* Localized copy button label */ + copy: string; + + /* Localized dialog button label */ + useGeneratedValue: string; + + /* Localized generated value label */ + credentialType: string; + + /** Localized algorithm description */ + description?: string; + + /** When true, credential generation must be explicitly requested. + * @remarks this property is useful when credential generation + * carries side effects, such as configuring a service external + * to Bitwarden. + */ + onlyOnRequest: boolean; + + /** Well-known fields to display on the options panel or collect from the environment. + * @remarks: at present, this is only used by forwarders + */ + request: readonly string[]; +}; diff --git a/libs/tools/generator/core/src/types/credential-generator-configuration.ts b/libs/tools/generator/core/src/types/credential-generator-configuration.ts deleted file mode 100644 index 36b0f3046a9..00000000000 --- a/libs/tools/generator/core/src/types/credential-generator-configuration.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { UserKeyDefinition } from "@bitwarden/common/platform/state"; -import { RestClient } from "@bitwarden/common/tools/integration/rpc"; -import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; -import { Constraints } from "@bitwarden/common/tools/types"; - -import { Randomizer } from "../abstractions"; -import { CredentialAlgorithm, CredentialCategory, PolicyConfiguration } from "../types"; - -import { CredentialGenerator } from "./credential-generator"; - -export type GeneratorDependencyProvider = { - randomizer: Randomizer; - client: RestClient; - i18nService: I18nService; -}; - -export type AlgorithmInfo = { - /** Uniquely identifies the credential configuration - * @example - * // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)` - * // to pattern test whether the credential describes a forwarder algorithm - * const meta : CredentialGeneratorInfo = // ... - * const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {}; - */ - id: CredentialAlgorithm; - - /** The kind of credential generated by this configuration */ - category: CredentialCategory; - - /** Localized algorithm name */ - name: string; - - /* Localized generate button label */ - generate: string; - - /** Localized "credential generated" informational message */ - onGeneratedMessage: string; - - /* Localized copy button label */ - copy: string; - - /* Localized dialog button label */ - useGeneratedValue: string; - - /* Localized generated value label */ - credentialType: string; - - /** Localized algorithm description */ - description?: string; - - /** When true, credential generation must be explicitly requested. - * @remarks this property is useful when credential generation - * carries side effects, such as configuring a service external - * to Bitwarden. - */ - onlyOnRequest: boolean; - - /** Well-known fields to display on the options panel or collect from the environment. - * @remarks: at present, this is only used by forwarders - */ - request: readonly string[]; -}; - -/** Credential generator metadata common across credential generators */ -export type CredentialGeneratorInfo = { - /** Uniquely identifies the credential configuration - * @example - * // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)` - * // to pattern test whether the credential describes a forwarder algorithm - * const meta : CredentialGeneratorInfo = // ... - * const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {}; - */ - id: CredentialAlgorithm; - - /** The kind of credential generated by this configuration */ - category: CredentialCategory; - - /** Localization key for the credential name */ - nameKey: string; - - /** Localization key for the credential description*/ - descriptionKey?: string; - - /** Localization key for the generate command label */ - generateKey: string; - - /** Localization key for the copy button label */ - copyKey: string; - - /** Localization key for the "credential generated" informational message */ - onGeneratedMessageKey: string; - - /** Localized "use generated credential" button label */ - useGeneratedValueKey: string; - - /** Localization key for describing the kind of credential generated - * by this generator. - */ - credentialTypeKey: string; - - /** When true, credential generation must be explicitly requested. - * @remarks this property is useful when credential generation - * carries side effects, such as configuring a service external - * to Bitwarden. - */ - onlyOnRequest: boolean; - - /** Well-known fields to display on the options panel or collect from the environment. - * @remarks: at present, this is only used by forwarders - */ - request: readonly string[]; -}; - -/** Credential generator metadata that relies upon typed setting and policy definitions. - * @example - * // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)` - * // to pattern test whether the credential describes a forwarder algorithm - * const meta : CredentialGeneratorInfo = // ... - * const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {}; - */ -export type CredentialGeneratorConfiguration = CredentialGeneratorInfo & { - /** An algorithm that generates credentials when ran. */ - engine: { - /** Factory for the generator - */ - // FIXME: note that this erases the engine's type so that credentials are - // generated uniformly. This property needs to be maintained for - // the credential generator, but engine configurations should return - // the underlying type. `create` may be able to do double-duty w/ an - // engine definition if `CredentialGenerator` can be made covariant. - create: (randomizer: GeneratorDependencyProvider) => CredentialGenerator; - }; - /** Defines the stored parameters for credential generation */ - settings: { - /** value used when an account's settings haven't been initialized - * @deprecated use `ObjectKey.initial` for your desired storage property instead - */ - initial: Readonly>; - - /** Application-global constraints that apply to account settings */ - constraints: Constraints; - - /** storage location for account-global settings */ - account: UserKeyDefinition | ObjectKey; - - /** storage location for *plaintext* settings imports */ - import?: UserKeyDefinition | ObjectKey, Settings>; - }; - - /** defines how to construct policy for this settings instance */ - policy: PolicyConfiguration; -}; diff --git a/libs/tools/generator/core/src/types/credential-preference.ts b/libs/tools/generator/core/src/types/credential-preference.ts new file mode 100644 index 00000000000..957299e5c31 --- /dev/null +++ b/libs/tools/generator/core/src/types/credential-preference.ts @@ -0,0 +1,10 @@ +import { CredentialAlgorithm, CredentialType } from "../metadata"; + +/** The kind of credential to generate using a compound configuration. */ +// FIXME: extend the preferences to include a preferred forwarder +export type CredentialPreference = { + [Key in CredentialType]: { + algorithm: CredentialAlgorithm; + updated: Date; + }; +}; diff --git a/libs/tools/generator/core/src/types/forwarder-options.ts b/libs/tools/generator/core/src/types/forwarder-options.ts index 7ba04da99a8..06084261283 100644 --- a/libs/tools/generator/core/src/types/forwarder-options.ts +++ b/libs/tools/generator/core/src/types/forwarder-options.ts @@ -7,32 +7,65 @@ import { import { EmailDomainSettings, EmailPrefixSettings } from "../engine"; +// FIXME: this type alias is in place for legacy support purposes; +// when replacing the forwarder implementation, eliminate `ForwarderId` and +// `IntegrationId`. The proper type is `VendorId`. /** Identifiers for email forwarding services. * @remarks These are used to select forwarder-specific options. * The must be kept in sync with the forwarder implementations. */ export type ForwarderId = IntegrationId; -/** Metadata format for email forwarding services. */ -export type ForwarderMetadata = { - /** The unique identifier for the forwarder. */ - id: ForwarderId; - - /** The name of the service the forwarder queries. */ - name: string; - - /** Whether the forwarder is valid for self-hosted instances of Bitwarden. */ - validForSelfHosted: boolean; -}; - -/** Options common to all forwarder APIs */ +/** Options common to all forwarder APIs + * @deprecated use {@link ForwarderOptions} instead. + */ export type ApiOptions = ApiSettings & IntegrationRequest; -/** Api configuration for forwarders that support self-hosted installations. */ +/** Api configuration for forwarders that support self-hosted installations. + * @deprecated use {@link ForwarderOptions} instead. + */ export type SelfHostedApiOptions = SelfHostedApiSettings & IntegrationRequest; -/** Api configuration for forwarders that support custom domains. */ +/** Api configuration for forwarders that support custom domains. + * @deprecated use {@link ForwarderOptions} instead. + */ export type EmailDomainOptions = EmailDomainSettings; -/** Api configuration for forwarders that support custom email parts. */ +/** Api configuration for forwarders that support custom email parts. + * @deprecated use {@link ForwarderOptions} instead. + */ export type EmailPrefixOptions = EmailDomainSettings & EmailPrefixSettings; + +/** These options are used by all forwarders; each forwarder uses a different set, + * as defined by `GeneratorMetadata.capabilities.fields`. + */ +export type ForwarderOptions = Partial< + { + /** bearer token that authenticates bitwarden to the forwarder. + * This is required to issue an API request. + */ + token: string; + + /** The base URL of the forwarder's API. + * When this is undefined or empty, the forwarder's default production API is used. + */ + baseUrl: string; + + /** The domain part of the generated email address. + * @remarks The domain should be authorized by the forwarder before + * submitting a request through bitwarden. + * @example If the domain is `domain.io` and the generated username + * is `jd`, then the generated email address will be `jd@domain.io` + */ + domain: string; + + /** A prefix joined to the generated email address' username. + * @example If the prefix is `foo`, the generated username is `bar`, + * and the domain is `domain.io`, then the generated email address is + * `foobar@domain.io`. + */ + prefix: string; + } & EmailDomainSettings & + EmailPrefixSettings & + SelfHostedApiSettings +>; diff --git a/libs/tools/generator/core/src/types/generate-request.ts b/libs/tools/generator/core/src/types/generate-request.ts index c7d5bf9c41c..9dbaaee12f6 100644 --- a/libs/tools/generator/core/src/types/generate-request.ts +++ b/libs/tools/generator/core/src/types/generate-request.ts @@ -1,6 +1,15 @@ +import { RequireExactlyOne } from "type-fest"; + +import { CredentialType, GeneratorProfile, CredentialAlgorithm } from "../metadata"; + /** Contextual information about the application state when a generator is invoked. */ -export type GenerateRequest = { +export type GenerateRequest = RequireExactlyOne< + { type: CredentialType; algorithm: CredentialAlgorithm }, + "type" | "algorithm" +> & { + profile?: GeneratorProfile; + /** Traces the origin of the generation request. This parameter is * copied to the generated credential. * diff --git a/libs/tools/generator/core/src/types/generated-credential.spec.ts b/libs/tools/generator/core/src/types/generated-credential.spec.ts index 6a498282fe3..3d8d2c9bd4d 100644 --- a/libs/tools/generator/core/src/types/generated-credential.spec.ts +++ b/libs/tools/generator/core/src/types/generated-credential.spec.ts @@ -1,40 +1,42 @@ -import { CredentialAlgorithm, GeneratedCredential } from "."; +import { Type } from "../metadata"; + +import { GeneratedCredential } from "./generated-credential"; describe("GeneratedCredential", () => { describe("constructor", () => { it("assigns credential", () => { - const result = new GeneratedCredential("example", "passphrase", new Date(100)); + const result = new GeneratedCredential("example", Type.password, new Date(100)); expect(result.credential).toEqual("example"); }); it("assigns category", () => { - const result = new GeneratedCredential("example", "passphrase", new Date(100)); + const result = new GeneratedCredential("example", Type.password, new Date(100)); - expect(result.category).toEqual("passphrase"); + expect(result.category).toEqual(Type.password); }); it("passes through date parameters", () => { - const result = new GeneratedCredential("example", "password", new Date(100)); + const result = new GeneratedCredential("example", Type.password, new Date(100)); expect(result.generationDate).toEqual(new Date(100)); }); it("converts numeric dates to Dates", () => { - const result = new GeneratedCredential("example", "password", 100); + const result = new GeneratedCredential("example", Type.password, 100); expect(result.generationDate).toEqual(new Date(100)); }); }); it("toJSON converts from a credential into a JSON object", () => { - const credential = new GeneratedCredential("example", "password", new Date(100)); + const credential = new GeneratedCredential("example", Type.password, new Date(100)); const result = credential.toJSON(); expect(result).toEqual({ credential: "example", - category: "password" as CredentialAlgorithm, + category: Type.password, generationDate: 100, }); }); @@ -42,7 +44,7 @@ describe("GeneratedCredential", () => { it("fromJSON converts Json objects into credentials", () => { const jsonValue = { credential: "example", - category: "password" as CredentialAlgorithm, + category: Type.password, generationDate: 100, }; diff --git a/libs/tools/generator/core/src/types/generated-credential.ts b/libs/tools/generator/core/src/types/generated-credential.ts index 99b864b9fd8..695e3866920 100644 --- a/libs/tools/generator/core/src/types/generated-credential.ts +++ b/libs/tools/generator/core/src/types/generated-credential.ts @@ -1,13 +1,13 @@ import { Jsonify } from "type-fest"; -import { CredentialAlgorithm } from "./generator-type"; +import { CredentialType } from "../metadata"; /** A credential generation result */ export class GeneratedCredential { /** * Instantiates a generated credential * @param credential The value of the generated credential (e.g. a password) - * @param category The kind of credential + * @param category The type of credential * @param generationDate The date that the credential was generated. * Numeric values should are interpreted using {@link Date.valueOf} * semantics. @@ -16,7 +16,9 @@ export class GeneratedCredential { */ constructor( readonly credential: string, - readonly category: CredentialAlgorithm, + // FIXME: create a way to migrate the data stored in `category` to a new `type` + // field. The hard part: This requires the migration occur post-decryption. + readonly category: CredentialType, generationDate: Date | number, readonly source?: string, readonly website?: string, diff --git a/libs/tools/generator/core/src/types/generator-type.ts b/libs/tools/generator/core/src/types/generator-type.ts deleted file mode 100644 index c75e4329610..00000000000 --- a/libs/tools/generator/core/src/types/generator-type.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { VendorId } from "@bitwarden/common/tools/extension"; -import { IntegrationId } from "@bitwarden/common/tools/integration"; - -import { EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from "../data/generator-types"; -import { AlgorithmsByType, CredentialType } from "../metadata"; - -/** A type of password that may be generated by the credential generator. */ -export type PasswordAlgorithm = (typeof PasswordAlgorithms)[number]; - -/** A type of username that may be generated by the credential generator. */ -export type UsernameAlgorithm = (typeof UsernameAlgorithms)[number]; - -/** A type of email address that may be generated by the credential generator. */ -export type EmailAlgorithm = (typeof EmailAlgorithms)[number]; - -export type ForwarderIntegration = { forwarder: IntegrationId & VendorId }; - -/** Returns true when the input algorithm is a forwarder integration. */ -export function isForwarderIntegration( - algorithm: CredentialAlgorithm, -): algorithm is ForwarderIntegration { - return algorithm && typeof algorithm === "object" && "forwarder" in algorithm; -} - -export function isSameAlgorithm(lhs: CredentialAlgorithm, rhs: CredentialAlgorithm) { - if (lhs === rhs) { - return true; - } else if (isForwarderIntegration(lhs) && isForwarderIntegration(rhs)) { - return lhs.forwarder === rhs.forwarder; - } else { - return false; - } -} - -/** A type of credential that may be generated by the credential generator. */ -export type CredentialAlgorithm = - | PasswordAlgorithm - | UsernameAlgorithm - | EmailAlgorithm - | ForwarderIntegration; - -/** Compound credential types supported by the credential generator. */ -export const CredentialCategories = Object.freeze({ - /** Lists algorithms in the "password" credential category */ - password: PasswordAlgorithms as Readonly, - - /** Lists algorithms in the "username" credential category */ - username: UsernameAlgorithms as Readonly, - - /** Lists algorithms in the "email" credential category */ - email: EmailAlgorithms as Readonly<(EmailAlgorithm | ForwarderIntegration)[]>, -}); - -/** Returns true when the input algorithm is a password algorithm. */ -export function isPasswordAlgorithm( - algorithm: CredentialAlgorithm, -): algorithm is PasswordAlgorithm { - return PasswordAlgorithms.includes(algorithm as any); -} - -/** Returns true when the input algorithm is a username algorithm. */ -export function isUsernameAlgorithm( - algorithm: CredentialAlgorithm, -): algorithm is UsernameAlgorithm { - return UsernameAlgorithms.includes(algorithm as any); -} - -/** Returns true when the input algorithm is an email algorithm. */ -export function isEmailAlgorithm(algorithm: CredentialAlgorithm): algorithm is EmailAlgorithm { - return EmailAlgorithms.includes(algorithm as any) || isForwarderIntegration(algorithm); -} - -/** A type of compound credential that may be generated by the credential generator. */ -export type CredentialCategory = keyof typeof CredentialCategories; - -/** The kind of credential to generate using a compound configuration. */ -// FIXME: extend the preferences to include a preferred forwarder -export type CredentialPreference = { - [Key in CredentialType & CredentialCategory]: { - algorithm: CredentialAlgorithm & (typeof AlgorithmsByType)[Key][number]; - updated: Date; - }; -}; diff --git a/libs/tools/generator/core/src/types/index.ts b/libs/tools/generator/core/src/types/index.ts index 3e392257b0c..f6460342b09 100644 --- a/libs/tools/generator/core/src/types/index.ts +++ b/libs/tools/generator/core/src/types/index.ts @@ -1,16 +1,14 @@ -import { EmailAlgorithm, PasswordAlgorithm, UsernameAlgorithm } from "./generator-type"; - export * from "./boundary"; export * from "./catchall-generator-options"; export * from "./credential-generator"; -export * from "./credential-generator-configuration"; +export * from "./algorithm-info"; export * from "./eff-username-generator-options"; export * from "./forwarder-options"; export * from "./generate-request"; export * from "./generator-constraints"; export * from "./generated-credential"; export * from "./generator-options"; -export * from "./generator-type"; +export * from "./credential-preference"; export * from "./no-policy"; export * from "./passphrase-generation-options"; export * from "./passphrase-generator-policy"; @@ -19,13 +17,3 @@ export * from "./password-generator-policy"; export * from "./policy-configuration"; export * from "./subaddress-generator-options"; export * from "./word-options"; - -/** Provided for backwards compatibility only. - * @deprecated Use one of the Algorithm types instead. - */ -export type GeneratorType = PasswordAlgorithm | UsernameAlgorithm | EmailAlgorithm; - -/** Provided for backwards compatibility only. - * @deprecated Use one of the Algorithm types instead. - */ -export type PasswordType = PasswordAlgorithm; diff --git a/libs/tools/generator/core/src/types/policy-configuration.ts b/libs/tools/generator/core/src/types/policy-configuration.ts index 07ded886609..e4db437575a 100644 --- a/libs/tools/generator/core/src/types/policy-configuration.ts +++ b/libs/tools/generator/core/src/types/policy-configuration.ts @@ -3,8 +3,6 @@ import { Policy as AdminPolicy } from "@bitwarden/common/admin-console/models/do import { PolicyEvaluator } from "../abstractions"; -import { GeneratorConstraints } from "./generator-constraints"; - /** Determines how to construct a password generator policy */ export type PolicyConfiguration = { type: PolicyType; @@ -22,15 +20,4 @@ export type PolicyConfiguration = { * Use `toConstraints` instead. */ createEvaluator: (policy: Policy) => PolicyEvaluator; - - /** Converts policy service data into actionable policy constraints. - * - * @param policy - the policy to map into policy constraints. - * @param email - the default email to extend. - * - * @remarks this version includes constraints needed for the reactive forms; - * it was introduced so that the constraints can be incrementally introduced - * as the new UI is built. - */ - toConstraints: (policy: Policy, email: string) => GeneratorConstraints; }; diff --git a/libs/tools/generator/core/src/util.spec.ts b/libs/tools/generator/core/src/util.spec.ts index 8ed95a9f268..5aeaeb3c67e 100644 --- a/libs/tools/generator/core/src/util.spec.ts +++ b/libs/tools/generator/core/src/util.spec.ts @@ -1,5 +1,6 @@ import { DefaultPassphraseGenerationOptions } from "./data"; -import { optionsToEffWordListRequest, optionsToRandomAsciiRequest, sum } from "./util"; +import { GeneratorConstraints, PassphraseGenerationOptions } from "./types"; +import { optionsToEffWordListRequest, optionsToRandomAsciiRequest, sum, equivalent } from "./util"; describe("sum", () => { it("returns 0 when the list is empty", () => { @@ -424,3 +425,90 @@ describe("optionsToEffWordListRequest", () => { }); }); }); + +describe("equivalent", () => { + // constructs a partial constraints object; only the properties compared + // by `equivalent` are included. + function createConstraints( + policyInEffect: boolean, + numWordsMin?: number, + capitalize?: boolean, + ): GeneratorConstraints { + return { + constraints: { + policyInEffect, + numWords: numWordsMin !== undefined ? { min: numWordsMin } : undefined, + capitalize: capitalize !== undefined ? { requiredValue: capitalize } : undefined, + }, + } as unknown as GeneratorConstraints; + } + + it("should return true for identical constraints", () => { + const lhs = createConstraints(false, 3, true); + const rhs = createConstraints(false, 3, true); + + expect(equivalent(lhs, rhs)).toBe(true); + }); + + it("should return false when policy effects differ", () => { + const lhs = createConstraints(true, 3); + const rhs = createConstraints(false, 3); + + expect(equivalent(lhs, rhs)).toBe(false); + }); + + it("should return false when constraint values differ", () => { + const lhs = createConstraints(false, 3); + const rhs = createConstraints(false, 4); + + expect(equivalent(lhs, rhs)).toBe(false); + }); + + it("should return false when one has additional constraints", () => { + const lhs = createConstraints(false, 3, true); + const rhs = createConstraints(false, 3); + + expect(equivalent(lhs, rhs)).toBe(false); + }); + + it("should handle undefined constraints", () => { + const lhs = createConstraints(false); + const rhs = createConstraints(false); + + expect(equivalent(lhs, rhs)).toBe(true); + }); + + it("should handle empty constraint objects", () => { + const lhs = { + constraints: { + policyInEffect: false, + numWords: {}, + }, + } as unknown as GeneratorConstraints; + const rhs = { + constraints: { + policyInEffect: false, + numWords: {}, + }, + } as unknown as GeneratorConstraints; + + expect(equivalent(lhs, rhs)).toBe(true); + }); + + it("should return false when inner constraint properties differ", () => { + const lhs = { + constraints: { + policyInEffect: false, + numWords: { min: 3, max: 5 }, + }, + } as any; + const rhs = { + constraints: { + policyInEffect: false, + numWords: { min: 3, max: 6 }, + }, + } as unknown as GeneratorConstraints; + + expect(equivalent(lhs, rhs)).toBe(false); + }); +}); diff --git a/libs/tools/generator/core/src/util.ts b/libs/tools/generator/core/src/util.ts index 4b6041ffeba..62f749faf56 100644 --- a/libs/tools/generator/core/src/util.ts +++ b/libs/tools/generator/core/src/util.ts @@ -14,7 +14,11 @@ import { DefaultPassphraseGenerationOptions, DefaultPasswordGenerationOptions, } from "./data"; -import { PassphraseGenerationOptions, PasswordGenerationOptions } from "./types"; +import { + PassphraseGenerationOptions, + PasswordGenerationOptions, + GeneratorConstraints, +} from "./types"; /** construct a method that outputs a copy of `defaultValue` as an observable. */ export function observe$PerUserId( @@ -135,3 +139,30 @@ export function optionsToEffWordListRequest(options: PassphraseGenerationOptions return request; } + +export function equivalent(lhs: GeneratorConstraints, rhs: GeneratorConstraints): boolean { + if (lhs.constraints.policyInEffect !== rhs.constraints.policyInEffect) { + return false; + } + + // safe because `Constraints` shares keys with `T` + const keys = Object.keys(lhs.constraints) as (keyof T)[]; + + // use `for` loop so that `equivalent` can return as soon as the constraints + // differ. Using `array.xyz` would evaluate the whole key list eagerly + for (const k of keys) { + if (!(k in rhs.constraints)) { + return false; + } + + const lhsConstraints: any = lhs.constraints[k] ?? {}; + const rhsConstraints: any = rhs.constraints[k] ?? {}; + + const innerKeys = Object.keys(lhsConstraints); + if (innerKeys.some((k) => lhsConstraints[k] !== rhsConstraints[k])) { + return false; + } + } + + return true; +} diff --git a/libs/tools/generator/core/tsconfig.json b/libs/tools/generator/core/tsconfig.json index a95b588686f..303f913ba23 100644 --- a/libs/tools/generator/core/tsconfig.json +++ b/libs/tools/generator/core/tsconfig.json @@ -8,10 +8,6 @@ "@bitwarden/key-management": ["../../../key-management/src"] } }, - "include": [ - "src", - "../extensions/src/history/generator-history.abstraction.ts", - "../extensions/src/navigation/generator-navigation.service.abstraction.ts" - ], + "include": ["src"], "exclude": ["node_modules", "dist"] } diff --git a/libs/tools/generator/extensions/history/src/generated-credential.spec.ts b/libs/tools/generator/extensions/history/src/generated-credential.spec.ts index 846f2ee96be..26a48cb83ea 100644 --- a/libs/tools/generator/extensions/history/src/generated-credential.spec.ts +++ b/libs/tools/generator/extensions/history/src/generated-credential.spec.ts @@ -1,40 +1,42 @@ -import { GeneratorCategory, GeneratedCredential } from "."; +import { Type } from "@bitwarden/generator-core"; + +import { GeneratedCredential } from "."; describe("GeneratedCredential", () => { describe("constructor", () => { it("assigns credential", () => { - const result = new GeneratedCredential("example", "passphrase", new Date(100)); + const result = new GeneratedCredential("example", Type.password, new Date(100)); expect(result.credential).toEqual("example"); }); it("assigns category", () => { - const result = new GeneratedCredential("example", "passphrase", new Date(100)); + const result = new GeneratedCredential("example", Type.password, new Date(100)); - expect(result.category).toEqual("passphrase"); + expect(result.category).toEqual(Type.password); }); it("passes through date parameters", () => { - const result = new GeneratedCredential("example", "password", new Date(100)); + const result = new GeneratedCredential("example", Type.password, new Date(100)); expect(result.generationDate).toEqual(new Date(100)); }); it("converts numeric dates to Dates", () => { - const result = new GeneratedCredential("example", "password", 100); + const result = new GeneratedCredential("example", Type.password, 100); expect(result.generationDate).toEqual(new Date(100)); }); }); it("toJSON converts from a credential into a JSON object", () => { - const credential = new GeneratedCredential("example", "password", new Date(100)); + const credential = new GeneratedCredential("example", Type.password, new Date(100)); const result = credential.toJSON(); expect(result).toEqual({ credential: "example", - category: "password" as GeneratorCategory, + category: Type.password, generationDate: 100, }); }); @@ -42,7 +44,7 @@ describe("GeneratedCredential", () => { it("fromJSON converts Json objects into credentials", () => { const jsonValue = { credential: "example", - category: "password" as GeneratorCategory, + category: Type.password, generationDate: 100, }; @@ -51,7 +53,7 @@ describe("GeneratedCredential", () => { expect(result).toBeInstanceOf(GeneratedCredential); expect(result).toEqual({ credential: "example", - category: "password", + category: Type.password, generationDate: new Date(100), }); }); diff --git a/libs/tools/generator/extensions/history/src/generated-credential.ts b/libs/tools/generator/extensions/history/src/generated-credential.ts index 32efb752258..bec9e5ac7ee 100644 --- a/libs/tools/generator/extensions/history/src/generated-credential.ts +++ b/libs/tools/generator/extensions/history/src/generated-credential.ts @@ -1,6 +1,6 @@ import { Jsonify } from "type-fest"; -import { CredentialAlgorithm } from "@bitwarden/generator-core"; +import { CredentialType } from "@bitwarden/generator-core"; /** A credential generation result */ export class GeneratedCredential { @@ -14,7 +14,7 @@ export class GeneratedCredential { */ constructor( readonly credential: string, - readonly category: CredentialAlgorithm, + readonly category: CredentialType, generationDate: Date | number, ) { if (typeof generationDate === "number") { diff --git a/libs/tools/generator/extensions/history/src/generator-history.abstraction.ts b/libs/tools/generator/extensions/history/src/generator-history.abstraction.ts index 3b8a0e05a9e..3e3d4002be4 100644 --- a/libs/tools/generator/extensions/history/src/generator-history.abstraction.ts +++ b/libs/tools/generator/extensions/history/src/generator-history.abstraction.ts @@ -3,7 +3,7 @@ import { Observable } from "rxjs"; import { UserId } from "@bitwarden/common/types/guid"; -import { CredentialAlgorithm } from "@bitwarden/generator-core"; +import { CredentialType } from "@bitwarden/generator-core"; import { GeneratedCredential } from "./generated-credential"; @@ -29,7 +29,7 @@ export abstract class GeneratorHistoryService { track: ( userId: UserId, credential: string, - category: CredentialAlgorithm, + category: CredentialType, date?: Date, ) => Promise; diff --git a/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts b/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts index 3621b2c24a9..caff4234386 100644 --- a/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts +++ b/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts @@ -7,6 +7,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; +import { Type } from "@bitwarden/generator-core"; import { KeyService } from "@bitwarden/key-management"; import { FakeStateProvider, awaitAsync, mockAccountServiceWith } from "../../../../../common/spec"; @@ -25,7 +26,8 @@ describe("LocalGeneratorHistoryService", () => { encryptService.encryptString.mockImplementation((p) => Promise.resolve(p as unknown as EncString), ); - encryptService.decryptString.mockImplementation((c) => Promise.resolve(c.encryptedString)); + // in the test environment `c.encryptedString` always has a value + encryptService.decryptString.mockImplementation((c) => Promise.resolve(c.encryptedString!)); keyService.getUserKey.mockImplementation(() => Promise.resolve(userKey)); keyService.userKey$.mockImplementation(() => of(true as unknown as UserKey)); }); @@ -50,35 +52,35 @@ describe("LocalGeneratorHistoryService", () => { const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - await history.track(SomeUser, "example", "password"); + await history.track(SomeUser, "example", Type.password); await awaitAsync(); const [result] = await firstValueFrom(history.credentials$(SomeUser)); - expect(result).toMatchObject({ credential: "example", category: "password" }); + expect(result).toMatchObject({ credential: "example", category: Type.password }); }); it("stores a passphrase", async () => { const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - await history.track(SomeUser, "example", "passphrase"); + await history.track(SomeUser, "example", Type.password); await awaitAsync(); const [result] = await firstValueFrom(history.credentials$(SomeUser)); - expect(result).toMatchObject({ credential: "example", category: "passphrase" }); + expect(result).toMatchObject({ credential: "example", category: Type.password }); }); it("stores a specific date when one is provided", async () => { const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - await history.track(SomeUser, "example", "password", new Date(100)); + await history.track(SomeUser, "example", Type.password, new Date(100)); await awaitAsync(); const [result] = await firstValueFrom(history.credentials$(SomeUser)); expect(result).toEqual({ credential: "example", - category: "password", + category: Type.password, generationDate: new Date(100), }); }); @@ -87,13 +89,13 @@ describe("LocalGeneratorHistoryService", () => { const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - await history.track(SomeUser, "example", "password"); - await history.track(SomeUser, "example", "password"); - await history.track(SomeUser, "example", "passphrase"); + await history.track(SomeUser, "example", Type.password); + await history.track(SomeUser, "example", Type.password); + await history.track(SomeUser, "example", Type.password); await awaitAsync(); const [firstResult, secondResult] = await firstValueFrom(history.credentials$(SomeUser)); - expect(firstResult).toMatchObject({ credential: "example", category: "password" }); + expect(firstResult).toMatchObject({ credential: "example", category: Type.password }); expect(secondResult).toBeUndefined(); }); @@ -101,13 +103,13 @@ describe("LocalGeneratorHistoryService", () => { const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - await history.track(SomeUser, "secondResult", "password"); - await history.track(SomeUser, "firstResult", "password"); + await history.track(SomeUser, "secondResult", Type.password); + await history.track(SomeUser, "firstResult", Type.password); await awaitAsync(); const [firstResult, secondResult] = await firstValueFrom(history.credentials$(SomeUser)); - expect(firstResult).toMatchObject({ credential: "firstResult", category: "password" }); - expect(secondResult).toMatchObject({ credential: "secondResult", category: "password" }); + expect(firstResult).toMatchObject({ credential: "firstResult", category: Type.password }); + expect(secondResult).toMatchObject({ credential: "secondResult", category: Type.password }); }); it("removes history items exceeding maxTotal configuration", async () => { @@ -116,12 +118,12 @@ describe("LocalGeneratorHistoryService", () => { maxTotal: 1, }); - await history.track(SomeUser, "removed result", "password"); - await history.track(SomeUser, "example", "password"); + await history.track(SomeUser, "removed result", Type.password); + await history.track(SomeUser, "example", Type.password); await awaitAsync(); const [firstResult, secondResult] = await firstValueFrom(history.credentials$(SomeUser)); - expect(firstResult).toMatchObject({ credential: "example", category: "password" }); + expect(firstResult).toMatchObject({ credential: "example", category: Type.password }); expect(secondResult).toBeUndefined(); }); @@ -131,8 +133,8 @@ describe("LocalGeneratorHistoryService", () => { maxTotal: 1, }); - await history.track(SomeUser, "some user example", "password"); - await history.track(AnotherUser, "another user example", "password"); + await history.track(SomeUser, "some user example", Type.password); + await history.track(AnotherUser, "another user example", Type.password); await awaitAsync(); const [someFirstResult, someSecondResult] = await firstValueFrom( history.credentials$(SomeUser), @@ -143,12 +145,12 @@ describe("LocalGeneratorHistoryService", () => { expect(someFirstResult).toMatchObject({ credential: "some user example", - category: "password", + category: Type.password, }); expect(someSecondResult).toBeUndefined(); expect(anotherFirstResult).toMatchObject({ credential: "another user example", - category: "password", + category: Type.password, }); expect(anotherSecondResult).toBeUndefined(); }); @@ -167,7 +169,7 @@ describe("LocalGeneratorHistoryService", () => { it("returns null when the credential wasn't found", async () => { const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - await history.track(SomeUser, "example", "password"); + await history.track(SomeUser, "example", Type.password); const result = await history.take(SomeUser, "not found"); @@ -177,20 +179,20 @@ describe("LocalGeneratorHistoryService", () => { it("returns a matching credential", async () => { const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - await history.track(SomeUser, "example", "password"); + await history.track(SomeUser, "example", Type.password); const result = await history.take(SomeUser, "example"); expect(result).toMatchObject({ credential: "example", - category: "password", + category: Type.password, }); }); it("removes a matching credential", async () => { const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - await history.track(SomeUser, "example", "password"); + await history.track(SomeUser, "example", Type.password); await history.take(SomeUser, "example"); await awaitAsync(); diff --git a/libs/tools/generator/extensions/history/src/local-generator-history.service.ts b/libs/tools/generator/extensions/history/src/local-generator-history.service.ts index cbfa55a184f..673319c1bfa 100644 --- a/libs/tools/generator/extensions/history/src/local-generator-history.service.ts +++ b/libs/tools/generator/extensions/history/src/local-generator-history.service.ts @@ -9,7 +9,7 @@ import { BufferedState } from "@bitwarden/common/tools/state/buffered-state"; import { PaddedDataPacker } from "@bitwarden/common/tools/state/padded-data-packer"; import { SecretState } from "@bitwarden/common/tools/state/secret-state"; import { UserId } from "@bitwarden/common/types/guid"; -import { CredentialAlgorithm } from "@bitwarden/generator-core"; +import { CredentialType } from "@bitwarden/generator-core"; import { KeyService } from "@bitwarden/key-management"; import { GeneratedCredential } from "./generated-credential"; @@ -36,12 +36,7 @@ export class LocalGeneratorHistoryService extends GeneratorHistoryService { private _credentialStates = new Map>(); /** {@link GeneratorHistoryService.track} */ - track = async ( - userId: UserId, - credential: string, - category: CredentialAlgorithm, - date?: Date, - ) => { + track = async (userId: UserId, credential: string, category: CredentialType, date?: Date) => { const state = this.getCredentialState(userId); let result: GeneratedCredential = null; diff --git a/libs/tools/generator/core/src/data/forwarders.ts b/libs/tools/generator/extensions/legacy/src/forwarders.ts similarity index 73% rename from libs/tools/generator/core/src/data/forwarders.ts rename to libs/tools/generator/extensions/legacy/src/forwarders.ts index e833fbf41d3..cb926ac3f66 100644 --- a/libs/tools/generator/core/src/data/forwarders.ts +++ b/libs/tools/generator/extensions/legacy/src/forwarders.ts @@ -1,4 +1,18 @@ -import { ForwarderMetadata } from "../types"; +import { IntegrationId } from "@bitwarden/common/tools/integration"; + +export type ForwarderId = IntegrationId; + +/** Metadata format for email forwarding services. */ +export type ForwarderMetadata = { + /** The unique identifier for the forwarder. */ + id: ForwarderId; + + /** The name of the service the forwarder queries. */ + name: string; + + /** Whether the forwarder is valid for self-hosted instances of Bitwarden. */ + validForSelfHosted: boolean; +}; /** Metadata about an email forwarding service. * @remarks This is used to populate the forwarder selection list diff --git a/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts b/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts index d932d013199..f575cd3b619 100644 --- a/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts +++ b/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts @@ -1,13 +1,12 @@ import { mock } from "jest-mock-extended"; import { of } from "rxjs"; -import { IntegrationId } from "@bitwarden/common/tools/integration"; +import { VendorId } from "@bitwarden/common/tools/extension"; import { UserId } from "@bitwarden/common/types/guid"; import { GeneratorService, DefaultPassphraseGenerationOptions, DefaultPasswordGenerationOptions, - Policies, PassphraseGenerationOptions, PassphraseGeneratorPolicy, PasswordGenerationOptions, @@ -38,12 +37,17 @@ const PasswordGeneratorOptionsEvaluator = policies.PasswordGeneratorOptionsEvalu function createPassphraseGenerator( options: PassphraseGenerationOptions = {}, - policy: PassphraseGeneratorPolicy = Policies.Passphrase.disabledValue, + policy?: PassphraseGeneratorPolicy, ) { let savedOptions = options; const generator = mock>({ evaluator$(id: UserId) { - const evaluator = new PassphraseGeneratorOptionsEvaluator(policy); + const active = policy ?? { + minNumberWords: 0, + capitalize: false, + includeNumber: false, + }; + const evaluator = new PassphraseGeneratorOptionsEvaluator(active); return of(evaluator); }, options$(id: UserId) { @@ -63,12 +67,21 @@ function createPassphraseGenerator( function createPasswordGenerator( options: PasswordGenerationOptions = {}, - policy: PasswordGeneratorPolicy = Policies.Password.disabledValue, + policy?: PasswordGeneratorPolicy, ) { let savedOptions = options; const generator = mock>({ evaluator$(id: UserId) { - const evaluator = new PasswordGeneratorOptionsEvaluator(policy); + const active = policy ?? { + minLength: 0, + useUppercase: false, + useLowercase: false, + useNumbers: false, + numberCount: 0, + useSpecial: false, + specialCount: 0, + }; + const evaluator = new PasswordGeneratorOptionsEvaluator(active); return of(evaluator); }, options$(id: UserId) { @@ -118,7 +131,13 @@ describe("LegacyPasswordGenerationService", () => { describe("generatePassword", () => { it("invokes the inner password generator to generate passwords", async () => { const innerPassword = createPasswordGenerator(); - const generator = new LegacyPasswordGenerationService(null, null, innerPassword, null, null); + const generator = new LegacyPasswordGenerationService( + null!, + null!, + innerPassword, + null!, + null!, + ); const options = { type: "password" } as PasswordGeneratorOptions; await generator.generatePassword(options); @@ -129,11 +148,11 @@ describe("LegacyPasswordGenerationService", () => { it("invokes the inner passphrase generator to generate passphrases", async () => { const innerPassphrase = createPassphraseGenerator(); const generator = new LegacyPasswordGenerationService( - null, - null, - null, + null!, + null!, + null!, innerPassphrase, - null, + null!, ); const options = { type: "passphrase" } as PasswordGeneratorOptions; @@ -147,11 +166,11 @@ describe("LegacyPasswordGenerationService", () => { it("invokes the inner passphrase generator", async () => { const innerPassphrase = createPassphraseGenerator(); const generator = new LegacyPasswordGenerationService( - null, - null, - null, + null!, + null!, + null!, innerPassphrase, - null, + null!, ); const options = {} as PasswordGeneratorOptions; @@ -185,7 +204,7 @@ describe("LegacyPasswordGenerationService", () => { const navigation = createNavigationGenerator({ type: "passphrase", username: "word", - forwarder: "simplelogin" as IntegrationId, + forwarder: "simplelogin" as VendorId, }); const accountService = mockAccountServiceWith(SomeUser); const generator = new LegacyPasswordGenerationService( @@ -193,7 +212,7 @@ describe("LegacyPasswordGenerationService", () => { navigation, innerPassword, innerPassphrase, - null, + null!, ); const [result] = await generator.getOptions(); @@ -220,16 +239,16 @@ describe("LegacyPasswordGenerationService", () => { }); it("sets default options when an inner service lacks a value", async () => { - const innerPassword = createPasswordGenerator(null); - const innerPassphrase = createPassphraseGenerator(null); - const navigation = createNavigationGenerator(null); + const innerPassword = createPasswordGenerator(null!); + const innerPassphrase = createPassphraseGenerator(null!); + const navigation = createNavigationGenerator(null!); const accountService = mockAccountServiceWith(SomeUser); const generator = new LegacyPasswordGenerationService( accountService, navigation, innerPassword, innerPassphrase, - null, + null!, ); const [result] = await generator.getOptions(); @@ -277,7 +296,7 @@ describe("LegacyPasswordGenerationService", () => { navigation, innerPassword, innerPassphrase, - null, + null!, ); const [, policy] = await generator.getOptions(); @@ -323,7 +342,7 @@ describe("LegacyPasswordGenerationService", () => { navigation, innerPassword, innerPassphrase, - null, + null!, ); const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options); @@ -363,7 +382,7 @@ describe("LegacyPasswordGenerationService", () => { navigation, innerPassword, innerPassphrase, - null, + null!, ); const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options); @@ -409,7 +428,7 @@ describe("LegacyPasswordGenerationService", () => { navigation, innerPassword, innerPassphrase, - null, + null!, ); const [, policy] = await generator.enforcePasswordGeneratorPoliciesOnOptions({}); @@ -441,7 +460,7 @@ describe("LegacyPasswordGenerationService", () => { navigation, innerPassword, innerPassphrase, - null, + null!, ); const options = { type: "password" as const, @@ -474,7 +493,7 @@ describe("LegacyPasswordGenerationService", () => { navigation, innerPassword, innerPassphrase, - null, + null!, ); const options = { type: "passphrase" as const, @@ -496,7 +515,7 @@ describe("LegacyPasswordGenerationService", () => { const navigation = createNavigationGenerator({ type: "password", username: "forwarded", - forwarder: "firefoxrelay" as IntegrationId, + forwarder: "firefoxrelay" as VendorId, }); const accountService = mockAccountServiceWith(SomeUser); const generator = new LegacyPasswordGenerationService( @@ -504,7 +523,7 @@ describe("LegacyPasswordGenerationService", () => { navigation, innerPassword, innerPassphrase, - null, + null!, ); const options = { type: "passphrase" as const, @@ -533,9 +552,9 @@ describe("LegacyPasswordGenerationService", () => { const accountService = mockAccountServiceWith(SomeUser); const generator = new LegacyPasswordGenerationService( accountService, - null, - null, - null, + null!, + null!, + null!, history, ); @@ -552,9 +571,9 @@ describe("LegacyPasswordGenerationService", () => { const accountService = mockAccountServiceWith(SomeUser); const generator = new LegacyPasswordGenerationService( accountService, - null, - null, - null, + null!, + null!, + null!, history, ); diff --git a/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.spec.ts b/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.spec.ts index 08ffce2eba5..5a4dce4f4a5 100644 --- a/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.spec.ts +++ b/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.spec.ts @@ -1,6 +1,15 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { mock } from "jest-mock-extended"; import { of } from "rxjs"; +import { AddyIo } from "@bitwarden/common/tools/extension/vendor/addyio"; +import { DuckDuckGo } from "@bitwarden/common/tools/extension/vendor/duckduckgo"; +import { Fastmail } from "@bitwarden/common/tools/extension/vendor/fastmail"; +import { ForwardEmail } from "@bitwarden/common/tools/extension/vendor/forwardemail"; +import { Mozilla } from "@bitwarden/common/tools/extension/vendor/mozilla"; +import { SimpleLogin } from "@bitwarden/common/tools/extension/vendor/simplelogin"; +import { IntegrationId } from "@bitwarden/common/tools/integration"; import { UserId } from "@bitwarden/common/types/guid"; import { ApiOptions, @@ -13,13 +22,6 @@ import { DefaultCatchallOptions, DefaultEffUsernameOptions, EffUsernameGenerationOptions, - DefaultAddyIoOptions, - DefaultDuckDuckGoOptions, - DefaultFastmailOptions, - DefaultFirefoxRelayOptions, - DefaultForwardEmailOptions, - DefaultSimpleLoginOptions, - Forwarders, DefaultSubaddressOptions, SubaddressGenerationOptions, policies, @@ -169,7 +171,7 @@ describe("LegacyUsernameGenerationService", () => { // set up an arbitrary forwarder for the username test; all forwarders tested in their own tests const options = { type: "forwarded", - forwardedService: Forwarders.AddyIo.id, + forwardedService: AddyIo.id, } as UsernameGeneratorOptions; const addyIo = createGenerator(null, null); addyIo.generate.mockResolvedValue("addyio@example.com"); @@ -249,7 +251,7 @@ describe("LegacyUsernameGenerationService", () => { describe("generateForwarded", () => { it("should generate a AddyIo username", async () => { const options = { - forwardedService: Forwarders.AddyIo.id, + forwardedService: AddyIo.id, forwardedAnonAddyApiToken: "token", forwardedAnonAddyBaseUrl: "https://example.com", forwardedAnonAddyDomain: "example.com", @@ -284,7 +286,7 @@ describe("LegacyUsernameGenerationService", () => { it("should generate a DuckDuckGo username", async () => { const options = { - forwardedService: Forwarders.DuckDuckGo.id, + forwardedService: DuckDuckGo.id, forwardedDuckDuckGoToken: "token", website: "example.com", } as UsernameGeneratorOptions; @@ -315,7 +317,7 @@ describe("LegacyUsernameGenerationService", () => { it("should generate a Fastmail username", async () => { const options = { - forwardedService: Forwarders.Fastmail.id, + forwardedService: Fastmail.id, forwardedFastmailApiToken: "token", website: "example.com", } as UsernameGeneratorOptions; @@ -346,7 +348,7 @@ describe("LegacyUsernameGenerationService", () => { it("should generate a FirefoxRelay username", async () => { const options = { - forwardedService: Forwarders.FirefoxRelay.id, + forwardedService: Mozilla.id, forwardedFirefoxApiToken: "token", website: "example.com", } as UsernameGeneratorOptions; @@ -377,7 +379,7 @@ describe("LegacyUsernameGenerationService", () => { it("should generate a ForwardEmail username", async () => { const options = { - forwardedService: Forwarders.ForwardEmail.id, + forwardedService: ForwardEmail.id, forwardedForwardEmailApiToken: "token", forwardedForwardEmailDomain: "example.com", website: "example.com", @@ -410,7 +412,7 @@ describe("LegacyUsernameGenerationService", () => { it("should generate a SimpleLogin username", async () => { const options = { - forwardedService: Forwarders.SimpleLogin.id, + forwardedService: SimpleLogin.id, forwardedSimpleLoginApiKey: "token", forwardedSimpleLoginBaseUrl: "https://example.com", website: "example.com", @@ -449,7 +451,7 @@ describe("LegacyUsernameGenerationService", () => { const navigation = createNavigationGenerator({ type: "username", username: "catchall", - forwarder: Forwarders.AddyIo.id, + forwarder: AddyIo.id, }); const catchall = createGenerator( @@ -557,7 +559,7 @@ describe("LegacyUsernameGenerationService", () => { subaddressEmail: "foo@example.com", catchallType: "random", catchallDomain: "example.com", - forwardedService: Forwarders.AddyIo.id, + forwardedService: AddyIo.id, forwardedAnonAddyApiToken: "addyIoToken", forwardedAnonAddyDomain: "addyio.example.com", forwardedAnonAddyBaseUrl: "https://addyio.api.example.com", @@ -583,21 +585,36 @@ describe("LegacyUsernameGenerationService", () => { null, DefaultSubaddressOptions, ); - const addyIo = createGenerator( - null, - DefaultAddyIoOptions, - ); - const duckDuckGo = createGenerator(null, DefaultDuckDuckGoOptions); - const fastmail = createGenerator( - null, - DefaultFastmailOptions, - ); - const firefoxRelay = createGenerator(null, DefaultFirefoxRelayOptions); - const forwardEmail = createGenerator( - null, - DefaultForwardEmailOptions, - ); - const simpleLogin = createGenerator(null, DefaultSimpleLoginOptions); + const addyIo = createGenerator(null, { + website: null, + baseUrl: "https://app.addy.io", + token: "", + domain: "", + }); + const duckDuckGo = createGenerator(null, { + website: null, + token: "", + }); + const fastmail = createGenerator(null, { + website: "", + domain: "", + prefix: "", + token: "", + }); + const firefoxRelay = createGenerator(null, { + website: null, + token: "", + }); + const forwardEmail = createGenerator(null, { + website: null, + token: "", + domain: "", + }); + const simpleLogin = createGenerator(null, { + website: null, + baseUrl: "https://app.simplelogin.io", + token: "", + }); const generator = new LegacyUsernameGenerationService( account, @@ -624,16 +641,16 @@ describe("LegacyUsernameGenerationService", () => { subaddressType: DefaultSubaddressOptions.subaddressType, subaddressEmail: DefaultSubaddressOptions.subaddressEmail, forwardedService: DefaultGeneratorNavigation.forwarder, - forwardedAnonAddyApiToken: DefaultAddyIoOptions.token, - forwardedAnonAddyDomain: DefaultAddyIoOptions.domain, - forwardedAnonAddyBaseUrl: DefaultAddyIoOptions.baseUrl, - forwardedDuckDuckGoToken: DefaultDuckDuckGoOptions.token, - forwardedFastmailApiToken: DefaultFastmailOptions.token, - forwardedFirefoxApiToken: DefaultFirefoxRelayOptions.token, - forwardedForwardEmailApiToken: DefaultForwardEmailOptions.token, - forwardedForwardEmailDomain: DefaultForwardEmailOptions.domain, - forwardedSimpleLoginApiKey: DefaultSimpleLoginOptions.token, - forwardedSimpleLoginBaseUrl: DefaultSimpleLoginOptions.baseUrl, + forwardedAnonAddyApiToken: "", + forwardedAnonAddyDomain: "", + forwardedAnonAddyBaseUrl: "https://app.addy.io", + forwardedDuckDuckGoToken: "", + forwardedFastmailApiToken: "", + forwardedFirefoxApiToken: "", + forwardedForwardEmailApiToken: "", + forwardedForwardEmailDomain: "", + forwardedSimpleLoginApiKey: "", + forwardedSimpleLoginBaseUrl: "https://app.simplelogin.io", }); }); }); @@ -678,7 +695,7 @@ describe("LegacyUsernameGenerationService", () => { subaddressEmail: "foo@example.com", catchallType: "random", catchallDomain: "example.com", - forwardedService: Forwarders.AddyIo.id, + forwardedService: AddyIo.id as IntegrationId, forwardedAnonAddyApiToken: "addyIoToken", forwardedAnonAddyDomain: "addyio.example.com", forwardedAnonAddyBaseUrl: "https://addyio.api.example.com", @@ -697,7 +714,7 @@ describe("LegacyUsernameGenerationService", () => { expect(navigation.saveOptions).toHaveBeenCalledWith(SomeUser, { type: "password", username: "catchall", - forwarder: Forwarders.AddyIo.id, + forwarder: AddyIo.id, }); expect(catchall.saveOptions).toHaveBeenCalledWith(SomeUser, { diff --git a/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.ts b/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.ts index c6e9118535f..48da3404cd6 100644 --- a/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.ts +++ b/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.ts @@ -3,6 +3,7 @@ import { zip, firstValueFrom, map, concatMap, combineLatest } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Vendor } from "@bitwarden/common/tools/extension/vendor/data"; import { IntegrationRequest } from "@bitwarden/common/tools/integration/rpc"; import { UserId } from "@bitwarden/common/types/guid"; import { @@ -14,13 +15,13 @@ import { GeneratorService, CatchallGenerationOptions, EffUsernameGenerationOptions, - Forwarders, SubaddressGenerationOptions, UsernameGeneratorType, ForwarderId, } from "@bitwarden/generator-core"; import { GeneratorNavigationService, GeneratorNavigation } from "@bitwarden/generator-navigation"; +import { Forwarders } from "./forwarders"; import { UsernameGeneratorOptions } from "./username-generation-options"; import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; @@ -89,12 +90,14 @@ export class LegacyUsernameGenerationService implements UsernameGenerationServic const stored = this.toStoredOptions(options); switch (options.forwardedService) { case Forwarders.AddyIo.id: + case Vendor.addyio: return this.addyIo.generate(stored.forwarders.addyIo); case Forwarders.DuckDuckGo.id: return this.duckDuckGo.generate(stored.forwarders.duckDuckGo); case Forwarders.Fastmail.id: return this.fastmail.generate(stored.forwarders.fastmail); case Forwarders.FirefoxRelay.id: + case Vendor.mozilla: return this.firefoxRelay.generate(stored.forwarders.firefoxRelay); case Forwarders.ForwardEmail.id: return this.forwardEmail.generate(stored.forwarders.forwardEmail); @@ -232,22 +235,24 @@ export class LegacyUsernameGenerationService implements UsernameGenerationServic options: MappedOptions, ) { switch (forwarder) { - case "anonaddy": + case Forwarders.AddyIo.id: + case Vendor.addyio: await this.addyIo.saveOptions(account, options.forwarders.addyIo); return true; - case "duckduckgo": + case Forwarders.DuckDuckGo.id: await this.duckDuckGo.saveOptions(account, options.forwarders.duckDuckGo); return true; - case "fastmail": + case Forwarders.Fastmail.id: await this.fastmail.saveOptions(account, options.forwarders.fastmail); return true; - case "firefoxrelay": + case Forwarders.FirefoxRelay.id: + case Vendor.mozilla: await this.firefoxRelay.saveOptions(account, options.forwarders.firefoxRelay); return true; - case "forwardemail": + case Forwarders.ForwardEmail.id: await this.forwardEmail.saveOptions(account, options.forwarders.forwardEmail); return true; - case "simplelogin": + case Forwarders.SimpleLogin.id: await this.simpleLogin.saveOptions(account, options.forwarders.simpleLogin); return true; default: diff --git a/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.spec.ts b/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.spec.ts index 218121a3a75..82b9e29e91a 100644 --- a/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.spec.ts +++ b/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.spec.ts @@ -24,7 +24,7 @@ describe("GeneratorNavigationEvaluator", () => { describe("applyPolicy", () => { it("returns the input options when a policy is not in effect", () => { - const evaluator = new GeneratorNavigationEvaluator(null); + const evaluator = new GeneratorNavigationEvaluator(null!); const options = { type: "password" as const }; const result = evaluator.applyPolicy(options); @@ -54,7 +54,7 @@ describe("GeneratorNavigationEvaluator", () => { }); it("defaults options to the default generator navigation type when a policy is not in effect", () => { - const evaluator = new GeneratorNavigationEvaluator(null); + const evaluator = new GeneratorNavigationEvaluator(null!); const result = evaluator.sanitize({}); diff --git a/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.ts b/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.ts index 2f891cdea03..5446c1f26ad 100644 --- a/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.ts +++ b/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { PasswordAlgorithms, PolicyEvaluator } from "@bitwarden/generator-core"; +import { AlgorithmsByType, PolicyEvaluator, Type } from "@bitwarden/generator-core"; import { DefaultGeneratorNavigation } from "./default-generator-navigation"; import { GeneratorNavigation } from "./generator-navigation"; @@ -19,7 +19,7 @@ export class GeneratorNavigationEvaluator /** {@link PolicyEvaluator.policyInEffect} */ get policyInEffect(): boolean { - return PasswordAlgorithms.includes(this.policy?.overridePasswordType); + return AlgorithmsByType[Type.password].includes(this.policy?.overridePasswordType); } /** Apply policy to the input options. diff --git a/libs/tools/generator/extensions/navigation/src/generator-navigation-policy.ts b/libs/tools/generator/extensions/navigation/src/generator-navigation-policy.ts index cba1f91dad3..fc920a66e0d 100644 --- a/libs/tools/generator/extensions/navigation/src/generator-navigation-policy.ts +++ b/libs/tools/generator/extensions/navigation/src/generator-navigation-policy.ts @@ -4,14 +4,14 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models // implement ADR-0002 import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { PasswordType } from "@bitwarden/generator-core"; +import { PasswordAlgorithm } from "@bitwarden/generator-core"; /** Policy settings affecting password generator navigation */ export type GeneratorNavigationPolicy = { /** The type of generator that should be shown by default when opening * the password generator. */ - overridePasswordType?: PasswordType; + overridePasswordType?: PasswordAlgorithm; }; /** Reduces a policy into an accumulator by preferring the password generator diff --git a/libs/tools/generator/extensions/navigation/src/generator-navigation.ts b/libs/tools/generator/extensions/navigation/src/generator-navigation.ts index 5a35e57d7b4..08e5025244d 100644 --- a/libs/tools/generator/extensions/navigation/src/generator-navigation.ts +++ b/libs/tools/generator/extensions/navigation/src/generator-navigation.ts @@ -1,4 +1,5 @@ -import { GeneratorType, ForwarderId, UsernameGeneratorType } from "@bitwarden/generator-core"; +import { VendorId } from "@bitwarden/common/tools/extension"; +import { UsernameGeneratorType, CredentialAlgorithm } from "@bitwarden/generator-core"; /** Stores credential generator UI state. */ export type GeneratorNavigation = { @@ -6,11 +7,11 @@ export type GeneratorNavigation = { * @remarks The legacy generator only supports "password" and "passphrase". * The componentized generator supports all values. */ - type?: GeneratorType; + type?: CredentialAlgorithm; /** When `type === "username"`, this stores the username algorithm. */ username?: UsernameGeneratorType; /** When `username === "forwarded"`, this stores the forwarder implementation. */ - forwarder?: ForwarderId | ""; + forwarder?: VendorId | ""; }; diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts index 8d24fc4acee..30775aa8a83 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts @@ -28,7 +28,7 @@ import { ToastService, TypographyModule, } from "@bitwarden/components"; -import { CredentialGeneratorService, GenerateRequest, Generators } from "@bitwarden/generator-core"; +import { CredentialGeneratorService, GenerateRequest, Type } from "@bitwarden/generator-core"; import { SendFormConfig } from "../../abstractions/send-form-config.service"; import { SendFormContainer } from "../../send-form-container"; @@ -122,12 +122,12 @@ export class SendOptionsComponent implements OnInit { } generatePassword = async () => { - const on$ = new BehaviorSubject({ source: "send" }); + const on$ = new BehaviorSubject({ source: "send", type: Type.password }); const account$ = this.accountService.activeAccount$.pipe( pin({ name: () => "send-options.component", distinct: (p, c) => p.id === c.id }), ); const generatedCredential = await firstValueFrom( - this.generatorService.generate$(Generators.password, { on$, account$ }), + this.generatorService.generate$({ on$, account$ }), ); this.sendOptionsForm.patchValue({ From b2d38249ba33416faa8c88ed361ae18cc330ff86 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 27 May 2025 16:14:36 +0200 Subject: [PATCH 150/163] Remove class user-select: The copy button contains all the information and is the preferred method (#14944) Co-authored-by: Daniel James Smith --- .../popup/settings/about-dialog/about-dialog.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/tools/popup/settings/about-dialog/about-dialog.component.html b/apps/browser/src/tools/popup/settings/about-dialog/about-dialog.component.html index 40dad4cde4b..af68959fe5d 100644 --- a/apps/browser/src/tools/popup/settings/about-dialog/about-dialog.component.html +++ b/apps/browser/src/tools/popup/settings/about-dialog/about-dialog.component.html @@ -6,7 +6,7 @@

© Bitwarden Inc. 2015-{{ year }}

-
+

{{ "version" | i18n }}: {{ version$ | async }}

SDK: {{ sdkVersion$ | async }}

From d1aa8422e0319ac3b33dd5d9365162ecfe3fa4e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Tue, 27 May 2025 11:09:50 -0400 Subject: [PATCH 151/163] align sdk generator types (#14967) --- .../core/src/metadata/password/sdk-eff-word-list.spec.ts | 3 ++- .../core/src/metadata/password/sdk-eff-word-list.ts | 9 +++------ .../src/metadata/password/sdk-random-password.spec.ts | 3 ++- .../core/src/metadata/password/sdk-random-password.ts | 9 +++------ 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/libs/tools/generator/core/src/metadata/password/sdk-eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/password/sdk-eff-word-list.spec.ts index 03f1275f73c..29378b4cdd3 100644 --- a/libs/tools/generator/core/src/metadata/password/sdk-eff-word-list.spec.ts +++ b/libs/tools/generator/core/src/metadata/password/sdk-eff-word-list.spec.ts @@ -5,7 +5,8 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { SdkPasswordRandomizer } from "../../engine"; import { PassphrasePolicyConstraints } from "../../policies"; -import { PassphraseGenerationOptions, GeneratorDependencyProvider } from "../../types"; +import { GeneratorDependencyProvider } from "../../providers"; +import { PassphraseGenerationOptions } from "../../types"; import { Profile } from "../data"; import { CoreProfileMetadata } from "../profile-metadata"; import { isCoreProfile } from "../util"; diff --git a/libs/tools/generator/core/src/metadata/password/sdk-eff-word-list.ts b/libs/tools/generator/core/src/metadata/password/sdk-eff-word-list.ts index 802ef0ef068..8892016b609 100644 --- a/libs/tools/generator/core/src/metadata/password/sdk-eff-word-list.ts +++ b/libs/tools/generator/core/src/metadata/password/sdk-eff-word-list.ts @@ -6,17 +6,14 @@ import { BitwardenClient } from "@bitwarden/sdk-internal"; import { SdkPasswordRandomizer } from "../../engine"; import { passphraseLeastPrivilege, PassphrasePolicyConstraints } from "../../policies"; -import { - CredentialGenerator, - GeneratorDependencyProvider, - PassphraseGenerationOptions, -} from "../../types"; +import { GeneratorDependencyProvider } from "../../providers"; +import { CredentialGenerator, PassphraseGenerationOptions } from "../../types"; import { Algorithm, Profile, Type } from "../data"; import { GeneratorMetadata } from "../generator-metadata"; const sdkPassphrase: GeneratorMetadata = { id: Algorithm.sdkPassphrase, - category: Type.password, + type: Type.password, weight: 130, i18nKeys: { name: "passphrase", diff --git a/libs/tools/generator/core/src/metadata/password/sdk-random-password.spec.ts b/libs/tools/generator/core/src/metadata/password/sdk-random-password.spec.ts index cec4704789f..1e9cf6dbd87 100644 --- a/libs/tools/generator/core/src/metadata/password/sdk-random-password.spec.ts +++ b/libs/tools/generator/core/src/metadata/password/sdk-random-password.spec.ts @@ -5,7 +5,8 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { SdkPasswordRandomizer } from "../../engine"; import { DynamicPasswordPolicyConstraints } from "../../policies"; -import { PasswordGenerationOptions, GeneratorDependencyProvider } from "../../types"; +import { GeneratorDependencyProvider } from "../../providers"; +import { PasswordGenerationOptions } from "../../types"; import { Profile } from "../data"; import { CoreProfileMetadata } from "../profile-metadata"; import { isCoreProfile } from "../util"; diff --git a/libs/tools/generator/core/src/metadata/password/sdk-random-password.ts b/libs/tools/generator/core/src/metadata/password/sdk-random-password.ts index d4bb6263b1e..d6544fa115e 100644 --- a/libs/tools/generator/core/src/metadata/password/sdk-random-password.ts +++ b/libs/tools/generator/core/src/metadata/password/sdk-random-password.ts @@ -6,17 +6,14 @@ import { BitwardenClient } from "@bitwarden/sdk-internal"; import { SdkPasswordRandomizer } from "../../engine"; import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies"; -import { - CredentialGenerator, - GeneratorDependencyProvider, - PasswordGeneratorSettings, -} from "../../types"; +import { GeneratorDependencyProvider } from "../../providers"; +import { CredentialGenerator, PasswordGeneratorSettings } from "../../types"; import { Algorithm, Profile, Type } from "../data"; import { GeneratorMetadata } from "../generator-metadata"; const sdkPassword: GeneratorMetadata = deepFreeze({ id: Algorithm.sdkPassword, - category: Type.password, + type: Type.password, weight: 120, i18nKeys: { name: "password", From abb01d9038819f39e8937dec6e995b9db62f7e44 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Tue, 27 May 2025 11:14:10 -0400 Subject: [PATCH 152/163] ensure loginview properties have correct defaults when usding SDK decryption (#14948) --- libs/common/src/vault/models/view/login.view.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/vault/models/view/login.view.ts b/libs/common/src/vault/models/view/login.view.ts index 41568f643d5..6bdc23f42b1 100644 --- a/libs/common/src/vault/models/view/login.view.ts +++ b/libs/common/src/vault/models/view/login.view.ts @@ -118,7 +118,7 @@ export class LoginView extends ItemView { const passwordRevisionDate = obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); - const uris = obj.uris?.map((uri) => LoginUriView.fromSdkLoginUriView(uri)); + const uris = obj.uris?.map((uri) => LoginUriView.fromSdkLoginUriView(uri)) || []; return Object.assign(new LoginView(), obj, { passwordRevisionDate, From 0f9f6a1c5ddfccc2632011066375cb1b847a6a47 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 18:58:56 +0200 Subject: [PATCH 153/163] [deps] Tools: Update papaparse to v5.5.3 (#14966) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 18 +++++++++--------- package.json | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 4c8d2918ce1..eeebf4dad6f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -86,7 +86,7 @@ "node-fetch": "2.6.12", "node-forge": "1.3.1", "open": "8.4.2", - "papaparse": "5.5.2", + "papaparse": "5.5.3", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", "tldts": "7.0.1", diff --git a/package-lock.json b/package-lock.json index ff21acbc20b..f0dd03b5e74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "node-forge": "1.3.1", "oidc-client-ts": "2.4.1", "open": "8.4.2", - "papaparse": "5.5.2", + "papaparse": "5.5.3", "patch-package": "8.0.0", "proper-lockfile": "4.1.2", "qrcode-parser": "2.1.3", @@ -111,7 +111,7 @@ "@types/node": "22.15.3", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", - "@types/papaparse": "5.3.15", + "@types/papaparse": "5.3.16", "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", @@ -219,7 +219,7 @@ "node-fetch": "2.6.12", "node-forge": "1.3.1", "open": "8.4.2", - "papaparse": "5.5.2", + "papaparse": "5.5.3", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", "tldts": "7.0.1", @@ -11939,9 +11939,9 @@ } }, "node_modules/@types/papaparse": { - "version": "5.3.15", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.15.tgz", - "integrity": "sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.16.tgz", + "integrity": "sha512-T3VuKMC2H0lgsjI9buTB3uuKj3EMD2eap1MOuEQuBQ44EnDx/IkGhU6EwiTf9zG3za4SKlmwKAImdDKdNnCsXg==", "dev": true, "license": "MIT", "dependencies": { @@ -30738,9 +30738,9 @@ "license": "(MIT AND Zlib)" }, "node_modules/papaparse": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.2.tgz", - "integrity": "sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", "license": "MIT" }, "node_modules/param-case": { diff --git a/package.json b/package.json index f63db41e1f3..fe7f69ff40d 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@types/node": "22.15.3", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", - "@types/papaparse": "5.3.15", + "@types/papaparse": "5.3.16", "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", @@ -194,7 +194,7 @@ "node-forge": "1.3.1", "oidc-client-ts": "2.4.1", "open": "8.4.2", - "papaparse": "5.5.2", + "papaparse": "5.5.3", "patch-package": "8.0.0", "proper-lockfile": "4.1.2", "qrcode-parser": "2.1.3", From 5f169af08e69c4b957df3e5c9aa6baef56cb4616 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 27 May 2025 10:03:29 -0700 Subject: [PATCH 154/163] [PM-21122] - Hide orgs in product switcher for single org policy users (#14803) * don't display orgs in account switcher for single org policy users * add comment * add test case * fix storybook * fix storybook again * use variable name instead of comment --- .../navigation-switcher.stories.ts | 7 +++++++ .../product-switcher/product-switcher.stories.ts | 7 +++++++ .../shared/product-switcher.service.spec.ts | 16 ++++++++++++++++ .../shared/product-switcher.service.ts | 16 ++++++++++++++-- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts index b1c1a0a906a..0ecec9d8944 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts @@ -5,6 +5,7 @@ import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; @@ -130,6 +131,12 @@ export default { return new I18nMockService(translations); }, }, + { + provide: PolicyService, + useValue: { + policyAppliesToUser$: () => of(false), + }, + }, ], }), applicationConfig({ diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts index 4525105e579..0b7304a3657 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts @@ -5,6 +5,7 @@ import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; @@ -126,6 +127,12 @@ export default { }); }, }, + { + provide: PolicyService, + useValue: { + policyAppliesToUser$: () => of(false), + }, + }, ], }), applicationConfig({ diff --git a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts index a1ac434d590..4abd85d7991 100644 --- a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts @@ -7,6 +7,7 @@ import { Observable, firstValueFrom, of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; @@ -27,6 +28,7 @@ describe("ProductSwitcherService", () => { let accountService: FakeAccountService; let platformUtilsService: MockProxy; let activeRouteParams = convertToParamMap({ organizationId: "1234" }); + let singleOrgPolicyEnabled = false; const getLastSync = jest.fn().mockResolvedValue(new Date("2024-05-14")); const userId = Utils.newGuid() as UserId; @@ -77,6 +79,12 @@ describe("ProductSwitcherService", () => { provide: SyncService, useValue: { getLastSync }, }, + { + provide: PolicyService, + useValue: { + policyAppliesToUser$: () => of(singleOrgPolicyEnabled), + }, + }, ], }); }); @@ -184,6 +192,14 @@ describe("ProductSwitcherService", () => { expect(products.bento.find((p) => p.name === "Admin Console")).toBeDefined(); expect(products.other.find((p) => p.name === "Organizations")).toBeUndefined(); }); + + it("does not include Organizations when the user's single org policy is enabled", async () => { + singleOrgPolicyEnabled = true; + initiateService(); + const products = await firstValueFrom(service.products$); + + expect(products.other.find((p) => p.name === "Organizations")).not.toBeDefined(); + }); }); describe("Provider Portal", () => { diff --git a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts index ec0d2c2651c..53ec3b0840f 100644 --- a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts +++ b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts @@ -6,6 +6,7 @@ import { combineLatest, concatMap, filter, + firstValueFrom, map, Observable, ReplaySubject, @@ -18,10 +19,12 @@ import { canAccessOrgAdmin, OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderType } from "@bitwarden/common/admin-console/enums"; +import { PolicyType, ProviderType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; @@ -104,6 +107,7 @@ export class ProductSwitcherService { private syncService: SyncService, private accountService: AccountService, private platformUtilsService: PlatformUtilsService, + private policyService: PolicyService, ) { this.pollUntilSynced(); } @@ -235,7 +239,15 @@ export class ProductSwitcherService { if (acOrg) { bento.push(products.ac); } else { - other.push(products.orgs); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getUserId), + ); + const userHasSingleOrgPolicy = await firstValueFrom( + this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, activeUserId), + ); + if (!userHasSingleOrgPolicy) { + other.push(products.orgs); + } } if (providers.length > 0) { From 677a435cadddf26dbca9461d78a0950c52360c5f Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 27 May 2025 10:05:58 -0700 Subject: [PATCH 155/163] prevent showing ssh key when cipher changes in desktop view (#14913) --- apps/desktop/src/vault/app/vault/view.component.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/vault/app/vault/view.component.ts b/apps/desktop/src/vault/app/vault/view.component.ts index 68bf8d6d1a3..33dfc600265 100644 --- a/apps/desktop/src/vault/app/vault/view.component.ts +++ b/apps/desktop/src/vault/app/vault/view.component.ts @@ -9,6 +9,7 @@ import { OnDestroy, OnInit, Output, + SimpleChanges, } from "@angular/core"; import { ViewComponent as BaseViewComponent } from "@bitwarden/angular/vault/components/view.component"; @@ -130,7 +131,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); } - async ngOnChanges() { + async ngOnChanges(changes: SimpleChanges) { if (this.cipher?.decryptionFailure) { DecryptionFailureDialogComponent.open(this.dialogService, { cipherIds: [this.cipherId as CipherId], @@ -138,6 +139,12 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro return; } this.passwordReprompted = this.masterPasswordAlreadyPrompted; + + if (changes["cipherId"]) { + if (changes["cipherId"].currentValue !== changes["cipherId"].previousValue) { + this.showPrivateKey = false; + } + } } viewHistory() { From 5423ab326898c77950172b4f8cdd852a7f12d1a5 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 27 May 2025 19:13:15 +0200 Subject: [PATCH 156/163] [PM-21934] Upgrade to eslint 9 (#14754) Upgrades to Eslint v9. Since this is a major version there were breaking changes, but since we've previously migrated to flat configs in #12806 those were minimal. --- .github/renovate.json5 | 2 + .../vault-timeout/vault-timeout.service.ts | 1 - .../popup/services/browser-router.service.ts | 2 + .../view-cache/popup-router-cache.service.ts | 2 + apps/cli/src/utils.ts | 1 - .../organization-reporting-routing.module.ts | 6 +- .../change-plan-dialog.component.ts | 2 + .../organization-payment-method.component.ts | 2 + .../shared/payment-method.component.ts | 2 + apps/web/src/app/core/router.service.ts | 3 + eslint.config.mjs | 6 +- .../device-trust.service.implementation.ts | 4 + libs/eslint/fix-jsdom.ts | 10 + libs/eslint/jest.config.js | 1 + libs/eslint/test.setup.mjs | 2 - libs/eslint/tsconfig.json | 3 +- .../dashlane/dashlane-csv-importer.ts | 2 +- package-lock.json | 2758 ++++++++++++----- package.json | 9 +- 19 files changed, 1961 insertions(+), 857 deletions(-) create mode 100644 libs/eslint/fix-jsdom.ts diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 1b84ccdab01..f30bc06e4a2 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -59,6 +59,7 @@ { matchPackageNames: [ "@angular-eslint/schematics", + "@eslint/compat", "@typescript-eslint/rule-tester", "@typescript-eslint/utils", "angular-eslint", @@ -81,6 +82,7 @@ { matchPackageNames: [ "@angular-eslint/schematics", + "@eslint/compat", "@typescript-eslint/rule-tester", "@typescript-eslint/utils", "angular-eslint", diff --git a/apps/browser/src/key-management/vault-timeout/vault-timeout.service.ts b/apps/browser/src/key-management/vault-timeout/vault-timeout.service.ts index 51f90fb98a6..c0de04b02d7 100644 --- a/apps/browser/src/key-management/vault-timeout/vault-timeout.service.ts +++ b/apps/browser/src/key-management/vault-timeout/vault-timeout.service.ts @@ -17,7 +17,6 @@ export default class VaultTimeoutService extends BaseVaultTimeoutService { // setIntervals. It works by calling the native extension which sleeps for 10s, // efficiently replicating setInterval. async checkSafari() { - // eslint-disable-next-line while (true) { try { await SafariApp.sendMessageToApp("sleep"); diff --git a/apps/browser/src/platform/popup/services/browser-router.service.ts b/apps/browser/src/platform/popup/services/browser-router.service.ts index 413bde5fcad..2d449b8a0f2 100644 --- a/apps/browser/src/platform/popup/services/browser-router.service.ts +++ b/apps/browser/src/platform/popup/services/browser-router.service.ts @@ -21,6 +21,8 @@ export class BrowserRouterService { child = child.firstChild; } + // TODO: Eslint upgrade. Please resolve this since the ?? does nothing + // eslint-disable-next-line no-constant-binary-expression const updateUrl = !child?.data?.doNotSaveUrl ?? true; if (updateUrl) { diff --git a/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts b/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts index aa0c0854eff..5fc508ac2a6 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts @@ -62,6 +62,8 @@ export class PopupRouterCacheService { child = child.firstChild; } + // TODO: Eslint upgrade. Please resolve this since the ?? does nothing + // eslint-disable-next-line no-constant-binary-expression return !child?.data?.doNotSaveUrl ?? true; }), switchMap((event) => this.push(event.url)), diff --git a/apps/cli/src/utils.ts b/apps/cli/src/utils.ts index fadede9c71b..e321adbfd5e 100644 --- a/apps/cli/src/utils.ts +++ b/apps/cli/src/utils.ts @@ -161,7 +161,6 @@ export class CliUtils { process.stdin.setEncoding("utf8"); process.stdin.on("readable", () => { - // eslint-disable-next-line while (true) { const chunk = process.stdin.read(); if (chunk == null) { diff --git a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts index 23751653331..4c825b26bb2 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts @@ -9,12 +9,16 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +// eslint-disable-next-line no-restricted-imports import { ExposedPasswordsReportComponent } from "../../../dirt/reports/pages/organizations/exposed-passwords-report.component"; +// eslint-disable-next-line no-restricted-imports import { InactiveTwoFactorReportComponent } from "../../../dirt/reports/pages/organizations/inactive-two-factor-report.component"; +// eslint-disable-next-line no-restricted-imports import { ReusedPasswordsReportComponent } from "../../../dirt/reports/pages/organizations/reused-passwords-report.component"; +// eslint-disable-next-line no-restricted-imports import { UnsecuredWebsitesReportComponent } from "../../../dirt/reports/pages/organizations/unsecured-websites-report.component"; +// eslint-disable-next-line no-restricted-imports import { WeakPasswordsReportComponent } from "../../../dirt/reports/pages/organizations/weak-passwords-report.component"; -/* eslint no-restricted-imports: "error" */ import { isPaidOrgGuard } from "../guards/is-paid-org.guard"; import { organizationPermissionsGuard } from "../guards/org-permissions.guard"; import { organizationRedirectGuard } from "../guards/org-redirect.guard"; diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index 9b6694a3bbe..a6e8670d944 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -607,6 +607,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return ( plan.PasswordManager.additionalStoragePricePerGb * + // TODO: Eslint upgrade. Please resolve this since the null check does nothing + // eslint-disable-next-line no-constant-binary-expression Math.abs(this.sub?.maxStorageGb ? this.sub?.maxStorageGb - 1 : 0 || 0) ); } diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts index c896ee6404c..fbd7453c712 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts @@ -148,6 +148,8 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { paymentSource, ); } + // TODO: Eslint upgrade. Please resolve this since the ?? does nothing + // eslint-disable-next-line no-constant-binary-expression this.isUnpaid = this.subscriptionStatus === "unpaid" ?? false; // If the flag `launchPaymentModalAutomatically` is set to true, // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index 27c9caf7186..74793bccc01 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -143,6 +143,8 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { [this.billing, this.sub] = await Promise.all([billingPromise, subPromise]); } + // TODO: Eslint upgrade. Please resolve this since the ?? does nothing + // eslint-disable-next-line no-constant-binary-expression this.isUnpaid = this.subscription?.status === "unpaid" ?? false; this.loading = false; // If the flag `launchPaymentModalAutomatically` is set to true, diff --git a/apps/web/src/app/core/router.service.ts b/apps/web/src/app/core/router.service.ts index ff0aea47b9a..603c171e95b 100644 --- a/apps/web/src/app/core/router.service.ts +++ b/apps/web/src/app/core/router.service.ts @@ -74,6 +74,9 @@ export class RouterService { const titleId: string = child?.snapshot?.data?.titleId; const rawTitle: string = child?.snapshot?.data?.title; + + // TODO: Eslint upgrade. Please resolve this since the ?? does nothing + // eslint-disable-next-line no-constant-binary-expression const updateUrl = !child?.snapshot?.data?.doNotSaveUrl ?? true; if (titleId != null || rawTitle != null) { diff --git a/eslint.config.mjs b/eslint.config.mjs index aa0b475da0b..5a4874457a0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,5 +1,5 @@ // @ts-check - +import { fixupPluginRules } from "@eslint/compat"; import eslint from "@eslint/js"; import tseslint from "typescript-eslint"; import angular from "angular-eslint"; @@ -31,8 +31,8 @@ export default tseslint.config( reportUnusedDisableDirectives: "error", }, plugins: { - rxjs: rxjs, - "rxjs-angular": angularRxjs, + rxjs: fixupPluginRules(rxjs), + "rxjs-angular": fixupPluginRules(angularRxjs), "@bitwarden/platform": platformPlugins, }, languageOptions: { diff --git a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts index 84ebf981f03..ecfeb10dcda 100644 --- a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts +++ b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts @@ -89,6 +89,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { ) { this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe( map((options) => { + // TODO: Eslint upgrade. Please resolve this since the ?? does nothing + // eslint-disable-next-line no-constant-binary-expression return options?.trustedDeviceOption != null ?? false; }), ); @@ -97,6 +99,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { supportsDeviceTrustByUserId$(userId: UserId): Observable { return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId).pipe( map((options) => { + // TODO: Eslint upgrade. Please resolve this since the ?? does nothing + // eslint-disable-next-line no-constant-binary-expression return options?.trustedDeviceOption != null ?? false; }), ); diff --git a/libs/eslint/fix-jsdom.ts b/libs/eslint/fix-jsdom.ts new file mode 100644 index 00000000000..f7c29a62723 --- /dev/null +++ b/libs/eslint/fix-jsdom.ts @@ -0,0 +1,10 @@ +import JSDOMEnvironment from "jest-environment-jsdom"; + +export default class FixJSDOMEnvironment extends JSDOMEnvironment { + constructor(...args: ConstructorParameters) { + super(...args); + + // FIXME https://github.com/jsdom/jsdom/issues/3363 + this.global.structuredClone = structuredClone; + } +} diff --git a/libs/eslint/jest.config.js b/libs/eslint/jest.config.js index 67436ff906e..5acadb023e4 100644 --- a/libs/eslint/jest.config.js +++ b/libs/eslint/jest.config.js @@ -3,6 +3,7 @@ const sharedConfig = require("../../libs/shared/jest.config.angular"); /** @type {import('jest').Config} */ module.exports = { ...sharedConfig, + testEnvironment: "./fix-jsdom.ts", testMatch: ["**/+(*.)+(spec).+(mjs)"], displayName: "libs/eslint tests", preset: "jest-preset-angular", diff --git a/libs/eslint/test.setup.mjs b/libs/eslint/test.setup.mjs index 45de6fcf3e1..19f35df9e7f 100644 --- a/libs/eslint/test.setup.mjs +++ b/libs/eslint/test.setup.mjs @@ -1,5 +1,3 @@ -/* eslint-disable no-undef */ - import { clearImmediate, setImmediate } from "node:timers"; Object.defineProperties(globalThis, { diff --git a/libs/eslint/tsconfig.json b/libs/eslint/tsconfig.json index eb2bb889d1a..bbf065886c4 100644 --- a/libs/eslint/tsconfig.json +++ b/libs/eslint/tsconfig.json @@ -1,5 +1,6 @@ { "extends": "../shared/tsconfig", "compilerOptions": {}, - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "files": ["empty.ts"] } diff --git a/libs/importer/src/importers/dashlane/dashlane-csv-importer.ts b/libs/importer/src/importers/dashlane/dashlane-csv-importer.ts index 5b282fb466c..2ead8c4bb21 100644 --- a/libs/importer/src/importers/dashlane/dashlane-csv-importer.ts +++ b/libs/importer/src/importers/dashlane/dashlane-csv-importer.ts @@ -92,7 +92,7 @@ export class DashlaneCsvImporter extends BaseImporter implements Importer { this.parseIdRecord(cipher, row); } - if ((rowKeys[0] === "type") != null && rowKeys[1] === "title") { + if (rowKeys[0] === "type" && rowKeys[1] === "title") { this.parsePersonalInformationRecord(cipher, row); } diff --git a/package-lock.json b/package-lock.json index f0dd03b5e74..691705cc280 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,6 +81,7 @@ "@compodoc/compodoc": "1.1.26", "@electron/notarize": "2.5.0", "@electron/rebuild": "3.7.2", + "@eslint/compat": "1.2.9", "@lit-labs/signals": "0.1.2", "@ngtools/webpack": "18.2.19", "@storybook/addon-a11y": "8.6.12", @@ -136,7 +137,7 @@ "electron-reload": "2.0.0-alpha.1", "electron-store": "8.2.0", "electron-updater": "6.6.4", - "eslint": "8.57.1", + "eslint": "9.26.0", "eslint-config-prettier": "10.1.2", "eslint-import-resolver-typescript": "4.3.4", "eslint-plugin-import": "2.31.0", @@ -398,14 +399,14 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1902.12", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.12.tgz", - "integrity": "sha512-LfUc7k84YL290hAxsG+FvjQpXugQXyw5aDzrQQB4iTYhBgaABu2aaNOU4eu3JH+F8NeXd2EBF/YMr2LDSkYlMw==", + "version": "0.1902.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.13.tgz", + "integrity": "sha512-ZMj+PjK22Ph2U8usG6L7LqEfvWlbaOvmiWXSrEt9YiC9QJt6rsumCkOgUIsmHQtucm/lK+9CMtyYdwH2fYycjg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@angular-devkit/core": "19.2.12", + "@angular-devkit/core": "19.2.13", "rxjs": "7.8.1" }, "engines": { @@ -762,6 +763,20 @@ "@types/send": "*" } }, + "node_modules/@angular-devkit/build-angular/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -828,6 +843,48 @@ "webpack": ">=5" } }, + "node_modules/@angular-devkit/build-angular/node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, "node_modules/@angular-devkit/build-angular/node_modules/browserslist": { "version": "4.24.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", @@ -899,6 +956,19 @@ "node": ">= 6" } }, + "node_modules/@angular-devkit/build-angular/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -906,6 +976,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@angular-devkit/build-angular/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", @@ -968,6 +1055,116 @@ "node": ">=4.0" } }, + "node_modules/@angular-devkit/build-angular/node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-devkit/build-angular/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-devkit/build-angular/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -1003,6 +1200,19 @@ "node": ">= 14" } }, + "node_modules/@angular-devkit/build-angular/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/immutable": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", @@ -1010,6 +1220,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@angular-devkit/build-angular/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/is-plain-obj": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", @@ -1076,6 +1296,62 @@ "dev": true, "license": "ISC" }, + "node_modules/@angular-devkit/build-angular/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/mini-css-extract-plugin": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", @@ -1097,6 +1373,16 @@ "webpack": "^5.0.0" } }, + "node_modules/@angular-devkit/build-angular/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/open": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", @@ -1133,6 +1419,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@angular-devkit/build-angular/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@angular-devkit/build-angular/node_modules/postcss": { "version": "8.4.41", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", @@ -1162,6 +1455,38 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/@angular-devkit/build-angular/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1263,6 +1588,88 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-devkit/build-angular/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/webpack": { "version": "5.94.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", @@ -1578,9 +1985,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "19.2.12", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.12.tgz", - "integrity": "sha512-v5pdfZHZ8MTZozfpkhKoPFBpXQW+2GFbTfdyis8FBtevJWCbIsCR3xhodgI4jwzkSEAraN4oVtWvSytdNyBC6A==", + "version": "19.2.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.13.tgz", + "integrity": "sha512-iq73hE5Uvms1w3uMUSk4i4NDXDMQ863VAifX8LOTadhG6U0xISjNJ11763egVCxQmaKmg7zbG4rda88wHJATzA==", "dev": true, "license": "MIT", "peer": true, @@ -6577,17 +6984,97 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/compat": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.9.tgz", + "integrity": "sha512-gCdSY54n7k+driCadyMNv8JSPzYLeDVM/ikZRtvtROBpRdFSkS8W9A82MqsaY7lZuwL0wiapgD0NT1xT0hyJsA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.10.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -6595,7 +7082,7 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6630,16 +7117,13 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6675,27 +7159,38 @@ "node": "*" } }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", + "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", "dev": true, "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@figspec/components": { @@ -6796,44 +7291,42 @@ "@hapi/hoek": "^9.0.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" }, "engines": { - "node": ">=10.10.0" + "node": ">=18.18.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "license": "Apache-2.0", "engines": { - "node": "*" + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/module-importer": { @@ -6850,13 +7343,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@inquirer/checkbox": { "version": "2.5.0", @@ -8126,6 +8625,66 @@ } } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.0.tgz", + "integrity": "sha512-m//7RlINx1F3sz3KqwY1WWzVgTcYX52HYk4bJ1hkBXV3zccAEth+jRvG8DBRrdaQuRsPAJOx2MH3zaHNCKL7Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/@msgpack/msgpack": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", @@ -9897,6 +10456,16 @@ "encoding": "^0.1.13" } }, + "node_modules/@sigstore/sign/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@sigstore/sign/node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -10540,6 +11109,29 @@ } } }, + "node_modules/@storybook/builder-webpack5/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@storybook/builder-webpack5/node_modules/style-loader": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", @@ -10883,9 +11475,9 @@ } }, "node_modules/@swc/core": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz", - "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.29.tgz", + "integrity": "sha512-g4mThMIpWbNhV8G2rWp5a5/Igv8/2UFRJx2yImrLGMgrDDYZIopqZ/z0jZxDgqNA1QDx93rpwNF7jGsxVWcMlA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -10901,16 +11493,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.11.24", - "@swc/core-darwin-x64": "1.11.24", - "@swc/core-linux-arm-gnueabihf": "1.11.24", - "@swc/core-linux-arm64-gnu": "1.11.24", - "@swc/core-linux-arm64-musl": "1.11.24", - "@swc/core-linux-x64-gnu": "1.11.24", - "@swc/core-linux-x64-musl": "1.11.24", - "@swc/core-win32-arm64-msvc": "1.11.24", - "@swc/core-win32-ia32-msvc": "1.11.24", - "@swc/core-win32-x64-msvc": "1.11.24" + "@swc/core-darwin-arm64": "1.11.29", + "@swc/core-darwin-x64": "1.11.29", + "@swc/core-linux-arm-gnueabihf": "1.11.29", + "@swc/core-linux-arm64-gnu": "1.11.29", + "@swc/core-linux-arm64-musl": "1.11.29", + "@swc/core-linux-x64-gnu": "1.11.29", + "@swc/core-linux-x64-musl": "1.11.29", + "@swc/core-win32-arm64-msvc": "1.11.29", + "@swc/core-win32-ia32-msvc": "1.11.29", + "@swc/core-win32-x64-msvc": "1.11.29" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -10922,9 +11514,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz", - "integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.29.tgz", + "integrity": "sha512-whsCX7URzbuS5aET58c75Dloby3Gtj/ITk2vc4WW6pSDQKSPDuONsIcZ7B2ng8oz0K6ttbi4p3H/PNPQLJ4maQ==", "cpu": [ "arm64" ], @@ -10939,9 +11531,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz", - "integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.29.tgz", + "integrity": "sha512-S3eTo/KYFk+76cWJRgX30hylN5XkSmjYtCBnM4jPLYn7L6zWYEPajsFLmruQEiTEDUg0gBEWLMNyUeghtswouw==", "cpu": [ "x64" ], @@ -10956,9 +11548,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz", - "integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.29.tgz", + "integrity": "sha512-o9gdshbzkUMG6azldHdmKklcfrcMx+a23d/2qHQHPDLUPAN+Trd+sDQUYArK5Fcm7TlpG4sczz95ghN0DMkM7g==", "cpu": [ "arm" ], @@ -10973,9 +11565,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz", - "integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.29.tgz", + "integrity": "sha512-sLoaciOgUKQF1KX9T6hPGzvhOQaJn+3DHy4LOHeXhQqvBgr+7QcZ+hl4uixPKTzxk6hy6Hb0QOvQEdBAAR1gXw==", "cpu": [ "arm64" ], @@ -10990,9 +11582,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz", - "integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.29.tgz", + "integrity": "sha512-PwjB10BC0N+Ce7RU/L23eYch6lXFHz7r3NFavIcwDNa/AAqywfxyxh13OeRy+P0cg7NDpWEETWspXeI4Ek8otw==", "cpu": [ "arm64" ], @@ -11007,9 +11599,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz", - "integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.29.tgz", + "integrity": "sha512-i62vBVoPaVe9A3mc6gJG07n0/e7FVeAvdD9uzZTtGLiuIfVfIBta8EMquzvf+POLycSk79Z6lRhGPZPJPYiQaA==", "cpu": [ "x64" ], @@ -11024,9 +11616,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz", - "integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.29.tgz", + "integrity": "sha512-YER0XU1xqFdK0hKkfSVX1YIyCvMDI7K07GIpefPvcfyNGs38AXKhb2byySDjbVxkdl4dycaxxhRyhQ2gKSlsFQ==", "cpu": [ "x64" ], @@ -11041,9 +11633,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz", - "integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.29.tgz", + "integrity": "sha512-po+WHw+k9g6FAg5IJ+sMwtA/fIUL3zPQ4m/uJgONBATCVnDDkyW6dBA49uHNVtSEvjvhuD8DVWdFP847YTcITw==", "cpu": [ "arm64" ], @@ -11058,9 +11650,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz", - "integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.29.tgz", + "integrity": "sha512-h+NjOrbqdRBYr5ItmStmQt6x3tnhqgwbj9YxdGPepbTDamFv7vFnhZR0YfB3jz3UKJ8H3uGJ65Zw1VsC+xpFkg==", "cpu": [ "ia32" ], @@ -11075,9 +11667,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz", - "integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.29.tgz", + "integrity": "sha512-Q8cs2BDV9wqDvqobkXOYdC+pLUSEpX/KvI0Dgfun1F+LzuLotRFuDhrvkU9ETJA6OnD2+Fn/ieHgloiKA/Mn/g==", "cpu": [ "x64" ], @@ -11505,9 +12097,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "dev": true, "license": "MIT" }, @@ -11827,9 +12419,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.17.16", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", - "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz", + "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==", "dev": true, "license": "MIT" }, @@ -11928,6 +12520,29 @@ "node": ">= 6" } }, + "node_modules/@types/node-fetch/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@types/node-fetch/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@types/node-forge": { "version": "1.3.11", "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", @@ -12709,13 +13324,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, "node_modules/@unrs/resolver-binding-darwin-arm64": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.2.tgz", @@ -13524,13 +14132,14 @@ } }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { "node": ">= 0.6" @@ -15099,87 +15708,24 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/body-parser/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, "node_modules/bonjour-service": { @@ -15713,6 +16259,27 @@ "node": ">= 6.0.0" } }, + "node_modules/cache-content-type/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cache-content-type/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -16357,6 +16924,76 @@ "node": ">=8.0.0" } }, + "node_modules/co-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co-body/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/co-body/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/co-body/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/co-body/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/co-body/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/code-block-writer": { "version": "13.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", @@ -16812,9 +17449,10 @@ "license": "MIT" }, "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -16839,9 +17477,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", "engines": { @@ -16849,11 +17487,14 @@ } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } }, "node_modules/cookies": { "version": "0.9.1", @@ -18008,16 +18649,16 @@ } }, "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, "engines": { - "node": ">=6.0.0" + "node": ">=0.10.0" } }, "node_modules/dom-accessibility-api": { @@ -18444,9 +19085,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.155", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", - "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", + "version": "1.5.157", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.157.tgz", + "integrity": "sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==", "dev": true, "license": "ISC" }, @@ -18497,9 +19138,9 @@ } }, "node_modules/electron/node_modules/@types/node": { - "version": "20.17.48", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.48.tgz", - "integrity": "sha512-KpSfKOHPsiSC4IkZeu2LsusFwExAIVGkhG1KkbaBMLwau0uMhj0fCrvyg9ddM2sAvd+gtiBJLir4LAw1MNMIaw==", + "version": "20.17.50", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.50.tgz", + "integrity": "sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==", "dev": true, "license": "MIT", "dependencies": { @@ -18552,9 +19193,10 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -18699,9 +19341,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.23.10", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.10.tgz", + "integrity": "sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==", "dev": true, "license": "MIT", "dependencies": { @@ -18709,18 +19351,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -18736,13 +19378,13 @@ "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", @@ -18755,7 +19397,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -18990,60 +19632,66 @@ } }, "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", + "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.26.0", + "@eslint/plugin-kit": "^0.2.8", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", + "@humanwhocodes/retry": "^0.4.2", + "@modelcontextprotocol/sdk": "^1.8.0", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "zod": "^3.24.2" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { @@ -19214,19 +19862,6 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eslint-plugin-import/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -19383,39 +20018,19 @@ "concat-map": "0.0.1" } }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, + "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -19446,32 +20061,32 @@ "node": "*" } }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -19607,6 +20222,16 @@ "node": ">=12.0.0" } }, + "node_modules/eventsource-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz", + "integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -19715,192 +20340,82 @@ "license": "Apache-2.0" }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dev": true, "license": "MIT", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/express/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "node_modules/express/node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "dev": true, "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { "node": ">= 0.8" } }, - "node_modules/express/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/express/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/express/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/express/node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/express/node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -20160,16 +20675,16 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/filelist": { @@ -20236,6 +20751,16 @@ "ms": "2.0.0" } }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -20243,6 +20768,29 @@ "dev": true, "license": "MIT" }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/find-cache-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", @@ -20355,81 +20903,17 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flat-cache/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/flat-cache/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/flat-cache/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/flat-cache/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=16" } }, "node_modules/flatted": { @@ -20729,6 +21213,27 @@ "node": ">= 6" } }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -20769,12 +21274,13 @@ "license": "ISC" }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/from": { @@ -21145,9 +21651,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -21937,6 +22443,15 @@ "node": ">= 0.6" } }, + "node_modules/http-assert/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/http-auth": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/http-auth/-/http-auth-4.1.9.tgz", @@ -22003,15 +22518,6 @@ "node": ">= 0.8" } }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/http-parser-js": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", @@ -22566,13 +23072,13 @@ } }, "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 0.10" } }, "node_modules/is-arguments": { @@ -22985,16 +23491,6 @@ "node": ">=8" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -23469,9 +23965,9 @@ } }, "node_modules/jackspeak": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", - "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -25643,6 +26139,49 @@ "node": ">=8.0.0" } }, + "node_modules/koa-bodyparser/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa-bodyparser/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa-bodyparser/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa-bodyparser/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/koa-compose": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", @@ -25678,6 +26217,49 @@ "streaming-json-stringify": "3" } }, + "node_modules/koa/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/koa/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/koa/node_modules/http-errors": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", @@ -25703,6 +26285,67 @@ "node": ">= 0.6" } }, + "node_modules/koa/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/launch-editor": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz", @@ -26967,6 +27610,16 @@ "node": ">=8" } }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/make-fetch-happen/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -27259,12 +27912,13 @@ } }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/memfs": { @@ -27291,11 +27945,14 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "dev": true, "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -27957,21 +28614,23 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -28351,6 +29010,19 @@ "dev": true, "license": "MIT" }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -28404,6 +29076,7 @@ "version": "1.4.5-lts.2", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", "license": "MIT", "dependencies": { "append-field": "^1.0.0", @@ -28418,6 +29091,36 @@ "node": ">= 6.0.0" } }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/multer/node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -28430,6 +29133,19 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", @@ -28582,9 +29298,10 @@ } }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -28966,6 +29683,16 @@ "encoding": "^0.1.13" } }, + "node_modules/node-gyp/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-gyp/node_modules/nopt": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", @@ -29462,6 +30189,16 @@ "encoding": "^0.1.13" } }, + "node_modules/npm-registry-fetch/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/npm-registry-fetch/node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -30202,9 +30939,9 @@ } }, "node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -31194,6 +31931,16 @@ "nice-napi": "^1.0.2" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-dir": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", @@ -32052,16 +32799,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -32227,32 +32964,21 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.4.24", + "iconv-lite": "0.6.3", "unpipe": "1.0.0" }, "engines": { "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -33136,6 +33862,47 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -33545,72 +34312,6 @@ "node": ">= 18" } }, - "node_modules/send/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", @@ -33671,6 +34372,20 @@ "node": ">= 0.8.0" } }, + "node_modules/serve-index/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serve-index/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -33714,6 +34429,29 @@ "dev": true, "license": "ISC" }, + "node_modules/serve-index/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -33721,6 +34459,16 @@ "dev": true, "license": "MIT" }, + "node_modules/serve-index/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serve-index/node_modules/setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", @@ -33728,118 +34476,30 @@ "dev": true, "license": "ISC" }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "dev": true, "license": "MIT", "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-static/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-static/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/serve-static/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/serve-static/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static/node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" + "node": ">= 18" } }, "node_modules/set-blocking": { @@ -34610,12 +35270,12 @@ } }, "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/steno": { @@ -35309,9 +35969,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", "dev": true, "license": "MIT", "dependencies": { @@ -35551,13 +36211,6 @@ "node": "*" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -36244,6 +36897,16 @@ "encoding": "^0.1.13" } }, + "node_modules/tuf-js/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/tuf-js/node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -36360,13 +37023,15 @@ } }, "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { "node": ">= 0.6" @@ -38024,17 +38689,27 @@ "url": "https://github.com/sponsors/streamich" } }, - "node_modules/webpack-dev-middleware/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "node_modules/webpack-dev-middleware/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", "dependencies": { - "ee-first": "1.1.1" + "mime-db": "1.52.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, "node_modules/webpack-dev-server": { @@ -38120,6 +38795,45 @@ "@types/send": "*" } }, + "node_modules/webpack-dev-server/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server/node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/webpack-dev-server/node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -38145,6 +38859,53 @@ "fsevents": "~2.3.2" } }, + "node_modules/webpack-dev-server/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack-dev-server/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/webpack-dev-server/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -38158,6 +38919,82 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/webpack-dev-server/node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/webpack-dev-server/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webpack-dev-server/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/webpack-dev-server/node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -38196,6 +39033,29 @@ } } }, + "node_modules/webpack-dev-server/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/webpack-dev-server/node_modules/is-plain-obj": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", @@ -38225,6 +39085,72 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/webpack-dev-server/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server/node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/webpack-dev-server/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/webpack-dev-server/node_modules/open": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", @@ -38244,6 +39170,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/webpack-dev-server/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" + }, "node_modules/webpack-dev-server/node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -38257,6 +39190,38 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/webpack-dev-server/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/webpack-dev-server/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/webpack-dev-server/node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -38270,6 +39235,71 @@ "node": ">=8.10.0" } }, + "node_modules/webpack-dev-server/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/webpack-dev-server/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webpack-dev-server/node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/webpack-dev-server/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/webpack-hot-middleware": { "version": "2.26.1", "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.26.1.tgz", @@ -38346,13 +39376,6 @@ "dev": true, "license": "MIT" }, - "node_modules/webpack/node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/webpack/node_modules/browserslist": { "version": "4.24.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", @@ -38417,6 +39440,29 @@ "dev": true, "license": "MIT" }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -38982,6 +40028,26 @@ "node": "*" } }, + "node_modules/zod": { + "version": "3.25.23", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.23.tgz", + "integrity": "sha512-Od2bdMosahjSrSgJtakrwjMDb1zM1A3VIHCPGveZt/3/wlrTWBya2lmEh2OYe4OIu8mPTmmr0gnLHIWQXdtWBg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, "node_modules/zone.js": { "version": "0.14.10", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz", diff --git a/package.json b/package.json index fe7f69ff40d..84164696f46 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@compodoc/compodoc": "1.1.26", "@electron/notarize": "2.5.0", "@electron/rebuild": "3.7.2", + "@eslint/compat": "1.2.9", "@lit-labs/signals": "0.1.2", "@ngtools/webpack": "18.2.19", "@storybook/addon-a11y": "8.6.12", @@ -102,7 +103,7 @@ "electron-reload": "2.0.0-alpha.1", "electron-store": "8.2.0", "electron-updater": "6.6.4", - "eslint": "8.57.1", + "eslint": "9.26.0", "eslint-config-prettier": "10.1.2", "eslint-import-resolver-typescript": "4.3.4", "eslint-plugin-import": "2.31.0", @@ -207,6 +208,12 @@ "zxcvbn": "4.4.2" }, "overrides": { + "eslint-plugin-rxjs": { + "eslint": "$eslint" + }, + "eslint-plugin-rxjs-angular": { + "eslint": "$eslint" + }, "tailwindcss": "$tailwindcss", "@storybook/angular": { "zone.js": "$zone.js" From 88bc7625218028cdea3d73663a00f8d24133ba6e Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Tue, 27 May 2025 14:51:40 -0400 Subject: [PATCH 157/163] PM-16645 (#14649) --- .../src/autofill/services/autofill.service.ts | 248 +----------------- libs/common/src/enums/feature-flag.enum.ts | 2 - 2 files changed, 1 insertion(+), 249 deletions(-) diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 525010bacc1..fdd881c2760 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -1579,252 +1579,6 @@ export default class AutofillService implements AutofillServiceInterface { return [expectedDateFormat, dateFormatPatterns]; } - /** - * Generates the autofill script for the specified page details and identify cipher item. - * @param {AutofillScript} fillScript - * @param {AutofillPageDetails} pageDetails - * @param {{[p: string]: AutofillField}} filledFields - * @param {GenerateFillScriptOptions} options - * @returns {AutofillScript} - * @private - */ - private async generateIdentityFillScript( - fillScript: AutofillScript, - pageDetails: AutofillPageDetails, - filledFields: { [id: string]: AutofillField }, - options: GenerateFillScriptOptions, - ): Promise { - if (await this.configService.getFeatureFlag(FeatureFlag.GenerateIdentityFillScriptRefactor)) { - return this._generateIdentityFillScript(fillScript, pageDetails, filledFields, options); - } - - if (!options.cipher.identity) { - return null; - } - - const fillFields: { [id: string]: AutofillField } = {}; - - pageDetails.fields.forEach((f) => { - if ( - AutofillService.isExcludedFieldType(f, AutoFillConstants.ExcludedAutofillTypes) || - ["current-password", "new-password"].includes(f.autoCompleteType) - ) { - return; - } - - for (let i = 0; i < IdentityAutoFillConstants.IdentityAttributes.length; i++) { - const attr = IdentityAutoFillConstants.IdentityAttributes[i]; - // eslint-disable-next-line - if (!f.hasOwnProperty(attr) || !f[attr] || !f.viewable) { - continue; - } - - // ref https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill - // ref https://developers.google.com/web/fundamentals/design-and-ux/input/forms/ - if ( - !fillFields.name && - AutofillService.isFieldMatch( - f[attr], - IdentityAutoFillConstants.FullNameFieldNames, - IdentityAutoFillConstants.FullNameFieldNameValues, - ) - ) { - fillFields.name = f; - break; - } else if ( - !fillFields.firstName && - AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.FirstnameFieldNames) - ) { - fillFields.firstName = f; - break; - } else if ( - !fillFields.middleName && - AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.MiddlenameFieldNames) - ) { - fillFields.middleName = f; - break; - } else if ( - !fillFields.lastName && - AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.LastnameFieldNames) - ) { - fillFields.lastName = f; - break; - } else if ( - !fillFields.title && - AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.TitleFieldNames) - ) { - fillFields.title = f; - break; - } else if ( - !fillFields.email && - AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.EmailFieldNames) - ) { - fillFields.email = f; - break; - } else if ( - !fillFields.address1 && - AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.Address1FieldNames) - ) { - fillFields.address1 = f; - break; - } else if ( - !fillFields.address2 && - AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.Address2FieldNames) - ) { - fillFields.address2 = f; - break; - } else if ( - !fillFields.address3 && - AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.Address3FieldNames) - ) { - fillFields.address3 = f; - break; - } else if ( - !fillFields.address && - AutofillService.isFieldMatch( - f[attr], - IdentityAutoFillConstants.AddressFieldNames, - IdentityAutoFillConstants.AddressFieldNameValues, - ) - ) { - fillFields.address = f; - break; - } else if ( - !fillFields.postalCode && - AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.PostalCodeFieldNames) - ) { - fillFields.postalCode = f; - break; - } else if ( - !fillFields.city && - AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.CityFieldNames) - ) { - fillFields.city = f; - break; - } else if ( - !fillFields.state && - AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.StateFieldNames) - ) { - fillFields.state = f; - break; - } else if ( - !fillFields.country && - AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.CountryFieldNames) - ) { - fillFields.country = f; - break; - } else if ( - !fillFields.phone && - AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.PhoneFieldNames) - ) { - fillFields.phone = f; - break; - } else if ( - !fillFields.username && - AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.UserNameFieldNames) - ) { - fillFields.username = f; - break; - } else if ( - !fillFields.company && - AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.CompanyFieldNames) - ) { - fillFields.company = f; - break; - } - } - }); - - const identity = options.cipher.identity; - this.makeScriptAction(fillScript, identity, fillFields, filledFields, "title"); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, "firstName"); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, "middleName"); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, "lastName"); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, "address1"); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, "address2"); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, "address3"); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, "city"); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, "postalCode"); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, "company"); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, "email"); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, "phone"); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, "username"); - - let filledState = false; - if (fillFields.state && identity.state && identity.state.length > 2) { - const stateLower = identity.state.toLowerCase(); - const isoState = - IdentityAutoFillConstants.IsoStates[stateLower] || - IdentityAutoFillConstants.IsoProvinces[stateLower]; - if (isoState) { - filledState = true; - this.makeScriptActionWithValue(fillScript, isoState, fillFields.state, filledFields); - } - } - - if (!filledState) { - this.makeScriptAction(fillScript, identity, fillFields, filledFields, "state"); - } - - let filledCountry = false; - if (fillFields.country && identity.country && identity.country.length > 2) { - const countryLower = identity.country.toLowerCase(); - const isoCountry = IdentityAutoFillConstants.IsoCountries[countryLower]; - if (isoCountry) { - filledCountry = true; - this.makeScriptActionWithValue(fillScript, isoCountry, fillFields.country, filledFields); - } - } - - if (!filledCountry) { - this.makeScriptAction(fillScript, identity, fillFields, filledFields, "country"); - } - - if (fillFields.name && (identity.firstName || identity.lastName)) { - let fullName = ""; - if (AutofillService.hasValue(identity.firstName)) { - fullName = identity.firstName; - } - if (AutofillService.hasValue(identity.middleName)) { - if (fullName !== "") { - fullName += " "; - } - fullName += identity.middleName; - } - if (AutofillService.hasValue(identity.lastName)) { - if (fullName !== "") { - fullName += " "; - } - fullName += identity.lastName; - } - - this.makeScriptActionWithValue(fillScript, fullName, fillFields.name, filledFields); - } - - if (fillFields.address && AutofillService.hasValue(identity.address1)) { - let address = ""; - if (AutofillService.hasValue(identity.address1)) { - address = identity.address1; - } - if (AutofillService.hasValue(identity.address2)) { - if (address !== "") { - address += ", "; - } - address += identity.address2; - } - if (AutofillService.hasValue(identity.address3)) { - if (address !== "") { - address += ", "; - } - address += identity.address3; - } - - this.makeScriptActionWithValue(fillScript, address, fillFields.address, filledFields); - } - - return fillScript; - } - /** * Generates the autofill script for the specified page details and identity cipher item. * @@ -1833,7 +1587,7 @@ export default class AutofillService implements AutofillServiceInterface { * @param filledFields - The fields that have already been filled, passed between method references * @param options - Contains data used to fill cipher items */ - private _generateIdentityFillScript( + private generateIdentityFillScript( fillScript: AutofillScript, pageDetails: AutofillPageDetails, filledFields: { [id: string]: AutofillField }, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index a5c5c615e70..8cd44f9d627 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -22,7 +22,6 @@ export enum FeatureFlag { BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain", DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2", EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill", - GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor", IdpAutoSubmitLogin = "idp-auto-submit-login", NotificationRefresh = "notification-refresh", UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", @@ -87,7 +86,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.BlockBrowserInjectionsByDomain]: FALSE, [FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE, [FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE, - [FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE, [FeatureFlag.IdpAutoSubmitLogin]: FALSE, [FeatureFlag.NotificationRefresh]: FALSE, [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, From cb770f5cd3381b38d34d502fb26f07659cf02d49 Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Tue, 27 May 2025 16:01:07 -0400 Subject: [PATCH 158/163] refactor(browser-platform-utils): Remove Deprecation and Fix Code (#14709) * refactor(browser-platform-utils): Remove Deprecation and Fix Code - Changed usages of firefox to private and moved the usages to the preferred public method and removed the deprecations. * fix(browser-platform-utils): Remove Deprecation and Fix Code - Tiny changes. * test(browser-platform-utils): Remove Deprecation and Fix Code - Fixed up test --- .../src/platform/listeners/update-badge.ts | 10 +++---- .../browser-platform-utils.service.spec.ts | 3 +- .../browser-platform-utils.service.ts | 30 ++++--------------- 3 files changed, 12 insertions(+), 31 deletions(-) diff --git a/apps/browser/src/platform/listeners/update-badge.ts b/apps/browser/src/platform/listeners/update-badge.ts index cd74b9ebf7d..c168ae44f3c 100644 --- a/apps/browser/src/platform/listeners/update-badge.ts +++ b/apps/browser/src/platform/listeners/update-badge.ts @@ -7,13 +7,13 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service"; import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import MainBackground from "../../background/main.background"; import IconDetails from "../../vault/background/models/icon-details"; import { BrowserApi } from "../browser/browser-api"; -import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service"; export type BadgeOptions = { tab?: chrome.tabs.Tab; @@ -28,6 +28,7 @@ export class UpdateBadge { private badgeAction: typeof chrome.action | typeof chrome.browserAction; private sidebarAction: OperaSidebarAction | FirefoxSidebarAction; private win: Window & typeof globalThis; + private platformUtilsService: PlatformUtilsService; constructor(win: Window & typeof globalThis, services: MainBackground) { this.badgeAction = BrowserApi.getBrowserAction(); @@ -38,6 +39,7 @@ export class UpdateBadge { this.authService = services.authService; this.cipherService = services.cipherService; this.accountService = services.accountService; + this.platformUtilsService = services.platformUtilsService; } async run(opts?: { tabId?: number; windowId?: number }): Promise { @@ -129,7 +131,7 @@ export class UpdateBadge { 38: "/images/icon38" + iconSuffix + ".png", }, }; - if (windowId && BrowserPlatformUtilsService.isFirefox()) { + if (windowId && this.platformUtilsService.isFirefox()) { options.windowId = windowId; } @@ -204,9 +206,7 @@ export class UpdateBadge { } private get useSyncApiCalls() { - return ( - BrowserPlatformUtilsService.isFirefox() || BrowserPlatformUtilsService.isSafari(this.win) - ); + return this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari(); } private isOperaSidebar( diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts index 38166d10a08..f75e9cc29a5 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts @@ -126,12 +126,11 @@ describe("Browser Utils Service", () => { configurable: true, value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0", }); - jest.spyOn(BrowserPlatformUtilsService, "isFirefox"); browserPlatformUtilsService.getDevice(); expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.FirefoxExtension); - expect(BrowserPlatformUtilsService.isFirefox).toHaveBeenCalledTimes(1); + expect(browserPlatformUtilsService.isFirefox()).toBe(true); }); }); diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts index 22708d8e425..4ae412fbda6 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts @@ -60,10 +60,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic return ClientType.Browser; } - /** - * @deprecated Do not call this directly, use getDevice() instead - */ - static isFirefox(): boolean { + private static isFirefox(): boolean { return ( navigator.userAgent.indexOf(" Firefox/") !== -1 || navigator.userAgent.indexOf(" Gecko/") !== -1 @@ -74,9 +71,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic return this.getDevice() === DeviceType.FirefoxExtension; } - /** - * @deprecated Do not call this directly, use getDevice() instead - */ private static isChrome(globalContext: Window | ServiceWorkerGlobalScope): boolean { return globalContext.chrome && navigator.userAgent.indexOf(" Chrome/") !== -1; } @@ -85,9 +79,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic return this.getDevice() === DeviceType.ChromeExtension; } - /** - * @deprecated Do not call this directly, use getDevice() instead - */ private static isEdge(): boolean { return navigator.userAgent.indexOf(" Edg/") !== -1; } @@ -96,9 +87,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic return this.getDevice() === DeviceType.EdgeExtension; } - /** - * @deprecated Do not call this directly, use getDevice() instead - */ private static isOpera(globalContext: Window | ServiceWorkerGlobalScope): boolean { return ( !!globalContext.opr?.addons || @@ -111,9 +99,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic return this.getDevice() === DeviceType.OperaExtension; } - /** - * @deprecated Do not call this directly, use getDevice() instead - */ private static isVivaldi(): boolean { return navigator.userAgent.indexOf(" Vivaldi/") !== -1; } @@ -122,10 +107,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic return this.getDevice() === DeviceType.VivaldiExtension; } - /** - * @deprecated Do not call this directly, use getDevice() instead - */ - static isSafari(globalContext: Window | ServiceWorkerGlobalScope): boolean { + private static isSafari(globalContext: Window | ServiceWorkerGlobalScope): boolean { // Opera masquerades as Safari, so make sure we're not there first return ( !BrowserPlatformUtilsService.isOpera(globalContext) && @@ -137,6 +119,10 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic return navigator.userAgent.match("Version/([0-9.]*)")?.[1]; } + isSafari(): boolean { + return this.getDevice() === DeviceType.SafariExtension; + } + /** * Safari previous to version 16.1 had a bug which caused artifacts on hover in large extension popups. * https://bugs.webkit.org/show_bug.cgi?id=218704 @@ -151,10 +137,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic return parts?.[0] < 16 || (parts?.[0] === 16 && parts?.[1] === 0); } - isSafari(): boolean { - return this.getDevice() === DeviceType.SafariExtension; - } - isIE(): boolean { return false; } From 50143a4b88dc170de92dd603b507545c334f2227 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Tue, 27 May 2025 16:50:39 -0400 Subject: [PATCH 159/163] pushes search text to a subject (#14880) use distinctUntilChanged to prevent duplicate filtering operations Run filtering outside angular zone to prevent change detection issues --- .../vault-search/vault-v2-search.component.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.ts index 32f5611f436..b68818454d1 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.ts @@ -1,8 +1,8 @@ import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, NgZone } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; -import { Subject, Subscription, debounceTime, filter } from "rxjs"; +import { Subject, Subscription, debounceTime, distinctUntilChanged, filter } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { SearchModule } from "@bitwarden/components"; @@ -22,13 +22,16 @@ export class VaultV2SearchComponent { private searchText$ = new Subject(); - constructor(private vaultPopupItemsService: VaultPopupItemsService) { + constructor( + private vaultPopupItemsService: VaultPopupItemsService, + private ngZone: NgZone, + ) { this.subscribeToLatestSearchText(); this.subscribeToApplyFilter(); } onSearchTextChanged() { - this.vaultPopupItemsService.applyFilter(this.searchText); + this.searchText$.next(this.searchText); } subscribeToLatestSearchText(): Subscription { @@ -44,9 +47,13 @@ export class VaultV2SearchComponent { subscribeToApplyFilter(): Subscription { return this.searchText$ - .pipe(debounceTime(SearchTextDebounceInterval), takeUntilDestroyed()) + .pipe(debounceTime(SearchTextDebounceInterval), distinctUntilChanged(), takeUntilDestroyed()) .subscribe((data) => { - this.vaultPopupItemsService.applyFilter(data); + this.ngZone.runOutsideAngular(() => { + this.ngZone.run(() => { + this.vaultPopupItemsService.applyFilter(data); + }); + }); }); } } From 4fcc4793bbfff10fd3d2a228be004d28ae5eb838 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 28 May 2025 09:41:56 +1000 Subject: [PATCH 160/163] Add additional jsdoc to policyservice (#14934) --- .../policy/policy.service.abstraction.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts index 68f9843c5bd..bf02872ed7c 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts @@ -7,6 +7,9 @@ import { MasterPasswordPolicyOptions } from "../../models/domain/master-password import { Policy } from "../../models/domain/policy"; import { ResetPasswordPolicyOptions } from "../../models/domain/reset-password-policy-options"; +/** + * The primary service for retrieving and evaluating policies from sync data. + */ export abstract class PolicyService { /** * All policies for the provided user from sync data. @@ -24,7 +27,7 @@ export abstract class PolicyService { abstract policiesByType$: (policyType: PolicyType, userId: UserId) => Observable; /** - * @returns true if a policy of the specified type applies to the specified user, otherwise false. + * @returns true if any policy of the specified type applies to the specified user, otherwise false. * A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner). * This does not take into account the policy's configuration - if that is important, use {@link policiesByType$} to get the * {@link Policy} objects and then filter by Policy.data. @@ -35,8 +38,12 @@ export abstract class PolicyService { /** * Combines all Master Password policies that apply to the user. + * If you are evaluating Master Password policies before the first sync has completed, + * you must supply your own `policies` value. + * @param userId The user against whom the policy needs to be enforced. + * @param policies The policies to be evaluated; if null or undefined, it will default to using policies from sync data. * @returns a set of options which represent the minimum Master Password settings that the user must - * comply with in order to comply with **all** Master Password policies. + * comply with in order to comply with **all** applicable Master Password policies. */ abstract masterPasswordPolicyOptions$: ( userId: UserId, @@ -62,7 +69,17 @@ export abstract class PolicyService { ) => [ResetPasswordPolicyOptions, boolean]; } +/** + * An "internal" extension of the `PolicyService` which allows the update of policy data in the local sync data. + * This does not update any policies on the server. + */ export abstract class InternalPolicyService extends PolicyService { + /** + * Upsert a policy in the local sync data. This does not update any policies on the server. + */ abstract upsert: (policy: PolicyData, userId: UserId) => Promise; + /** + * Replace a policy in the local sync data. This does not update any policies on the server. + */ abstract replace: (policies: { [id: string]: PolicyData }, userId: UserId) => Promise; } From d1fb37d696b98c64de0a291af868a56db99788cd Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Wed, 28 May 2025 15:00:30 +0200 Subject: [PATCH 161/163] [PM-17635] [PM-18601] Simplifying mocking and usage of the sdk (#14287) * feat: add our own custom deep mocker * feat: use new mock service in totp tests * feat: implement userClient mocking * chore: move mock files * feat: replace existing manual sdkService mocking * chore: rename to 'client' * chore: improve docs * feat: refactor sdkService to never return undefined BitwardenClient --- .../platform/abstractions/sdk/sdk.service.ts | 9 +- .../services/sdk/default-sdk.service.spec.ts | 8 +- .../services/sdk/default-sdk.service.ts | 5 +- .../src/platform/spec/mock-deep.spec.ts | 58 ++++ libs/common/src/platform/spec/mock-deep.ts | 271 ++++++++++++++++++ .../src/platform/spec/mock-sdk.service.ts | 81 ++++++ .../src/vault/services/totp.service.spec.ts | 31 +- .../src/services/import.service.spec.ts | 10 +- ...symmetric-key-regeneration.service.spec.ts | 25 +- 9 files changed, 444 insertions(+), 54 deletions(-) create mode 100644 libs/common/src/platform/spec/mock-deep.spec.ts create mode 100644 libs/common/src/platform/spec/mock-deep.ts create mode 100644 libs/common/src/platform/spec/mock-sdk.service.ts diff --git a/libs/common/src/platform/abstractions/sdk/sdk.service.ts b/libs/common/src/platform/abstractions/sdk/sdk.service.ts index 07dfb2aa0df..d629e4fe9fa 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk.service.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk.service.ts @@ -53,15 +53,18 @@ export abstract class SdkService { * This client can be used for operations that require a user context, such as retrieving ciphers * and operations involving crypto. It can also be used for operations that don't require a user context. * + * - If the user is not logged when the subscription is created, the observable will complete + * immediately with {@link UserNotLoggedInError}. + * - If the user is logged in, the observable will emit the client and complete whithout an error + * when the user logs out. + * * **WARNING:** Do not use `firstValueFrom(userClient$)`! Any operations on the client must be done within the observable. * The client will be destroyed when the observable is no longer subscribed to. * Please let platform know if you need a client that is not destroyed when the observable is no longer subscribed to. * * @param userId The user id for which to retrieve the client - * - * @throws {UserNotLoggedInError} If the user is not logged in */ - abstract userClient$(userId: UserId): Observable | undefined>; + abstract userClient$(userId: UserId): Observable>; /** * This method is used during/after an authentication procedure to set a new client for a specific user. diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts index 6531be58f05..70a08257471 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts @@ -132,15 +132,13 @@ describe("DefaultSdkService", () => { ); keyService.userKey$.calledWith(userId).mockReturnValue(userKey$); - const subject = new BehaviorSubject | undefined>(undefined); - service.userClient$(userId).subscribe(subject); - await new Promise(process.nextTick); + const userClientTracker = new ObservableTracker(service.userClient$(userId), false); + await userClientTracker.pauseUntilReceived(1); userKey$.next(undefined); - await new Promise(process.nextTick); + await userClientTracker.expectCompletion(); expect(mockClient.free).toHaveBeenCalledTimes(1); - expect(subject.value).toBe(undefined); }); }); diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index 8e84642fb99..6be89a4b376 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -71,7 +71,7 @@ export class DefaultSdkService implements SdkService { private userAgent: string | null = null, ) {} - userClient$(userId: UserId): Observable | undefined> { + userClient$(userId: UserId): Observable> { return this.sdkClientOverrides.pipe( takeWhile((clients) => clients[userId] !== UnsetClient, false), map((clients) => { @@ -88,6 +88,7 @@ export class DefaultSdkService implements SdkService { return this.internalClient$(userId); }), + takeWhile((client) => client !== undefined, false), throwIfEmpty(() => new UserNotLoggedInError(userId)), ); } @@ -112,7 +113,7 @@ export class DefaultSdkService implements SdkService { * @param userId The user id for which to create the client * @returns An observable that emits the client for the user */ - private internalClient$(userId: UserId): Observable | undefined> { + private internalClient$(userId: UserId): Observable> { const cached = this.sdkClientCache.get(userId); if (cached !== undefined) { return cached; diff --git a/libs/common/src/platform/spec/mock-deep.spec.ts b/libs/common/src/platform/spec/mock-deep.spec.ts new file mode 100644 index 00000000000..535e02c11dd --- /dev/null +++ b/libs/common/src/platform/spec/mock-deep.spec.ts @@ -0,0 +1,58 @@ +import { mockDeep } from "./mock-deep"; + +class ToBeMocked { + property = "value"; + + method() { + return "method"; + } + + sub() { + return new SubToBeMocked(); + } +} + +class SubToBeMocked { + subProperty = "subValue"; + + sub() { + return new SubSubToBeMocked(); + } +} + +class SubSubToBeMocked { + subSubProperty = "subSubValue"; +} + +describe("deepMock", () => { + it("can mock properties", () => { + const mock = mockDeep(); + mock.property.replaceProperty("mocked value"); + expect(mock.property).toBe("mocked value"); + }); + + it("can mock methods", () => { + const mock = mockDeep(); + mock.method.mockReturnValue("mocked method"); + expect(mock.method()).toBe("mocked method"); + }); + + it("can mock sub-properties", () => { + const mock = mockDeep(); + mock.sub.mockDeep().subProperty.replaceProperty("mocked sub value"); + expect(mock.sub().subProperty).toBe("mocked sub value"); + }); + + it("can mock sub-sub-properties", () => { + const mock = mockDeep(); + mock.sub.mockDeep().sub.mockDeep().subSubProperty.replaceProperty("mocked sub-sub value"); + expect(mock.sub().sub().subSubProperty).toBe("mocked sub-sub value"); + }); + + it("returns the same mock object when calling mockDeep multiple times", () => { + const mock = mockDeep(); + const subMock1 = mock.sub.mockDeep(); + const subMock2 = mock.sub.mockDeep(); + expect(subMock1).toBe(subMock2); + }); +}); diff --git a/libs/common/src/platform/spec/mock-deep.ts b/libs/common/src/platform/spec/mock-deep.ts new file mode 100644 index 00000000000..89ef9a25451 --- /dev/null +++ b/libs/common/src/platform/spec/mock-deep.ts @@ -0,0 +1,271 @@ +// This is a modification of the code found in https://github.com/marchaos/jest-mock-extended +// to better support deep mocking of objects. + +// MIT License + +// Copyright (c) 2019 Marc McIntyre + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { jest } from "@jest/globals"; +import { FunctionLike } from "jest-mock"; +import { calledWithFn, MatchersOrLiterals } from "jest-mock-extended"; +import { PartialDeep } from "type-fest"; + +type ProxiedProperty = string | number | symbol; + +export interface GlobalConfig { + // ignoreProps is required when we don't want to return anything for a mock (for example, when mocking a promise). + ignoreProps?: ProxiedProperty[]; +} + +const DEFAULT_CONFIG: GlobalConfig = { + ignoreProps: ["then"], +}; + +let GLOBAL_CONFIG = DEFAULT_CONFIG; + +export const JestMockExtended = { + DEFAULT_CONFIG, + configure: (config: GlobalConfig) => { + // Shallow merge so they can override anything they want. + GLOBAL_CONFIG = { ...DEFAULT_CONFIG, ...config }; + }, + resetConfig: () => { + GLOBAL_CONFIG = DEFAULT_CONFIG; + }, +}; + +export interface CalledWithMock extends jest.Mock { + calledWith: (...args: [...MatchersOrLiterals>]) => jest.Mock; +} + +export interface MockDeepMock { + mockDeep: () => DeepMockProxy; +} + +export interface ReplaceProperty { + /** + * mockDeep will by default return a jest.fn() for all properties, + * but this allows you to replace the property with a value. + * @param value The value to replace the property with. + */ + replaceProperty(value: T): void; +} + +export type _MockProxy = { + [K in keyof T]: T[K] extends FunctionLike ? T[K] & CalledWithMock : T[K]; +}; + +export type MockProxy = _MockProxy & T; + +export type _DeepMockProxy = { + // This supports deep mocks in the else branch + [K in keyof T]: T[K] extends (...args: infer A) => infer R + ? T[K] & CalledWithMock & MockDeepMock + : T[K] & ReplaceProperty & _DeepMockProxy; +}; + +// we intersect with T here instead of on the mapped type above to +// prevent immediate type resolution on a recursive type, this will +// help to improve performance for deeply nested recursive mocking +// at the same time, this intersection preserves private properties +export type DeepMockProxy = _DeepMockProxy & T; + +export type _DeepMockProxyWithFuncPropSupport = { + // This supports deep mocks in the else branch + [K in keyof T]: T[K] extends FunctionLike + ? CalledWithMock & DeepMockProxy + : DeepMockProxy; +}; + +export type DeepMockProxyWithFuncPropSupport = _DeepMockProxyWithFuncPropSupport & T; + +export interface MockOpts { + deep?: boolean; + fallbackMockImplementation?: (...args: any[]) => any; +} + +export const mockClear = (mock: MockProxy) => { + for (const key of Object.keys(mock)) { + if (mock[key] === null || mock[key] === undefined) { + continue; + } + + if (mock[key]._isMockObject) { + mockClear(mock[key]); + } + + if (mock[key]._isMockFunction) { + mock[key].mockClear(); + } + } + + // This is a catch for if they pass in a jest.fn() + if (!mock._isMockObject) { + return mock.mockClear(); + } +}; + +export const mockReset = (mock: MockProxy) => { + for (const key of Object.keys(mock)) { + if (mock[key] === null || mock[key] === undefined) { + continue; + } + + if (mock[key]._isMockObject) { + mockReset(mock[key]); + } + if (mock[key]._isMockFunction) { + mock[key].mockReset(); + } + } + + // This is a catch for if they pass in a jest.fn() + // Worst case, we will create a jest.fn() (since this is a proxy) + // below in the get and call mockReset on it + if (!mock._isMockObject) { + return mock.mockReset(); + } +}; + +export function mockDeep( + opts: { + funcPropSupport?: true; + fallbackMockImplementation?: MockOpts["fallbackMockImplementation"]; + }, + mockImplementation?: PartialDeep, +): DeepMockProxyWithFuncPropSupport; +export function mockDeep(mockImplementation?: PartialDeep): DeepMockProxy; +export function mockDeep(arg1: any, arg2?: any) { + const [opts, mockImplementation] = + typeof arg1 === "object" && + (typeof arg1.fallbackMockImplementation === "function" || arg1.funcPropSupport === true) + ? [arg1, arg2] + : [{}, arg1]; + return mock(mockImplementation, { + deep: true, + fallbackMockImplementation: opts.fallbackMockImplementation, + }); +} + +const overrideMockImp = (obj: PartialDeep, opts?: MockOpts) => { + const proxy = new Proxy>(obj, handler(opts)); + for (const name of Object.keys(obj)) { + if (typeof obj[name] === "object" && obj[name] !== null) { + proxy[name] = overrideMockImp(obj[name], opts); + } else { + proxy[name] = obj[name]; + } + } + + return proxy; +}; + +const handler = (opts?: MockOpts): ProxyHandler => ({ + ownKeys(target: MockProxy) { + return Reflect.ownKeys(target); + }, + + set: (obj: MockProxy, property: ProxiedProperty, value: any) => { + obj[property] = value; + return true; + }, + + get: (obj: MockProxy, property: ProxiedProperty) => { + const fn = calledWithFn({ fallbackMockImplementation: opts?.fallbackMockImplementation }); + + if (!(property in obj)) { + if (GLOBAL_CONFIG.ignoreProps?.includes(property)) { + return undefined; + } + // Jest's internal equality checking does some wierd stuff to check for iterable equality + if (property === Symbol.iterator) { + return obj[property]; + } + + if (property === "_deepMock") { + return obj[property]; + } + // So this calls check here is totally not ideal - jest internally does a + // check to see if this is a spy - which we want to say no to, but blindly returning + // an proxy for calls results in the spy check returning true. This is another reason + // why deep is opt in. + if (opts?.deep && property !== "calls") { + obj[property] = new Proxy>(fn, handler(opts)); + obj[property].replaceProperty = (value: T[K]) => { + obj[property] = value; + }; + obj[property].mockDeep = () => { + if (obj[property]._deepMock) { + return obj[property]._deepMock; + } + + const mock = mockDeep({ + fallbackMockImplementation: opts?.fallbackMockImplementation, + }); + (obj[property] as CalledWithMock).mockReturnValue(mock); + obj[property]._deepMock = mock; + return mock; + }; + obj[property]._isMockObject = true; + } else { + obj[property] = calledWithFn({ + fallbackMockImplementation: opts?.fallbackMockImplementation, + }); + } + } + + // @ts-expect-error Hack by author of jest-mock-extended + if (obj instanceof Date && typeof obj[property] === "function") { + // @ts-expect-error Hack by author of jest-mock-extended + return obj[property].bind(obj); + } + + return obj[property]; + }, +}); + +const mock = & T = MockProxy & T>( + mockImplementation: PartialDeep = {} as PartialDeep, + opts?: MockOpts, +): MockedReturn => { + // @ts-expect-error private + mockImplementation!._isMockObject = true; + return overrideMockImp(mockImplementation, opts); +}; + +export const mockFn = (): CalledWithMock & T => { + // @ts-expect-error Hack by author of jest-mock-extended + return calledWithFn(); +}; + +export const stub = (): T => { + return new Proxy({} as T, { + get: (obj, property: ProxiedProperty) => { + if (property in obj) { + // @ts-expect-error Hack by author of jest-mock-extended + return obj[property]; + } + return jest.fn(); + }, + }); +}; + +export default mock; diff --git a/libs/common/src/platform/spec/mock-sdk.service.ts b/libs/common/src/platform/spec/mock-sdk.service.ts new file mode 100644 index 00000000000..66a6ab3ec84 --- /dev/null +++ b/libs/common/src/platform/spec/mock-sdk.service.ts @@ -0,0 +1,81 @@ +import { + BehaviorSubject, + distinctUntilChanged, + map, + Observable, + takeWhile, + throwIfEmpty, +} from "rxjs"; + +import { BitwardenClient } from "@bitwarden/sdk-internal"; + +import { UserId } from "../../types/guid"; +import { SdkService, UserNotLoggedInError } from "../abstractions/sdk/sdk.service"; +import { Rc } from "../misc/reference-counting/rc"; + +import { DeepMockProxy, mockDeep } from "./mock-deep"; + +export class MockSdkService implements SdkService { + private userClients$ = new BehaviorSubject<{ + [userId: UserId]: Rc | undefined; + }>({}); + + private _client$ = new BehaviorSubject(mockDeep()); + client$ = this._client$.asObservable(); + + version$ = new BehaviorSubject("0.0.1-test").asObservable(); + + userClient$(userId: UserId): Observable> { + return this.userClients$.pipe( + takeWhile((clients) => clients[userId] !== undefined, false), + map((clients) => clients[userId] as Rc), + distinctUntilChanged(), + throwIfEmpty(() => new UserNotLoggedInError(userId)), + ); + } + + setClient(): void { + throw new Error("Not supported in mock service"); + } + + /** + * Returns the non-user scoped client mock. + * This is what is returned by the `client$` observable. + */ + get client(): DeepMockProxy { + return this._client$.value; + } + + readonly simulate = { + /** + * Simulates a user login, and returns a user-scoped mock for the user. + * This will be return by the `userClient$` observable. + * + * @param userId The userId to simulate login for. + * @returns A user-scoped mock for the user. + */ + userLogin: (userId: UserId) => { + const client = mockDeep(); + this.userClients$.next({ + ...this.userClients$.getValue(), + [userId]: new Rc(client), + }); + return client; + }, + + /** + * Simulates a user logout, and disposes the user-scoped mock for the user. + * This will remove the user-scoped mock from the `userClient$` observable. + * + * @param userId The userId to simulate logout for. + */ + userLogout: (userId: UserId) => { + const clients = this.userClients$.value; + clients[userId]?.markForDisposal(); + this.userClients$.next({ + ...clients, + [userId]: undefined, + }); + }, + }; +} diff --git a/libs/common/src/vault/services/totp.service.spec.ts b/libs/common/src/vault/services/totp.service.spec.ts index c653b4ce1db..4aca262d537 100644 --- a/libs/common/src/vault/services/totp.service.spec.ts +++ b/libs/common/src/vault/services/totp.service.spec.ts @@ -1,38 +1,27 @@ -import { mock } from "jest-mock-extended"; -import { of, take } from "rxjs"; +import { take } from "rxjs"; -import { BitwardenClient, TotpResponse } from "@bitwarden/sdk-internal"; +import { TotpResponse } from "@bitwarden/sdk-internal"; -import { SdkService } from "../../platform/abstractions/sdk/sdk.service"; +import { MockSdkService } from "../../platform/spec/mock-sdk.service"; import { TotpService } from "./totp.service"; describe("TotpService", () => { - let totpService: TotpService; - let generateTotpMock: jest.Mock; - - const sdkService = mock(); + let totpService!: TotpService; + let sdkService!: MockSdkService; beforeEach(() => { - generateTotpMock = jest - .fn() - .mockReturnValueOnce({ + sdkService = new MockSdkService(); + sdkService.client.vault + .mockDeep() + .totp.mockDeep() + .generate_totp.mockReturnValueOnce({ code: "123456", period: 30, }) .mockReturnValueOnce({ code: "654321", period: 30 }) .mockReturnValueOnce({ code: "567892", period: 30 }); - const mockBitwardenClient = { - vault: () => ({ - totp: () => ({ - generate_totp: generateTotpMock, - }), - }), - }; - - sdkService.client$ = of(mockBitwardenClient as unknown as BitwardenClient); - totpService = new TotpService(sdkService); // TOTP is time-based, so we need to mock the current time diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index 30309a3d9c2..6c8656f4c1d 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -1,5 +1,4 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { of } 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 @@ -8,14 +7,13 @@ import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { MockSdkService } from "@bitwarden/common/platform/spec/mock-sdk.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { KeyService } from "@bitwarden/key-management"; -import { BitwardenClient } from "@bitwarden/sdk-internal"; import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer"; import { Importer } from "../importers/importer"; @@ -35,7 +33,7 @@ describe("ImportService", () => { let encryptService: MockProxy; let pinService: MockProxy; let accountService: MockProxy; - let sdkService: MockProxy; + let sdkService: MockSdkService; beforeEach(() => { cipherService = mock(); @@ -46,9 +44,7 @@ describe("ImportService", () => { keyService = mock(); encryptService = mock(); pinService = mock(); - const mockClient = mock(); - sdkService = mock(); - sdkService.client$ = of(mockClient, mockClient, mockClient); + sdkService = new MockSdkService(); importService = new ImportService( cipherService, diff --git a/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts index 35cef914588..84d1dd7ad72 100644 --- a/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts +++ b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts @@ -5,17 +5,17 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { MockSdkService } from "@bitwarden/common/platform/spec/mock-sdk.service"; import { makeStaticByteArray, mockEnc } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; -import { BitwardenClient, VerifyAsymmetricKeysResponse } from "@bitwarden/sdk-internal"; +import { VerifyAsymmetricKeysResponse } from "@bitwarden/sdk-internal"; import { KeyService } from "../../abstractions/key.service"; import { UserAsymmetricKeysRegenerationApiService } from "../abstractions/user-asymmetric-key-regeneration-api.service"; @@ -24,24 +24,17 @@ import { DefaultUserAsymmetricKeysRegenerationService } from "./default-user-asy function setupVerificationResponse( mockVerificationResponse: VerifyAsymmetricKeysResponse, - sdkService: MockProxy, + sdkService: MockSdkService, ) { const mockKeyPairResponse = { userPublicKey: "userPublicKey", userKeyEncryptedPrivateKey: "userKeyEncryptedPrivateKey", }; - sdkService.client$ = of({ - crypto: () => ({ - verify_asymmetric_keys: jest.fn().mockReturnValue(mockVerificationResponse), - make_key_pair: jest.fn().mockReturnValue(mockKeyPairResponse), - }), - free: jest.fn(), - echo: jest.fn(), - version: jest.fn(), - throw: jest.fn(), - catch: jest.fn(), - } as unknown as BitwardenClient); + sdkService.client.crypto + .mockDeep() + .verify_asymmetric_keys.mockReturnValue(mockVerificationResponse); + sdkService.client.crypto.mockDeep().make_key_pair.mockReturnValue(mockKeyPairResponse); } function setupUserKeyValidation( @@ -74,7 +67,7 @@ describe("regenerateIfNeeded", () => { let cipherService: MockProxy; let userAsymmetricKeysRegenerationApiService: MockProxy; let logService: MockProxy; - let sdkService: MockProxy; + let sdkService: MockSdkService; let apiService: MockProxy; let configService: MockProxy; let encryptService: MockProxy; @@ -84,7 +77,7 @@ describe("regenerateIfNeeded", () => { cipherService = mock(); userAsymmetricKeysRegenerationApiService = mock(); logService = mock(); - sdkService = mock(); + sdkService = new MockSdkService(); apiService = mock(); configService = mock(); encryptService = mock(); From 4bf1a3b670961eedbcf6f86dba2ac9e280ace708 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 07:16:46 -0700 Subject: [PATCH 162/163] [deps] Platform: Update Rust crate bytes to v1.10.1 (#14922) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/desktop/desktop_native/Cargo.lock | 4 ++-- apps/desktop/desktop_native/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 34819a3981b..28d64e4f504 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -498,9 +498,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "camino" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index c4a2ed98e70..1fce5a7c597 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -18,7 +18,7 @@ base64 = "=0.22.1" bindgen = "=0.71.1" bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "3d48f140fd506412d186203238993163a8c4e536" } byteorder = "=1.5.0" -bytes = "=1.9.0" +bytes = "=1.10.1" cbc = "=0.1.2" core-foundation = "=0.10.0" dirs = "=6.0.0" From 169fdd5883352acae4fcc976d4122da5e9ea0a29 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Wed, 28 May 2025 10:56:44 -0400 Subject: [PATCH 163/163] [PM-20650] Feature flag addition to clients (#14824) * Feature flag addition to clients * Updating feature flag name --- libs/common/src/enums/feature-flag.enum.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 8cd44f9d627..43b36c5692f 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -59,6 +59,7 @@ export enum FeatureFlag { CipherKeyEncryption = "cipher-key-encryption", PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms", EndUserNotifications = "pm-10609-end-user-notifications", + RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy", /* Platform */ IpcChannelFramework = "ipc-channel-framework", @@ -107,6 +108,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE, [FeatureFlag.EndUserNotifications]: FALSE, [FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE, + [FeatureFlag.RemoveCardItemTypePolicy]: FALSE, /* Auth */ [FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: FALSE,