-
-
+## Semantic Color Tokens
+
+> **Note:** Due to Tailwind's utility naming and our semantic token structure, class names will
+> appear repetitive (e.g., `tw-bg-bg-primary`). This repetition is intentional:
+>
+> - `tw-` = Tailwind prefix
+> - `bg-` = Tailwind utility type (background)
+> - `bg-primary` = Our semantic token name
+
+### Background Colors
+
+Use `tw-bg-bg-*` for background colors. These tokens automatically adapt to dark mode.
+
+export const Swatch = ({ name }) => {
+ const swatchClass = `tw-h-10 tw-w-10 tw-shrink-0 tw-rounded-lg tw-border tw-border-border-base tw-bg-${name}`;
+ return
;
+};
+
+export const BackgroundCard = ({ name, primitiveColor }) => {
+ const bgClass = `tw-flex tw-items-center tw-gap-3 tw-rounded-xl tw-p-4 tw-border tw-border-border-base tw-bg-bg-primary`;
+ const swatchClass = `tw-h-10 tw-w-10 tw-shrink-0 tw-rounded-lg tw-border tw-border-base tw-bg-bg-${name}`;
+ return (
+
+
+
bg-{name}
+
({primitiveColor})
+
+
+
+ );
+};
+
+
+
+
Light mode
+
+
+
Neutral
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Status
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Accent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Dark mode
+
+
+
Neutral
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Status
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Accent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+
+### Foreground Colors
+
+Use `tw-text-fg-*` for text colors. These tokens automatically adapt to dark mode.
+
+export const ForegroundCard = ({ name, primitiveColor }) => {
+ const textClass = `tw-text-fg-${name} tw-text-2xl tw-font-bold tw-shrink-0`;
+ return (
+
+
+
fg-{name}
+
({primitiveColor})
+
+
+
+ );
+};
+
+
+
+
Light mode
+
+
+
Neutral
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Status
+
+
+
+
+
+
+
+
+
+
+
+
+
Accent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Dark mode
+
+
+
Neutral
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Status
+
+
+
+
+
+
+
+
+
+
+
+
+
Accent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+
+### Border Colors
+
+Use `tw-border-border-*` for border colors. These tokens automatically adapt to dark mode.
+
+export const BorderCard = ({ name, primitiveColor }) => {
+ return (
+
+
+
+ border-{name}
+
+
({primitiveColor})
+
+
+
+ );
+};
+
+
+
+
Light mode
+
+
+
Neutral
+
+
+
+
+
+
+
+
+
+
+
+
+
Status
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Accent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Dark mode
+
+
+
Neutral
+
+
+
+
+
+
+
+
+
+
+
+
+
Status
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Accent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+
+## Usage Guidelines
+
+### ✅ DO - Use semantic tokens via Tailwind
+
+```html
+
+
Heading text
+
Body text
+
Brand action
+
Error message
+
+
+
Primary background
+
Secondary background
+
Brand button
+
Danger alert
+
+
+
Base border
+
+
Brand border
+
Danger border
+
+
+
+ Success alert with matching colors
+
+
+
+
Hover effect
+
+
+
Modal overlay
+```
+
+### ❌ DON'T - Use primitive colors directly
+
+```html
+
+
Text
+
Background
+
+
+
Text
+
Background
+
+
+
Text
+
Background
+```
+
+**Why this is wrong:** Primitives aren't semantic and may change. Always use semantic tokens like
+`tw-text-fg-brand`, `tw-bg-success`, etc.
+
+---
+
+## Dark Mode
+
+- Semantic tokens automatically adapt to dark mode via `.theme_dark` class
+- No component changes needed when theme switches
+- The same semantic token name works in both light and dark themes
+- All color values are automatically swapped based on the active theme
+
+---
+
+## Migration Strategy
+
+- **New components:** Use semantic tokens (`fg-*`, `bg-*`, `border-*`) exclusively
+- **Existing components:** Keep legacy tokens until refactoring
+- **When refactoring:** Replace legacy tokens with semantic equivalents
+
+---
+
+## Legacy Colors
+
+**Legacy colors (RGB format)** still exist for backwards compatibility:
+
+- `primary-*`, `secondary-*`, `success-*`, `danger-*`, `warning-*`, etc.
+- Use these only when updating existing components
+- Migrate to new semantic tokens when refactoring
+
+The following legacy colors are displayed below with both light and dark mode values:
+
+export const LegacyCard = ({ name }) => {
+ return (
+
+
+
{name}
+
(legacy RGB format)
+
+
+
+ );
+};
+
+
+
+
Light mode
+
+
+
General
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Secondary
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Dark mode
+
+
+
General
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Secondary
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css
index f0e55ddd9e1..757859985d6 100644
--- a/libs/components/src/tw-theme.css
+++ b/libs/components/src/tw-theme.css
@@ -13,6 +13,12 @@
@tailwind utilities;
:root {
+ /* ========================================
+ * LEGACY COLORS (RGB format)
+ * These are the original colors used throughout the app.
+ * Use these for existing components until migration is complete.
+ * ======================================== */
+
--color-transparent-hover: rgb(0 0 0 / 0.02);
--color-shadow: 168 179 200;
@@ -74,6 +80,279 @@
--color-illustration-bg-tertiary: 255 255 255;
--color-illustration-tertiary: 255 191 0;
--color-illustration-logo: 23 93 220;
+
+ /* ========================================
+ * NEW COLOR PALETTE (Hex format)
+ * These colors are from the new Figma design system.
+ * Use these for new components and features.
+ * Format: --color-{family}-{shade} where shade ranges from 050 to 950
+ * ======================================== */
+
+ /* Brand Colors */
+ --color-brand-050: #eef6ff;
+ --color-brand-100: #dbeafe;
+ --color-brand-200: #bedbff;
+ --color-brand-300: #8ec5ff;
+ --color-brand-400: #6baefa;
+ --color-brand-500: #418bfb;
+ --color-brand-600: #2a70f4;
+ --color-brand-700: #175ddc;
+ --color-brand-800: #0d43af;
+ --color-brand-900: #0c3276;
+ --color-brand-950: #162455;
+
+ /* Gray Colors */
+ --color-gray-050: #f9fafb;
+ --color-gray-100: #f3f4f6;
+ --color-gray-200: #e5e7eb;
+ --color-gray-300: #d1d5dc;
+ --color-gray-400: #99a1af;
+ --color-gray-500: #6a7282;
+ --color-gray-600: #4a5565;
+ --color-gray-700: #333e4f;
+ --color-gray-800: #1e2939;
+ --color-gray-900: #101828;
+ --color-gray-950: #070b18;
+ --color-gray-950-rgb: 7, 11, 24;
+
+ /* Red Colors */
+ --color-red-050: #fef2f2;
+ --color-red-100: #ffe2e2;
+ --color-red-200: #ffc9c9;
+ --color-red-300: #ffa2a2;
+ --color-red-400: #ff6467;
+ --color-red-500: #fb2c36;
+ --color-red-600: #e7000b;
+ --color-red-700: #c10007;
+ --color-red-800: #9f0712;
+ --color-red-900: #791112;
+ --color-red-950: #460809;
+
+ /* Orange Colors */
+ --color-orange-050: #fff8f1;
+ --color-orange-100: #feecdc;
+ --color-orange-200: #fcd9bd;
+ --color-orange-300: #fdba8c;
+ --color-orange-400: #ff8a4c;
+ --color-orange-500: #ff5a1f;
+ --color-orange-600: #d03801;
+ --color-orange-700: #b43403;
+ --color-orange-800: #8a2c0d;
+ --color-orange-900: #70240b;
+ --color-orange-950: #441306;
+
+ /* Yellow Colors */
+ --color-yellow-050: #fefce8;
+ --color-yellow-100: #fef9c2;
+ --color-yellow-200: #fff085;
+ --color-yellow-300: #ffdf20;
+ --color-yellow-400: #fdc700;
+ --color-yellow-500: #f0b100;
+ --color-yellow-600: #d08700;
+ --color-yellow-700: #a65f00;
+ --color-yellow-800: #894b00;
+ --color-yellow-900: #733e0a;
+ --color-yellow-950: #432004;
+
+ /* Green Colors */
+ --color-green-050: #f0fdf4;
+ --color-green-100: #dcfce7;
+ --color-green-200: #b9f8cf;
+ --color-green-300: #7bf1a8;
+ --color-green-400: #18dc7a;
+ --color-green-500: #0abf52;
+ --color-green-600: #00a63e;
+ --color-green-700: #008236;
+ --color-green-800: #016630;
+ --color-green-900: #0d542b;
+ --color-green-950: #032e15;
+
+ /* Pink Colors */
+ --color-pink-050: #fdf2f8;
+ --color-pink-100: #fce7f3;
+ --color-pink-200: #fccee8;
+ --color-pink-300: #fda5d5;
+ --color-pink-400: #fb64b6;
+ --color-pink-500: #f6339a;
+ --color-pink-600: #e60076;
+ --color-pink-700: #c6005c;
+ --color-pink-800: #a3004c;
+ --color-pink-900: #861043;
+ --color-pink-950: #510424;
+
+ /* Coral Colors */
+ --color-coral-050: #fff2f0;
+ --color-coral-100: #ffe0dc;
+ --color-coral-200: #ffc1b9;
+ --color-coral-300: #ff9585;
+ --color-coral-400: #ff6550;
+ --color-coral-500: #ff4026;
+ --color-coral-600: #e11f05;
+ --color-coral-700: #c71800;
+ --color-coral-800: #a81400;
+ --color-coral-900: #7e0f00;
+ --color-coral-950: #4d0900;
+
+ /* Teal Colors */
+ --color-teal-050: #ecfeff;
+ --color-teal-100: #cefafe;
+ --color-teal-200: #a2f4fd;
+ --color-teal-300: #70ecf5;
+ --color-teal-400: #2cdde9;
+ --color-teal-500: #00c5db;
+ --color-teal-600: #009cb8;
+ --color-teal-700: #007c95;
+ --color-teal-800: #006278;
+ --color-teal-900: #0f495c;
+ --color-teal-950: #042e3e;
+
+ /* Purple Colors */
+ --color-purple-050: #faf5ff;
+ --color-purple-100: #f3e8ff;
+ --color-purple-200: #e9d4ff;
+ --color-purple-300: #dab2ff;
+ --color-purple-400: #c27aff;
+ --color-purple-500: #ad46ff;
+ --color-purple-600: #9810fa;
+ --color-purple-700: #8200db;
+ --color-purple-800: #6e11b0;
+ --color-purple-900: #59168b;
+ --color-purple-950: #3c0366;
+
+ /* White and Black */
+ --color-white: #ffffff;
+ --color-white-rgb: 255, 255, 255;
+ --color-black: #000000;
+
+ /* ========================================
+ * SEMANTIC FOREGROUND COLORS (Light Mode)
+ * These are the tokens that should be exposed to Tailwind
+ * They reference the primitive colors above
+ * ======================================== */
+
+ /* Neutral Foreground */
+ --color-fg-white: var(--color-white);
+ --color-fg-dark: var(--color-gray-900);
+ --color-fg-contrast: var(--color-white);
+ --color-fg-heading: var(--color-gray-900);
+ --color-fg-body: var(--color-gray-600);
+ --color-fg-body-subtle: var(--color-gray-500);
+ --color-fg-disabled: var(--color-gray-400);
+
+ /* Brand Foreground */
+ --color-fg-brand-soft: var(--color-brand-200);
+ --color-fg-brand: var(--color-brand-700);
+ --color-fg-brand-strong: var(--color-brand-900);
+
+ /* Status Foreground */
+ --color-fg-success: var(--color-green-700);
+ --color-fg-success-strong: var(--color-green-900);
+ --color-fg-danger: var(--color-red-700);
+ --color-fg-danger-strong: var(--color-red-900);
+ --color-fg-warning: var(--color-orange-600);
+ --color-fg-warning-strong: var(--color-orange-900);
+ --color-fg-sensitive: var(--color-pink-600);
+
+ /* Accent Foreground */
+ --color-fg-accent-primary-soft: var(--color-teal-200);
+ --color-fg-accent-primary: var(--color-teal-400);
+ --color-fg-accent-primary-strong: var(--color-teal-800);
+ --color-fg-accent-secondary-soft: var(--color-coral-200);
+ --color-fg-accent-secondary: var(--color-coral-400);
+ --color-fg-accent-secondary-strong: var(--color-coral-900);
+ --color-fg-accent-tertiary-soft: var(--color-purple-200);
+ --color-fg-accent-tertiary: var(--color-purple-700);
+ --color-fg-accent-tertiary-strong: var(--color-purple-900);
+
+ /* ========================================
+ * SEMANTIC BACKGROUND COLORS (Light Mode)
+ * ======================================== */
+
+ /* Neutral Background */
+ --color-bg-white: var(--color-white);
+ --color-bg-dark: var(--color-gray-800);
+ --color-bg-contrast: var(--color-gray-800);
+ --color-bg-contrast-strong: var(--color-gray-950);
+ --color-bg-primary: var(--color-white);
+ --color-bg-secondary: var(--color-gray-050);
+ --color-bg-tertiary: var(--color-gray-050);
+ --color-bg-quaternary: var(--color-gray-200);
+ --color-bg-gray: var(--color-gray-300);
+ --color-bg-disabled: var(--color-gray-100);
+
+ /* Brand Background */
+ --color-bg-brand-softer: var(--color-brand-050);
+ --color-bg-brand-soft: var(--color-brand-100);
+ --color-bg-brand-medium: var(--color-brand-200);
+ --color-bg-brand: var(--color-brand-700);
+ --color-bg-brand-strong: var(--color-brand-800);
+
+ /* Status Background */
+ --color-bg-success-soft: var(--color-green-050);
+ --color-bg-success-medium: var(--color-green-100);
+ --color-bg-success: var(--color-green-700);
+ --color-bg-success-strong: var(--color-green-800);
+ --color-bg-danger-soft: var(--color-red-050);
+ --color-bg-danger-medium: var(--color-red-100);
+ --color-bg-danger: var(--color-red-700);
+ --color-bg-danger-strong: var(--color-red-800);
+ --color-bg-warning-soft: var(--color-orange-050);
+ --color-bg-warning-medium: var(--color-orange-100);
+ --color-bg-warning: var(--color-orange-600);
+ --color-bg-warning-strong: var(--color-orange-700);
+
+ /* Accent Background */
+ --color-bg-accent-primary-soft: var(--color-teal-050);
+ --color-bg-accent-primary-medium: var(--color-teal-100);
+ --color-bg-accent-primary: var(--color-teal-400);
+ --color-bg-accent-secondary-soft: var(--color-coral-050);
+ --color-bg-accent-secondary-medium: var(--color-coral-100);
+ --color-bg-accent-secondary: var(--color-coral-400);
+ --color-bg-accent-tertiary-soft: var(--color-purple-050);
+ --color-bg-accent-tertiary-medium: var(--color-purple-100);
+ --color-bg-accent-tertiary: var(--color-purple-600);
+
+ /* Hover & Overlay */
+ --color-bg-hover: rgba(var(--color-gray-950-rgb), 0.05);
+ --color-bg-overlay: rgba(var(--color-gray-950-rgb), 0.3);
+
+ /* ========================================
+ * SEMANTIC BORDER COLORS (Light Mode)
+ * ======================================== */
+
+ /* Neutral Border */
+ --color-border-muted: var(--color-gray-050);
+ --color-border-light: var(--color-gray-100);
+ --color-border-base: var(--color-gray-200);
+ --color-border-strong: var(--color-gray-800);
+ --color-border-buffer: var(--color-white);
+
+ /* Brand Border */
+ --color-border-brand-soft: var(--color-brand-200);
+ --color-border-brand: var(--color-brand-700);
+ --color-border-brand-strong: var(--color-brand-900);
+
+ /* Status Border */
+ --color-border-success-soft: var(--color-green-200);
+ --color-border-success: var(--color-green-700);
+ --color-border-success-strong: var(--color-green-900);
+ --color-border-danger-soft: var(--color-red-200);
+ --color-border-danger: var(--color-red-700);
+ --color-border-danger-strong: var(--color-red-900);
+ --color-border-warning-soft: var(--color-orange-200);
+ --color-border-warning: var(--color-orange-600);
+ --color-border-warning-strong: var(--color-orange-900);
+
+ /* Accent Border */
+ --color-border-accent-primary-soft: var(--color-teal-200);
+ --color-border-accent-primary: var(--color-teal-600);
+ --color-border-accent-secondary-soft: var(--color-coral-200);
+ --color-border-accent-secondary: var(--color-coral-600);
+ --color-border-accent-tertiary-soft: var(--color-purple-200);
+ --color-border-accent-tertiary: var(--color-purple-600);
+
+ /* Focus Border */
+ --color-border-focus: var(--color-black);
}
.theme_light {
@@ -140,6 +419,129 @@
--color-illustration-bg-tertiary: 243 246 249;
--color-illustration-tertiary: 255 191 0;
--color-illustration-logo: 255 255 255;
+
+ /* ========================================
+ * SEMANTIC FOREGROUND COLORS (Dark Mode Overrides)
+ * ======================================== */
+
+ /* Neutral Foreground */
+ --color-fg-contrast: var(--color-gray-900);
+ --color-fg-heading: var(--color-gray-050);
+ --color-fg-body: var(--color-gray-200);
+ --color-fg-body-subtle: var(--color-gray-400);
+ --color-fg-disabled: var(--color-gray-600);
+
+ /* Brand Foreground */
+ --color-fg-brand-soft: var(--color-brand-500);
+ --color-fg-brand: var(--color-brand-400);
+ --color-fg-brand-strong: var(--color-brand-200);
+
+ /* Status Foreground */
+ --color-fg-success: var(--color-green-400);
+ --color-fg-success-strong: var(--color-green-100);
+ --color-fg-danger: var(--color-red-400);
+ --color-fg-danger-strong: var(--color-red-100);
+ --color-fg-warning: var(--color-orange-400);
+ --color-fg-warning-strong: var(--color-orange-100);
+ --color-fg-sensitive: var(--color-pink-300);
+
+ /* Accent Foreground */
+ --color-fg-accent-primary-soft: var(--color-teal-400);
+ --color-fg-accent-primary: var(--color-teal-300);
+ --color-fg-accent-primary-strong: var(--color-teal-100);
+ --color-fg-accent-secondary-soft: var(--color-coral-500);
+ --color-fg-accent-secondary: var(--color-coral-400);
+ --color-fg-accent-secondary-strong: var(--color-coral-100);
+ --color-fg-accent-tertiary-soft: var(--color-purple-500);
+ --color-fg-accent-tertiary: var(--color-purple-400);
+ --color-fg-accent-tertiary-strong: var(--color-purple-100);
+
+ /* ========================================
+ * SEMANTIC BACKGROUND COLORS (Dark Mode Overrides)
+ * ======================================== */
+
+ /* Neutral Background */
+ --color-bg-contrast: var(--color-gray-050);
+ --color-bg-contrast-strong: var(--color-gray-050);
+ --color-bg-primary: var(--color-gray-900);
+ --color-bg-secondary: var(--color-gray-800);
+ --color-bg-tertiary: var(--color-gray-950);
+ --color-bg-quaternary: var(--color-gray-700);
+ --color-bg-gray: var(--color-gray-600);
+ --color-bg-disabled: var(--color-gray-950);
+
+ /* Brand Background */
+ --color-bg-brand-softer: var(--color-brand-950);
+ --color-bg-brand-soft: var(--color-brand-900);
+ --color-bg-brand-medium: var(--color-brand-800);
+ --color-bg-brand: var(--color-brand-400);
+ --color-bg-brand-strong: var(--color-brand-300);
+
+ /* Status Background */
+ --color-bg-success-soft: var(--color-green-950);
+ --color-bg-success-medium: var(--color-green-900);
+ --color-bg-success: var(--color-green-400);
+ --color-bg-success-strong: var(--color-green-300);
+ --color-bg-danger-soft: var(--color-red-950);
+ --color-bg-danger-medium: var(--color-red-900);
+ --color-bg-danger: var(--color-red-400);
+ --color-bg-danger-strong: var(--color-red-300);
+ --color-bg-warning-soft: var(--color-orange-950);
+ --color-bg-warning-medium: var(--color-orange-900);
+ --color-bg-warning: var(--color-orange-400);
+ --color-bg-warning-strong: var(--color-orange-300);
+
+ /* Accent Background */
+ --color-bg-accent-primary-soft: var(--color-teal-950);
+ --color-bg-accent-primary-medium: var(--color-teal-900);
+ --color-bg-accent-primary: var(--color-teal-400);
+ --color-bg-accent-secondary-soft: var(--color-coral-950);
+ --color-bg-accent-secondary-medium: var(--color-coral-900);
+ --color-bg-accent-secondary: var(--color-coral-400);
+ --color-bg-accent-tertiary-soft: var(--color-purple-950);
+ --color-bg-accent-tertiary-medium: var(--color-purple-900);
+ --color-bg-accent-tertiary: var(--color-purple-600);
+
+ /* Hover & Overlay */
+ --color-bg-hover: rgba(var(--color-white-rgb), 0.05);
+ --color-bg-overlay: rgba(var(--color-gray-950-rgb), 0.85);
+
+ /* ========================================
+ * SEMANTIC BORDER COLORS (Dark Mode Overrides)
+ * ======================================== */
+
+ /* Neutral Border */
+ --color-border-muted: var(--color-gray-900);
+ --color-border-light: var(--color-gray-800);
+ --color-border-base: var(--color-gray-700);
+ --color-border-strong: var(--color-gray-400);
+ --color-border-buffer: var(--color-gray-950);
+
+ /* Brand Border */
+ --color-border-brand-soft: var(--color-brand-800);
+ --color-border-brand: var(--color-brand-400);
+ --color-border-brand-strong: var(--color-brand-200);
+
+ /* Status Border */
+ --color-border-success-soft: var(--color-green-800);
+ --color-border-success: var(--color-green-400);
+ --color-border-success-strong: var(--color-green-200);
+ --color-border-danger-soft: var(--color-red-800);
+ --color-border-danger: var(--color-red-400);
+ --color-border-danger-strong: var(--color-red-200);
+ --color-border-warning-soft: var(--color-orange-800);
+ --color-border-warning: var(--color-orange-400);
+ --color-border-warning-strong: var(--color-orange-200);
+
+ /* Accent Border */
+ --color-border-accent-primary-soft: var(--color-teal-800);
+ --color-border-accent-secondary-soft: var(--color-coral-800);
+ --color-border-accent-secondary: var(--color-coral-500);
+ --color-border-accent-tertiary-soft: var(--color-purple-800);
+ --color-border-accent-tertiary: var(--color-purple-500);
+
+ /* Focus Border */
+ --color-border-focus: var(--color-white);
}
@layer components {
diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js
index e41cff16e48..bd88f5471ff 100644
--- a/libs/components/tailwind.config.base.js
+++ b/libs/components/tailwind.config.base.js
@@ -9,9 +9,9 @@ function rgba(color) {
module.exports = {
prefix: "tw-",
content: [
- "./src/**/*.{html,ts}",
+ "./src/**/*.{html,ts,mdx}",
"../../libs/assets/src/**/*.{html,ts}",
- "../../libs/components/src/**/*.{html,ts}",
+ "../../libs/components/src/**/*.{html,ts,mdx}",
"../../libs/key-management-ui/src/**/*.{html,ts}",
"../../libs/auth/src/**/*.{html,ts}",
],
@@ -78,6 +78,46 @@ module.exports = {
alt3: rgba("--color-background-alt3"),
alt4: rgba("--color-background-alt4"),
},
+ bg: {
+ white: "var(--color-bg-white)",
+ dark: "var(--color-bg-dark)",
+ contrast: "var(--color-bg-contrast)",
+ "contrast-strong": "var(--color-bg-contrast-strong)",
+ primary: "var(--color-bg-primary)",
+ secondary: "var(--color-bg-secondary)",
+ tertiary: "var(--color-bg-tertiary)",
+ quaternary: "var(--color-bg-quaternary)",
+ gray: "var(--color-bg-gray)",
+ disabled: "var(--color-bg-disabled)",
+ "brand-softer": "var(--color-bg-brand-softer)",
+ "brand-soft": "var(--color-bg-brand-soft)",
+ "brand-medium": "var(--color-bg-brand-medium)",
+ brand: "var(--color-bg-brand)",
+ "brand-strong": "var(--color-bg-brand-strong)",
+ "success-soft": "var(--color-bg-success-soft)",
+ "success-medium": "var(--color-bg-success-medium)",
+ success: "var(--color-bg-success)",
+ "success-strong": "var(--color-bg-success-strong)",
+ "danger-soft": "var(--color-bg-danger-soft)",
+ "danger-medium": "var(--color-bg-danger-medium)",
+ danger: "var(--color-bg-danger)",
+ "danger-strong": "var(--color-bg-danger-strong)",
+ "warning-soft": "var(--color-bg-warning-soft)",
+ "warning-medium": "var(--color-bg-warning-medium)",
+ warning: "var(--color-bg-warning)",
+ "warning-strong": "var(--color-bg-warning-strong)",
+ "accent-primary-soft": "var(--color-bg-accent-primary-soft)",
+ "accent-primary-medium": "var(--color-bg-accent-primary-medium)",
+ "accent-primary": "var(--color-bg-accent-primary)",
+ "accent-secondary-soft": "var(--color-bg-accent-secondary-soft)",
+ "accent-secondary-medium": "var(--color-bg-accent-secondary-medium)",
+ "accent-secondary": "var(--color-bg-accent-secondary)",
+ "accent-tertiary-soft": "var(--color-bg-accent-tertiary-soft)",
+ "accent-tertiary-medium": "var(--color-bg-accent-tertiary-medium)",
+ "accent-tertiary": "var(--color-bg-accent-tertiary)",
+ hover: "var(--color-bg-hover)",
+ overlay: "var(--color-bg-overlay)",
+ },
hover: {
default: "var(--color-hover-default)",
contrast: "var(--color-hover-contrast)",
@@ -92,8 +132,62 @@ module.exports = {
tertiary: rgba("--color-illustration-tertiary"),
logo: rgba("--color-illustration-logo"),
},
+ fg: {
+ white: "var(--color-fg-white)",
+ dark: "var(--color-fg-dark)",
+ contrast: "var(--color-fg-contrast)",
+ heading: "var(--color-fg-heading)",
+ body: "var(--color-fg-body)",
+ "body-subtle": "var(--color-fg-body-subtle)",
+ disabled: "var(--color-fg-disabled)",
+ "brand-soft": "var(--color-fg-brand-soft)",
+ brand: "var(--color-fg-brand)",
+ "brand-strong": "var(--color-fg-brand-strong)",
+ success: "var(--color-fg-success)",
+ "success-strong": "var(--color-fg-success-strong)",
+ danger: "var(--color-fg-danger)",
+ "danger-strong": "var(--color-fg-danger-strong)",
+ warning: "var(--color-fg-warning)",
+ "warning-strong": "var(--color-fg-warning-strong)",
+ sensitive: "var(--color-fg-sensitive)",
+ "accent-primary-soft": "var(--color-fg-accent-primary-soft)",
+ "accent-primary": "var(--color-fg-accent-primary)",
+ "accent-primary-strong": "var(--color-fg-accent-primary-strong)",
+ "accent-secondary-soft": "var(--color-fg-accent-secondary-soft)",
+ "accent-secondary": "var(--color-fg-accent-secondary)",
+ "accent-secondary-strong": "var(--color-fg-accent-secondary-strong)",
+ "accent-tertiary-soft": "var(--color-fg-accent-tertiary-soft)",
+ "accent-tertiary": "var(--color-fg-accent-tertiary)",
+ "accent-tertiary-strong": "var(--color-fg-accent-tertiary-strong)",
+ },
+ border: {
+ muted: "var(--color-border-muted)",
+ light: "var(--color-border-light)",
+ base: "var(--color-border-base)",
+ strong: "var(--color-border-strong)",
+ buffer: "var(--color-border-buffer)",
+ "brand-soft": "var(--color-border-brand-soft)",
+ brand: "var(--color-border-brand)",
+ "brand-strong": "var(--color-border-brand-strong)",
+ "success-soft": "var(--color-border-success-soft)",
+ success: "var(--color-border-success)",
+ "success-strong": "var(--color-border-success-strong)",
+ "danger-soft": "var(--color-border-danger-soft)",
+ danger: "var(--color-border-danger)",
+ "danger-strong": "var(--color-border-danger-strong)",
+ "warning-soft": "var(--color-border-warning-soft)",
+ warning: "var(--color-border-warning)",
+ "warning-strong": "var(--color-border-warning-strong)",
+ "accent-primary-soft": "var(--color-border-accent-primary-soft)",
+ "accent-primary": "var(--color-border-accent-primary)",
+ "accent-secondary-soft": "var(--color-border-accent-secondary-soft)",
+ "accent-secondary": "var(--color-border-accent-secondary)",
+ "accent-tertiary-soft": "var(--color-border-accent-tertiary-soft)",
+ "accent-tertiary": "var(--color-border-accent-tertiary)",
+ focus: "var(--color-border-focus)",
+ },
},
- textColor: {
+ textColor: () => ({
main: rgba("--color-text-main"),
muted: rgba("--color-text-muted"),
contrast: rgba("--color-text-contrast"),
@@ -132,7 +226,62 @@ module.exports = {
notification: {
600: rgba("--color-notification-600"),
},
- },
+ // New semantic fg tokens - manually flattened to generate tw-text-fg-* utilities
+ "fg-white": "var(--color-fg-white)",
+ "fg-dark": "var(--color-fg-dark)",
+ "fg-contrast": "var(--color-fg-contrast)",
+ "fg-heading": "var(--color-fg-heading)",
+ "fg-body": "var(--color-fg-body)",
+ "fg-body-subtle": "var(--color-fg-body-subtle)",
+ "fg-disabled": "var(--color-fg-disabled)",
+ "fg-brand-soft": "var(--color-fg-brand-soft)",
+ "fg-brand": "var(--color-fg-brand)",
+ "fg-brand-strong": "var(--color-fg-brand-strong)",
+ "fg-success": "var(--color-fg-success)",
+ "fg-success-strong": "var(--color-fg-success-strong)",
+ "fg-danger": "var(--color-fg-danger)",
+ "fg-danger-strong": "var(--color-fg-danger-strong)",
+ "fg-warning": "var(--color-fg-warning)",
+ "fg-warning-strong": "var(--color-fg-warning-strong)",
+ "fg-sensitive": "var(--color-fg-sensitive)",
+ "fg-accent-primary-soft": "var(--color-fg-accent-primary-soft)",
+ "fg-accent-primary": "var(--color-fg-accent-primary)",
+ "fg-accent-primary-strong": "var(--color-fg-accent-primary-strong)",
+ "fg-accent-secondary-soft": "var(--color-fg-accent-secondary-soft)",
+ "fg-accent-secondary": "var(--color-fg-accent-secondary)",
+ "fg-accent-secondary-strong": "var(--color-fg-accent-secondary-strong)",
+ "fg-accent-tertiary-soft": "var(--color-fg-accent-tertiary-soft)",
+ "fg-accent-tertiary": "var(--color-fg-accent-tertiary)",
+ "fg-accent-tertiary-strong": "var(--color-fg-accent-tertiary-strong)",
+ }),
+ borderColor: ({ theme }) => ({
+ ...theme("colors"),
+ // New semantic border tokens - manually flattened to generate tw-border-border-* utilities
+ "border-muted": "var(--color-border-muted)",
+ "border-light": "var(--color-border-light)",
+ "border-base": "var(--color-border-base)",
+ "border-strong": "var(--color-border-strong)",
+ "border-buffer": "var(--color-border-buffer)",
+ "border-brand-soft": "var(--color-border-brand-soft)",
+ "border-brand": "var(--color-border-brand)",
+ "border-brand-strong": "var(--color-border-brand-strong)",
+ "border-success-soft": "var(--color-border-success-soft)",
+ "border-success": "var(--color-border-success)",
+ "border-success-strong": "var(--color-border-success-strong)",
+ "border-danger-soft": "var(--color-border-danger-soft)",
+ "border-danger": "var(--color-border-danger)",
+ "border-danger-strong": "var(--color-border-danger-strong)",
+ "border-warning-soft": "var(--color-border-warning-soft)",
+ "border-warning": "var(--color-border-warning)",
+ "border-warning-strong": "var(--color-border-warning-strong)",
+ "border-accent-primary-soft": "var(--color-border-accent-primary-soft)",
+ "border-accent-primary": "var(--color-border-accent-primary)",
+ "border-accent-secondary-soft": "var(--color-border-accent-secondary-soft)",
+ "border-accent-secondary": "var(--color-border-accent-secondary)",
+ "border-accent-tertiary-soft": "var(--color-border-accent-tertiary-soft)",
+ "border-accent-tertiary": "var(--color-border-accent-tertiary)",
+ "border-focus": "var(--color-border-focus)",
+ }),
fontFamily: {
sans: "var(--font-sans)",
serif: "var(--font-serif)",
diff --git a/libs/components/tailwind.config.js b/libs/components/tailwind.config.js
index d8cef6596dc..0fa5b259bb6 100644
--- a/libs/components/tailwind.config.js
+++ b/libs/components/tailwind.config.js
@@ -11,11 +11,16 @@ config.content = [
"bitwarden_license/bit-web/src/**/*.{html,ts,mdx}",
".storybook/preview.tsx",
];
+
+// Safelist is required for dynamic color classes in Storybook color documentation (colors.mdx).
+// Tailwind's JIT compiler cannot detect dynamically constructed class names like `tw-bg-${name}`,
+// so we must explicitly safelist these patterns to ensure all color utilities are generated.
config.safelist = [
{
pattern: /tw-bg-(.*)/,
},
];
+
config.corePlugins.preflight = true;
module.exports = config;
From 5ddfd91a14c09f3fa9bdbe6db60585573767f44a Mon Sep 17 00:00:00 2001
From: Alex <55413326+AlexRubik@users.noreply.github.com>
Date: Fri, 26 Dec 2025 17:28:10 -0500
Subject: [PATCH 30/81] correct virtual scroll rowSize for password reports
(#18058)
The Exposed Passwords and Weak Passwords reports were using an incorrect
rowSize value (53px instead of 75px) for their virtual scroll tables.
This caused the \"Back to reports\" button to collide with table entries.
The issue was cumulative - more items meant more visible collision. This
fix aligns both reports with the Reused Passwords report which correctly
uses 75px for identical row structures.
---
.../dirt/reports/pages/exposed-passwords-report.component.html | 2 +-
.../app/dirt/reports/pages/weak-passwords-report.component.html | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html
index eb476090963..fcdb3f6ca64 100644
--- a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html
+++ b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html
@@ -26,7 +26,7 @@
-
+
{{ "name" | i18n }}
diff --git a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html
index 5fa2806d133..92d56c1c7a3 100644
--- a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html
+++ b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html
@@ -31,7 +31,7 @@
-
+
{{ "name" | i18n }}
From c5484616506f1ef5a66bcf73db1f0660e5d5403e Mon Sep 17 00:00:00 2001
From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com>
Date: Sun, 28 Dec 2025 10:41:29 +0100
Subject: [PATCH 31/81] Autosync the updated translations (#18118)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
---
apps/web/src/locales/it/messages.json | 64 ++++++++++++------------
apps/web/src/locales/pt_BR/messages.json | 4 +-
2 files changed, 34 insertions(+), 34 deletions(-)
diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json
index 0b98058ae82..b4942bd75f6 100644
--- a/apps/web/src/locales/it/messages.json
+++ b/apps/web/src/locales/it/messages.json
@@ -1970,11 +1970,11 @@
"message": "Le chiavi di cifratura dell'account sono uniche per ogni account utente Bitwarden, quindi non è possibile importare un'esportazione cifrata in un account diverso."
},
"exportNoun": {
- "message": "Export",
+ "message": "Esporta",
"description": "The noun form of the word Export"
},
"exportVerb": {
- "message": "Export",
+ "message": "Esporta",
"description": "The verb form of the word Export"
},
"exportFrom": {
@@ -2303,11 +2303,11 @@
"message": "Strumenti"
},
"importNoun": {
- "message": "Import",
+ "message": "Importa",
"description": "The noun form of the word Import"
},
"importVerb": {
- "message": "Import",
+ "message": "Importa",
"description": "The verb form of the word Import"
},
"importData": {
@@ -3294,7 +3294,7 @@
"message": "Avvia abbonamento cloud"
},
"launchCloudSubscriptionSentenceCase": {
- "message": "Launch cloud subscription"
+ "message": "Avvia abbonamento cloud"
},
"storage": {
"message": "Spazio di archiviazione"
@@ -4212,10 +4212,10 @@
}
},
"userAcceptedTransfer": {
- "message": "Accepted transfer to organization ownership."
+ "message": "Trasferimento di proprietà all'organizzazione accettato."
},
"userDeclinedTransfer": {
- "message": "Revoked for declining transfer to organization ownership."
+ "message": "Revocato per il rifiuto di trasferimento di proprietà all'organizzazione."
},
"invitedUserId": {
"message": "Utente $ID$ invitato.",
@@ -11607,7 +11607,7 @@
"message": "Togli dall'archivio"
},
"unArchiveAndSave": {
- "message": "Unarchive and save"
+ "message": "Togli dall'archivio e salva"
},
"itemsInArchive": {
"message": "Elementi archiviati"
@@ -12251,43 +12251,43 @@
}
},
"removeMasterPasswordForOrgUserKeyConnector": {
- "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
+ "message": "La tua organizzazione non utilizza più le password principali per accedere a Bitwarden. Per continuare, verifica l'organizzazione e il dominio."
},
"continueWithLogIn": {
- "message": "Continue with log in"
+ "message": "Accedi e continua"
},
"doNotContinue": {
- "message": "Do not continue"
+ "message": "Non continuare"
},
"domain": {
- "message": "Domain"
+ "message": "Dominio"
},
"keyConnectorDomainTooltip": {
- "message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
+ "message": "Questo dominio memorizzerà le chiavi di crittografia del tuo account, quindi assicurati di impostarlo come affidabile. Se non hai la certezza che lo sia, verifica con l'amministratore."
},
"verifyYourOrganization": {
- "message": "Verify your organization to log in"
+ "message": "Verifica la tua organizzazione per accedere"
},
"organizationVerified": {
- "message": "Organization verified"
+ "message": "Organizzazione verificata"
},
"domainVerified": {
- "message": "Domain verified"
+ "message": "Dominio verificato"
},
"leaveOrganizationContent": {
- "message": "If you don't verify your organization, your access to the organization will be revoked."
+ "message": "Se non verifichi l'organizzazione, il tuo accesso sarà revocato."
},
"leaveNow": {
- "message": "Leave now"
+ "message": "Abbandona"
},
"verifyYourDomainToLogin": {
- "message": "Verify your domain to log in"
+ "message": "Verifica il tuo dominio per accedere"
},
"verifyYourDomainDescription": {
- "message": "To continue with log in, verify this domain."
+ "message": "Per continuare con l'accesso, verifica questo dominio."
},
"confirmKeyConnectorOrganizationUserDescription": {
- "message": "To continue with log in, verify the organization and domain."
+ "message": "Per continuare con l'accesso, verifica l'organizzazione e il dominio."
},
"confirmNoSelectedCriticalApplicationsTitle": {
"message": "Non ci sono applicazioni contrassegnate come critiche"
@@ -12433,13 +12433,13 @@
"message": "Perché vedo questo avviso?"
},
"youHaveBitwardenPremium": {
- "message": "You have Bitwarden Premium"
+ "message": "Hai Bitwarden Premium"
},
"viewAndManagePremiumSubscription": {
- "message": "View and manage your Premium subscription"
+ "message": "Visualizza e gestisci il tuo abbonamento Premium"
},
"youNeedToUpdateLicenseFile": {
- "message": "You'll need to update your license file"
+ "message": "Dovrai aggiornare il tuo file di licenza"
},
"youNeedToUpdateLicenseFileDate": {
"message": "$DATE$.",
@@ -12451,16 +12451,16 @@
}
},
"uploadLicenseFile": {
- "message": "Upload license file"
+ "message": "Carica il file di licenza"
},
"uploadYourLicenseFile": {
- "message": "Upload your license file"
+ "message": "Carica il file di licenza"
},
"uploadYourPremiumLicenseFile": {
- "message": "Upload your Premium license file"
+ "message": "Carica il tuo file di licenza Premium"
},
"uploadLicenseFileDesc": {
- "message": "Your license file name will be similar to: $FILE_NAME$",
+ "message": "Il nome del file di licenza sarà simile a $FILE_NAME$",
"placeholders": {
"file_name": {
"content": "$1",
@@ -12469,15 +12469,15 @@
}
},
"alreadyHaveSubscriptionQuestion": {
- "message": "Already have a subscription?"
+ "message": "Hai già un abbonamento?"
},
"alreadyHaveSubscriptionSelfHostedMessage": {
- "message": "Open the subscription page on your Bitwarden cloud account and download your license file. Then return to this screen and upload it below."
+ "message": "Vai alla pagina degli abbonamenti del tuo account Bitwarden e scarica il file di licenza, poi torna a caricarlo qui."
},
"viewAllPlans": {
- "message": "View all plans"
+ "message": "Visualizza tutti i piani"
},
"planDescPremium": {
- "message": "Complete online security"
+ "message": "Sicurezza online completa"
}
}
diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json
index fbfaf08d030..28e95ed6379 100644
--- a/apps/web/src/locales/pt_BR/messages.json
+++ b/apps/web/src/locales/pt_BR/messages.json
@@ -1943,7 +1943,7 @@
"message": "Copiar UUID"
},
"errorRefreshingAccessToken": {
- "message": "Erro de recarregamento do token de acesso"
+ "message": "Erro de Recarregamento do Token de Acesso"
},
"errorRefreshingAccessTokenDesc": {
"message": "Nenhum token de atualização ou chave de API foi encontrado. Tente se desconectar e se conectar novamente."
@@ -3294,7 +3294,7 @@
"message": "Iniciar Assinatura na Nuvem"
},
"launchCloudSubscriptionSentenceCase": {
- "message": "Launch cloud subscription"
+ "message": "Executar assinatura na nuvem"
},
"storage": {
"message": "Armazenamento"
From 8acbb246a1e4f0b53c8176e5ad4809f21138998b Mon Sep 17 00:00:00 2001
From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com>
Date: Sun, 28 Dec 2025 09:56:36 +0000
Subject: [PATCH 32/81] Autosync the updated translations (#18128)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
---
apps/desktop/src/locales/zh_CN/messages.json | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json
index 7d1c1648bb6..b80e1cea689 100644
--- a/apps/desktop/src/locales/zh_CN/messages.json
+++ b/apps/desktop/src/locales/zh_CN/messages.json
@@ -709,7 +709,7 @@
"message": "添加附件"
},
"itemsTransferred": {
- "message": "项目已传输"
+ "message": "项目已转移"
},
"fixEncryption": {
"message": "修复加密"
@@ -4454,7 +4454,7 @@
"message": "我该如何管理我的密码库?"
},
"transferItemsToOrganizationTitle": {
- "message": "传输项目到 $ORGANIZATION$",
+ "message": "转移项目到 $ORGANIZATION$",
"placeholders": {
"organization": {
"content": "$1",
@@ -4463,7 +4463,7 @@
}
},
"transferItemsToOrganizationContent": {
- "message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以传输您的项目的所有权。",
+ "message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以转移您的项目的所有权。",
"placeholders": {
"organization": {
"content": "$1",
@@ -4472,7 +4472,7 @@
}
},
"acceptTransfer": {
- "message": "接受传输"
+ "message": "接受转移"
},
"declineAndLeave": {
"message": "拒绝并退出"
From 00b53294308d9d95313fa7304a3c422aae59815c Mon Sep 17 00:00:00 2001
From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com>
Date: Sun, 28 Dec 2025 09:57:05 +0000
Subject: [PATCH 33/81] Autosync the updated translations (#18129)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
---
apps/browser/src/_locales/az/messages.json | 4 ++--
apps/browser/src/_locales/zh_CN/messages.json | 12 ++++++------
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json
index 3f98313c2b8..d7257bab478 100644
--- a/apps/browser/src/_locales/az/messages.json
+++ b/apps/browser/src/_locales/az/messages.json
@@ -2498,7 +2498,7 @@
}
},
"topLayerHijackWarning": {
- "message": "This page is interfering with the Bitwarden experience. The Bitwarden inline menu has been temporarily disabled as a safety measure."
+ "message": "Bu səhifə Bitwarden təcrübəsinə müdaxilə edir. Bitwarden daxili menyusu, təhlükəsizlik tədbiri olaraq müvəqqəti sıradan çıxarılıb."
},
"setMasterPassword": {
"message": "Ana parolu ayarla"
@@ -4124,7 +4124,7 @@
"message": "Avto-doldurula bilmir"
},
"cannotAutofillExactMatch": {
- "message": "Default matching is set to 'Exact Match'. The current website does not exactly match the saved login details for this item."
+ "message": "İlkin uyuşma 'Tam Uyuşur' olaraq ayarlanıb. Hazırkı veb sayt, bu element üçün saxlanılmış giriş məlumatları ilə tam uyuşmur."
},
"okay": {
"message": "Oldu"
diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json
index a699be016eb..c7f7fbcd618 100644
--- a/apps/browser/src/_locales/zh_CN/messages.json
+++ b/apps/browser/src/_locales/zh_CN/messages.json
@@ -1486,7 +1486,7 @@
"message": "选择一个文件"
},
"itemsTransferred": {
- "message": "项目已传输"
+ "message": "项目已转移"
},
"maxFileSize": {
"message": "文件最大为 500 MB。"
@@ -3804,7 +3804,7 @@
"description": "Browser extension/addon"
},
"desktop": {
- "message": "桌面",
+ "message": "桌面端",
"description": "Desktop app"
},
"webVault": {
@@ -5707,7 +5707,7 @@
"message": "导入现有密码"
},
"emptyVaultNudgeBody": {
- "message": "使用导入器快速将登录传输到 Bitwarden 而无需手动添加。"
+ "message": "使用导入器快速将登录转移到 Bitwarden 而无需手动添加。"
},
"emptyVaultNudgeButton": {
"message": "立即导入"
@@ -6014,7 +6014,7 @@
"message": "我该如何管理我的密码库?"
},
"transferItemsToOrganizationTitle": {
- "message": "传输项目到 $ORGANIZATION$",
+ "message": "转移项目到 $ORGANIZATION$",
"placeholders": {
"organization": {
"content": "$1",
@@ -6023,7 +6023,7 @@
}
},
"transferItemsToOrganizationContent": {
- "message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以传输您的项目的所有权。",
+ "message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以转移您的项目的所有权。",
"placeholders": {
"organization": {
"content": "$1",
@@ -6032,7 +6032,7 @@
}
},
"acceptTransfer": {
- "message": "接受传输"
+ "message": "接受转移"
},
"declineAndLeave": {
"message": "拒绝并退出"
From d4a276f1de303bb895d737b7e2ba994ccd8005c7 Mon Sep 17 00:00:00 2001
From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com>
Date: Sun, 28 Dec 2025 09:57:37 +0000
Subject: [PATCH 34/81] Autosync the updated translations (#18130)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
---
apps/web/src/locales/az/messages.json | 4 ++--
apps/web/src/locales/de/messages.json | 4 ++--
apps/web/src/locales/zh_CN/messages.json | 24 ++++++++++++------------
3 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json
index 275ee56dd5c..a86dbdb6406 100644
--- a/apps/web/src/locales/az/messages.json
+++ b/apps/web/src/locales/az/messages.json
@@ -5871,7 +5871,7 @@
"description": "This is the policy description shown in the policy list."
},
"organizationDataOwnershipDescContent": {
- "message": "All items will be owned and saved to the organization, enabling organization-wide controls, visibility, and reporting. When turned on, a default collection will be available for each member to store items. Learn more about managing the ",
+ "message": "Bütün elementlər bir təşkilata məxsus olacaq və orada saxlanılacaq, bu da təşkilat üzrə kontrollar, görünürlük və hesabatları mümkün edəcək. İşə salındığı zaman, hər üzv üçün elementləri saxlaya biləcəyi ilkin bir kolleksiya mövcud olacaq. Daha ətraflı ",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'All items will be owned and saved to the organization, enabling organization-wide controls, visibility, and reporting. When turned on, a default collection will be available for each member to store items. Learn more about managing the credential lifecycle.'"
},
"organizationDataOwnershipContentAnchor": {
@@ -6752,7 +6752,7 @@
"message": "Bütün üzvlər üçün maksimum bitmə vaxtını \"Heç vaxt\" olaraq icazə vermək istədiyinizə əminsiniz?"
},
"sessionTimeoutConfirmationNeverDescription": {
- "message": "This option will save your members' encryption keys on their devices. If you choose this option, ensure that their devices are adequately protected."
+ "message": "Bu seçim, üzvlərinizin şifrələmə açarlarını onların cihazlarında saxlayacaq. Bu seçimi seçsəniz, onların cihazlarının lazımi səviyyədə qorunduğuna əmin olun."
},
"learnMoreAboutDeviceProtection": {
"message": "Cihaz mühafizəsi barədə daha ətraflı"
diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json
index ccde12d8614..ae95c0ca9cb 100644
--- a/apps/web/src/locales/de/messages.json
+++ b/apps/web/src/locales/de/messages.json
@@ -12436,7 +12436,7 @@
"message": "Du hast Bitwarden Premium"
},
"viewAndManagePremiumSubscription": {
- "message": "View and manage your Premium subscription"
+ "message": "Dein Premium-Abonnement anzeigen und verwalten"
},
"youNeedToUpdateLicenseFile": {
"message": "Du musst deine Lizenzdatei aktualisieren"
@@ -12472,7 +12472,7 @@
"message": "Du hast bereits ein Abonnement?"
},
"alreadyHaveSubscriptionSelfHostedMessage": {
- "message": "Open the subscription page on your Bitwarden cloud account and download your license file. Then return to this screen and upload it below."
+ "message": "Öffne die Abonnementseite in deinem Bitwarden Cloud-Konto und lade deine Lizenzdatei herunter. Gehe dann zu dieser Seite zurück und lade sie unten hoch."
},
"viewAllPlans": {
"message": "Alle Tarife anzeigen"
diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json
index d1ee6e0f659..b41635b948c 100644
--- a/apps/web/src/locales/zh_CN/messages.json
+++ b/apps/web/src/locales/zh_CN/messages.json
@@ -3862,7 +3862,7 @@
"description": "Browser extension/addon"
},
"desktop": {
- "message": "桌面版应用",
+ "message": "桌面端",
"description": "Desktop app"
},
"webVault": {
@@ -4212,10 +4212,10 @@
}
},
"userAcceptedTransfer": {
- "message": "Accepted transfer to organization ownership."
+ "message": "接受了转移至组织所有权。"
},
"userDeclinedTransfer": {
- "message": "Revoked for declining transfer to organization ownership."
+ "message": "因拒绝转移至组织所有权而被撤销。"
},
"invitedUserId": {
"message": "邀请了用户 $ID$。",
@@ -5195,7 +5195,7 @@
"message": "需要先修复您的密码库中的旧文件附件,然后才能轮换您账户的加密密钥。"
},
"itemsTransferred": {
- "message": "项目已传输"
+ "message": "项目已转移"
},
"yourAccountsFingerprint": {
"message": "您的账户指纹短语",
@@ -6825,7 +6825,7 @@
"message": "密码库超时不在允许的范围内。"
},
"disableExport": {
- "message": "移除导出"
+ "message": "禁用导出"
},
"disablePersonalVaultExportDescription": {
"message": "不允许成员从个人密码库导出数据。"
@@ -12406,7 +12406,7 @@
"message": "我该如何管理我的密码库?"
},
"transferItemsToOrganizationTitle": {
- "message": "传输项目到 $ORGANIZATION$",
+ "message": "转移项目到 $ORGANIZATION$",
"placeholders": {
"organization": {
"content": "$1",
@@ -12415,7 +12415,7 @@
}
},
"transferItemsToOrganizationContent": {
- "message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以传输您的项目的所有权。",
+ "message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以转移您的项目的所有权。",
"placeholders": {
"organization": {
"content": "$1",
@@ -12424,7 +12424,7 @@
}
},
"acceptTransfer": {
- "message": "接受传输"
+ "message": "接受转移"
},
"declineAndLeave": {
"message": "拒绝并退出"
@@ -12439,7 +12439,7 @@
"message": "查看和管理您的高级版订阅"
},
"youNeedToUpdateLicenseFile": {
- "message": "您需要更新您的许可文件"
+ "message": "您需要更新您的许可证文件"
},
"youNeedToUpdateLicenseFileDate": {
"message": "$DATE$。",
@@ -12469,13 +12469,13 @@
}
},
"alreadyHaveSubscriptionQuestion": {
- "message": "已经有一个订阅?"
+ "message": "已经有一个订阅了吗?"
},
"alreadyHaveSubscriptionSelfHostedMessage": {
- "message": "打开您的 Bitwarden 云账户上的订阅页面并下载您的许可证文件,然后返回此屏幕并上传。"
+ "message": "打开您的 Bitwarden 云账户中的订阅页面并下载您的许可证文件。然后返回此界面并在下方上传该文件。"
},
"viewAllPlans": {
- "message": "查看所有套餐"
+ "message": "查看所有方案"
},
"planDescPremium": {
"message": "全面的在线安全防护"
From 47eb28be345f51db4e50812f74c0ae654262f78d Mon Sep 17 00:00:00 2001
From: Github Actions
Date: Mon, 29 Dec 2025 14:59:06 +0000
Subject: [PATCH 35/81] 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 cf2be624a22..7055aabf4fd 100644
--- a/apps/browser/package.json
+++ b/apps/browser/package.json
@@ -1,6 +1,6 @@
{
"name": "@bitwarden/browser",
- "version": "2025.12.0",
+ "version": "2025.12.1",
"scripts": {
"build": "npm run build:chrome",
"build:bit": "npm run build:bit:chrome",
diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json
index 1651f616e03..26add57d1ae 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.12.0",
+ "version": "2025.12.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 67399192b64..64d182ebd3d 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.12.0",
+ "version": "2025.12.1",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",
diff --git a/apps/cli/package.json b/apps/cli/package.json
index ff74664ac76..5174e324586 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.12.0",
+ "version": "2025.12.1",
"keywords": [
"bitwarden",
"password",
diff --git a/apps/web/package.json b/apps/web/package.json
index a5399de920e..b92fc5f736a 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@bitwarden/web-vault",
- "version": "2025.12.1",
+ "version": "2025.12.2",
"scripts": {
"build:oss": "webpack",
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
diff --git a/package-lock.json b/package-lock.json
index 014c291c38c..c40b5361cc8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -192,11 +192,11 @@
},
"apps/browser": {
"name": "@bitwarden/browser",
- "version": "2025.12.0"
+ "version": "2025.12.1"
},
"apps/cli": {
"name": "@bitwarden/cli",
- "version": "2025.12.0",
+ "version": "2025.12.1",
"license": "SEE LICENSE IN LICENSE.txt",
"dependencies": {
"@koa/multer": "4.0.0",
@@ -491,7 +491,7 @@
},
"apps/web": {
"name": "@bitwarden/web-vault",
- "version": "2025.12.1"
+ "version": "2025.12.2"
},
"libs/admin-console": {
"name": "@bitwarden/admin-console",
From d3701c38d14e62befc0ef3acfa88eec2b38827f3 Mon Sep 17 00:00:00 2001
From: neuronull <9162534+neuronull@users.noreply.github.com>
Date: Mon, 29 Dec 2025 08:10:18 -0700
Subject: [PATCH 36/81] Desktop Autotype introduce strict type for keyboard
input (#17141)
* Desktop Autotype introduce strict type for keyboard input
* cleanup
* fix doc typo
* unecessary into()
* use str
* propagate error
* better var name
* pass a slice
* doc comment
* napi fix
* add ownership renovate for new dep
* add code comment about modifier keys being released
* fmt
* remove keytar
* fix input struct size compute
* improve debug comment
---
.github/renovate.json5 | 1 +
apps/desktop/desktop_native/Cargo.lock | 16 ++
apps/desktop/desktop_native/Cargo.toml | 1 +
.../desktop_native/autotype/Cargo.toml | 1 +
.../desktop_native/autotype/src/lib.rs | 2 +-
.../desktop_native/autotype/src/linux.rs | 2 +-
.../desktop_native/autotype/src/macos.rs | 2 +-
.../autotype/src/windows/mod.rs | 31 ++-
.../autotype/src/windows/type_input.rs | 196 ++++++++++--------
.../autotype/src/windows/window_title.rs | 14 +-
apps/desktop/desktop_native/napi/src/lib.rs | 5 +-
11 files changed, 162 insertions(+), 109 deletions(-)
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index acd181310d6..c4c24799da1 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -157,6 +157,7 @@
"html-webpack-injector",
"html-webpack-plugin",
"interprocess",
+ "itertools",
"json5",
"keytar",
"libc",
diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock
index 5978659f21e..f5e5cf7ee18 100644
--- a/apps/desktop/desktop_native/Cargo.lock
+++ b/apps/desktop/desktop_native/Cargo.lock
@@ -329,6 +329,7 @@ name = "autotype"
version = "0.0.0"
dependencies = [
"anyhow",
+ "itertools",
"mockall",
"serial_test",
"tracing",
@@ -1026,6 +1027,12 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
[[package]]
name = "elliptic-curve"
version = "0.13.8"
@@ -1617,6 +1624,15 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
[[package]]
name = "itoa"
version = "1.0.15"
diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml
index 26f791fd660..86eb507a6c1 100644
--- a/apps/desktop/desktop_native/Cargo.toml
+++ b/apps/desktop/desktop_native/Cargo.toml
@@ -39,6 +39,7 @@ futures = "=0.3.31"
hex = "=0.4.3"
homedir = "=0.3.6"
interprocess = "=2.2.1"
+itertools = "=0.14.0"
libc = "=0.2.178"
linux-keyutils = "=0.2.4"
memsec = "=0.7.0"
diff --git a/apps/desktop/desktop_native/autotype/Cargo.toml b/apps/desktop/desktop_native/autotype/Cargo.toml
index 580df30e72d..6bf3218d98a 100644
--- a/apps/desktop/desktop_native/autotype/Cargo.toml
+++ b/apps/desktop/desktop_native/autotype/Cargo.toml
@@ -9,6 +9,7 @@ publish.workspace = true
anyhow = { workspace = true }
[target.'cfg(windows)'.dependencies]
+itertools.workspace = true
mockall = "=0.14.0"
serial_test = "=3.2.0"
tracing.workspace = true
diff --git a/apps/desktop/desktop_native/autotype/src/lib.rs b/apps/desktop/desktop_native/autotype/src/lib.rs
index c87fea23b60..4b9e65180e6 100644
--- a/apps/desktop/desktop_native/autotype/src/lib.rs
+++ b/apps/desktop/desktop_native/autotype/src/lib.rs
@@ -28,6 +28,6 @@ pub fn get_foreground_window_title() -> Result {
/// This function returns an `anyhow::Error` if there is any
/// issue in typing the input. Detailed reasons will
/// vary based on platform implementation.
-pub fn type_input(input: Vec, keyboard_shortcut: Vec) -> Result<()> {
+pub fn type_input(input: &[u16], keyboard_shortcut: &[String]) -> Result<()> {
windowing::type_input(input, keyboard_shortcut)
}
diff --git a/apps/desktop/desktop_native/autotype/src/linux.rs b/apps/desktop/desktop_native/autotype/src/linux.rs
index 9fda0ed9e33..e7b0ee8117e 100644
--- a/apps/desktop/desktop_native/autotype/src/linux.rs
+++ b/apps/desktop/desktop_native/autotype/src/linux.rs
@@ -2,6 +2,6 @@ pub fn get_foreground_window_title() -> anyhow::Result {
todo!("Bitwarden does not yet support Linux autotype");
}
-pub fn type_input(_input: Vec, _keyboard_shortcut: Vec) -> anyhow::Result<()> {
+pub fn type_input(_input: &[u16], _keyboard_shortcut: &[String]) -> anyhow::Result<()> {
todo!("Bitwarden does not yet support Linux autotype");
}
diff --git a/apps/desktop/desktop_native/autotype/src/macos.rs b/apps/desktop/desktop_native/autotype/src/macos.rs
index c6681a3291e..56995a7f810 100644
--- a/apps/desktop/desktop_native/autotype/src/macos.rs
+++ b/apps/desktop/desktop_native/autotype/src/macos.rs
@@ -2,6 +2,6 @@ pub fn get_foreground_window_title() -> anyhow::Result {
todo!("Bitwarden does not yet support macOS autotype");
}
-pub fn type_input(_input: Vec, _keyboard_shortcut: Vec) -> anyhow::Result<()> {
+pub fn type_input(_input: &[u16], _keyboard_shortcut: &[String]) -> anyhow::Result<()> {
todo!("Bitwarden does not yet support macOS autotype");
}
diff --git a/apps/desktop/desktop_native/autotype/src/windows/mod.rs b/apps/desktop/desktop_native/autotype/src/windows/mod.rs
index 3ea63b2b8f4..9cd9bc0cbe5 100644
--- a/apps/desktop/desktop_native/autotype/src/windows/mod.rs
+++ b/apps/desktop/desktop_native/autotype/src/windows/mod.rs
@@ -1,6 +1,10 @@
use anyhow::Result;
+use itertools::Itertools;
use tracing::debug;
-use windows::Win32::Foundation::{GetLastError, SetLastError, WIN32_ERROR};
+use windows::Win32::{
+ Foundation::{GetLastError, SetLastError, WIN32_ERROR},
+ UI::Input::KeyboardAndMouse::INPUT,
+};
mod type_input;
mod window_title;
@@ -12,7 +16,7 @@ const WIN32_SUCCESS: WIN32_ERROR = WIN32_ERROR(0);
/// win32 errors.
#[cfg_attr(test, mockall::automock)]
trait ErrorOperations {
- /// https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setlasterror
+ ///
fn set_last_error(err: u32) {
debug!(err, "Calling SetLastError");
unsafe {
@@ -20,7 +24,7 @@ trait ErrorOperations {
}
}
- /// https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror
+ ///
fn get_last_error() -> WIN32_ERROR {
let last_err = unsafe { GetLastError() };
debug!("GetLastError(): {}", last_err.to_hresult().message());
@@ -36,6 +40,23 @@ pub fn get_foreground_window_title() -> Result {
window_title::get_foreground_window_title()
}
-pub fn type_input(input: Vec, keyboard_shortcut: Vec) -> Result<()> {
- type_input::type_input(input, keyboard_shortcut)
+/// `KeyboardShortcutInput` is an `INPUT` of one of the valid shortcut keys:
+/// - Control
+/// - Alt
+/// - Super
+/// - Shift
+/// - \[a-z\]\[A-Z\]
+struct KeyboardShortcutInput(INPUT);
+
+pub fn type_input(input: &[u16], keyboard_shortcut: &[String]) -> Result<()> {
+ debug!(?keyboard_shortcut, "type_input() called.");
+
+ // convert the raw string input to Windows input and error
+ // if any key is not a valid keyboard shortcut input
+ let keyboard_shortcut: Vec = keyboard_shortcut
+ .iter()
+ .map(|s| KeyboardShortcutInput::try_from(s.as_str()))
+ .try_collect()?;
+
+ type_input::type_input(input, &keyboard_shortcut)
}
diff --git a/apps/desktop/desktop_native/autotype/src/windows/type_input.rs b/apps/desktop/desktop_native/autotype/src/windows/type_input.rs
index b2f4c6b82df..b62dd7290d1 100644
--- a/apps/desktop/desktop_native/autotype/src/windows/type_input.rs
+++ b/apps/desktop/desktop_native/autotype/src/windows/type_input.rs
@@ -5,7 +5,15 @@ use windows::Win32::UI::Input::KeyboardAndMouse::{
VIRTUAL_KEY,
};
-use super::{ErrorOperations, Win32ErrorOperations};
+use super::{ErrorOperations, KeyboardShortcutInput, Win32ErrorOperations};
+
+const SHIFT_KEY_STR: &str = "Shift";
+const CONTROL_KEY_STR: &str = "Control";
+const ALT_KEY_STR: &str = "Alt";
+const LEFT_WINDOWS_KEY_STR: &str = "Super";
+
+const IS_VIRTUAL_KEY: bool = true;
+const IS_REAL_KEY: bool = false;
/// `InputOperations` provides an interface to Window32 API for
/// working with inputs.
@@ -13,7 +21,7 @@ use super::{ErrorOperations, Win32ErrorOperations};
trait InputOperations {
/// Attempts to type the provided input wherever the user's cursor is.
///
- /// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
+ ///
fn send_input(inputs: &[INPUT]) -> u32;
}
@@ -21,8 +29,11 @@ struct Win32InputOperations;
impl InputOperations for Win32InputOperations {
fn send_input(inputs: &[INPUT]) -> u32 {
- const INPUT_STRUCT_SIZE: i32 = std::mem::size_of:: () as i32;
- let insert_count = unsafe { SendInput(inputs, INPUT_STRUCT_SIZE) };
+ const INPUT_STRUCT_SIZE: usize = std::mem::size_of:: ();
+
+ let size = i32::try_from(INPUT_STRUCT_SIZE).expect("INPUT size to fit in i32");
+
+ let insert_count = unsafe { SendInput(inputs, size) };
debug!(insert_count, "SendInput() called.");
@@ -33,40 +44,37 @@ impl InputOperations for Win32InputOperations {
/// Attempts to type the input text wherever the user's cursor is.
///
/// `input` must be a vector of utf-16 encoded characters to insert.
-/// `keyboard_shortcut` must be a vector of Strings, where valid shortcut keys: Control, Alt, Super,
-/// Shift, letters a - Z
+/// `keyboard_shortcut` is a vector of valid shortcut keys.
///
-/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
-pub(super) fn type_input(input: Vec, keyboard_shortcut: Vec) -> Result<()> {
+///
+pub(super) fn type_input(input: &[u16], keyboard_shortcut: &[KeyboardShortcutInput]) -> Result<()> {
// the length of this vec is always shortcut keys to release + (2x length of input chars)
let mut keyboard_inputs: Vec =
Vec::with_capacity(keyboard_shortcut.len() + (input.len() * 2));
- debug!(?keyboard_shortcut, "Converting keyboard shortcut to input.");
-
- // Add key "up" inputs for the shortcut
- for key in keyboard_shortcut {
- keyboard_inputs.push(convert_shortcut_key_to_up_input(key)?);
+ // insert the keyboard shortcut
+ for shortcut in keyboard_shortcut {
+ keyboard_inputs.push(shortcut.0);
}
- add_input(&input, &mut keyboard_inputs);
+ add_input(input, &mut keyboard_inputs);
- send_input::(keyboard_inputs)
+ send_input::(&keyboard_inputs)
}
// Add key "down" and "up" inputs for the input
// (currently in this form: {username}/t{password})
fn add_input(input: &[u16], keyboard_inputs: &mut Vec ) {
- const TAB_KEY: u8 = 9;
+ const TAB_KEY: u16 = 9;
for i in input {
- let next_down_input = if *i == TAB_KEY.into() {
- build_virtual_key_input(InputKeyPress::Down, *i as u8)
+ let next_down_input = if *i == TAB_KEY {
+ build_virtual_key_input(InputKeyPress::Down, *i)
} else {
build_unicode_input(InputKeyPress::Down, *i)
};
- let next_up_input = if *i == TAB_KEY.into() {
- build_virtual_key_input(InputKeyPress::Up, *i as u8)
+ let next_up_input = if *i == TAB_KEY {
+ build_virtual_key_input(InputKeyPress::Up, *i)
} else {
build_unicode_input(InputKeyPress::Up, *i)
};
@@ -76,26 +84,27 @@ fn add_input(input: &[u16], keyboard_inputs: &mut Vec ) {
}
}
-/// Converts a valid shortcut key to an "up" keyboard input.
-///
-/// `input` must be a valid shortcut key: Control, Alt, Super, Shift, letters [a-z][A-Z]
-fn convert_shortcut_key_to_up_input(key: String) -> Result {
- const SHIFT_KEY: u8 = 0x10;
- const SHIFT_KEY_STR: &str = "Shift";
- const CONTROL_KEY: u8 = 0x11;
- const CONTROL_KEY_STR: &str = "Control";
- const ALT_KEY: u8 = 0x12;
- const ALT_KEY_STR: &str = "Alt";
- const LEFT_WINDOWS_KEY: u8 = 0x5B;
- const LEFT_WINDOWS_KEY_STR: &str = "Super";
+impl TryFrom<&str> for KeyboardShortcutInput {
+ type Error = anyhow::Error;
- Ok(match key.as_str() {
- SHIFT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, SHIFT_KEY),
- CONTROL_KEY_STR => build_virtual_key_input(InputKeyPress::Up, CONTROL_KEY),
- ALT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, ALT_KEY),
- LEFT_WINDOWS_KEY_STR => build_virtual_key_input(InputKeyPress::Up, LEFT_WINDOWS_KEY),
- _ => build_unicode_input(InputKeyPress::Up, get_alphabetic_hotkey(key)?),
- })
+ fn try_from(key: &str) -> std::result::Result {
+ const SHIFT_KEY: u16 = 0x10;
+ const CONTROL_KEY: u16 = 0x11;
+ const ALT_KEY: u16 = 0x12;
+ const LEFT_WINDOWS_KEY: u16 = 0x5B;
+
+ // the modifier keys are using the Up keypress variant because the user has already
+ // pressed those keys in order to trigger the feature.
+ let input = match key {
+ SHIFT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, SHIFT_KEY),
+ CONTROL_KEY_STR => build_virtual_key_input(InputKeyPress::Up, CONTROL_KEY),
+ ALT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, ALT_KEY),
+ LEFT_WINDOWS_KEY_STR => build_virtual_key_input(InputKeyPress::Up, LEFT_WINDOWS_KEY),
+ _ => build_unicode_input(InputKeyPress::Up, get_alphabetic_hotkey(key)?),
+ };
+
+ Ok(KeyboardShortcutInput(input))
+ }
}
/// Given a letter that is a String, get the utf16 encoded
@@ -105,7 +114,7 @@ fn convert_shortcut_key_to_up_input(key: String) -> Result {
/// Because we only accept [a-z][A-Z], the decimal u16
/// cast of the letter is safe because the unicode code point
/// of these characters fits in a u16.
-fn get_alphabetic_hotkey(letter: String) -> Result {
+fn get_alphabetic_hotkey(letter: &str) -> Result {
if letter.len() != 1 {
error!(
len = letter.len(),
@@ -135,23 +144,28 @@ fn get_alphabetic_hotkey(letter: String) -> Result {
}
/// An input key can be either pressed (down), or released (up).
+#[derive(Copy, Clone)]
enum InputKeyPress {
Down,
Up,
}
-/// A function for easily building keyboard unicode INPUT structs used in SendInput().
-///
-/// Before modifying this function, make sure you read the SendInput() documentation:
-/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
-fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
+/// Before modifying this function, make sure you read the `SendInput()` documentation:
+///
+///
+fn build_input(key_press: InputKeyPress, character: u16, is_virtual: bool) -> INPUT {
+ let (w_vk, w_scan) = if is_virtual {
+ (VIRTUAL_KEY(character), 0)
+ } else {
+ (VIRTUAL_KEY::default(), character)
+ };
match key_press {
InputKeyPress::Down => INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 {
ki: KEYBDINPUT {
- wVk: Default::default(),
- wScan: character,
+ wVk: w_vk,
+ wScan: w_scan,
dwFlags: KEYEVENTF_UNICODE,
time: 0,
dwExtraInfo: 0,
@@ -162,8 +176,8 @@ fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 {
ki: KEYBDINPUT {
- wVk: Default::default(),
- wScan: character,
+ wVk: w_vk,
+ wScan: w_scan,
dwFlags: KEYEVENTF_KEYUP | KEYEVENTF_UNICODE,
time: 0,
dwExtraInfo: 0,
@@ -173,53 +187,29 @@ fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
}
}
-/// A function for easily building keyboard virtual-key INPUT structs used in SendInput().
-///
-/// Before modifying this function, make sure you read the SendInput() documentation:
-/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
-/// https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
-fn build_virtual_key_input(key_press: InputKeyPress, virtual_key: u8) -> INPUT {
- match key_press {
- InputKeyPress::Down => INPUT {
- r#type: INPUT_KEYBOARD,
- Anonymous: INPUT_0 {
- ki: KEYBDINPUT {
- wVk: VIRTUAL_KEY(virtual_key as u16),
- wScan: Default::default(),
- dwFlags: Default::default(),
- time: 0,
- dwExtraInfo: 0,
- },
- },
- },
- InputKeyPress::Up => INPUT {
- r#type: INPUT_KEYBOARD,
- Anonymous: INPUT_0 {
- ki: KEYBDINPUT {
- wVk: VIRTUAL_KEY(virtual_key as u16),
- wScan: Default::default(),
- dwFlags: KEYEVENTF_KEYUP,
- time: 0,
- dwExtraInfo: 0,
- },
- },
- },
- }
+/// A function for easily building keyboard unicode `INPUT` structs used in `SendInput()`.
+fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
+ build_input(key_press, character, IS_REAL_KEY)
}
-fn send_input(inputs: Vec ) -> Result<()>
+/// A function for easily building keyboard virtual-key `INPUT` structs used in `SendInput()`.
+fn build_virtual_key_input(key_press: InputKeyPress, character: u16) -> INPUT {
+ build_input(key_press, character, IS_VIRTUAL_KEY)
+}
+
+fn send_input(inputs: &[INPUT]) -> Result<()>
where
I: InputOperations,
E: ErrorOperations,
{
- let insert_count = I::send_input(&inputs);
+ let insert_count = I::send_input(inputs);
if insert_count == 0 {
let last_err = E::get_last_error().to_hresult().message();
error!(GetLastError = %last_err, "SendInput sent 0 inputs. Input was blocked by another thread.");
return Err(anyhow!("SendInput sent 0 inputs. Input was blocked by another thread. GetLastError: {last_err}"));
- } else if insert_count != inputs.len() as u32 {
+ } else if insert_count != u32::try_from(inputs.len()).expect("to convert inputs len to u32") {
let last_err = E::get_last_error().to_hresult().message();
error!(sent = %insert_count, expected = inputs.len(), GetLastError = %last_err,
"SendInput sent does not match expected."
@@ -237,8 +227,9 @@ where
mod tests {
//! For the mocking of the traits that are static methods, we need to use the `serial_test`
//! crate in order to mock those, since the mock expectations set have to be global in
- //! absence of a `self`. More info: https://docs.rs/mockall/latest/mockall/#static-methods
+ //! absence of a `self`. More info:
+ use itertools::Itertools;
use serial_test::serial;
use windows::Win32::Foundation::WIN32_ERROR;
@@ -249,7 +240,7 @@ mod tests {
fn get_alphabetic_hot_key_succeeds() {
for c in ('a'..='z').chain('A'..='Z') {
let letter = c.to_string();
- let converted = get_alphabetic_hotkey(letter).unwrap();
+ let converted = get_alphabetic_hotkey(&letter).unwrap();
assert_eq!(converted, c as u16);
}
}
@@ -258,14 +249,14 @@ mod tests {
#[should_panic = "Final keyboard shortcut key should be a single character: foo"]
fn get_alphabetic_hot_key_fail_not_single_char() {
let letter = String::from("foo");
- get_alphabetic_hotkey(letter).unwrap();
+ get_alphabetic_hotkey(&letter).unwrap();
}
#[test]
#[should_panic = "Letter is not ASCII Alphabetic ([a-z][A-Z]): '}'"]
fn get_alphabetic_hot_key_fail_not_alphabetic() {
let letter = String::from("}");
- get_alphabetic_hotkey(letter).unwrap();
+ get_alphabetic_hotkey(&letter).unwrap();
}
#[test]
@@ -275,7 +266,7 @@ mod tests {
ctxi.checkpoint();
ctxi.expect().returning(|_| 1);
- send_input::(vec![build_unicode_input(
+ send_input::(&[build_unicode_input(
InputKeyPress::Up,
0,
)])
@@ -284,6 +275,29 @@ mod tests {
drop(ctxi);
}
+ #[test]
+ #[serial]
+ fn keyboard_shortcut_conversion_succeeds() {
+ let keyboard_shortcut = [CONTROL_KEY_STR, SHIFT_KEY_STR, "B"];
+ let _: Vec = keyboard_shortcut
+ .iter()
+ .map(|s| KeyboardShortcutInput::try_from(*s))
+ .try_collect()
+ .unwrap();
+ }
+
+ #[test]
+ #[serial]
+ #[should_panic = "Letter is not ASCII Alphabetic ([a-z][A-Z]): '1'"]
+ fn keyboard_shortcut_conversion_fails_invalid_key() {
+ let keyboard_shortcut = [CONTROL_KEY_STR, SHIFT_KEY_STR, "1"];
+ let _: Vec = keyboard_shortcut
+ .iter()
+ .map(|s| KeyboardShortcutInput::try_from(*s))
+ .try_collect()
+ .unwrap();
+ }
+
#[test]
#[serial]
#[should_panic(
@@ -298,7 +312,7 @@ mod tests {
ctxge.checkpoint();
ctxge.expect().returning(|| WIN32_ERROR(1));
- send_input::(vec![build_unicode_input(
+ send_input::(&[build_unicode_input(
InputKeyPress::Up,
0,
)])
@@ -320,7 +334,7 @@ mod tests {
ctxge.checkpoint();
ctxge.expect().returning(|| WIN32_ERROR(1));
- send_input::(vec![build_unicode_input(
+ send_input::(&[build_unicode_input(
InputKeyPress::Up,
0,
)])
diff --git a/apps/desktop/desktop_native/autotype/src/windows/window_title.rs b/apps/desktop/desktop_native/autotype/src/windows/window_title.rs
index 4fc0b3bb3ad..12e6501a7c5 100644
--- a/apps/desktop/desktop_native/autotype/src/windows/window_title.rs
+++ b/apps/desktop/desktop_native/autotype/src/windows/window_title.rs
@@ -11,10 +11,10 @@ use super::{ErrorOperations, Win32ErrorOperations, WIN32_SUCCESS};
#[cfg_attr(test, mockall::automock)]
trait WindowHandleOperations {
- // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw
+ //
fn get_window_text_length_w(&self) -> Result;
- // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw
+ //
fn get_window_text_w(&self, buffer: &mut Vec) -> Result;
}
@@ -70,7 +70,7 @@ pub(super) fn get_foreground_window_title() -> Result {
/// Retrieves the foreground window handle and validates it.
fn get_foreground_window_handle() -> Result {
- // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow
+ //
let handle = unsafe { GetForegroundWindow() };
debug!("GetForegroundWindow() called.");
@@ -87,7 +87,7 @@ fn get_foreground_window_handle() -> Result {
///
/// # Errors
///
-/// - If the length zero and GetLastError() != 0, return the GetLastError() message.
+/// - If the length zero and `GetLastError()` != 0, return the `GetLastError()` message.
fn get_window_title_length(window_handle: &H) -> Result
where
H: WindowHandleOperations,
@@ -128,7 +128,7 @@ where
/// # Errors
///
/// - If the actual window title length (what the win32 API declares was written into the buffer),
-/// is length zero and GetLastError() != 0 , return the GetLastError() message.
+/// is length zero and `GetLastError()` != 0 , return the `GetLastError()` message.
fn get_window_title(window_handle: &H, expected_title_length: usize) -> Result
where
H: WindowHandleOperations,
@@ -140,7 +140,7 @@ where
// The upstream will make a contains comparison on what we return, so an empty string
// will not result on a match.
warn!("Window title length is zero.");
- return Ok(String::from(""));
+ return Ok(String::new());
}
let mut buffer: Vec = vec![0; expected_title_length + 1]; // add extra space for the null character
@@ -171,7 +171,7 @@ where
mod tests {
//! For the mocking of the traits that are static methods, we need to use the `serial_test`
//! crate in order to mock those, since the mock expectations set have to be global in
- //! absence of a `self`. More info: https://docs.rs/mockall/latest/mockall/#static-methods
+ //! absence of a `self`. More info:
use mockall::predicate;
use serial_test::serial;
diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs
index fe084349501..588f757631c 100644
--- a/apps/desktop/desktop_native/napi/src/lib.rs
+++ b/apps/desktop/desktop_native/napi/src/lib.rs
@@ -1241,8 +1241,7 @@ pub mod autotype {
input: Vec,
keyboard_shortcut: Vec,
) -> napi::Result<(), napi::Status> {
- autotype::type_input(input, keyboard_shortcut).map_err(|_| {
- napi::Error::from_reason("Autotype Error: failed to type input".to_string())
- })
+ autotype::type_input(&input, &keyboard_shortcut)
+ .map_err(|e| napi::Error::from_reason(format!("Autotype Error: {e}")))
}
}
From 4e1cca132d5874b33e6a9699d12def0c474012e5 Mon Sep 17 00:00:00 2001
From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
Date: Mon, 29 Dec 2025 16:10:34 +0100
Subject: [PATCH 37/81] Bump year in copyright (#18132)
Co-authored-by: Daniel James Smith
---
apps/browser/src/safari/desktop/Info.plist | 2 +-
apps/browser/src/safari/safari/Info.plist | 2 +-
apps/cli/stores/chocolatey/bitwarden-cli.nuspec | 2 +-
apps/desktop/electron-builder.beta.json | 2 +-
apps/desktop/electron-builder.json | 2 +-
apps/desktop/stores/chocolatey/bitwarden.nuspec | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/apps/browser/src/safari/desktop/Info.plist b/apps/browser/src/safari/desktop/Info.plist
index b687d9d2f3a..94542609351 100644
--- a/apps/browser/src/safari/desktop/Info.plist
+++ b/apps/browser/src/safari/desktop/Info.plist
@@ -25,7 +25,7 @@
LSMinimumSystemVersion
$(MACOSX_DEPLOYMENT_TARGET)
NSHumanReadableCopyright
- Copyright © 2015-2025 Bitwarden Inc. All rights reserved.
+ Copyright © 2015-2026 Bitwarden Inc. All rights reserved.
NSMainStoryboardFile
Main
NSPrincipalClass
diff --git a/apps/browser/src/safari/safari/Info.plist b/apps/browser/src/safari/safari/Info.plist
index 95172846758..68b872610e9 100644
--- a/apps/browser/src/safari/safari/Info.plist
+++ b/apps/browser/src/safari/safari/Info.plist
@@ -30,7 +30,7 @@
$(PRODUCT_MODULE_NAME).SafariWebExtensionHandler
NSHumanReadableCopyright
- Copyright © 2015-2025 Bitwarden Inc. All rights reserved.
+ Copyright © 2015-2026 Bitwarden Inc. All rights reserved.
NSHumanReadableDescription
A secure and free password manager for all of your devices.
SFSafariAppExtensionBundleIdentifiersToReplace
diff --git a/apps/cli/stores/chocolatey/bitwarden-cli.nuspec b/apps/cli/stores/chocolatey/bitwarden-cli.nuspec
index f7f86bc843f..9552ccc282c 100644
--- a/apps/cli/stores/chocolatey/bitwarden-cli.nuspec
+++ b/apps/cli/stores/chocolatey/bitwarden-cli.nuspec
@@ -10,7 +10,7 @@
Bitwarden Inc.
https://bitwarden.com/
https://raw.githubusercontent.com/bitwarden/brand/master/icons/256x256.png
- Copyright © 2015-2025 Bitwarden Inc.
+ Copyright © 2015-2026 Bitwarden Inc.
https://github.com/bitwarden/clients/
https://help.bitwarden.com/article/cli/
https://github.com/bitwarden/clients/issues
diff --git a/apps/desktop/electron-builder.beta.json b/apps/desktop/electron-builder.beta.json
index 630a956560d..0c95c7f01a6 100644
--- a/apps/desktop/electron-builder.beta.json
+++ b/apps/desktop/electron-builder.beta.json
@@ -5,7 +5,7 @@
"productName": "Bitwarden Beta",
"appId": "com.bitwarden.desktop.beta",
"buildDependenciesFromSource": true,
- "copyright": "Copyright © 2015-2025 Bitwarden Inc.",
+ "copyright": "Copyright © 2015-2026 Bitwarden Inc.",
"directories": {
"buildResources": "resources",
"output": "dist",
diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json
index f979df81fd0..a4e1c44dc5b 100644
--- a/apps/desktop/electron-builder.json
+++ b/apps/desktop/electron-builder.json
@@ -5,7 +5,7 @@
"productName": "Bitwarden",
"appId": "com.bitwarden.desktop",
"buildDependenciesFromSource": true,
- "copyright": "Copyright © 2015-2025 Bitwarden Inc.",
+ "copyright": "Copyright © 2015-2026 Bitwarden Inc.",
"directories": {
"buildResources": "resources",
"output": "dist",
diff --git a/apps/desktop/stores/chocolatey/bitwarden.nuspec b/apps/desktop/stores/chocolatey/bitwarden.nuspec
index 450fa734736..567002d0d8c 100644
--- a/apps/desktop/stores/chocolatey/bitwarden.nuspec
+++ b/apps/desktop/stores/chocolatey/bitwarden.nuspec
@@ -10,7 +10,7 @@
Bitwarden Inc.
https://bitwarden.com/
https://raw.githubusercontent.com/bitwarden/brand/master/icons/256x256.png
- Copyright © 2015-2025 Bitwarden Inc.
+ Copyright © 2015-2026 Bitwarden Inc.
https://github.com/bitwarden/clients/
https://bitwarden.com/help/
https://github.com/bitwarden/clients/issues
From e2a1cfcbe881c8eb0fb37e25ba403c8776583373 Mon Sep 17 00:00:00 2001
From: Jason Ng
Date: Mon, 29 Dec 2025 10:11:12 -0500
Subject: [PATCH 38/81] [PM29951] add archive flag check to desktop vault-v2
(#18056)
---
apps/desktop/src/vault/app/vault/vault-v2.component.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 6c4ebe13f14..730891f6dea 100644
--- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts
+++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts
@@ -565,7 +565,7 @@ export class VaultV2Component
}
}
- if (!cipher.organizationId && !cipher.isDeleted && !cipher.isArchived) {
+ if (userCanArchive && !cipher.isDeleted && !cipher.isArchived) {
menu.push({
label: this.i18nService.t("archiveVerb"),
click: async () => {
From 146e2c0a12e6119aca5b807dba7de49ce6af0b14 Mon Sep 17 00:00:00 2001
From: Todd Martin <106564991+trmartin4@users.noreply.github.com>
Date: Mon, 29 Dec 2025 11:35:56 -0500
Subject: [PATCH 39/81] chore(feature-flags): Remove notification on inactive
and locked user feature flags
---
libs/common/src/enums/feature-flag.enum.ts | 4 -
...ult-server-notifications.multiuser.spec.ts | 10 --
.../default-server-notifications.service.ts | 98 +++++--------------
3 files changed, 26 insertions(+), 86 deletions(-)
diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts
index 837418f92cf..5a6eeebd001 100644
--- a/libs/common/src/enums/feature-flag.enum.ts
+++ b/libs/common/src/enums/feature-flag.enum.ts
@@ -70,8 +70,6 @@ export enum FeatureFlag {
/* Platform */
IpcChannelFramework = "ipc-channel-framework",
- InactiveUserServerNotification = "pm-25130-receive-push-notifications-for-inactive-users",
- PushNotificationsWhenLocked = "pm-19388-push-notifications-when-locked",
/* Innovation */
PM19148_InnovationArchive = "pm-19148-innovation-archive",
@@ -157,8 +155,6 @@ export const DefaultFeatureFlagValue = {
/* Platform */
[FeatureFlag.IpcChannelFramework]: FALSE,
- [FeatureFlag.InactiveUserServerNotification]: FALSE,
- [FeatureFlag.PushNotificationsWhenLocked]: FALSE,
/* Innovation */
[FeatureFlag.PM19148_InnovationArchive]: FALSE,
diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts
index 46178f62a07..7aacc783e65 100644
--- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts
+++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts
@@ -5,7 +5,6 @@ import { BehaviorSubject, bufferCount, firstValueFrom, Subject, ObservedValueOf
import { LogoutReason } from "@bitwarden/auth/common";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
-import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { mockAccountInfoWith } from "../../../../spec";
import { AccountService } from "../../../auth/abstractions/account.service";
@@ -130,15 +129,6 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
authRequestAnsweringService = mock();
- configService = mock();
- configService.getFeatureFlag$.mockImplementation((flag: FeatureFlag) => {
- const flagValueByFlag: Partial> = {
- [FeatureFlag.InactiveUserServerNotification]: true,
- [FeatureFlag.PushNotificationsWhenLocked]: true,
- };
- return new BehaviorSubject(flagValueByFlag[flag] ?? false) as any;
- });
-
policyService = mock();
defaultServerNotificationsService = new DefaultServerNotificationsService(
diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts
index 5ee288351d5..5b026add1a2 100644
--- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts
+++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts
@@ -71,48 +71,20 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
private readonly configService: ConfigService,
private readonly policyService: InternalPolicyService,
) {
- this.notifications$ = this.configService
- .getFeatureFlag$(FeatureFlag.InactiveUserServerNotification)
- .pipe(
- distinctUntilChanged(),
- switchMap((inactiveUserServerNotificationEnabled) => {
- if (inactiveUserServerNotificationEnabled) {
- return this.accountService.accounts$.pipe(
- map((accounts: Record): Set => {
- const validUserIds = Object.entries(accounts)
- .filter(
- ([_, accountInfo]) => accountInfo.email !== "" || accountInfo.emailVerified,
- )
- .map(([userId, _]) => userId as UserId);
- return new Set(validUserIds);
- }),
- trackedMerge((id: UserId) => {
- return this.userNotifications$(id as UserId).pipe(
- map(
- (notification: NotificationResponse) => [notification, id as UserId] as const,
- ),
- );
- }),
- );
- }
-
- return this.accountService.activeAccount$.pipe(
- map((account) => account?.id),
- distinctUntilChanged(),
- switchMap((activeAccountId) => {
- if (activeAccountId == null) {
- // We don't emit server-notifications for inactive accounts currently
- return EMPTY;
- }
-
- return this.userNotifications$(activeAccountId).pipe(
- map((notification) => [notification, activeAccountId] as const),
- );
- }),
- );
- }),
- share(), // Multiple subscribers should only create a single connection to the server
- );
+ this.notifications$ = this.accountService.accounts$.pipe(
+ map((accounts: Record): Set => {
+ const validUserIds = Object.entries(accounts)
+ .filter(([_, accountInfo]) => accountInfo.email !== "" || accountInfo.emailVerified)
+ .map(([userId, _]) => userId as UserId);
+ return new Set(validUserIds);
+ }),
+ trackedMerge((id: UserId) => {
+ return this.userNotifications$(id as UserId).pipe(
+ map((notification: NotificationResponse) => [notification, id as UserId] as const),
+ );
+ }),
+ share(), // Multiple subscribers should only create a single connection to the server
+ );
}
/**
@@ -175,25 +147,13 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
}
private hasAccessToken$(userId: UserId) {
- return this.configService.getFeatureFlag$(FeatureFlag.PushNotificationsWhenLocked).pipe(
+ return this.authService.authStatusFor$(userId).pipe(
+ map(
+ (authStatus) =>
+ authStatus === AuthenticationStatus.Locked ||
+ authStatus === AuthenticationStatus.Unlocked,
+ ),
distinctUntilChanged(),
- switchMap((featureFlagEnabled) => {
- if (featureFlagEnabled) {
- return this.authService.authStatusFor$(userId).pipe(
- map(
- (authStatus) =>
- authStatus === AuthenticationStatus.Locked ||
- authStatus === AuthenticationStatus.Unlocked,
- ),
- distinctUntilChanged(),
- );
- } else {
- return this.authService.authStatusFor$(userId).pipe(
- map((authStatus) => authStatus === AuthenticationStatus.Unlocked),
- distinctUntilChanged(),
- );
- }
- }),
);
}
@@ -208,19 +168,13 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
return;
}
- if (
- await firstValueFrom(
- this.configService.getFeatureFlag$(FeatureFlag.InactiveUserServerNotification),
- )
- ) {
- const activeAccountId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
+ const activeAccountId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((a) => a?.id)),
+ );
- const isActiveUser = activeAccountId === userId;
- if (!isActiveUser && !AllowedMultiUserNotificationTypes.has(notification.type)) {
- return;
- }
+ const notificationIsForActiveUser = activeAccountId === userId;
+ if (!notificationIsForActiveUser && !AllowedMultiUserNotificationTypes.has(notification.type)) {
+ return;
}
switch (notification.type) {
From b7d2ce9d0eb8f8c2e1af1cc00c6a340f165fbf32 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 29 Dec 2025 12:03:32 -0500
Subject: [PATCH 40/81] [deps]: Update actions/checkout action to v6 (#17715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
---
.../workflows/alert-ddg-files-modified.yml | 2 +-
.github/workflows/auto-branch-updater.yml | 2 +-
.github/workflows/build-browser.yml | 12 +++++------
.github/workflows/build-cli.yml | 8 ++++----
.github/workflows/build-desktop.yml | 20 +++++++++----------
.github/workflows/build-web.yml | 8 ++++----
.github/workflows/chromatic.yml | 2 +-
.github/workflows/crowdin-pull.yml | 2 +-
.github/workflows/lint-crowdin-config.yml | 2 +-
.github/workflows/lint.yml | 4 ++--
.github/workflows/locales-lint.yml | 4 ++--
.github/workflows/nx.yml | 2 +-
.github/workflows/publish-cli.yml | 6 +++---
.github/workflows/publish-desktop.yml | 6 +++---
.github/workflows/publish-web.yml | 4 ++--
.github/workflows/release-browser.yml | 4 ++--
.github/workflows/release-cli.yml | 2 +-
.github/workflows/release-desktop.yml | 2 +-
.github/workflows/release-web.yml | 2 +-
.github/workflows/repository-management.yml | 4 ++--
.../workflows/sdk-breaking-change-check.yml | 2 +-
.../workflows/test-browser-interactions.yml | 2 +-
.github/workflows/test.yml | 8 ++++----
.github/workflows/version-auto-bump.yml | 2 +-
24 files changed, 56 insertions(+), 56 deletions(-)
diff --git a/.github/workflows/alert-ddg-files-modified.yml b/.github/workflows/alert-ddg-files-modified.yml
index 90c055a97b8..35eb0515c10 100644
--- a/.github/workflows/alert-ddg-files-modified.yml
+++ b/.github/workflows/alert-ddg-files-modified.yml
@@ -14,7 +14,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout code
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: false
diff --git a/.github/workflows/auto-branch-updater.yml b/.github/workflows/auto-branch-updater.yml
index 02176b3169e..be9cd338e82 100644
--- a/.github/workflows/auto-branch-updater.yml
+++ b/.github/workflows/auto-branch-updater.yml
@@ -30,7 +30,7 @@ jobs:
run: echo "branch=${GITHUB_REF#refs/heads/}" >> "$GITHUB_OUTPUT"
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: 'eu-web-${{ steps.setup.outputs.branch }}'
fetch-depth: 0
diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml
index ab932c561ba..b5859516eaa 100644
--- a/.github/workflows/build-browser.yml
+++ b/.github/workflows/build-browser.yml
@@ -55,7 +55,7 @@ jobs:
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -94,7 +94,7 @@ jobs:
working-directory: apps/browser
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -146,7 +146,7 @@ jobs:
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -254,7 +254,7 @@ jobs:
artifact_name: "dist-opera-MV3"
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -386,7 +386,7 @@ jobs:
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -542,7 +542,7 @@ jobs:
- build-safari
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml
index 964cbc834c5..704a9810b27 100644
--- a/.github/workflows/build-cli.yml
+++ b/.github/workflows/build-cli.yml
@@ -59,7 +59,7 @@ jobs:
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -114,7 +114,7 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -311,7 +311,7 @@ jobs:
_WIN_PKG_VERSION: 3.5
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -520,7 +520,7 @@ jobs:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml
index 5a7703adb78..f3cdf80f710 100644
--- a/.github/workflows/build-desktop.yml
+++ b/.github/workflows/build-desktop.yml
@@ -55,7 +55,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -88,7 +88,7 @@ jobs:
working-directory: apps/desktop
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: true
@@ -173,7 +173,7 @@ jobs:
working-directory: apps/desktop
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha }}
@@ -343,7 +343,7 @@ jobs:
working-directory: apps/desktop
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -491,7 +491,7 @@ jobs:
NODE_OPTIONS: --max_old_space_size=4096
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -759,7 +759,7 @@ jobs:
NODE_OPTIONS: --max_old_space_size=4096
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -1004,7 +1004,7 @@ jobs:
working-directory: apps/desktop
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -1244,7 +1244,7 @@ jobs:
working-directory: apps/desktop
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -1519,7 +1519,7 @@ jobs:
working-directory: apps/desktop
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -1860,7 +1860,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml
index 02ab7727c24..7d302fb453b 100644
--- a/.github/workflows/build-web.yml
+++ b/.github/workflows/build-web.yml
@@ -64,7 +64,7 @@ jobs:
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -144,7 +144,7 @@ jobs:
_VERSION: ${{ needs.setup.outputs.version }}
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
@@ -174,7 +174,7 @@ jobs:
echo "server_ref=$SERVER_REF" >> "$GITHUB_OUTPUT"
- name: Check out Server repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
path: server
repository: bitwarden/server
@@ -367,7 +367,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml
index 44ea21276e2..c7d80b82baa 100644
--- a/.github/workflows/chromatic.yml
+++ b/.github/workflows/chromatic.yml
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml
index 5475c4dd692..e99034c499a 100644
--- a/.github/workflows/crowdin-pull.yml
+++ b/.github/workflows/crowdin-pull.yml
@@ -58,7 +58,7 @@ jobs:
permission-pull-requests: write # for generating pull requests
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
token: ${{ steps.app-token.outputs.token }}
persist-credentials: false
diff --git a/.github/workflows/lint-crowdin-config.yml b/.github/workflows/lint-crowdin-config.yml
index b0efeb50823..dff253a8da2 100644
--- a/.github/workflows/lint-crowdin-config.yml
+++ b/.github/workflows/lint-crowdin-config.yml
@@ -22,7 +22,7 @@ jobs:
]
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 1
persist-credentials: false
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index b46204514b8..3aeb75dcbf6 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -95,7 +95,7 @@ jobs:
steps:
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
diff --git a/.github/workflows/locales-lint.yml b/.github/workflows/locales-lint.yml
index 8335d6aacad..e431854aea2 100644
--- a/.github/workflows/locales-lint.yml
+++ b/.github/workflows/locales-lint.yml
@@ -17,11 +17,11 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Checkout base branch repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
diff --git a/.github/workflows/nx.yml b/.github/workflows/nx.yml
index 0f01aa27899..1e23c31b033 100644
--- a/.github/workflows/nx.yml
+++ b/.github/workflows/nx.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: false
diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml
index 8fcd1fe7c98..ef287b0de08 100644
--- a/.github/workflows/publish-cli.yml
+++ b/.github/workflows/publish-cli.yml
@@ -103,7 +103,7 @@ jobs:
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
steps:
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -151,7 +151,7 @@ jobs:
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
steps:
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -203,7 +203,7 @@ jobs:
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
steps:
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml
index 3d512d49559..f013abbbb3b 100644
--- a/.github/workflows/publish-desktop.yml
+++ b/.github/workflows/publish-desktop.yml
@@ -204,7 +204,7 @@ jobs:
_RELEASE_TAG: ${{ needs.setup.outputs.tag_name }}
steps:
- name: Checkout Repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -258,7 +258,7 @@ jobs:
_RELEASE_TAG: ${{ needs.setup.outputs.tag_name }}
steps:
- name: Checkout Repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -315,7 +315,7 @@ jobs:
_RELEASE_TAG: ${{ needs.setup.outputs.tag_name }}
steps:
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml
index fb1de5a1bc5..be0087800f7 100644
--- a/.github/workflows/publish-web.yml
+++ b/.github/workflows/publish-web.yml
@@ -28,7 +28,7 @@ jobs:
contents: read
steps:
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -74,7 +74,7 @@ jobs:
echo "Github Release Option: $_RELEASE_OPTION"
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml
index ff5fb669faf..f7e45919308 100644
--- a/.github/workflows/release-browser.yml
+++ b/.github/workflows/release-browser.yml
@@ -28,7 +28,7 @@ jobs:
release_version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -61,7 +61,7 @@ jobs:
contents: read
steps:
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml
index 08045b8d3c7..3f7b7e326d9 100644
--- a/.github/workflows/release-cli.yml
+++ b/.github/workflows/release-cli.yml
@@ -29,7 +29,7 @@ jobs:
release_version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml
index 2239cb1268f..ec529d7b4d8 100644
--- a/.github/workflows/release-desktop.yml
+++ b/.github/workflows/release-desktop.yml
@@ -31,7 +31,7 @@ jobs:
release_channel: ${{ steps.release_channel.outputs.channel }}
steps:
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml
index fc0ac340234..f6feb3386a7 100644
--- a/.github/workflows/release-web.yml
+++ b/.github/workflows/release-web.yml
@@ -25,7 +25,7 @@ jobs:
tag_version: ${{ steps.version.outputs.tag }}
steps:
- name: Checkout repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml
index 0a343be878c..b2edf0171db 100644
--- a/.github/workflows/repository-management.yml
+++ b/.github/workflows/repository-management.yml
@@ -105,7 +105,7 @@ jobs:
permission-contents: write # for committing and pushing to current branch
- name: Check out branch
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.ref }}
token: ${{ steps.app-token.outputs.token }}
@@ -471,7 +471,7 @@ jobs:
permission-contents: write # for creating and pushing new branch
- name: Check out target ref
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ inputs.target_ref }}
token: ${{ steps.app-token.outputs.token }}
diff --git a/.github/workflows/sdk-breaking-change-check.yml b/.github/workflows/sdk-breaking-change-check.yml
index 14547b3942f..ecc803ebd5c 100644
--- a/.github/workflows/sdk-breaking-change-check.yml
+++ b/.github/workflows/sdk-breaking-change-check.yml
@@ -64,7 +64,7 @@ jobs:
uses: bitwarden/gh-actions/azure-logout@main
- name: Check out clients repository
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
diff --git a/.github/workflows/test-browser-interactions.yml b/.github/workflows/test-browser-interactions.yml
index c8f4c959c52..6e236f2352c 100644
--- a/.github/workflows/test-browser-interactions.yml
+++ b/.github/workflows/test-browser-interactions.yml
@@ -18,7 +18,7 @@ jobs:
id-token: write
steps:
- name: Checkout code
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: false
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e3ba6112b7d..e8f062ea345 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -24,7 +24,7 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -103,7 +103,7 @@ jobs:
sudo apt-get install -y gnome-keyring dbus-x11
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -137,7 +137,7 @@ jobs:
runs-on: macos-14
steps:
- name: Checkout
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -173,7 +173,7 @@ jobs:
- rust-coverage
steps:
- name: Check out repo
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
diff --git a/.github/workflows/version-auto-bump.yml b/.github/workflows/version-auto-bump.yml
index 65f004149de..d66c48fcf58 100644
--- a/.github/workflows/version-auto-bump.yml
+++ b/.github/workflows/version-auto-bump.yml
@@ -39,7 +39,7 @@ jobs:
permission-contents: write # for committing and pushing to the current branch
- name: Check out target ref
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: main
token: ${{ steps.app-token.outputs.token }}
From 2707811de8145ce00ea7d064efb49f4507a1faf8 Mon Sep 17 00:00:00 2001
From: Dave <3836813+enmande@users.noreply.github.com>
Date: Mon, 29 Dec 2025 12:19:37 -0500
Subject: [PATCH 41/81] feat(2fa-webauthn) [PM-20109]: Increase 2FA WebAuthn
Security Key Limit (#18040)
* feat(2fa-webauthn) [PM-20109]: Update WebAuthN credential handling.
* feat(messages) [PM-20109]: Add 'Unnamed key' translation.
* refactor(2fa-webauthn) [PM-20109]: Refactor nextId for type safety.
* refactor(2fa-webauthn) [PM-20109]: Clean up template comments.
* fix(webauthn-2fa) [PM-3611]: Key name is required.
---
.../two-factor-setup-webauthn.component.html | 43 ++++++------
.../two-factor-setup-webauthn.component.ts | 68 +++++++++++++------
apps/web/src/locales/en/messages.json | 3 +
3 files changed, 72 insertions(+), 42 deletions(-)
diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html
index c272a8e5b70..8a538cb961c 100644
--- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html
+++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html
@@ -16,27 +16,26 @@
-
-
- {{ "webAuthnkeyX" | i18n: (i + 1).toString() }}
-
-
- {{ k.name }}
-
-
-
- {{ "webAuthnMigrated" | i18n }}
+
+
+
+ {{ k.name || ("unnamedKey" | i18n) }}
+
+
+
+ {{ "webAuthnMigrated" | i18n }}
+
+
+ 1 && k.configured">
+
+ -
+ {{ "remove" | i18n }}
-
- 1 && k.configured">
-
- -
- {{ "remove" | i18n }}
@@ -60,7 +59,9 @@
type="button"
[bitAction]="readKey"
buttonType="secondary"
- [disabled]="$any(readKeyBtn).loading() || webAuthnListening || !keyIdAvailable"
+ [disabled]="
+ $any(readKeyBtn).loading() || webAuthnListening || !keyIdAvailable || formGroup.invalid
+ "
class="tw-mr-2"
#readKeyBtn
>
diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts
index 11ba5955902..57001acc4d2 100644
--- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts
+++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts
@@ -1,6 +1,6 @@
import { CommonModule } from "@angular/common";
import { Component, Inject, NgZone } from "@angular/core";
-import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";
+import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
@@ -99,7 +99,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
toastService,
);
this.formGroup = new FormGroup({
- name: new FormControl({ value: "", disabled: false }),
+ name: new FormControl({ value: "", disabled: false }, Validators.required),
});
this.auth(data);
}
@@ -213,7 +213,22 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
this.webAuthnListening = listening;
}
+ private findNextAvailableKeyId(existingIds: Set): number {
+ // Search for first gap, bounded by current key count + 1
+ for (let i = 1; i <= existingIds.size + 1; i++) {
+ if (!existingIds.has(i)) {
+ return i;
+ }
+ }
+
+ // This should never be reached due to loop bounds, but TypeScript requires a return
+ throw new Error("Unable to find next available key ID");
+ }
+
private processResponse(response: TwoFactorWebAuthnResponse) {
+ if (!response.keys || response.keys.length === 0) {
+ response.keys = [];
+ }
this.resetWebAuthn();
this.keys = [];
this.keyIdAvailable = null;
@@ -223,26 +238,37 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
nameControl.setValue("");
}
this.keysConfiguredCount = 0;
- for (let i = 1; i <= 5; i++) {
- if (response.keys != null) {
- const key = response.keys.filter((k) => k.id === i);
- if (key.length > 0) {
- this.keysConfiguredCount++;
- this.keys.push({
- id: i,
- name: key[0].name,
- configured: true,
- migrated: key[0].migrated,
- removePromise: null,
- });
- continue;
- }
- }
- this.keys.push({ id: i, name: "", configured: false, removePromise: null });
- if (this.keyIdAvailable == null) {
- this.keyIdAvailable = i;
- }
+
+ // Build configured keys
+ for (const key of response.keys) {
+ this.keysConfiguredCount++;
+ this.keys.push({
+ id: key.id,
+ name: key.name,
+ configured: true,
+ migrated: key.migrated,
+ removePromise: null,
+ });
}
+
+ // [PM-20109]: To accommodate the existing form logic with minimal changes,
+ // we need to have at least one unconfigured key slot available to the collection.
+ // Prior to PM-20109, both client and server had hard checks for IDs <= 5.
+ // While we don't have any technical constraints _at this time_, we should avoid
+ // unbounded growth of key IDs over time as users add/remove keys;
+ // this strategy gap-fills key IDs.
+ const existingIds = new Set(response.keys.map((k) => k.id));
+ const nextId = this.findNextAvailableKeyId(existingIds);
+
+ // Add unconfigured slot, which can be used to add a new key
+ this.keys.push({
+ id: nextId,
+ name: "",
+ configured: false,
+ removePromise: null,
+ });
+ this.keyIdAvailable = nextId;
+
this.enabled = response.enabled;
this.onUpdated.emit(this.enabled);
}
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index ac40f78e43f..4721c971dcc 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -2634,6 +2634,9 @@
"key": {
"message": "Key"
},
+ "unnamedKey": {
+ "message": "Unnamed key"
+ },
"twoStepAuthenticatorEnterCodeV2": {
"message": "Verification code"
},
From f689fd88b76789ddf8a98951f0062a54f611ac88 Mon Sep 17 00:00:00 2001
From: Bernd Schoolmann
Date: Mon, 29 Dec 2025 18:31:15 +0100
Subject: [PATCH 42/81] [PM-30285] Add soundness check to cipher and folder
recovery step (#18120)
* Add soundness check to cipher and folder recovery step
* fix tests
---------
Co-authored-by: Maciej Zieniuk
---
.../data-recovery/steps/cipher-step.spec.ts | 36 ++++++++++++++++---
.../data-recovery/steps/cipher-step.ts | 6 +++-
.../data-recovery/steps/folder-step.ts | 6 +++-
3 files changed, 42 insertions(+), 6 deletions(-)
diff --git a/apps/web/src/app/key-management/data-recovery/steps/cipher-step.spec.ts b/apps/web/src/app/key-management/data-recovery/steps/cipher-step.spec.ts
index a894fce0c41..9ae0600fb2a 100644
--- a/apps/web/src/app/key-management/data-recovery/steps/cipher-step.spec.ts
+++ b/apps/web/src/app/key-management/data-recovery/steps/cipher-step.spec.ts
@@ -132,7 +132,10 @@ describe("CipherStep", () => {
userKey: null,
encryptedPrivateKey: null,
isPrivateKeyCorrupt: false,
- ciphers: [{ id: "cipher-1", organizationId: null } as Cipher],
+ ciphers: [
+ { id: "cipher-1", organizationId: null } as Cipher,
+ { id: "cipher-2", organizationId: null } as Cipher,
+ ],
folders: [],
};
@@ -144,14 +147,39 @@ describe("CipherStep", () => {
expect(result).toBe(false);
});
- it("returns true when there are undecryptable ciphers", async () => {
+ it("returns true when there are undecryptable ciphers but at least one decryptable cipher", async () => {
const userId = "user-id" as UserId;
const workingData: RecoveryWorkingData = {
userId,
userKey: null,
encryptedPrivateKey: null,
isPrivateKeyCorrupt: false,
- ciphers: [{ id: "cipher-1", organizationId: null } as Cipher],
+ ciphers: [
+ { id: "cipher-1", organizationId: null } as Cipher,
+ { id: "cipher-2", organizationId: null } as Cipher,
+ ],
+ folders: [],
+ };
+
+ cipherEncryptionService.decrypt.mockRejectedValueOnce(new Error("Decryption failed"));
+
+ await cipherStep.runDiagnostics(workingData, logger);
+ const result = cipherStep.canRecover(workingData);
+
+ expect(result).toBe(true);
+ });
+
+ it("returns false when all ciphers are undecryptable", async () => {
+ const userId = "user-id" as UserId;
+ const workingData: RecoveryWorkingData = {
+ userId,
+ userKey: null,
+ encryptedPrivateKey: null,
+ isPrivateKeyCorrupt: false,
+ ciphers: [
+ { id: "cipher-1", organizationId: null } as Cipher,
+ { id: "cipher-2", organizationId: null } as Cipher,
+ ],
folders: [],
};
@@ -160,7 +188,7 @@ describe("CipherStep", () => {
await cipherStep.runDiagnostics(workingData, logger);
const result = cipherStep.canRecover(workingData);
- expect(result).toBe(true);
+ expect(result).toBe(false);
});
});
diff --git a/apps/web/src/app/key-management/data-recovery/steps/cipher-step.ts b/apps/web/src/app/key-management/data-recovery/steps/cipher-step.ts
index b44e8afc54d..01c2d9bc2a1 100644
--- a/apps/web/src/app/key-management/data-recovery/steps/cipher-step.ts
+++ b/apps/web/src/app/key-management/data-recovery/steps/cipher-step.ts
@@ -10,6 +10,7 @@ export class CipherStep implements RecoveryStep {
title = "recoveryStepCipherTitle";
private undecryptableCipherIds: string[] = [];
+ private decryptableCipherIds: string[] = [];
constructor(
private apiService: ApiService,
@@ -31,18 +32,21 @@ export class CipherStep implements RecoveryStep {
for (const cipher of userCiphers) {
try {
await this.cipherService.decrypt(cipher, workingData.userId);
+ this.decryptableCipherIds.push(cipher.id);
} catch {
logger.record(`Cipher ID ${cipher.id} was undecryptable`);
this.undecryptableCipherIds.push(cipher.id);
}
}
logger.record(`Found ${this.undecryptableCipherIds.length} undecryptable ciphers`);
+ logger.record(`Found ${this.decryptableCipherIds.length} decryptable ciphers`);
return this.undecryptableCipherIds.length == 0;
}
canRecover(workingData: RecoveryWorkingData): boolean {
- return this.undecryptableCipherIds.length > 0;
+ // If everything fails to decrypt, it's a deeper issue and we shouldn't offer recovery here.
+ return this.undecryptableCipherIds.length > 0 && this.decryptableCipherIds.length > 0;
}
async runRecovery(workingData: RecoveryWorkingData, logger: LogRecorder): Promise {
diff --git a/apps/web/src/app/key-management/data-recovery/steps/folder-step.ts b/apps/web/src/app/key-management/data-recovery/steps/folder-step.ts
index bc0ae31efba..90e252ce6c3 100644
--- a/apps/web/src/app/key-management/data-recovery/steps/folder-step.ts
+++ b/apps/web/src/app/key-management/data-recovery/steps/folder-step.ts
@@ -11,6 +11,7 @@ export class FolderStep implements RecoveryStep {
title = "recoveryStepFoldersTitle";
private undecryptableFolderIds: string[] = [];
+ private decryptableFolderIds: string[] = [];
constructor(
private folderService: FolderApiServiceAbstraction,
@@ -36,18 +37,21 @@ export class FolderStep implements RecoveryStep {
folder.name.encryptedString,
workingData.userKey.toEncoded(),
);
+ this.decryptableFolderIds.push(folder.id);
} catch {
logger.record(`Folder name for folder ID ${folder.id} was undecryptable`);
this.undecryptableFolderIds.push(folder.id);
}
}
logger.record(`Found ${this.undecryptableFolderIds.length} undecryptable folders`);
+ logger.record(`Found ${this.decryptableFolderIds.length} decryptable folders`);
return this.undecryptableFolderIds.length == 0;
}
canRecover(workingData: RecoveryWorkingData): boolean {
- return this.undecryptableFolderIds.length > 0;
+ // If everything fails to decrypt, it's a deeper issue and we shouldn't offer recovery here.
+ return this.undecryptableFolderIds.length > 0 && this.decryptableFolderIds.length > 0;
}
async runRecovery(workingData: RecoveryWorkingData, logger: LogRecorder): Promise {
From 1c16b8edb92dea60fee93b8d912be34ec87691f3 Mon Sep 17 00:00:00 2001
From: shivam
Date: Mon, 29 Dec 2025 23:01:31 +0530
Subject: [PATCH 43/81] fix(ui): clean up unintended character on login page
(#18101)
---
apps/web/src/index.html | 1 -
1 file changed, 1 deletion(-)
diff --git a/apps/web/src/index.html b/apps/web/src/index.html
index 06f7587a123..5e56df553fc 100644
--- a/apps/web/src/index.html
+++ b/apps/web/src/index.html
@@ -122,7 +122,6 @@
- `;