diff --git a/.github/workflows/build-web-ee.yml b/.github/workflows/build-web-ee.yml new file mode 100644 index 00000000000..678ccd83200 --- /dev/null +++ b/.github/workflows/build-web-ee.yml @@ -0,0 +1,16 @@ +--- +name: Build Web for EE + +on: + workflow_dispatch: + +jobs: + stub: + name: stub + runs-on: ubuntu-20.04 + steps: + - name: Checkout repo + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v3.0.2 + + - name: Stub + run: print 'This is only a stub' diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index a5fc5f6185b..94396b75709 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -117,15 +117,11 @@ jobs: working-directory: apps/web run: npm run ${{ matrix.npm_command }} - - name: Package ${{ matrix.name }} artifact - working-directory: apps/web - run: zip -r web-$_VERSION-${{ matrix.name }}.zip build - - name: Upload ${{ matrix.name }} artifact uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0 with: name: web-${{ env._VERSION }}-${{ matrix.name }}.zip - path: apps/web/web-${{ env._VERSION }}-${{ matrix.name }}.zip + path: apps/web/build if-no-files-found: error build-commercial-selfhost-image: @@ -155,11 +151,7 @@ jobs: uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 with: name: web-${{ env._VERSION }}-selfhosted-COMMERCIAL.zip - path: apps/web - - - name: Extract selfhosted-COMMERCIAL artifact - working-directory: apps/web - run: unzip web-${{ env._VERSION }}-selfhosted-COMMERCIAL.zip + path: apps/web/build - name: Build Docker image working-directory: apps/web @@ -266,11 +258,7 @@ jobs: uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 with: name: web-${{ env._VERSION }}-cloud-QA.zip - path: apps/web - - - name: Extract cloud-QA artifact - working-directory: apps/web - run: unzip web-${{ env._VERSION }}-cloud-QA.zip + path: apps/web/build - name: Build Docker image working-directory: apps/web @@ -367,6 +355,7 @@ jobs: - cloc - setup - build-artifacts + - build-commercial-selfhost-image - build-qa - crowdin-push steps: @@ -376,6 +365,7 @@ jobs: CLOC_STATUS: ${{ needs.cloc.result }} SETUP_STATUS: ${{ needs.setup.result }} ARTIFACT_STATUS: ${{ needs.build-artifacts.result }} + BUILD_SELFHOST_STATUS: ${{ needs.build-commercial-selfhost-image.result }} BUILD_QA_STATUS: ${{ needs.build-qa.result }} CROWDIN_PUSH_STATUS: ${{ needs.crowdin-push.result }} run: | @@ -385,6 +375,8 @@ jobs: exit 1 elif [ "$ARTIFACT_STATUS" = "failure" ]; then exit 1 + elif [ "$BUILD_SELFHOST_STATUS" = "failure" ]; then + exit 1 elif [ "$BUILD_QA_STATUS" = "failure" ]; then exit 1 elif [ "$CROWDIN_PUSH_STATUS" = "failure" ]; then diff --git a/.github/workflows/release-qa-web.yml b/.github/workflows/release-qa-web.yml index 80d15ac653d..1e31c95cbeb 100644 --- a/.github/workflows/release-qa-web.yml +++ b/.github/workflows/release-qa-web.yml @@ -2,94 +2,12 @@ name: QA - Web Release on: - workflow_dispatch: - inputs: - image_extension: - description: "Image tag extension" - required: false - azure_publish: - description: 'Release to Azure' - required: false - default: true - type: boolean - cloudflare_publish: - description: 'Release to Cloudflare' - required: false - default: true - type: boolean - -env: - _QA_CLUSTER_RESOURCE_GROUP: "bw-env-qa" - _QA_CLUSTER_NAME: "bw-aks-qa" - _QA_K8S_NAMESPACE: "bw-qa" - _QA_K8S_APP_NAME: "bw-web" + workflow_dispatch: {} jobs: - deploy: - name: Deploy QA Web - if: inputs.azure_publish - runs-on: ubuntu-20.04 - steps: - - name: Checkout Repo - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v3.0.2 - - - name: Setup - run: export PATH=$PATH:~/work/web/web - - - name: Login to Azure - uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010 # v1.1 - with: - creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }} - - - name: Retrieve secrets - id: retrieve-secrets - env: - KEYVAULT: bitwarden-qa-kv - SECRETS: | - qa-aks-kubectl-credentials - run: | - for i in ${SECRETS//,/ } - do - VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv) - echo "::add-mask::$VALUE" - echo "::set-output name=$i::$VALUE" - done - - - name: Login with qa-aks-kubectl-credentials SP - uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010 # v1.1 - with: - creds: ${{ env.qa-aks-kubectl-credentials }} - - - name: Setup AKS access - run: | - echo "---az install---" - az aks install-cli --install-location ./kubectl --kubelogin-install-location ./kubelogin - echo "---az get-creds---" - az aks get-credentials -n $_QA_CLUSTER_NAME -g $_QA_CLUSTER_RESOURCE_GROUP - - - name: Get image tag - id: image_tag - run: | - IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") - TAG_EXTENSION=${{ github.event.inputs.image_extension }} - - if [[ $TAG_EXTENSION ]]; then - IMAGE_TAG=$IMAGE_TAG-$TAG_EXTENSION - fi - echo "::set-output name=value::$IMAGE_TAG" - - - name: Deploy Web image - env: - IMAGE_TAG: ${{ steps.image_tag.outputs.value }} - run: | - kubectl set image -n $_QA_K8S_NAMESPACE deployment/web web=bitwardenqa.azurecr.io/web:$IMAGE_TAG --record - kubectl rollout restart -n $_QA_K8S_NAMESPACE deployment/web - kubectl rollout status deployment/web -n $_QA_K8S_NAMESPACE - cfpages-deploy: name: Deploy Web Vault to QA CloudFlare Pages branch runs-on: ubuntu-20.04 - if: inputs.cloudflare_publish steps: - name: Create GitHub deployment uses: chrnorm/deployment-action@1b599fe41a0ef1f95191e7f2eec4743f2d7dfc48 diff --git a/.prettierignore b/.prettierignore index 8790a1e73ff..b1c9359fa1e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -27,3 +27,6 @@ libs/.github # Github Workflows .github/workflows + +# Forked library files +libs/common/src/types/deep-jsonify.ts diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index bbcdf2e2b26..a6fb87d2305 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -157,7 +157,7 @@ "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "twoStepLogin": { - "message": "Two-step login" + "message": "تسجيل الدخول بخطوتين" }, "logOut": { "message": "تسجيل الخروج" @@ -258,7 +258,7 @@ "description": "Make the first letter of a work uppercase." }, "includeNumber": { - "message": "Include Number" + "message": "تضمين الرقم" }, "minNumbers": { "message": "الحد الأدنى من الأرقام" @@ -315,7 +315,7 @@ "message": "عرض العنصر" }, "launch": { - "message": "Launch" + "message": "بدء" }, "website": { "message": "الموقع الإلكتروني" @@ -327,7 +327,7 @@ "message": "إدارة" }, "other": { - "message": "Other" + "message": "الأخرى" }, "rateExtension": { "message": "قيِّم هذه الإضافة" @@ -412,7 +412,7 @@ "message": "مطلقاً" }, "security": { - "message": "Security" + "message": "الأمان" }, "errorOccurred": { "message": "لقد حدث خطأ ما" @@ -488,10 +488,10 @@ "message": "تغيير كلمة المرور الرئيسية" }, "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "message": "يمكنك تغيير كلمة المرور الرئيسية من خزنة الويب في bitwarden.com. هل تريد زيارة الموقع الآن؟" }, "twoStepLoginConfirmation": { - "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?" + "message": "تسجيل الدخول بخطوتين يجعل حسابك أكثر أمنا من خلال مطالبتك بالتحقق من تسجيل الدخول باستخدام جهاز آخر مثل مفتاح الأمان، تطبيق المصادقة، الرسائل القصيرة، المكالمة الهاتفية، أو البريد الإلكتروني. يمكن تمكين تسجيل الدخول بخطوتين على خزنة الويب bitwarden.com. هل تريد زيارة الموقع الآن؟" }, "editedFolder": { "message": "Edited folder" @@ -500,13 +500,13 @@ "message": "هل أنت متأكد من حذف هذا المجلّد؟" }, "deletedFolder": { - "message": "Deleted folder" + "message": "تم حذف المجلد" }, "gettingStartedTutorial": { - "message": "Getting Started Tutorial" + "message": "لنبدأ نتعلم معاً" }, "gettingStartedTutorialVideo": { - "message": "Watch our getting started tutorial to learn how to get the most out of the browser extension." + "message": "مشاهدة دروس البَدْء تمكنك من الحصول على أقصى أستفادة من ملحق المتصفح." }, "syncingComplete": { "message": "تم إكمال المزامنة" @@ -531,31 +531,31 @@ } }, "newUri": { - "message": "New URI" + "message": "رابط جديد" }, "addedItem": { - "message": "Added item" + "message": "تمت إضافة العنصر" }, "editedItem": { - "message": "Edited item" + "message": "تم تعديل العنصر" }, "deleteItemConfirmation": { - "message": "Do you really want to send to the trash?" + "message": "هل تريد حقاً أن ترسل إلى سلة المهملات؟" }, "deletedItem": { "message": "تم إرسال العنصر إلى سلة المهملات" }, "overwritePassword": { - "message": "Overwrite Password" + "message": "الكتابة فوق كلمة المرور" }, "overwritePasswordConfirmation": { - "message": "Are you sure you want to overwrite the current password?" + "message": "هل أنت متأكد من أنك تريد الكتابة فوق كلمة المرور الموجودة؟" }, "overwriteUsername": { - "message": "Overwrite Username" + "message": "الكتابة فوق اسم المستخدم" }, "overwriteUsernameConfirmation": { - "message": "Are you sure you want to overwrite the current username?" + "message": "هل أنت متأكد من أنك تريد الكتابة فوق اسم المستخدم الموجود؟" }, "searchFolder": { "message": "إبحث في المجلّد" @@ -589,15 +589,15 @@ "message": "List identity items on the Tab page for easy auto-fill." }, "clearClipboard": { - "message": "Clear clipboard", + "message": "مسح الحافظة", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "clearClipboardDesc": { - "message": "Automatically clear copied values from your clipboard.", + "message": "مسح القيم المنسوخة تلقائيًا من حافظتك.", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "notificationAddDesc": { - "message": "Should Bitwarden remember this password for you?" + "message": "هل يجب على Bitwarden تذكر كلمة المرور هذه لك؟" }, "notificationAddSave": { "message": "حفظ" @@ -683,10 +683,10 @@ "message": "Move to Organization" }, "share": { - "message": "Share" + "message": "مشاركة" }, "movedItemToOrg": { - "message": "$ITEMNAME$ moved to $ORGNAME$", + "message": "$ITEMNAME$ انتقل إلى $ORGNAME$", "placeholders": { "itemname": { "content": "$1", @@ -699,16 +699,16 @@ } }, "moveToOrgDesc": { - "message": "Choose an organization that you wish to move this item to. Moving to an organization transfers ownership of the item to that organization. You will no longer be the direct owner of this item once it has been moved." + "message": "اختر المؤسسة التي ترغب في نقل هذا العنصر إليها. يؤدي النقل إلى مؤسسة إلى نقل ملكية العنصر إلى تلك المؤسسة. لن تكون المالك المباشر لهذا العنصر بعد نقله." }, "learnMore": { - "message": "Learn more" + "message": "معرفة المزيد" }, "authenticatorKeyTotp": { - "message": "Authenticator Key (TOTP)" + "message": "مفتاح المصادقة (TOTP)" }, "verificationCodeTotp": { - "message": "Verification Code (TOTP)" + "message": "رمز التحقق (TOTP)" }, "copyVerificationCode": { "message": "Copy Verification Code" @@ -1124,16 +1124,16 @@ "message": "Dr" }, "firstName": { - "message": "First Name" + "message": "الاسم الأول" }, "middleName": { - "message": "Middle Name" + "message": "الاسم الأوسط" }, "lastName": { - "message": "Last Name" + "message": "الاسم الأخير" }, "fullName": { - "message": "Full Name" + "message": "الاسم الكامل" }, "identityName": { "message": "Identity Name" @@ -1289,7 +1289,7 @@ "description": "The URI of one of the current open tabs in the browser." }, "organization": { - "message": "Organization", + "message": "المؤسسة", "description": "An entity of multiple related people (ex. a team or business organization)." }, "types": { @@ -1672,17 +1672,17 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { - "message": "Expiration Date" + "message": "تاريخ انتهاء الصلاحية" }, "expirationDateDesc": { "message": "If set, access to this Send will expire on the specified date and time.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "oneDay": { - "message": "1 day" + "message": "يوم واحد" }, "days": { - "message": "$DAYS$ days", + "message": "$DAYS$ أيام", "placeholders": { "days": { "content": "$1", @@ -1731,7 +1731,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "newPassword": { - "message": "New Password" + "message": "كلمة المرور الجديدة" }, "sendDisabled": { "message": "Send Disabled", @@ -1766,7 +1766,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" }, "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", + "message": "انقر هنا", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" }, "sendFirefoxCustomDatePopoutMessage3": { @@ -1798,37 +1798,37 @@ "message": "Master password re-prompt" }, "passwordConfirmation": { - "message": "Master password confirmation" + "message": "تأكيد كلمة المرور الرئيسية" }, "passwordConfirmationDesc": { - "message": "This action is protected. To continue, please re-enter your master password to verify your identity." + "message": "هذا الإجراء محمي. للاستمرار، يرجى إعادة إدخال كلمة المرور الرئيسية للتحقق من هويتك." }, "emailVerificationRequired": { - "message": "Email Verification Required" + "message": "تأكيد البريد الإلكتروني مطلوب" }, "emailVerificationRequiredDesc": { - "message": "You must verify your email to use this feature. You can verify your email in the web vault." + "message": "يجب عليك تأكيد بريدك الإلكتروني لاستخدام هذه الميزة. يمكنك تأكيد بريدك الإلكتروني في خزنة الويب." }, "updatedMasterPassword": { "message": "Updated Master Password" }, "updateMasterPassword": { - "message": "Update Master Password" + "message": "تحديث كلمة المرور الرئيسية" }, "updateMasterPasswordWarning": { - "message": "Your Master Password was recently changed by an administrator in your organization. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "تم تغيير كلمة المرور الرئيسية الخاصة بك مؤخرًا من قبل مسؤول في مؤسستك. من أجل الوصول إلى الخزنة، يجب عليك تحديثها الآن. سيتم تسجيل خروجك من الجلسة الحالية، مما يتطلب منك تسجيل الدخول مرة أخرى. قد تظل الجلسات النشطة على أجهزة أخرى نشطة لمدة تصل إلى ساعة واحدة." }, "resetPasswordPolicyAutoEnroll": { - "message": "Automatic Enrollment" + "message": "التسجيل التلقائي" }, "resetPasswordAutoEnrollInviteWarning": { - "message": "This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password." + "message": "هذه المؤسسة لديها سياسة تقوم تلقائياً بتسجيلك في إعادة تعيين كلمة المرور. هذا التسجيل سيسمح لمسؤولي المؤسسة بتغيير كلمة المرور الرئيسية الخاصة بك." }, "selectFolder": { "message": "Select folder..." }, "ssoCompleteRegistration": { - "message": "In order to complete logging in with SSO, please set a master password to access and protect your vault." + "message": "من أجل إكمال تسجيل الدخول باستخدام SSO، يرجى تعيين كلمة المرور الرئيسية للوصول لخزنتك وحمايتها." }, "hours": { "message": "ساعات" @@ -1837,7 +1837,7 @@ "message": "دقائق" }, "vaultTimeoutPolicyInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed Vault Timeout is $HOURS$ hour(s) and $MINUTES$ minute(s)", + "message": "سياسات مؤسستك تؤثر على مهلة الخزنة الخاص بك. الحد الأقصى المسموح به لمهلة الخزنة هو $HOURS$ ساعة/ساعات و $MINUTES$ دقيقة/دقائق", "placeholders": { "hours": { "content": "$1", @@ -1850,19 +1850,19 @@ } }, "vaultTimeoutTooLarge": { - "message": "Your vault timeout exceeds the restrictions set by your organization." + "message": "مهلة خزنتك تتجاوز القيود التي تضعها مؤسستك." }, "vaultExportDisabled": { - "message": "Vault Export Disabled" + "message": "تصدير الخزنة مُعطّل" }, "personalVaultExportPolicyInEffect": { - "message": "One or more organization policies prevents you from exporting your personal vault." + "message": "واحدة أو أكثر من سياسات المؤسسة تمنعك من تصدير خزانتك الشخصية." }, "copyCustomFieldNameInvalidElement": { - "message": "Unable to identify a valid form element. Try inspecting the HTML instead." + "message": "غير قادر على التعرف على نموذج صالح. حاول فحص HTML بدلا من ذلك." }, "copyCustomFieldNameNotUnique": { - "message": "No unique identifier found." + "message": "لم يتم العثور على معرف فريد." }, "convertOrganizationEncryptionDesc": { "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", @@ -1923,10 +1923,10 @@ "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" }, "plusAddressedEmailDesc": { - "message": "Use your email provider's sub-addressing capabilities." + "message": "استخدم قدرات العنوان الفرعي لمزود البريد الإلكتروني الخاص بك." }, "catchallEmail": { - "message": "Catch-all Email" + "message": "تجميع كل البريد الإلكتروني" }, "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." @@ -1950,10 +1950,10 @@ "message": "الخدمة" }, "forwardedEmail": { - "message": "Forwarded Email Alias" + "message": "إعادة توجيه الاسم المستعار للبريد الإلكتروني" }, "forwardedEmailDesc": { - "message": "Generate an email alias with an external forwarding service." + "message": "إنشاء بريد إلكتروني مستعار مع خدمة إعادة توجيه خارجية." }, "hostname": { "message": "اسم المضيف", @@ -1993,7 +1993,7 @@ "message": "تم تعديل الإعدادات" }, "environmentEditedClick": { - "message": "Click here" + "message": "انقر هنا" }, "environmentEditedReset": { "message": "لإعادة تعيين الإعدادات المُعدة مسبقاً" @@ -2017,7 +2017,7 @@ } }, "lastSeenOn": { - "message": "last seen on $DATE$", + "message": "آخر ظهور في $DATE$", "placeholders": { "date": { "content": "$1", diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index b132e0c464a..e7f0f7a8015 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -20,7 +20,7 @@ "message": "Увайсці" }, "enterpriseSingleSignOn": { - "message": "Адзіны ўваход у карпаратыўную сістэму (SSO)." + "message": "Адзіны ўваход прадпрыемства (SSO)" }, "cancel": { "message": "Скасаваць" @@ -32,7 +32,7 @@ "message": "Адправіць" }, "emailAddress": { - "message": "Адрас эл. пошты" + "message": "Адрас электроннай пошты" }, "masterPass": { "message": "Асноўны пароль" @@ -41,7 +41,7 @@ "message": "Асноўны пароль — ключ да вашага бяспечнага сховішча. Ён вельмі важны, таму не забывайце яго. Аднавіць асноўны пароль немагчыма." }, "masterPassHintDesc": { - "message": "Падказка да асноўнага пароля можа дапамагчы вам яго ўспомніць." + "message": "Падказка да асноўнага пароля можа дапамагчы вам успомніць яго, калі вы яго забылі." }, "reTypeMasterPass": { "message": "Увядзіце асноўны пароль паўторна" @@ -71,34 +71,34 @@ "message": "Бягучая ўкладка" }, "copyPassword": { - "message": "Капіяваць пароль" + "message": "Скапіяваць пароль" }, "copyNote": { - "message": "Капіяваць нататку" + "message": "Скапіяваць нататку" }, "copyUri": { - "message": "Капіяваць URI" + "message": "Скапіяваць URI" }, "copyUsername": { - "message": "Капіяваць імя карыстальніка" + "message": "Скапіяваць імя карыстальніка" }, "copyNumber": { - "message": "Капіяваць нумар" + "message": "Скапіяваць нумар" }, "copySecurityCode": { - "message": "Капіяваць код бяспекі" + "message": "Скапіяваць код бяспекі" }, "autoFill": { "message": "Аўтазапаўненне" }, "generatePasswordCopied": { - "message": "Стварыць пароль (з капіяваннем)" + "message": "Генерыраваць пароль (з капіяваннем)" }, "copyElementIdentifier": { "message": "Скапіяваць назву карыстальніцкага пароля" }, "noMatchingLogins": { - "message": "Няма падыходных уліковых даных." + "message": "Няма адпаведных лагінаў." }, "unlockVaultMenu": { "message": "Разблакіраваць сховішча" @@ -110,7 +110,7 @@ "message": "Няма ўліковых даных, даступных для аўтазапаўнення ў бягучую ўкладку браўзера." }, "addLogin": { - "message": "Дадаць ўліковыя даныя" + "message": "Дадаць лагін" }, "addItem": { "message": "Дадаць элемент" @@ -119,10 +119,10 @@ "message": "Падказка да пароля" }, "enterEmailToGetHint": { - "message": "Увядзіце адрас электроннай пошты ўліковага запісу для атрымання падказкі для асноўнага пароля." + "message": "Увядзіце адрас электроннай пошты ўліковага запісу для атрымання падказкі да асноўнага пароля." }, "getMasterPasswordHint": { - "message": "Атрымаць падказку для асноўнага пароля" + "message": "Атрымаць падказку да асноўнага пароля" }, "continue": { "message": "Працягнуць" @@ -208,23 +208,23 @@ "message": "Генератар пароляў" }, "generator": { - "message": "Згенерыраваць", + "message": "Генератар", "description": "Short for 'Password Generator'." }, "passGenInfo": { - "message": "Аўтаматычна ствараць моцныя, унікальныя паролі для вашых уліковых даных." + "message": "Аўтаматычна генерыруйце надзейныя і ўнікальныя паролі для вашых лагінаў." }, "bitWebVault": { "message": "Вэб-сховішча Bitwarden" }, "importItems": { - "message": "Імпарт элементаў" + "message": "Імпартаванне элементаў" }, "select": { "message": "Выбраць" }, "generatePassword": { - "message": "Стварыць пароль" + "message": "Генерыраваць пароль" }, "regeneratePassword": { "message": "Стварыць новы пароль" @@ -254,17 +254,17 @@ "message": "Раздзяляльнік слоў" }, "capitalize": { - "message": "З вялікай літары", + "message": "Вялікія літары", "description": "Make the first letter of a work uppercase." }, "includeNumber": { "message": "Уключыць лічбу" }, "minNumbers": { - "message": "Мін. колькасць лічбаў" + "message": "Мінімум лічбаў" }, "minSpecial": { - "message": "Мін. колькасць сімвалаў" + "message": "Мінімум спецыяльных сімвалаў" }, "avoidAmbChar": { "message": "Пазбягаць неадназначных сімвалаў" @@ -300,10 +300,10 @@ "message": "Нататкі" }, "note": { - "message": "Нататкі" + "message": "Нататка" }, "editItem": { - "message": "Рэдагаванне элемента" + "message": "Рэдагаваць элемент" }, "folder": { "message": "Папка" @@ -324,7 +324,7 @@ "message": "Пераключыць бачнасць" }, "manage": { - "message": "Кіраваць" + "message": "Кіраванне" }, "other": { "message": "Iншае" @@ -348,7 +348,7 @@ "message": "Разблакіраваць" }, "loggedInAsOn": { - "message": "Выкананы ўваход на $HOSTNAME$ як $EMAIL$.", + "message": "Вы ўвайшлі як $HOSTNAME$ у $EMAIL$.", "placeholders": { "email": { "content": "$1", @@ -403,7 +403,7 @@ "message": "4 гадзіны" }, "onLocked": { - "message": "Разам з камп'ютарам" + "message": "Пры блакіраванні сістэмы" }, "onRestart": { "message": "Пры перазапуску браўзера" @@ -433,7 +433,7 @@ "message": "Асноўны пароль павінен быць даўжынёй не менш за 8 сімвалаў." }, "masterPassDoesntMatch": { - "message": "Асноўныя паролі не супадаюць." + "message": "Пацвярджэнне асноўнага пароля не супадае." }, "newAccountCreated": { "message": "Ваш уліковы запіс створаны! Вы можаце ўвайсці." @@ -448,7 +448,7 @@ "message": "Памылковы праверачны код" }, "valueCopied": { - "message": "$VALUE$ скапіяваны(-а)", + "message": "$VALUE$ скапіяваны", "description": "Value has been copied to the clipboard.", "placeholders": { "value": { @@ -458,10 +458,10 @@ } }, "autofillError": { - "message": "Не ўдаецца аўтаматычна запоўніць выбраны элемент на гэтай старонцы. Скапіюйце і ўстаўце інфармацыю ўручную." + "message": "Немагчыма аўтазапоўніць выбраны элемент на гэтай старонцы. Скапіюйце і ўстаўце інфармацыю ўручную." }, "loggedOut": { - "message": "Вы выйшлі са сховішча" + "message": "Вы выйшлі" }, "loginExpired": { "message": "Скончыўся тэрмін дзеяння вашага сеансу." @@ -479,7 +479,7 @@ "message": "Адбылася нечаканая памылка." }, "nameRequired": { - "message": "Патрэбна назва." + "message": "Патрабуецца назва." }, "addedFolder": { "message": "Папка дададзена" @@ -503,7 +503,7 @@ "message": "Папка выдалена" }, "gettingStartedTutorial": { - "message": "Дапаможнік па пачатку працы" + "message": "Уводзіны ў карыстанне праграмай" }, "gettingStartedTutorialVideo": { "message": "Праглядзіце невялікі навучальны матэрыял, каб даведацца. як атрымаць максімальную аддачу ад пашырэння браўзера." @@ -540,7 +540,7 @@ "message": "Элемент адрэдагаваны" }, "deleteItemConfirmation": { - "message": "Вы ўпэўнены, што хочаце выдаліць гэты элемент?" + "message": "Вы ўпэўнены, што хочаце адправіць гэты элемент у сметніцу?" }, "deletedItem": { "message": "Выдалены элемент" @@ -574,19 +574,19 @@ "message": "Пытацца пры дадаванні лагіна" }, "addLoginNotificationDesc": { - "message": "Апавяшчэнне аб даданні ўліковых даных аўтаматычна прапануе вам захаваць новыя ўліковыя даныя ў сховішчы." + "message": "Пытацца пра дадаванне элемента, калі ён адсутнічае ў вашым сховішчы." }, "showCardsCurrentTab": { "message": "Паказваць карткі на старонцы з укладкамі" }, "showCardsCurrentTabDesc": { - "message": "Паказваць спіс элементаў на старонцы з укладкамі для лёгкага аўтазапаўнення." + "message": "Спіс элементаў картак на старонцы з укладкамі для лёгкага аўтазапаўнення." }, "showIdentitiesCurrentTab": { "message": "Паказваць пасведчанні на старонцы з укладкамі" }, "showIdentitiesCurrentTabDesc": { - "message": "Паказваць пасведчанні элементаў на старонцы з укладкамі для лёгкага аўтазапаўнення." + "message": "Спіс элементаў пасведчання на старонцы з укладкамі для лёгкага аўтазапаўнення." }, "clearClipboard": { "message": "Ачыстка буфера абмену", @@ -603,13 +603,13 @@ "message": "Так, захаваць зараз" }, "enableChangedPasswordNotification": { - "message": "Пытацца пра абнаўленні існуючых даных уваходу" + "message": "Пытацца пра абнаўленні існуючага лагіна" }, "changedPasswordNotificationDesc": { - "message": "Пытаць пра абнаўленне пароля ўваходу пры выяўленні змяненняў на вэб-сайце." + "message": "Пытацца пра абнаўленне пароля ад лагіна пры выяўленні змяненняў на вэб-сайце." }, "notificationChangeDesc": { - "message": "Вы хочаце абнавіць гэты пароль у Bitwarden?" + "message": "Хочаце абнавіць гэты пароль у Bitwarden?" }, "notificationChangeSave": { "message": "Так, абнавіць зараз" @@ -621,17 +621,17 @@ "message": "Выкарыстоўваць падвоены націск для доступу да генератара пароля і супастаўлення лагінаў для вэб-сайтаў. " }, "defaultUriMatchDetection": { - "message": "Выяўленне супадзення URI па змаўчанні", + "message": "Прадвызначанае выяўленне супадзення URI", "description": "Default URI match detection for auto-fill." }, "defaultUriMatchDetectionDesc": { - "message": "Выберыце спосаб па змаўчанні, які выкарыстоўваецца пры вызначэнні адпаведнасці URI для ўліковых даных пры выкананні такіх дзеянняў, як аўтаматычнае запаўненне." + "message": "Выберыце прадвызначаны спосаб вызначэння адпаведнасці URI для лагінаў пры выкананні такіх дзеянняў, як аўтаматычнае запаўненне." }, "theme": { "message": "Тэма" }, "themeDesc": { - "message": "Змена колеравай тэмы праграмы." + "message": "Змена каляровай тэмы праграмы." }, "dark": { "message": "Цёмная", @@ -646,7 +646,7 @@ "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportVault": { - "message": "Экспарт сховішча" + "message": "Экспартаваць сховішча" }, "fileFormat": { "message": "Фармат файла" @@ -656,7 +656,7 @@ "description": "WARNING (should stay in capitalized letters if the language permits)" }, "confirmVaultExport": { - "message": "Пацвердзіць экспарт сховішча" + "message": "Пацвердзіць экспартаванне сховішча" }, "exportWarningDesc": { "message": "Экспартуемы файл утрымлівае даныя вашага сховішча ў незашыфраваным фармаце. Яго не варта захоўваць ці адпраўляць па небяспечным каналам (напрыклад, па электроннай пошце). Выдаліце яго адразу пасля выкарыстання." @@ -668,7 +668,7 @@ "message": "Ключы шыфравання з'яўляюцца ўнікальнымі для кожнага ўліковага запісу Bitwarden, таму нельга імпартаваць зашыфраванае сховішча ў іншы ўліковы запіс." }, "exportMasterPassword": { - "message": "Увядзіце ваш асноўны пароль для экспарту даных са сховішча." + "message": "Увядзіце ваш асноўны пароль для экспартавання даных сховішча." }, "shared": { "message": "Абагуленыя" @@ -705,13 +705,13 @@ "message": "Даведацца больш" }, "authenticatorKeyTotp": { - "message": "Ключ праверкі сапраўднасці (TOTP)" + "message": "Ключ аўтэнтыфікацыі (TOTP)" }, "verificationCodeTotp": { "message": "Код праверкі (TOTP)" }, "copyVerificationCode": { - "message": "Капіяваць код праверкі" + "message": "Скапіяваць праверачны код" }, "attachments": { "message": "Далучэнні" @@ -753,7 +753,7 @@ "message": "Прэміяльны статус" }, "premiumManage": { - "message": "Кіраванне статусам" + "message": "Кіраваць статусам" }, "premiumManageAlert": { "message": "Вы можаце кіраваць сваім статусам на bitwarden.com. Перайсці на сайт зараз?" @@ -798,7 +798,7 @@ "message": "Дзякуем вам за падтрымку Bitwarden." }, "premiumPrice": { - "message": "Усяго толькі за $PRICE$ на год!", + "message": "Усяго за $PRICE$ у год!", "placeholders": { "price": { "content": "$1", @@ -810,13 +810,13 @@ "message": "Абнаўленне завершана" }, "enableAutoTotpCopy": { - "message": "Капіяваць TOTP аўтаматычна" + "message": "Скапіяваць TOTP аўтаматычна" }, "disableAutoTotpCopyDesc": { - "message": "Калі да вашых уліковых даных прымацаваны ключ праверкі сапраўднасці, то код пацвярджэння TOTP будзе скапіяваны пры аўтазапаўненні ўліковых даных." + "message": "Калі ў лагіна ёсць ключ аўтэнтыфікацыі, то праверачны код TOTP будзе скапіяваны ў буфер абмену пры аўтазапаўненні ўваходу." }, "enableAutoBiometricsPrompt": { - "message": "Запытваць біяметрыю пры запуску" + "message": "Пытацца пра біяметрыю пры запуску" }, "premiumRequired": { "message": "Патрабуецца прэміяльны статус" @@ -825,10 +825,10 @@ "message": "Для выкарыстання гэтай функцыі патрабуецца прэміяльны статус." }, "enterVerificationCodeApp": { - "message": "Увядзіце 6 лічбаў кода праверкі з вашай праграмы праверкі сапраўднасці." + "message": "Увядзіце 6 лічбаў праверачнага кода з вашай праграмы аўтэнтыфікацыі." }, "enterVerificationCodeEmail": { - "message": "Увядзіце 6 лічбаў кода праверкі, які быў адпраўлены на $EMAIL$.", + "message": "Увядзіце 6 лічбаў праверачнага кода, які быў адпраўлены на $EMAIL$.", "placeholders": { "email": { "content": "$1", @@ -855,10 +855,10 @@ "message": "Выкарыстоўваць іншы метад двухэтапнага ўваходу" }, "insertYubiKey": { - "message": "Устаўце ваш YubiKey ў порт USB вашага камп'ютара, затым націсніце на кнопку." + "message": "Устаўце свой YubiKey у порт USB камп'ютара, а потым націсніце на кнопку." }, "insertU2f": { - "message": "Устаўце ваш ключ бяспекі ў порт USB вашага камп'ютара. Калі на ім ёсць кнопка, націсніце на яе." + "message": "Устаўце ваш ключ бяспекі ў порт USB камп'ютара. Калі на ім ёсць кнопка, націсніце на яе." }, "webAuthnNewTab": { "message": "Каб пачаць праверку WebAuthn 2FA, націсніце кнопку знізу для адкрыцця новай укладкі і прытрымлівайцеся інструкцый, якія паказаны ў новай укладцы." @@ -882,13 +882,13 @@ "message": "Параметры двухэтапнага ўваходу" }, "recoveryCodeDesc": { - "message": "Згубілі доступ да ўсіх варыянтаў двухэтапнага ўваходу? Скарыстайцеся кодам аднаўлення, каб адключыць двухэтапны ўваход для вашага ўліковага запісу." + "message": "Згубілі доступ да ўсіх варыянтаў доступу пастаўшчыкоў двухэтапнай аўтэнтыфікацыі? Скарыстайцеся кодам аднаўлення, каб адключыць праверку пастаўшчыкоў двухэтапнай аўтэнтыфікацыі для вашага ўліковага запісу." }, "recoveryCodeTitle": { "message": "Код аднаўлення" }, "authenticatorAppTitle": { - "message": "Праграма праверкі сапраўднасці" + "message": "Праграма аўтэнтыфікацыі" }, "authenticatorAppDesc": { "message": "Выкарыстоўвайце праграму для праверкі сапраўднасці (напрыклад, Authy або Google Authenticator) для стварэння кодаў праверкі на аснове часу.", @@ -927,22 +927,22 @@ "message": "Увядзіце асноўны URL-адрас на вашым серверы." }, "customEnvironment": { - "message": "Налады асяроддзя" + "message": "Карыстальніцкае асяроддзе" }, "customEnvironmentFooter": { - "message": "Для вопытных карыстальнікаў. Можна ўвесці URL-адрасы асобна для кжонай службы." + "message": "Для дасведчаных карыстальнікаў. Можна ўвесці URL-адрасы асобна для кожнай службы." }, "baseUrl": { "message": "URL-адрас сервера" }, "apiUrl": { - "message": "API URL-адраса сервера" + "message": "Сервер URL-адраса API" }, "webVaultUrl": { "message": "URL-адрас сервера вэб-сховішча" }, "identityUrl": { - "message": "URL-адрас сервера ідэнтыфікацыі" + "message": "URL-адрас сервера пасведчання" }, "notificationsUrl": { "message": "URL-адрас сервера апавяшчэнняў" @@ -954,22 +954,22 @@ "message": "URL-адрас сервера асяроддзя захаваны." }, "enableAutoFillOnPageLoad": { - "message": "Уключыць аўтазапаўненне пры загрузцы старонкі" + "message": "Аўтазапаўненне пры загрузцы старонкі" }, "enableAutoFillOnPageLoadDesc": { - "message": "Пры выяўленні формы ўваходу выконваецца аўтазапаўненне падчас загрузкі вэб-старонкі." + "message": "Калі выяўлена форма ўваходу, то будзе выканана яе аўтазапаўненне падчас загрузкі вэб-старонкі." }, "experimentalFeature": { "message": "Гэта эксперыментальная функцыя. Выкарыстоўвайце на свой страх і рызыку." }, "defaultAutoFillOnPageLoad": { - "message": "Прадвызначана налада аўтазапаўнення для элементаў уваходу" + "message": "Прадвызначаная налада аўтазапаўнення для элементаў уваходу" }, "defaultAutoFillOnPageLoadDesc": { "message": "Вы можаце выключыць аўтазапаўненне на старонцы загрузцы для асобных элементаў уваходу ў меню \"Рэдагаваць\"." }, "itemAutoFillOnPageLoad": { - "message": "Аўтазапаўненне пры загрузцы (калі ўключана ў параметрах праграмы)" + "message": "Аўтазапаўненне пры загрузцы старонкі (калі ўключана ў параметрах праграмы)" }, "autoFillOnPageLoadUseDefault": { "message": "Выкарыстоўваць прадвызначаныя налады" @@ -987,10 +987,10 @@ "message": "Адкрыць сховішча ў бакавой панэлі" }, "commandAutofillDesc": { - "message": "Аўтазапаўненне апошніх скарыстаных уліковых даных для бягучага вэб-сайта." + "message": "Аўтазапаўненне апошняга скарыстанага лагіна для бягучага вэб-сайта" }, "commandGeneratePasswordDesc": { - "message": "Стварыць і капіяваць новы выпадковы парольу буфер абмену." + "message": "Генерыраваць і скапіяваць новы выпадковы пароль у буфер абмену" }, "commandLockVaultDesc": { "message": "Заблакіраваць сховішча" @@ -1002,7 +1002,7 @@ "message": "Карыстальніцкія палі" }, "copyValue": { - "message": "Капіяваць значэнне" + "message": "Скапіяваць значэнне" }, "value": { "message": "Значэнне" @@ -1020,7 +1020,7 @@ "message": "Схавана" }, "cfTypeBoolean": { - "message": "Лагічнае" + "message": "Булева" }, "cfTypeLinked": { "message": "Звязана", @@ -1031,7 +1031,7 @@ "description": "This describes a value that is 'linked' (tied) to another value." }, "popup2faCloseMessage": { - "message": "Націск за межамі гэтага акна для прагляду кода праверкі з электроннай пошты прывядзе да яго закрыцця. Адкрыць bitwarden у новым акне?" + "message": "Націск за межамі ўсплывальнага акна для прагляду праверачнага кода прывядзе да яго закрыцця. Адкрыць гэта ўсплывальнае акно ў новым акне, якое не закрыецца?" }, "popupU2fCloseMessage": { "message": "Гэты браўзар не можа апрацоўваць запыты U2F у гэтым усплывальным акне. Вы хочаце адкрыць гэта ўсплывальнае акно ў новым акне, каб мець магчымасць увайсці ў сістэму, выкарыстоўваючы U2F?" @@ -1058,10 +1058,10 @@ "message": "Тып карткі" }, "expirationMonth": { - "message": "Месяц заканчэння" + "message": "Месяц завяршэння" }, "expirationYear": { - "message": "Год заканчэння" + "message": "Год завяршэння" }, "expiration": { "message": "Тэрмін дзеяння" @@ -1079,7 +1079,7 @@ "message": "Красавік" }, "may": { - "message": "Май" + "message": "Травень" }, "june": { "message": "Чэрвень" @@ -1121,7 +1121,7 @@ "message": "Пані" }, "dr": { - "message": "Док." + "message": "Доктар" }, "firstName": { "message": "Імя" @@ -1136,7 +1136,7 @@ "message": "Поўнае імя" }, "identityName": { - "message": "Імя" + "message": "Імя пасведчання" }, "company": { "message": "Кампанія" @@ -1160,13 +1160,13 @@ "message": "Адрас" }, "address1": { - "message": "Радок адрасу 1" + "message": "Адрас 1" }, "address2": { - "message": "Радок адрасу 2" + "message": "Адрас 2" }, "address3": { - "message": "Радок адрасу 3" + "message": "Адрас 3" }, "cityTown": { "message": "Горад / Пасёлак" @@ -1184,7 +1184,7 @@ "message": "Тып" }, "typeLogin": { - "message": "Уліковыя даныя" + "message": "Лагін" }, "typeLogins": { "message": "Уліковыя даныя" @@ -1223,7 +1223,7 @@ "message": "Пасведчанні" }, "logins": { - "message": "Уліковыя даныя" + "message": "Лагіны" }, "secureNotes": { "message": "Бяспечныя нататкі" @@ -1256,7 +1256,7 @@ "description": "Domain name. Ex. website.com" }, "host": { - "message": "Хост", + "message": "Вузел", "description": "A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'." }, "exact": { @@ -1274,7 +1274,7 @@ "description": "URI match detection for auto-fill." }, "defaultMatchDetection": { - "message": "Метад выяўлення па змаўчанні", + "message": "Прадвызначаны метад выяўлення", "description": "Default URI match detection for auto-fill." }, "toggleOptions": { @@ -1285,7 +1285,7 @@ "description": "Toggle the display of the URIs of the currently open tabs in the browser." }, "currentUri": { - "message": "Бягучы URI укладкі", + "message": "Бягучы URI", "description": "The URI of one of the current open tabs in the browser." }, "organization": { @@ -1305,7 +1305,7 @@ "message": "Выдаліць" }, "default": { - "message": "Па змаўчанні" + "message": "Прадвызначана" }, "dateUpdated": { "message": "Абноўлена", @@ -1316,7 +1316,7 @@ "description": "ex. Date this password was updated" }, "neverLockWarning": { - "message": "Вы ўпэўнены, што хочаце адключыць блакіроўку сховішча? У гэтым выпадку ключ шыфравання вашага сховішча будзе захаваны на вашай прыладзе. Адключаючы блакіроўку, вы павінны пераканацца, што ваша прылада надзейна абаронена." + "message": "Вы ўпэўнены, што хочаце адключыць блакіроўку сховішча? Прызначыўшы параметр блакіравання \"Ніколі\", ключ шыфравання будзе захоўвацца на вашай прыладзе. Калі вы выкарыстоўваеце гэты параметр, вы павінны быць упэўнены ў тым, што ваша прылада надзейна абаронена." }, "noOrganizationsList": { "message": "Вы не з'яўляецеся членам якой-небудзь арганізацыі. Арганізацыі дазваляюць бяспечна абменьвацца элементамі з іншымі карыстальнікамі." @@ -1331,7 +1331,7 @@ "message": "Каму належыць гэты элемент?" }, "strong": { - "message": "Моцны", + "message": "Надзейны", "description": "ex. A strong password. Scale: Weak -> Good -> Strong" }, "good": { @@ -1339,14 +1339,14 @@ "description": "ex. A good password. Scale: Weak -> Good -> Strong" }, "weak": { - "message": "Слабы", + "message": "Ненадзейны", "description": "ex. A weak password. Scale: Weak -> Good -> Strong" }, "weakMasterPassword": { "message": "Слабы асноўны пароль" }, "weakMasterPasswordDesc": { - "message": "Асноўны пароль, выбраны вамі, з'яўляецца слабым. Для належнай абароны ўліковага запісу Bitwarden, вы павінны выкарыстоўваць моцны асноўны пароль (або парольную фразу). Вы ўпэўнены, што хочаце выкарыстоўваць гэты асноўны пароль?" + "message": "Асноўны пароль, які вы выбралі з'яўляецца ненадзейным. Для належнай абароны ўліковага запісу Bitwarden, вы павінны выкарыстоўваць надзейны асноўны пароль (або парольную фразу). Вы ўпэўнены, што хочаце выкарыстоўваць гэты асноўны пароль?" }, "pin": { "message": "PIN-код", @@ -1374,7 +1374,7 @@ "message": "Для ўключэння біяметрыі ў браўзеры, пацвердзіце гэта ў праграме Bitwarden на сваім камп'ютары." }, "lockWithMasterPassOnRestart": { - "message": "Блакіраваць асноўным паролем пры перазапуску браўзера" + "message": "Заблакіраваць асноўным паролем пры перазапуску браўзера" }, "selectOneCollection": { "message": "Вы павінны выбраць прынамсі адну калекцыю." @@ -1386,7 +1386,7 @@ "message": "Кланіраваць" }, "passwordGeneratorPolicyInEffect": { - "message": "На налады генератара ўплываюць адна або некалькі палітык арганізацый." + "message": "Адна або больш палітык арганізацыі ўплывае на налады генератара." }, "vaultTimeoutAction": { "message": "Дзеянне пры тайм-аўце" @@ -1415,19 +1415,19 @@ "message": "Аднавіць элемент" }, "restoreItemConfirmation": { - "message": "Вы сапраўды жадаеце аднавіць гэты элемент?" + "message": "Вы ўпэўнены, што хочаце аднавіць гэты элемент?" }, "restoredItem": { "message": "Элемент адноўлены" }, "vaultTimeoutLogOutConfirmation": { - "message": "Выхад з сістэмы выдаліць доступ да сховішча і спатрабуе праверку сапраўднасці анлайн па заканчэнні перыяду чакання. Вы сапраўды жадаеце ўключыць гэтую наладу?" + "message": "Выхад з сістэмы скасуе ўсе магчымасці доступу да сховішча і запатрабуе аўтэнтыфікацыю праз інтэрнэт пасля завяршэння часу чакання. Вы ўпэўнены, што хочаце выкарыстоўваць гэты параметр?" }, "vaultTimeoutLogOutConfirmationTitle": { "message": "Пацвярджэнне дзеяння для тайм-аута" }, "autoFillAndSave": { - "message": "Запоўніць і захаваць" + "message": "Аўтазапоўніць і захаваць" }, "autoFillSuccessAndSavedUri": { "message": "Аўтазапоўнены элемент і захаваны URI" @@ -1439,7 +1439,7 @@ "message": "Задаць асноўны пароль" }, "masterPasswordPolicyInEffect": { - "message": "Згодна з адной або некалькімі палітыкамі арганізацыі неабходна, каб ваш асноўны пароль адказваў наступным патрабаванням:" + "message": "Адна або больш палітык арганізацыі патрабуе, каб ваш асноўны пароль адпавядаў наступным патрабаванням:" }, "policyInEffectMinComplexity": { "message": "Мінімальны ўзровень складанасці $SCORE$", @@ -1460,13 +1460,13 @@ } }, "policyInEffectUppercase": { - "message": "Уключыць адну ці больш прапісных літар" + "message": "Уключыць адну або некалькі вялікіх літар" }, "policyInEffectLowercase": { - "message": "Уключыць адну ці больш малых літар" + "message": "Уключыць адну або некалькі малых літар" }, "policyInEffectNumbers": { - "message": "Уключыць адну ці больш лічбаў" + "message": "Уключыць адну або некалькі лічбаў" }, "policyInEffectSpecial": { "message": "Уключаць хаця б адзін з наступных спецыяльных сімвалаў $CHARS$", @@ -1481,7 +1481,7 @@ "message": "Ваш новы асноўны пароль не адпавядае патрабаванням палітыкі арганізацыі." }, "acceptPolicies": { - "message": "Ставіўшы гэты сцяжок вы пагаджаецеся з наступным:" + "message": "Ставячы гэты сцяжок, вы пагаджаецеся з наступным:" }, "acceptPoliciesRequired": { "message": "Умовы выкарыстання і Палітыка прыватнасці не былі пацверджаны." @@ -1496,7 +1496,7 @@ "message": "Падказка для пароля не можа супадаць з паролем." }, "ok": { - "message": "ОК" + "message": "Добра" }, "desktopSyncVerificationTitle": { "message": "Праверка сінхранізацыі на камп'ютары" @@ -1538,7 +1538,7 @@ "message": "Біяметрыя не ўключана" }, "biometricsNotEnabledDesc": { - "message": "Для актывацыі біяметрыі ў браўзеры спачатку неабходна ўключыць біяметрыю ў праграме на камп'ютары." + "message": "Для актывацыі біяметрыі ў браўзеры неабходна спачатку ўключыць яе ў наладах праграмы для камп'ютара." }, "biometricsNotSupportedTitle": { "message": "Біяметрыя не падтрымліваецца" @@ -1559,7 +1559,7 @@ "message": "Гэта дзеянне немагчыма выканаць у бакавой панэлі. Паспрабуйце паўтарыць гэта дзеянне ва ўсплывальным або асобным акне." }, "personalOwnershipSubmitError": { - "message": "У адпаведнасці з карпаратыўнай палітыкай вам забаронена захоўваць элементы ў асабістым сховішчы. Змяніце параметры ўласнасці на арганізацыю і выберыце з даступных калекцый." + "message": "У адпаведнасці з палітыкай прадпрыемства вам забаронена захоўваць элементы ў асабістым сховішчы. Змяніце параметры ўласнасці на арганізацыю і выберыце з даступных калекцый." }, "personalOwnershipPolicyInEffect": { "message": "Палітыка арганізацыі ўплывае на вашы параметры ўласнасці." @@ -1615,7 +1615,7 @@ "message": "Абаронена паролем" }, "copySendLink": { - "message": "Капіяваць спасылку Send", + "message": "Скапіяваць спасылку на Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "removePassword": { @@ -1628,7 +1628,7 @@ "message": "Пароль выдалены" }, "deletedSend": { - "message": "Выдалены Send", + "message": "Send выдалены", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLink": { @@ -1682,7 +1682,7 @@ "message": "1 дзень" }, "days": { - "message": "$DAYS$ дзён", + "message": "Дзён: $DAYS$", "placeholders": { "days": { "content": "$1", @@ -1697,7 +1697,7 @@ "message": "Максімальная колькасць доступаў" }, "maximumAccessCountDesc": { - "message": "Калі зададзена, то карыстальнікі больш не змогуць атрымаць доступ да гэтага Send пасля таго, як будзе дасягнута максімальная колькасць зваротаў.", + "message": "Калі прызначана, то карыстальнікі больш не змогуць атрымаць доступ да гэтага Send пасля таго, як будзе дасягнута максімальная колькасць зваротаў.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendPasswordDesc": { @@ -1738,7 +1738,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDisabledWarning": { - "message": "У адпаведнасці з карпаратыўнай палітыкай, вы можаце выдаліць толькі бягучы Send.", + "message": "У адпаведнасці з палітыкай прадпрыемства, вы можаце выдаліць толькі бягучы Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSend": { @@ -1746,7 +1746,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { - "message": "Адрэдагаваны Send", + "message": "Send адрэдагаваны", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { @@ -1756,7 +1756,7 @@ "message": "Для выбару файла з выкарыстаннем Firefox неабходна адкрыць пашырэнне на бакавой панэлі або перайсці ў новае акно, націснуўшы на гэты банэр." }, "sendSafariFileWarning": { - "message": "Для выбару файла з выкарыстаннем Safari неабходна перайсці ў асобнае акно, націснуўшы на гэты банэр." + "message": "Для выбару файла з выкарыстаннем Safari неабходна перайсці ў новае акно, націснуўшы на гэты банэр." }, "sendFileCalloutHeader": { "message": "Перад тым, як пачаць" @@ -1795,7 +1795,7 @@ "message": "Адна або больш палітык арганізацыі ўплываюць на параметры Send." }, "passwordPrompt": { - "message": "Паўторны запыт галоўнага пароля" + "message": "Паўторны запыт асноўнага пароля" }, "passwordConfirmation": { "message": "Пацвярджэнне асноўнага пароля" @@ -1828,7 +1828,7 @@ "message": "Выбраць папку..." }, "ssoCompleteRegistration": { - "message": "Для завяршэння ўваходу праз SSO, задайце асноўны пароль для доступу і абароны вашаго сховішча." + "message": "Для завяршэння працэсу ўваходу з дапамогай SSO, прызначце асноўны пароль для доступу да вашага сховішча і яго абароны." }, "hours": { "message": "Гадзіны" @@ -1859,13 +1859,13 @@ "message": "Адна або больш палітык арганізацыі не дазваляюць вам экспартаваць асабістае сховішча." }, "copyCustomFieldNameInvalidElement": { - "message": "Не атрымалася ідэнтыфікаваць дзеючы элемент формы. Паспрабуйце замест гэтага праверыць HTML." + "message": "Немагчыма ідэнтыфікаваць дзеючы элемент формы. Паспрабуйце замест гэтага праверыць HTML." }, "copyCustomFieldNameNotUnique": { "message": "Не знойдзены ўнікальны ідэнтыфікатар." }, "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ выкарыстоўвае SSO з уласным серверам ключоў. Для аўтарызацыі ўдзельнікам гэтай арганізацыі больш няма неабходнасці выкарыстоўваць асноўны пароль.", + "message": "$ORGANIZATION$ выкарыстоўвае SSO з уласным серверам ключоў. Асноўны пароль для ўдзельнікаў гэтай арганізацыі больш не патрабуецца.", "placeholders": { "organization": { "content": "$1", @@ -1874,7 +1874,7 @@ } }, "leaveOrganization": { - "message": "Пакінуць арганізацыю" + "message": "Выйсці з арганізацыі" }, "removeMasterPassword": { "message": "Выдаліць асноўны пароль" @@ -1883,7 +1883,7 @@ "message": "Асноўны пароль выдалены." }, "leaveOrganizationConfirmation": { - "message": "Вы ўпэўнены, што хочаце пакінуць гэту арганізацыю?" + "message": "Вы ўпэўнены, што хочаце выйсці з гэтай арганізацыі?" }, "leftOrganization": { "message": "Вы пакінулі арганізацыю." @@ -1898,7 +1898,7 @@ "message": "Экспартаванне асабістага сховішча" }, "exportingPersonalVaultDescription": { - "message": "Толькі элементы асабістага сховішча, якія звязаны з $EMAIL$ будуць экспартаваныя. Элементы сховішча арганізацыі не будуць уключаны.", + "message": "Будуць экспартаваны толькі асабістыя элементы сховішча, якія звязаны з $EMAIL$. Элементы сховішча арганізацыі не будуць уключаны.", "placeholders": { "email": { "content": "$1", @@ -1975,7 +1975,7 @@ "message": "Арганізацыя адключана." }, "disabledOrganizationFilterError": { - "message": "Элементы ў адключаных арганізацыя недаступны. Звяжыцеся з уладальнікам вашай арганізацыі для атрымання дапамогі." + "message": "Доступ да элементаў у адключаных арганізацыях немагчымы. Звяжыце з уладальнікам арганізацыі для атрымання дапамогі." }, "cardBrandMir": { "message": "Mir" @@ -1999,16 +1999,16 @@ "message": "для скіду да прадвызначаных наладаў" }, "serverVersion": { - "message": "Server Version" + "message": "Версія сервера" }, "selfHosted": { - "message": "Self-Hosted" + "message": "Уласнае размяшчэнне" }, "thirdParty": { - "message": "Third-Party" + "message": "Іншы пастаўшчык" }, "thirdPartyServerMessage": { - "message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.", + "message": "Падлучэнне да сервера іншага пастаўшчыка $SERVERNAME$. Калі ласка, праверце памылкі з дапамогай афіцыйнага сервера або паведаміце пра іх пастаўшчыку сервера.", "placeholders": { "servername": { "content": "$1", @@ -2017,7 +2017,7 @@ } }, "lastSeenOn": { - "message": "last seen on $DATE$", + "message": "апошні раз быў(-ла) $DATE$", "placeholders": { "date": { "content": "$1", diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index f4574ce9c6d..fe994510046 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -1999,16 +1999,16 @@ "message": "per restablir els paràmetres preconfigurats" }, "serverVersion": { - "message": "Server Version" + "message": "Versió del servidor" }, "selfHosted": { - "message": "Self-Hosted" + "message": "Autoallotjat" }, "thirdParty": { - "message": "Third-Party" + "message": "Tercers" }, "thirdPartyServerMessage": { - "message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.", + "message": "Connectat a la implementació del servidor de tercers, $SERVERNAME$. Verifiqueu els errors utilitzant el servidor oficial o comuniqueu-los al servidor de tercers.", "placeholders": { "servername": { "content": "$1", @@ -2017,7 +2017,7 @@ } }, "lastSeenOn": { - "message": "last seen on $DATE$", + "message": "vist per última vegada el $DATE$", "placeholders": { "date": { "content": "$1", diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 378c6c28167..6674489c638 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -1999,7 +1999,7 @@ "message": "επαναφορά στις προ-ρυθμισμένες ρυθμίσεις" }, "serverVersion": { - "message": "Server Version" + "message": "Έκδοση διακομιστή" }, "selfHosted": { "message": "Self-Hosted" diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index ea449c6cc97..a85bcbf7e6d 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -430,7 +430,7 @@ "message": "마스터 비밀번호를 재입력해야 합니다." }, "masterPasswordMinlength": { - "message": "Master password must be at least 8 characters long." + "message": "마스터 비밀번호는 최소 8자 이상이어야 합니다." }, "masterPassDoesntMatch": { "message": "마스터 비밀번호 확인과 마스터 비밀번호가 일치하지 않습니다." diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 9297eac5ae3..cc5e8dcff1c 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -406,7 +406,7 @@ "message": "Po zablokowaniu komputera" }, "onRestart": { - "message": "Po uruchomieniu przeglądarki" + "message": "Po restarcie przeglądarki" }, "never": { "message": "Nigdy" @@ -577,16 +577,16 @@ "message": "\"Dodaj powiadomienia logowania\" automatycznie wyświetla monit o zapisanie nowych danych logowania do sejfu przy każdym pierwszym logowaniu." }, "showCardsCurrentTab": { - "message": "Pokaż karty na stronie Karta" + "message": "Pokaż karty na stronie głównej" }, "showCardsCurrentTabDesc": { - "message": "Wyświetlaj elementy karty na stronie Karta, aby ułatwić autouzupełnianie." + "message": "Pokaż elementy karty na stronie głównej, aby ułatwić autouzupełnianie." }, "showIdentitiesCurrentTab": { - "message": "Pokaż tożsamości na stronie Karta" + "message": "Pokaż tożsamości na stronie głównej" }, "showIdentitiesCurrentTabDesc": { - "message": "Wyświetlaj elementy tożsamości na stronie Karta, aby ułatwić autouzupełnianie." + "message": "Pokaż elementy tożsamości na stronie głównej, aby ułatwić autouzupełnianie." }, "clearClipboard": { "message": "Wyczyść schowek", @@ -621,7 +621,7 @@ "message": "Użyj drugiego kliknięcia, aby uzyskać dostęp do generowania haseł i pasujących danych logowania do witryny. " }, "defaultUriMatchDetection": { - "message": "Domyślna metoda dopasowania adresu", + "message": "Domyślne wykrywanie dopasowania", "description": "Default URI match detection for auto-fill." }, "defaultUriMatchDetectionDesc": { diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index a81faf65245..4de3a20245b 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -1969,19 +1969,19 @@ "message": "Erro de Key Connector: certifique-se de que a Key Connector está disponível e funcionando corretamente." }, "premiumSubcriptionRequired": { - "message": "Premium subscription required" + "message": "Assinatura Premium necessária" }, "organizationIsDisabled": { - "message": "Organization is disabled." + "message": "Organização está desabilitada." }, "disabledOrganizationFilterError": { - "message": "Items in disabled Organizations cannot be accessed. Contact your Organization owner for assistance." + "message": "Itens em Organizações Desativadas não podem ser acessados. Entre em contato com o proprietário da sua Organização para obter assistência." }, "cardBrandMir": { "message": "Mir" }, "loggingInTo": { - "message": "Logging in to $DOMAIN$", + "message": "Fazendo login em $DOMAIN$", "placeholders": { "domain": { "content": "$1", @@ -1990,22 +1990,22 @@ } }, "settingsEdited": { - "message": "Settings have been edited" + "message": "As configurações foram editadas" }, "environmentEditedClick": { - "message": "Click here" + "message": "Clique aqui" }, "environmentEditedReset": { - "message": "to reset to pre-configured settings" + "message": "para redefinir para as configurações pré-configuradas" }, "serverVersion": { - "message": "Server Version" + "message": "Versão do servidor" }, "selfHosted": { - "message": "Self-Hosted" + "message": "Auto-hospedado" }, "thirdParty": { - "message": "Third-Party" + "message": "Terceiros" }, "thirdPartyServerMessage": { "message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.", diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index b2b6ca8c7aa..ab2033e54f0 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -1999,13 +1999,13 @@ "message": "за рисетовање на подразумевана подешавања" }, "serverVersion": { - "message": "Server Version" + "message": "Верзија сервера" }, "selfHosted": { - "message": "Self-Hosted" + "message": "Личан хостинг" }, "thirdParty": { - "message": "Third-Party" + "message": "Трећа страна" }, "thirdPartyServerMessage": { "message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.", @@ -2017,7 +2017,7 @@ } }, "lastSeenOn": { - "message": "last seen on $DATE$", + "message": "последње виђено у $DATE$", "placeholders": { "date": { "content": "$1", diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index be7bbefb8dd..3e79a9e586d 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -146,10 +146,10 @@ "message": "Hesap" }, "changeMasterPassword": { - "message": "Ana Parolayı Değiştir" + "message": "Ana parolayı değiştir" }, "fingerprintPhrase": { - "message": "Özgün Cümle", + "message": "Parmak izi ifadesi", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "yourAccountsFingerprint": { @@ -157,10 +157,10 @@ "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "twoStepLogin": { - "message": "İki Aşamalı Giriş" + "message": "İki aşamalı giriş" }, "logOut": { - "message": "Çıkış Yap" + "message": "Çıkış yap" }, "about": { "message": "Hakkında" @@ -193,7 +193,7 @@ "message": "Listelenecek klasör yok." }, "helpFeedback": { - "message": "Yardım & Geribildirim" + "message": "Yardım ve geribildirim" }, "sync": { "message": "Eşitle" @@ -215,10 +215,10 @@ "message": "Hesaplarınız için otomatik olarak güçlü, özgün parolalar oluşturun." }, "bitWebVault": { - "message": "Bitwarden Web Kasası" + "message": "Bitwarden web kasası" }, "importItems": { - "message": "Hesapları İçe Aktar" + "message": "Hesapları içe aktar" }, "select": { "message": "Seç" @@ -367,7 +367,7 @@ "message": "Kasa zaman aşımı" }, "lockNow": { - "message": "Şimdi Kilitle" + "message": "Şimdi kilitle" }, "immediately": { "message": "Hemen" @@ -403,10 +403,10 @@ "message": "4 saat" }, "onLocked": { - "message": "Sistem Kilitliyken" + "message": "Sistem kilitlenince" }, "onRestart": { - "message": "Tarayıcı Yeniden Başlatıldığında" + "message": "Tarayıcı yeniden başlatılınca" }, "never": { "message": "Asla" @@ -574,7 +574,7 @@ "message": "Hesap eklemeyi öner" }, "addLoginNotificationDesc": { - "message": "\"Hesap Ekle Bildirimi\" otomatik olarak, ilk kez oturum açtığınız hesabınızı kasanıza kaydetmeniz için uyarı verir." + "message": "\"Hesap ekle\" bildirimi, ilk kez kullandığınız hesap bilgilerini kasanıza kaydetmek isteyip istemediğinizi otomatik olarak sorar." }, "showCardsCurrentTab": { "message": "Sekme sayfasında kartları göster" @@ -589,7 +589,7 @@ "message": "Kolay otomatik doldurma için sekme sayfasında kimlik öğelerini listele" }, "clearClipboard": { - "message": "Panoyu Temizle", + "message": "Panoyu temizle", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "clearClipboardDesc": { @@ -646,7 +646,7 @@ "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportVault": { - "message": "Kasayı Dışa Aktar" + "message": "Kasayı dışa aktar" }, "fileFormat": { "message": "Dosya biçimi" @@ -750,7 +750,7 @@ "message": "Şifreleme anahtarınızı güncellemeden bu özelliği kullanamazsınız." }, "premiumMembership": { - "message": "Premium Üyelik" + "message": "Premium üyelik" }, "premiumManage": { "message": "Üyeliğimi yönet" @@ -966,7 +966,7 @@ "message": "Hesaplar için varsayılan otomatik doldurma ayarı" }, "defaultAutoFillOnPageLoadDesc": { - "message": "\"Sayfa yüklendiğinde otomatik doldur\"u açtıktan sonra her hesap için bu özelliği ayrı ayrı açıp kapatabilirsiniz. Bu ayar, özellikle ayarlama yapmadığınız hesaplarda kullanılacak varsayılan ayardır." + "message": "\"Sayfa yüklendiğinde otomatik doldur\"u her hesabın \"Düzenle\" görünümünden ayrı ayrı kapatabilirsiniz." }, "itemAutoFillOnPageLoad": { "message": "Sayfa yüklendiğinde otomatik doldur (seçeneklerde etkinleştirilmişse)" @@ -1898,7 +1898,7 @@ "message": "Kişisel Kasayı Dışa Aktar" }, "exportingPersonalVaultDescription": { - "message": "Yalnızca $EMAIL$ ile ilişkili kişisel kasa öğeleri dışa aktarılacaktır. Kuruluş kasası öğeleri dahil edilmeyecektir.", + "message": "Yalnızca $EMAIL$ ile ilişkili kişisel kasadaki kayıtlar dışa aktarılacaktır. Kuruluş kasasındaki kayıtlar dahil edilmeyecektir.", "placeholders": { "email": { "content": "$1", @@ -2008,7 +2008,7 @@ "message": "Üçüncü Taraf" }, "thirdPartyServerMessage": { - "message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.", + "message": "$SERVERNAME$ adresindeki üçüncü taraf sunucuya bağlandınız. Lütfen resmi sunucuyu kullanarak hataları doğrulayın veya üçüncü taraf sunucuya bildirin.", "placeholders": { "servername": { "content": "$1", diff --git a/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts b/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts index c82f2700213..b177d118f82 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts @@ -19,6 +19,7 @@ describe("sessionSync decorator", () => { ctor: ctor, initializer: initializer, }), + testClass.testProperty.complete(), ]); }); }); diff --git a/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts b/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts index f2df60ad7a4..5286cece1bb 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts @@ -5,6 +5,7 @@ import { BrowserApi } from "../../browser/browserApi"; import { StateService } from "../../services/abstractions/state.service"; import { SessionSyncer } from "./session-syncer"; +import { SyncedItemMetadata } from "./sync-item-metadata"; describe("session syncer", () => { const propertyKey = "behaviorSubject"; @@ -140,12 +141,14 @@ describe("session syncer", () => { }); it("should update from message on emit from another instance", async () => { + const builder = jest.fn(); + jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder); stateService.getFromSessionMemory.mockResolvedValue("test"); await sut.updateFromMessage({ command: `${sessionKey}_update`, id: "different_id" }); expect(stateService.getFromSessionMemory).toHaveBeenCalledTimes(1); - expect(stateService.getFromSessionMemory).toHaveBeenCalledWith(sessionKey); + expect(stateService.getFromSessionMemory).toHaveBeenCalledWith(sessionKey, builder); expect(nextSpy).toHaveBeenCalledTimes(1); expect(nextSpy).toHaveBeenCalledWith("test"); diff --git a/apps/browser/src/decorators/session-sync-observable/session-syncer.ts b/apps/browser/src/decorators/session-sync-observable/session-syncer.ts index 0c97b983f75..c757a44c7f2 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-syncer.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-syncer.ts @@ -66,8 +66,8 @@ export class SessionSyncer { if (message.command != this.updateMessageCommand || message.id === this.id) { return; } - const keyValuePair = await this.stateService.getFromSessionMemory(this.metaData.sessionKey); - const value = SyncedItemMetadata.buildFromKeyValuePair(keyValuePair, this.metaData); + const builder = SyncedItemMetadata.builder(this.metaData); + const value = await this.stateService.getFromSessionMemory(this.metaData.sessionKey, builder); this.ignoreNextUpdate = true; this.behaviorSubject.next(value); } diff --git a/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts b/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts index e225db61967..2b3f4715d46 100644 --- a/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts +++ b/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts @@ -5,19 +5,15 @@ export class SyncedItemMetadata { initializer?: (keyValuePair: any) => any; initializeAsArray?: boolean; - static buildFromKeyValuePair(keyValuePair: any, metadata: SyncedItemMetadata): any { - const builder = SyncedItemMetadata.getBuilder(metadata); - + static builder(metadata: SyncedItemMetadata): (o: any) => any { + const itemBuilder = + metadata.initializer != null + ? metadata.initializer + : (o: any) => Object.assign(new metadata.ctor(), o); if (metadata.initializeAsArray) { - return keyValuePair.map((o: any) => builder(o)); + return (keyValuePair: any) => keyValuePair.map((o: any) => itemBuilder(o)); } else { - return builder(keyValuePair); + return (keyValuePair: any) => itemBuilder(keyValuePair); } } - - private static getBuilder(metadata: SyncedItemMetadata): (o: any) => any { - return metadata.initializer != null - ? metadata.initializer - : (o: any) => Object.assign(new metadata.ctor(), o); - } } diff --git a/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts b/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts index da65be04903..5cd869a5b67 100644 --- a/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts +++ b/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts @@ -1,59 +1,39 @@ import { SyncedItemMetadata } from "./sync-item-metadata"; -describe("build from key value pair", () => { +describe("builder", () => { const propertyKey = "propertyKey"; const key = "key"; const initializer = (s: any) => "used initializer"; class TestClass {} const ctor = TestClass; - it("should call initializer if provided", () => { - const actual = SyncedItemMetadata.buildFromKeyValuePair( - {}, - { - propertyKey, - sessionKey: "key", - initializer: initializer, - } - ); - - expect(actual).toEqual("used initializer"); + it("should use initializer if provided", () => { + const metadata = { propertyKey, sessionKey: key, initializer }; + const builder = SyncedItemMetadata.builder(metadata); + expect(builder({})).toBe("used initializer"); }); - it("should call ctor if provided", () => { - const expected = { provided: "value" }; - const actual = SyncedItemMetadata.buildFromKeyValuePair(expected, { - propertyKey, - sessionKey: key, - ctor: ctor, - }); - - expect(actual).toBeInstanceOf(ctor); - expect(actual).toEqual(expect.objectContaining(expected)); + it("should use ctor if initializer is not provided", () => { + const metadata = { propertyKey, sessionKey: key, ctor }; + const builder = SyncedItemMetadata.builder(metadata); + expect(builder({})).toBeInstanceOf(TestClass); }); - it("should prefer using initializer if both are provided", () => { - const actual = SyncedItemMetadata.buildFromKeyValuePair( - {}, - { - propertyKey, - sessionKey: key, - initializer: initializer, - ctor: ctor, - } - ); - - expect(actual).toEqual("used initializer"); + it("should prefer initializer over ctor", () => { + const metadata = { propertyKey, sessionKey: key, ctor, initializer }; + const builder = SyncedItemMetadata.builder(metadata); + expect(builder({})).toBe("used initializer"); }); it("should honor initialize as array", () => { - const actual = SyncedItemMetadata.buildFromKeyValuePair([1, 2], { + const metadata = { propertyKey, sessionKey: key, initializer: initializer, initializeAsArray: true, - }); - - expect(actual).toEqual(["used initializer", "used initializer"]); + }; + const builder = SyncedItemMetadata.builder(metadata); + expect(builder([{}])).toBeInstanceOf(Array); + expect(builder([{}])[0]).toBe("used initializer"); }); }); diff --git a/apps/browser/src/popup/accounts/login.component.html b/apps/browser/src/popup/accounts/login.component.html index 4c18805f452..1b32f63819a 100644 --- a/apps/browser/src/popup/accounts/login.component.html +++ b/apps/browser/src/popup/accounts/login.component.html @@ -1,4 +1,4 @@ -
+
@@ -18,15 +18,7 @@
- +
@@ -34,10 +26,8 @@
diff --git a/apps/browser/src/popup/accounts/login.component.ts b/apps/browser/src/popup/accounts/login.component.ts index 83c654d6737..5028af06bfb 100644 --- a/apps/browser/src/popup/accounts/login.component.ts +++ b/apps/browser/src/popup/accounts/login.component.ts @@ -1,10 +1,12 @@ import { Component, NgZone } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; import { Router } from "@angular/router"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component"; import { AuthService } from "@bitwarden/common/abstractions/auth.service"; import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; +import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; @@ -30,7 +32,9 @@ export class LoginComponent extends BaseLoginComponent { protected cryptoFunctionService: CryptoFunctionService, syncService: SyncService, logService: LogService, - ngZone: NgZone + ngZone: NgZone, + formBuilder: FormBuilder, + formValidationErrorService: FormValidationErrorsService ) { super( authService, @@ -42,7 +46,9 @@ export class LoginComponent extends BaseLoginComponent { passwordGenerationService, cryptoFunctionService, logService, - ngZone + ngZone, + formBuilder, + formValidationErrorService ); super.onSuccessfulLogin = async () => { await syncService.fullSync(true); diff --git a/apps/browser/src/services/abstractions/state.service.ts b/apps/browser/src/services/abstractions/state.service.ts index ca2a5ced7fa..2f0112ff64f 100644 --- a/apps/browser/src/services/abstractions/state.service.ts +++ b/apps/browser/src/services/abstractions/state.service.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.service"; import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions"; @@ -7,7 +9,7 @@ import { BrowserGroupingsComponentState } from "src/models/browserGroupingsCompo import { BrowserSendComponentState } from "src/models/browserSendComponentState"; export abstract class StateService extends BaseStateServiceAbstraction { - abstract getFromSessionMemory(key: string): Promise; + abstract getFromSessionMemory(key: string, deserializer?: (obj: Jsonify) => T): Promise; abstract setInSessionMemory(key: string, value: any): Promise; getBrowserGroupingComponentState: ( options?: StorageOptions diff --git a/apps/browser/src/services/localBackedSessionStorage.service.spec.ts b/apps/browser/src/services/localBackedSessionStorage.service.spec.ts index de88b6d8b2f..f7101ddae2e 100644 --- a/apps/browser/src/services/localBackedSessionStorage.service.spec.ts +++ b/apps/browser/src/services/localBackedSessionStorage.service.spec.ts @@ -96,6 +96,13 @@ describe("Browser Session Storage Service", () => { expect(cache.has("test")).toBe(true); expect(cache.get("test")).toEqual(session.test); }); + + it("should use a deserializer if provided", async () => { + const deserializer = jest.fn().mockReturnValue(testObj); + const result = await sut.get("test", { deserializer: deserializer }); + expect(deserializer).toHaveBeenCalledWith(session.test); + expect(result).toEqual(testObj); + }); }); }); }); diff --git a/apps/browser/src/services/localBackedSessionStorage.service.ts b/apps/browser/src/services/localBackedSessionStorage.service.ts index cdc7d4cf154..dea2e75a5ed 100644 --- a/apps/browser/src/services/localBackedSessionStorage.service.ts +++ b/apps/browser/src/services/localBackedSessionStorage.service.ts @@ -1,6 +1,12 @@ +import { Jsonify } from "type-fest"; + import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service"; -import { AbstractCachedStorageService } from "@bitwarden/common/abstractions/storage.service"; +import { + AbstractCachedStorageService, + MemoryStorageServiceInterface, +} from "@bitwarden/common/abstractions/storage.service"; import { EncString } from "@bitwarden/common/models/domain/encString"; +import { MemoryStorageOptions } from "@bitwarden/common/models/domain/storageOptions"; import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey"; import { devFlag } from "../decorators/dev-flag.decorator"; @@ -15,7 +21,10 @@ const keys = { sessionKey: "session", }; -export class LocalBackedSessionStorageService extends AbstractCachedStorageService { +export class LocalBackedSessionStorageService + extends AbstractCachedStorageService + implements MemoryStorageServiceInterface +{ private cache = new Map(); private localStorage = new BrowserLocalStorageService(); private sessionStorage = new BrowserMemoryStorageService(); @@ -27,21 +36,26 @@ export class LocalBackedSessionStorageService extends AbstractCachedStorageServi super(); } - async get(key: string): Promise { + async get(key: string, options?: MemoryStorageOptions): Promise { if (this.cache.has(key)) { return this.cache.get(key) as T; } - return await this.getBypassCache(key); + return await this.getBypassCache(key, options); } - async getBypassCache(key: string): Promise { + async getBypassCache(key: string, options?: MemoryStorageOptions): Promise { const session = await this.getLocalSession(await this.getSessionEncKey()); if (session == null || !Object.keys(session).includes(key)) { return null; } - this.cache.set(key, session[key]); + let value = session[key]; + if (options?.deserializer != null) { + value = options.deserializer(value as Jsonify); + } + + this.cache.set(key, value); return this.cache.get(key) as T; } diff --git a/apps/browser/src/services/state.service.spec.ts b/apps/browser/src/services/state.service.spec.ts index 60813c22931..f3b6c74a5e3 100644 --- a/apps/browser/src/services/state.service.spec.ts +++ b/apps/browser/src/services/state.service.spec.ts @@ -1,8 +1,8 @@ -import { Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; +import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { - AbstractCachedStorageService, + MemoryStorageServiceInterface, AbstractStorageService, } from "@bitwarden/common/abstractions/storage.service"; import { SendType } from "@bitwarden/common/enums/sendType"; @@ -49,7 +49,7 @@ describe("Browser State Service", () => { }); describe("direct memory storage access", () => { - let memoryStorageService: AbstractCachedStorageService; + let memoryStorageService: LocalBackedSessionStorageService; beforeEach(() => { // We need `AbstractCachedStorageService` in the prototype chain to correctly test cache bypass. @@ -79,12 +79,12 @@ describe("Browser State Service", () => { }); describe("state methods", () => { - let memoryStorageService: SubstituteOf; + let memoryStorageService: SubstituteOf; beforeEach(() => { memoryStorageService = Substitute.for(); const stateGetter = (key: string) => Promise.resolve(JSON.parse(JSON.stringify(state))); - memoryStorageService.get("state").mimicks(stateGetter); + memoryStorageService.get("state", Arg.any()).mimicks(stateGetter); sut = new StateService( diskStorageService, diff --git a/apps/browser/src/services/state.service.ts b/apps/browser/src/services/state.service.ts index 6685f495e06..78bc721031a 100644 --- a/apps/browser/src/services/state.service.ts +++ b/apps/browser/src/services/state.service.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { AbstractCachedStorageService } from "@bitwarden/common/abstractions/storage.service"; import { GlobalState } from "@bitwarden/common/models/domain/globalState"; import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions"; @@ -17,9 +19,9 @@ export class StateService extends BaseStateService implements StateServiceAbstraction { - async getFromSessionMemory(key: string): Promise { + async getFromSessionMemory(key: string, deserializer?: (obj: Jsonify) => T): Promise { return this.memoryStorageService instanceof AbstractCachedStorageService - ? await this.memoryStorageService.getBypassCache(key) + ? await this.memoryStorageService.getBypassCache(key, { deserializer: deserializer }) : await this.memoryStorageService.get(key); } diff --git a/apps/browser/store/locales/be/copy.resx b/apps/browser/store/locales/be/copy.resx index 76c10a400e4..f0f63d5394a 100644 --- a/apps/browser/store/locales/be/copy.resx +++ b/apps/browser/store/locales/be/copy.resx @@ -141,7 +141,7 @@ Bitwarden — гэта праграмнае забеспячэнне з адкр Сінхранізацыя і доступ да сховішча з некалькіх прылад - Кіруйце ўсімі вашымі імёнамі карыстальніка і паролямі з бяспечнага сховішча + Кіруйце ўсімі вашымі лагінамі і паролямі з бяспечнага сховішча Хутка і аўтаматычна запаўняйце свае ўліковыя даныя на любым вэб-сайце @@ -150,7 +150,7 @@ Bitwarden — гэта праграмнае забеспячэнне з адкр У вас ёсць зручны доступ да сховішча з кантэкстнага меню - Аўтаматычна генерыруйце моцныя, выпадковыя і бяспечныя паролі + Аўтаматычна генерыруйце надзейныя, выпадковыя і бяспечныя паролі Вашыя звесткі надзейна захоўваюцца, дзякуючы шыфраванню AES-256 diff --git a/apps/browser/store/locales/ca/copy.resx b/apps/browser/store/locales/ca/copy.resx index aaabe81d590..0bd454addb2 100644 --- a/apps/browser/store/locales/ca/copy.resx +++ b/apps/browser/store/locales/ca/copy.resx @@ -148,7 +148,8 @@ Traduccions globals Les traduccions de Bitwarden existeixen en 40 idiomes i creixen, gràcies a la nostra comunitat global. Aplicacions de plataforma creuada -Assegureu-vos i compartiu dades sensibles a la vostra caixa forta de Bitwarden des de qualsevol navegador, dispositiu mòbil o S.O. d'escriptori, i molt més. +Assegureu-vos i compartiu dades sensibles a la vostra caixa forta de Bitwarden des de qualsevol navegador, dispositiu mòbil o S.O. d'escriptori, i molt més. + Administrador de contrasenyes segur i gratuït per a tots els vostres dispositius diff --git a/apps/browser/store/locales/fi/copy.resx b/apps/browser/store/locales/fi/copy.resx index 5a85be3b48a..7511ed5915e 100644 --- a/apps/browser/store/locales/fi/copy.resx +++ b/apps/browser/store/locales/fi/copy.resx @@ -134,7 +134,7 @@ Luo usein käyttämillesi sivustoille automaattisesti vahvoja, yksilöllisiä ja Bitwarden Send -ominaisuudella lähetät tietoa nopeasti salattuna — tiedostoja ja tekstiä — suoraan kenelle tahansa. -Yrityksille Bitwarden tarjoaa Teams- ja Enterprise-tilaukset, jotka mahdollistavat turvallisen salasanojen jaon kollegoiden kesken. +Yrityksille Bitwarden tarjoaa Teams- ja Enterprise-tilaukset, jotka mahdollistavat turvallisen salasanojen jakamisen kollegoiden kesken. Miksi Bitwarden?: diff --git a/apps/browser/store/locales/tr/copy.resx b/apps/browser/store/locales/tr/copy.resx index 2763c3ec91b..1fc3e2a34b3 100644 --- a/apps/browser/store/locales/tr/copy.resx +++ b/apps/browser/store/locales/tr/copy.resx @@ -139,7 +139,7 @@ Bitwarden, parolaları iş arkadaşlarınızla güvenli bir şekilde paylaşabil Neden Bitwarden? Üst düzey şifreleme -Parolalarınız gelişmiş uçtan uca şifreleme (AES-256 bit, salted hashtag ve PBKDF2 SHA-256) ile korunuyor, böylece verileriniz güvende ve gizli kalıyor. +Parolalarınız gelişmiş uçtan uca şifreleme (AES-256 bit, salted hashing ve PBKDF2 SHA-256) ile korunuyor, böylece verileriniz güvende ve gizli kalıyor. Dahili parola oluşturucu Sık kullandığınız web siteleri için güvenlik gereksinimlerinize uygun, güçlü, benzersiz ve rastgele parolalar oluşturabilirsiniz. @@ -148,7 +148,8 @@ Sık kullandığınız web siteleri için güvenlik gereksinimlerinize uygun, g Bitwarden 40 dilde kullanılabiliyor ve gönüllü topluluğumuz sayesinde çeviri sayısı giderek artıyor. Her platformla uyumlu uygulamalar -Bitwarden kasanızdaki hassas verilere her tarayıcıdan, mobil cihazdan veya masaüstü işletim sisteminden ulaşabilir ve onları paylaşabilirsiniz. +Bitwarden kasanızdaki hassas verilere her tarayıcıdan, mobil cihazdan veya masaüstü işletim sisteminden ulaşabilir ve onları paylaşabilirsiniz. + Tüm cihazarınız için güvenli ve ücretsiz bir parola yöneticisi diff --git a/apps/cli/src/send.program.ts b/apps/cli/src/send.program.ts index 69e5eeaedf3..f92e2a305e1 100644 --- a/apps/cli/src/send.program.ts +++ b/apps/cli/src/send.program.ts @@ -139,7 +139,7 @@ export class SendProgram extends Program { return new program.Command("template") .arguments("") .description("Get json templates for send objects", { - object: "Valid objects are: send, send.text, send.file", + object: "Valid objects are: send.text, send.file", }) .action(async (object) => { const cmd = new GetCommand( @@ -176,12 +176,12 @@ export class SendProgram extends Program { writeLn(""); writeLn(" Examples:"); writeLn(""); - writeLn(" bw get send searchText"); - writeLn(" bw get send id"); - writeLn(" bw get send searchText --text"); - writeLn(" bw get send searchText --file"); - writeLn(" bw get send searchText --file --output ../Photos/photo.jpg"); - writeLn(" bw get send searchText --file --raw"); + writeLn(" bw send get searchText"); + writeLn(" bw send get id"); + writeLn(" bw send get searchText --text"); + writeLn(" bw send get searchText --file"); + writeLn(" bw send get searchText --file --output ../Photos/photo.jpg"); + writeLn(" bw send get searchText --file --raw"); writeLn("", true); }) .action(async (id: string, options: program.OptionValues) => { diff --git a/apps/cli/stores/snap/snapcraft.yaml b/apps/cli/stores/snap/snapcraft.yaml index d76e30ef104..19e6897faa3 100644 --- a/apps/cli/stores/snap/snapcraft.yaml +++ b/apps/cli/stores/snap/snapcraft.yaml @@ -32,7 +32,9 @@ base: core18 apps: bw: command: bw - plugs: [network, home, network-bind] + environment: + XDG_CONFIG_HOME: $SNAP_USER_DATA + plugs: [network, network-bind] parts: bw: plugin: dump diff --git a/apps/desktop/config/development.json b/apps/desktop/config/development.json index 3c93018e65f..b587e9ecfb9 100644 --- a/apps/desktop/config/development.json +++ b/apps/desktop/config/development.json @@ -1,4 +1,6 @@ { "devFlags": {}, - "flags": {} + "flags": { + "showDDGSetting": true + } } diff --git a/apps/desktop/config/production.json b/apps/desktop/config/production.json index b04d1531a2f..56f19341304 100644 --- a/apps/desktop/config/production.json +++ b/apps/desktop/config/production.json @@ -1,3 +1,5 @@ { - "flags": {} + "flags": { + "showDDGSetting": true + } } diff --git a/apps/desktop/native-messaging-test-runner/.eslintrc.json b/apps/desktop/native-messaging-test-runner/.eslintrc.json new file mode 100644 index 00000000000..d5ba8f9d9ca --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-console": "off" + } +} diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json new file mode 100644 index 00000000000..34b1bb54068 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -0,0 +1,681 @@ +{ + "name": "native-messaging-test-runner", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "native-messaging-test-runner", + "version": "1.0.0", + "license": "GPL-3.0", + "dependencies": { + "@bitwarden/common": "file:../../../libs/common", + "@bitwarden/node": "file:../../../libs/node", + "module-alias": "^2.2.2", + "node-ipc": "9.2.1", + "ts-node": "^10.9.1", + "uuid": "^8.3.2", + "yargs": "^17.5.1" + }, + "devDependencies": { + "@tsconfig/node16": "^1.0.3", + "@types/node": "^18.6.5", + "@types/node-ipc": "9.2.0", + "typescript": "^4.7.4" + } + }, + "../../../libs/common": { + "name": "@bitwarden/common", + "version": "0.0.0", + "license": "GPL-3.0" + }, + "../../../libs/node": { + "name": "@bitwarden/node", + "version": "0.0.0", + "license": "GPL-3.0", + "dependencies": { + "@bitwarden/common": "file:../common" + } + }, + "node_modules/@bitwarden/common": { + "resolved": "../../../libs/common", + "link": true + }, + "node_modules/@bitwarden/node": { + "resolved": "../../../libs/node", + "link": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "18.6.5", + "license": "MIT" + }, + "node_modules/@types/node-ipc": { + "version": "9.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/acorn": { + "version": "8.8.0", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/create-require": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/easy-stack": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz", + "integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/event-pubsub": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz", + "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-message": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz", + "integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/js-queue": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz", + "integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==", + "dependencies": { + "easy-stack": "^1.0.1" + }, + "engines": { + "node": ">=1.0.0" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "license": "ISC" + }, + "node_modules/module-alias": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.2.tgz", + "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==" + }, + "node_modules/node-ipc": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.2.1.tgz", + "integrity": "sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ==", + "dependencies": { + "event-pubsub": "4.3.0", + "js-message": "1.0.7", + "js-queue": "2.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "4.7.4", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + } + }, + "dependencies": { + "@bitwarden/common": { + "version": "file:../../../libs/common" + }, + "@bitwarden/node": { + "version": "file:../../../libs/node", + "requires": { + "@bitwarden/common": "file:../common" + } + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@tsconfig/node10": { + "version": "1.0.9" + }, + "@tsconfig/node12": { + "version": "1.0.11" + }, + "@tsconfig/node14": { + "version": "1.0.3" + }, + "@tsconfig/node16": { + "version": "1.0.3" + }, + "@types/node": { + "version": "18.6.5" + }, + "@types/node-ipc": { + "version": "9.2.0", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "acorn": { + "version": "8.8.0" + }, + "acorn-walk": { + "version": "8.2.0" + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "arg": { + "version": "4.1.3" + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "create-require": { + "version": "1.1.1" + }, + "diff": { + "version": "4.0.2" + }, + "easy-stack": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz", + "integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "event-pubsub": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz", + "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "js-message": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz", + "integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==", + "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz", + "integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==" + }, + "js-queue": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz", + "integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==", + "resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz", + "integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==", + "requires": { + "easy-stack": "^1.0.1" + } + }, + "make-error": { + "version": "1.3.6" + }, + "module-alias": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.2.tgz", + "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==" + }, + "node-ipc": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.2.1.tgz", + "integrity": "sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ==", + "requires": { + "event-pubsub": "4.3.0", + "js-message": "1.0.7", + "js-queue": "2.0.2" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "ts-node": { + "version": "10.9.1", + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, + "typescript": { + "version": "4.7.4" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "v8-compile-cache-lib": { + "version": "3.0.1" + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + }, + "yn": { + "version": "3.1.1" + } + } +} diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json new file mode 100644 index 00000000000..85d4a1ab6a6 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -0,0 +1,35 @@ +{ + "name": "native-messaging-test-runner", + "version": "1.0.0", + "description": "Test runner for Desktop native messaging", + "main": "dist/bw-handshake.ts", + "scripts": { + "handshake": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-handshake.js", + "status": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-status.js", + "retrieve": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-retrieval.js", + "create": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-create.js", + "update": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-update.js", + "generate": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-generate-password.js" + }, + "author": "Bitwarden Inc. (https://bitwarden.com)", + "license": "GPL-3.0", + "dependencies": { + "@bitwarden/common": "file:../../../libs/common", + "@bitwarden/node": "file:../../../libs/node", + "module-alias": "^2.2.2", + "node-ipc": "9.2.1", + "ts-node": "^10.9.1", + "uuid": "^8.3.2", + "yargs": "^17.5.1" + }, + "devDependencies": { + "@tsconfig/node16": "^1.0.3", + "@types/node": "^18.6.5", + "@types/node-ipc": "9.2.0", + "typescript": "^4.7.4" + }, + "_moduleAliases": { + "@bitwarden/common": "dist/libs/common/src", + "@bitwarden/node/services/nodeCryptoFunction.service": "dist/libs/node/src/services/nodeCryptoFunction.service" + } +} diff --git a/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-create.ts b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-create.ts new file mode 100644 index 00000000000..a017b4c72cb --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-create.ts @@ -0,0 +1,66 @@ +import "module-alias/register"; + +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; + +import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion"; + +import { CredentialCreatePayload } from "../../../src/models/nativeMessaging/encryptedMessagePayloads/credentialCreatePayload"; +import { LogUtils } from "../logUtils"; +import NativeMessageService from "../nativeMessageService"; +import * as config from "../variables"; + +const argv: any = yargs(hideBin(process.argv)).option("name", { + alias: "n", + demand: true, + describe: "Name that the created login will be given", + type: "string", +}).argv; + +const { name } = argv; + +(async () => { + const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One); + // Handshake + LogUtils.logInfo("Sending Handshake"); + const handshakeResponse = await nativeMessageService.sendHandshake( + config.testRsaPublicKey, + config.applicationName + ); + + if (!handshakeResponse.status) { + LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error); + nativeMessageService.disconnect(); + return; + } + + // Get active account userId + const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey); + + const activeUser = status.payload.filter((a) => a.active === true && a.status === "unlocked")[0]; + if (activeUser === undefined) { + LogUtils.logError("No active or unlocked user"); + } + LogUtils.logInfo("Active userId: " + activeUser.id); + + LogUtils.logSuccess("Handshake success response"); + const response = await nativeMessageService.credentialCreation(handshakeResponse.sharedKey, { + name: name, + userName: "SuperAwesomeUser", + password: "dolhpin", + uri: "google.com", + userId: activeUser.id, + } as CredentialCreatePayload); + + if (response.payload.status === "failure") { + LogUtils.logError("Failure response returned "); + } else if (response.payload.status === "success") { + LogUtils.logSuccess("Success response returned "); + } else if (response.payload.error === "locked") { + LogUtils.logError("Error: vault is locked"); + } else { + LogUtils.logWarning("Other response: ", response); + } + + nativeMessageService.disconnect(); +})(); diff --git a/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-retrieval.ts b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-retrieval.ts new file mode 100644 index 00000000000..17244623b55 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-retrieval.ts @@ -0,0 +1,46 @@ +import "module-alias/register"; + +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; + +import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion"; + +import { LogUtils } from "../logUtils"; +import NativeMessageService from "../nativeMessageService"; +import * as config from "../variables"; + +const argv: any = yargs(hideBin(process.argv)).option("uri", { + alias: "u", + demand: true, + describe: "The uri to retrieve logins for", + type: "string", +}).argv; + +const { uri } = argv; + +(async () => { + const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One); + // Handshake + LogUtils.logInfo("Sending Handshake"); + const handshakeResponse = await nativeMessageService.sendHandshake( + config.testRsaPublicKey, + config.applicationName + ); + + if (!handshakeResponse.status) { + LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error); + nativeMessageService.disconnect(); + return; + } + + LogUtils.logSuccess("Handshake success response"); + const response = await nativeMessageService.credentialRetrieval(handshakeResponse.sharedKey, uri); + + if (response.payload.error != null) { + LogUtils.logError("Error response returned: ", response.payload.error); + } else { + LogUtils.logSuccess("Credentials returned ", response); + } + + nativeMessageService.disconnect(); +})(); diff --git a/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-update.ts b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-update.ts new file mode 100644 index 00000000000..ecfbd3f5bb8 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-update.ts @@ -0,0 +1,89 @@ +import "module-alias/register"; + +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; + +import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion"; + +import { CredentialUpdatePayload } from "../../../src/models/nativeMessaging/encryptedMessagePayloads/credentialUpdatePayload"; +import { LogUtils } from "../logUtils"; +import NativeMessageService from "../nativeMessageService"; +import * as config from "../variables"; + +// Command line arguments +const argv: any = yargs(hideBin(process.argv)) + .option("name", { + alias: "n", + demand: true, + describe: "Name that the updated login will be given", + type: "string", + }) + .option("username", { + alias: "u", + demand: true, + describe: "Username that the login will be given", + type: "string", + }) + .option("password", { + alias: "p", + demand: true, + describe: "Password that the login will be given", + type: "string", + }) + .option("uri", { + demand: true, + describe: "Uri that the login will be given", + type: "string", + }) + .option("credentialId", { + demand: true, + describe: "GUID of the credential to update", + type: "string", + }).argv; + +const { name, username, password, uri, credentialId } = argv; + +(async () => { + const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One); + // Handshake + LogUtils.logInfo("Sending Handshake"); + const handshakeResponse = await nativeMessageService.sendHandshake( + config.testRsaPublicKey, + config.applicationName + ); + + if (!handshakeResponse.status) { + LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error); + nativeMessageService.disconnect(); + return; + } + LogUtils.logSuccess("Handshake success response"); + + // Get active account userId + const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey); + + const activeUser = status.payload.filter((a) => a.active === true && a.status === "unlocked")[0]; + if (activeUser === undefined) { + LogUtils.logError("No active or unlocked user"); + } + LogUtils.logInfo("Active userId: " + activeUser.id); + + const response = await nativeMessageService.credentialUpdate(handshakeResponse.sharedKey, { + name: name, + password: password, + userName: username, + uri: uri, + userId: activeUser.id, + credentialId: credentialId, + } as CredentialUpdatePayload); + + if (response.payload.status === "failure") { + LogUtils.logError("Failure response returned "); + } else if (response.payload.status === "success") { + LogUtils.logSuccess("Success response returned "); + } else { + LogUtils.logWarning("Other response: ", response); + } + + nativeMessageService.disconnect(); +})(); diff --git a/apps/desktop/native-messaging-test-runner/src/commands/bw-generate-password.ts b/apps/desktop/native-messaging-test-runner/src/commands/bw-generate-password.ts new file mode 100644 index 00000000000..34bb41abb95 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/commands/bw-generate-password.ts @@ -0,0 +1,46 @@ +import "module-alias/register"; + +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; + +import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion"; + +import { LogUtils } from "../logUtils"; +import NativeMessageService from "../nativeMessageService"; +import * as config from "../variables"; + +const argv: any = yargs(hideBin(process.argv)).option("userId", { + alias: "u", + demand: true, + describe: "UserId to generate password for", + type: "string", +}).argv; + +const { userId } = argv; + +(async () => { + const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One); + // Handshake + LogUtils.logInfo("Sending Handshake"); + const handshakeResponse = await nativeMessageService.sendHandshake( + config.testRsaPublicKey, + config.applicationName + ); + + if (!handshakeResponse.status) { + LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error); + nativeMessageService.disconnect(); + return; + } + + LogUtils.logSuccess("Handshake success response"); + const response = await nativeMessageService.generatePassword(handshakeResponse.sharedKey, userId); + + if (response.payload.error != null) { + LogUtils.logError("Error response returned: ", response.payload.error); + } else { + LogUtils.logSuccess("Response: ", response); + } + + nativeMessageService.disconnect(); +})(); diff --git a/apps/desktop/native-messaging-test-runner/src/commands/bw-handshake.ts b/apps/desktop/native-messaging-test-runner/src/commands/bw-handshake.ts new file mode 100644 index 00000000000..f3098062c46 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/commands/bw-handshake.ts @@ -0,0 +1,25 @@ +import "module-alias/register"; + +import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion"; + +import { LogUtils } from "../logUtils"; +import NativeMessageService from "../nativeMessageService"; +import * as config from "../variables"; + +(async () => { + const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One); + + const response = await nativeMessageService.sendHandshake( + config.testRsaPublicKey, + config.applicationName + ); + LogUtils.logSuccess("Received response to handshake request"); + if (response.status) { + LogUtils.logSuccess("Handshake success response"); + } else if (response.error === "canceled") { + LogUtils.logWarning("Handshake canceled by user"); + } else { + LogUtils.logError("Handshake failure response"); + } + nativeMessageService.disconnect(); +})(); diff --git a/apps/desktop/native-messaging-test-runner/src/commands/bw-status.ts b/apps/desktop/native-messaging-test-runner/src/commands/bw-status.ts new file mode 100644 index 00000000000..7782e203cb8 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/commands/bw-status.ts @@ -0,0 +1,29 @@ +import "module-alias/register"; + +import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion"; + +import { LogUtils } from "../logUtils"; +import NativeMessageService from "../nativeMessageService"; +import * as config from "../variables"; + +(async () => { + const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One); + + LogUtils.logInfo("Sending Handshake"); + const handshakeResponse = await nativeMessageService.sendHandshake( + config.testRsaPublicKey, + config.applicationName + ); + LogUtils.logSuccess("Received response to handshake request"); + + if (!handshakeResponse.status) { + LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error); + nativeMessageService.disconnect(); + return; + } + LogUtils.logSuccess("Handshake success response"); + const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey); + + LogUtils.logSuccess("Status output is: ", status); + nativeMessageService.disconnect(); +})(); diff --git a/apps/desktop/native-messaging-test-runner/src/deferred.ts b/apps/desktop/native-messaging-test-runner/src/deferred.ts new file mode 100644 index 00000000000..b6478bcf268 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/deferred.ts @@ -0,0 +1,26 @@ +// Wrapper for a promise that we can await the promise in one case +// while allowing an unrelated event to fulfill it elsewhere. +export default class Deferred { + private promise: Promise; + private resolver: (T?) => void; + private rejecter: (Error?) => void; + + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolver = resolve; + this.rejecter = reject; + }); + } + + resolve(value?: T) { + this.resolver(value); + } + + reject(error?: Error) { + this.rejecter(error); + } + + getPromise(): Promise { + return this.promise; + } +} diff --git a/apps/desktop/native-messaging-test-runner/src/ipcService.ts b/apps/desktop/native-messaging-test-runner/src/ipcService.ts new file mode 100644 index 00000000000..eadc69a3513 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/ipcService.ts @@ -0,0 +1,166 @@ +import { homedir } from "os"; + +import * as NodeIPC from "node-ipc"; + +import { MessageCommon } from "../../src/models/nativeMessaging/messageCommon"; +import { UnencryptedMessageResponse } from "../../src/models/nativeMessaging/unencryptedMessageResponse"; + +import Deferred from "./deferred"; +import { race } from "./race"; + +NodeIPC.config.id = "native-messaging-test-runner"; +NodeIPC.config.maxRetries = 0; +NodeIPC.config.silent = true; + +const DESKTOP_APP_PATH = `${homedir}/tmp/app.bitwarden`; +const DEFAULT_MESSAGE_TIMEOUT = 10 * 1000; // 10 seconds + +export type MessageHandler = (MessageCommon) => void; + +export enum IPCConnectionState { + Disconnected = "disconnected", + Connecting = "connecting", + Connected = "connected", +} + +export type IPCOptions = { + overrideTimeout?: number; +}; + +export default class IPCService { + // The current connection state of the socket. + private connectionState: IPCConnectionState = IPCConnectionState.Disconnected; + + // Messages that have been sent, but have not yet received responses + private pendingMessages = new Map>(); + + // A set of deferred promises that are awaiting socket connection + private awaitingConnection = new Set>(); + + constructor(private socketName: string, private messageHandler: MessageHandler) {} + + async connect(): Promise { + console.log("[IPCService] connecting..."); + if (this.connectionState === IPCConnectionState.Connected) { + // Socket is already connected. Don't throw, just allow the callsite to proceed + return; + } + + const deferredConnections = new Deferred(); + + this.awaitingConnection.add(deferredConnections); + + // If the current connection state is disconnected, we should start trying to connect. + // The only other possible connection state at this point is "connecting" and if this + // is the case, we just want to add a deferred promise to the awaitingConnection collection + // and not try to initiate the connection again. + if (this.connectionState === IPCConnectionState.Disconnected) { + this._connect(); + } + + return deferredConnections.getPromise(); + } + + private _connect() { + this.connectionState = IPCConnectionState.Connecting; + + NodeIPC.connectTo(this.socketName, DESKTOP_APP_PATH, () => { + // Process incoming message + this.getSocket().on("message", (message: any) => { + this.processMessage(message); + }); + + this.getSocket().on("error", (error: Error) => { + // Only makes sense as long as config.maxRetries stays set to 0. Otherwise this will be + // invoked multiple times each time a connection error happens + console.log("[IPCService] errored"); + console.log( + "\x1b[33m Please make sure the desktop app is running locally and 'Allow DuckDuckGo browser integration' setting is enabled \x1b[0m" + ); + this.awaitingConnection.forEach((deferred) => { + console.log(`rejecting: ${deferred}`); + deferred.reject(error); + }); + this.awaitingConnection.clear(); + }); + + this.getSocket().on("connect", () => { + console.log("[IPCService] connected"); + this.connectionState = IPCConnectionState.Connected; + + this.awaitingConnection.forEach((deferred) => { + deferred.resolve(null); + }); + this.awaitingConnection.clear(); + }); + + this.getSocket().on("disconnect", () => { + console.log("[IPCService] disconnected"); + this.connectionState = IPCConnectionState.Disconnected; + }); + }); + } + + disconnect() { + console.log("[IPCService] disconnecting..."); + if (this.connectionState !== IPCConnectionState.Disconnected) { + NodeIPC.disconnect(this.socketName); + } + } + + async sendMessage( + message: MessageCommon, + options: IPCOptions = {} + ): Promise { + console.log("[IPCService] sendMessage"); + if (this.pendingMessages.has(message.messageId)) { + throw new Error(`A message with the id: ${message.messageId} has already been sent.`); + } + + // Creates a new deferred promise that allows us to convert a message received over the IPC socket + // into a response for a message that we previously sent. This mechanism relies on the fact that we + // create a unique message id and attach it with each message. Response messages are expected to + // include the message id of the message they are responding to. + const deferred = new Deferred(); + + this.pendingMessages.set(message.messageId, deferred); + + this.getSocket().emit("message", message); + + try { + // Since we can not guarentee that a response message will ever be sent, we put a timeout + // on messages + return race({ + promise: deferred.getPromise(), + timeout: options?.overrideTimeout ?? DEFAULT_MESSAGE_TIMEOUT, + error: new Error(`Message: ${message.messageId} timed out`), + }); + } catch (error) { + // If there is a timeout, remove the message from the pending messages set + // before triggering error handling elsewhere. + this.pendingMessages.delete(message.messageId); + throw error; + } + } + + private getSocket() { + return NodeIPC.of[this.socketName]; + } + + private processMessage(message: any) { + // If the message is a response to a previous message, resolve the deferred promise that + // is awaiting that response. Otherwise, assume this was a new message that wasn't sent as a + // response and invoke the message handler. + if (message.messageId && this.pendingMessages.has(message.messageId)) { + const deferred = this.pendingMessages.get(message.messageId); + + // In the future, this could be improved to add ability to reject, but most messages coming in are + // encrypted at this point so we're unable to determine if they contain error info. + deferred.resolve(message); + + this.pendingMessages.delete(message.messageId); + } else { + this.messageHandler(message); + } + } +} diff --git a/apps/desktop/native-messaging-test-runner/src/logUtils.ts b/apps/desktop/native-messaging-test-runner/src/logUtils.ts new file mode 100644 index 00000000000..0e7c39742e3 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/logUtils.ts @@ -0,0 +1,29 @@ +/* eslint-disable no-console */ + +// Class for logging messages with colors for ease of reading important info +// Reference: https://stackoverflow.com/a/41407246 +export class LogUtils { + static logSuccess(message: string, payload?: any): void { + this.logFormat(message, "32", payload); + } + + static logWarning(message: string, payload?: any): void { + this.logFormat(message, "33", payload); + } + + static logError(message: string, payload?: any): void { + this.logFormat(message, "31", payload); + } + + static logInfo(message: string, payload?: any): void { + this.logFormat(message, "36", payload); + } + + private static logFormat(message: string, color: string, payload?: any) { + if (payload) { + console.log(`\x1b[${color}m ${message} \x1b[0m`, payload); + } else { + console.log(`\x1b[${color}m ${message} \x1b[0m`); + } + } +} diff --git a/apps/desktop/native-messaging-test-runner/src/nativeMessageService.ts b/apps/desktop/native-messaging-test-runner/src/nativeMessageService.ts new file mode 100644 index 00000000000..ada09064e3c --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/nativeMessageService.ts @@ -0,0 +1,235 @@ +import "module-alias/register"; + +import { v4 as uuidv4 } from "uuid"; + +import { Utils } from "@bitwarden/common/misc/utils"; +import { EncString } from "@bitwarden/common/models/domain/encString"; +import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey"; +import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service"; +import { EncryptService } from "@bitwarden/common/services/encrypt.service"; +import { NodeCryptoFunctionService } from "@bitwarden/node/services/nodeCryptoFunction.service"; + +import { DecryptedCommandData } from "../../src/models/nativeMessaging/decryptedCommandData"; +import { EncryptedMessage } from "../../src/models/nativeMessaging/encryptedMessage"; +import { CredentialCreatePayload } from "../../src/models/nativeMessaging/encryptedMessagePayloads/credentialCreatePayload"; +import { CredentialUpdatePayload } from "../../src/models/nativeMessaging/encryptedMessagePayloads/credentialUpdatePayload"; +import { EncryptedMessageResponse } from "../../src/models/nativeMessaging/encryptedMessageResponse"; +import { MessageCommon } from "../../src/models/nativeMessaging/messageCommon"; +import { UnencryptedMessage } from "../../src/models/nativeMessaging/unencryptedMessage"; +import { UnencryptedMessageResponse } from "../../src/models/nativeMessaging/unencryptedMessageResponse"; + +import IPCService, { IPCOptions } from "./ipcService"; +import * as config from "./variables"; + +type HandshakeResponse = { + status: boolean; + sharedKey: string; + error?: "canceled" | "cannot-decrypt"; +}; + +const CONFIRMATION_MESSAGE_TIMEOUT = 100 * 1000; // 100 seconds + +export default class NativeMessageService { + private ipcService: IPCService; + private nodeCryptoFunctionService: NodeCryptoFunctionService; + private encryptService: EncryptService; + + constructor(private apiVersion: number) { + console.log("Starting native messaging service"); + this.ipcService = new IPCService(`bitwarden`, (rawMessage) => { + console.log(`Received unexpected: `, rawMessage); + }); + + this.nodeCryptoFunctionService = new NodeCryptoFunctionService(); + this.encryptService = new EncryptService( + this.nodeCryptoFunctionService, + new ConsoleLogService(false), + false + ); + } + + // Commands + + async sendHandshake(publicKey: string, applicationName: string): Promise { + const rawResponse = await this.sendUnencryptedMessage( + { + command: "bw-handshake", + payload: { + publicKey, + applicationName: applicationName, + }, + }, + { + overrideTimeout: CONFIRMATION_MESSAGE_TIMEOUT, + } + ); + return rawResponse.payload as HandshakeResponse; + } + + async checkStatus(key: string): Promise { + const encryptedCommand = await this.encryptCommandData( + { + command: "bw-status", + }, + key + ); + + const response = await this.sendEncryptedMessage({ + encryptedCommand, + }); + + return this.decryptResponsePayload(response.encryptedPayload, key); + } + + async credentialRetrieval(key: string, uri: string): Promise { + const encryptedCommand = await this.encryptCommandData( + { + command: "bw-credential-retrieval", + payload: { + uri: uri, + }, + }, + key + ); + const response = await this.sendEncryptedMessage({ + encryptedCommand, + }); + + return this.decryptResponsePayload(response.encryptedPayload, key); + } + + async credentialCreation( + key: string, + credentialData: CredentialCreatePayload + ): Promise { + const encryptedCommand = await this.encryptCommandData( + { + command: "bw-credential-create", + payload: credentialData, + }, + key + ); + const response = await this.sendEncryptedMessage({ + encryptedCommand, + }); + + return this.decryptResponsePayload(response.encryptedPayload, key); + } + + async credentialUpdate( + key: string, + credentialData: CredentialUpdatePayload + ): Promise { + const encryptedCommand = await this.encryptCommandData( + { + command: "bw-credential-update", + payload: credentialData, + }, + key + ); + const response = await this.sendEncryptedMessage({ + encryptedCommand, + }); + + return this.decryptResponsePayload(response.encryptedPayload, key); + } + + async generatePassword(key: string, userId: string): Promise { + const encryptedCommand = await this.encryptCommandData( + { + command: "bw-generate-password", + payload: { + userId: userId, + }, + }, + key + ); + const response = await this.sendEncryptedMessage({ + encryptedCommand, + }); + + return this.decryptResponsePayload(response.encryptedPayload, key); + } + + // Private message sending + + private async sendEncryptedMessage( + message: Omit, + options: IPCOptions = {} + ): Promise { + const result = await this.sendMessage(message, options); + return result as EncryptedMessageResponse; + } + + private async sendUnencryptedMessage( + message: Omit, + options: IPCOptions = {} + ): Promise { + const result = await this.sendMessage(message, options); + return result as UnencryptedMessageResponse; + } + + private async sendMessage( + message: + | Omit + | Omit, + options: IPCOptions + ): Promise { + // Attempt to connect before sending any messages. If the connection has already + // been made, this is a NOOP within the IPCService. + await this.ipcService.connect(); + + const commonFields: MessageCommon = { + // Create a messageId that can be used as a lookup when we get a response + messageId: uuidv4(), + version: this.apiVersion, + }; + const fullMessage: UnencryptedMessage | EncryptedMessage = { + ...message, + ...commonFields, + }; + + console.log(`[NativeMessageService] sendMessage with id: ${fullMessage.messageId}`); + + const response = await this.ipcService.sendMessage(fullMessage, options); + + console.log(`[NativeMessageService] received response for message: ${fullMessage.messageId}`); + + return response; + } + + disconnect() { + this.ipcService.disconnect(); + } + + // Data Encryption + private async encryptCommandData( + commandData: DecryptedCommandData, + key: string + ): Promise { + const commandDataString = JSON.stringify(commandData); + + const sharedKey = await this.getSharedKeyForKey(key); + + return this.encryptService.encrypt(commandDataString, sharedKey); + } + + private async decryptResponsePayload( + payload: EncString, + key: string + ): Promise { + const sharedKey = await this.getSharedKeyForKey(key); + const decrypted = await this.encryptService.decryptToUtf8(payload, sharedKey); + + return JSON.parse(decrypted); + } + + private async getSharedKeyForKey(key: string): Promise { + const dataBuffer = Utils.fromB64ToArray(key).buffer; + const privKey = Utils.fromB64ToArray(config.testRsaPrivateKey).buffer; + + return new SymmetricCryptoKey( + await this.nodeCryptoFunctionService.rsaDecrypt(dataBuffer, privKey, "sha1") + ); + } +} diff --git a/apps/desktop/native-messaging-test-runner/src/race.ts b/apps/desktop/native-messaging-test-runner/src/race.ts new file mode 100644 index 00000000000..7aba3aa41f9 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/race.ts @@ -0,0 +1,25 @@ +export const race = ({ + promise, + timeout, + error, +}: { + promise: Promise; + timeout: number; + error?: Error; +}) => { + let timer = null; + + // Similar to Promise.all, but instead of waiting for all, it resolves once one promise finishes. + // Using this so we can reject if the timeout threshold is hit + return Promise.race([ + new Promise((_, reject) => { + timer = setTimeout(reject, timeout, error); + return timer; + }), + + promise.then((value) => { + clearTimeout(timer); + return value; + }), + ]); +}; diff --git a/apps/desktop/native-messaging-test-runner/src/variables.ts b/apps/desktop/native-messaging-test-runner/src/variables.ts new file mode 100644 index 00000000000..973da2c224b --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/variables.ts @@ -0,0 +1,27 @@ +export const applicationName = "Native Messaging Test Runner"; +export const encryptionAlogrithm = "sha1"; +export const testRsaPublicKey = + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP" + + "4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP" + + "RQ1H7iNHVZz3K8Db9GCSKPeC8MbW6gVCzb15esCe1gGzg6wkMuWYDFYPoh/oBqcIqrGah7firqB1nDedzEjw32heP2DAffVN" + + "084iTDjiWrJNUxBJ2pDD5Z9dT3MzQ2s09ew1yMWK2z37rT3YerC7OgEDmo3WYo3xL3qYJznu3EO2nmrYjiRa40wKSjxsTlUc" + + "xDF+F0uMW8oR9EMUHgepdepfAtLsSAQIDAQAB"; +export const testRsaPrivateKey = + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS8Hz" + + "YUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86L" + + "nhD56A9FDUfuI0dVnPcrwNv0YJIo94LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfaF4/" + + "YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6AQOajdZijfEvepgnOe7cQ7aeatiOJFrjTApK" + + "PGxOVRzEMX4XS4xbyhH0QxQeB6l16l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq92q" + + "Buwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tPdr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapj" + + "WpxEF+11x7r+wM+0xRZQ8sNFYG46aPfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLXUIh" + + "5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTRbuDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk" + + "1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxucvOU" + + "BeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjAhCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIf" + + "TFKC/hDk6FKZlgwvupWYJyU9RkyfstPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQYUcU" + + "q4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vszv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVv" + + "q1UTXIeQcQnoY5lGHJl3K8mbS3TnXE6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEPjNX" + + "5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBezMRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1" + + "eLLGd7YV0H+J3fgNc7gGWK51hOrF9JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXgAoE" + + "Z18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGpIs3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8" + + "+tPVgppLcG0+tMdLjigFQiDUQk2y3WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEzXKZ" + + "BokBGnjFnTnKcs7nv/O8="; diff --git a/apps/desktop/native-messaging-test-runner/tsconfig.json b/apps/desktop/native-messaging-test-runner/tsconfig.json new file mode 100644 index 00000000000..a34554a264f --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "outDir": "dist", + "target": "es6", + "module": "CommonJS", + "moduleResolution": "node", + "sourceMap": false, + "declaration": false, + "paths": { + "@src/*": ["src/*"], + "@bitwarden/node/*": ["../../../libs/node/src/*"], + "@bitwarden/common/*": ["../../../libs/common/src/*"] + } + }, + "exclude": ["node_modules"] +} diff --git a/apps/desktop/src/app/accounts/login.component.html b/apps/desktop/src/app/accounts/login.component.html index 314b0550621..c11ed881b00 100644 --- a/apps/desktop/src/app/accounts/login.component.html +++ b/apps/desktop/src/app/accounts/login.component.html @@ -16,6 +16,7 @@ #form (ngSubmit)="submit()" [appApiAction]="formPromise" + [formGroup]="formGroup" attr.aria-hidden="{{ showingModal }}" >
@@ -25,14 +26,7 @@
- +
@@ -40,10 +34,8 @@
diff --git a/apps/desktop/src/app/accounts/login.component.ts b/apps/desktop/src/app/accounts/login.component.ts index 959c8a45650..33eefbd57ec 100644 --- a/apps/desktop/src/app/accounts/login.component.ts +++ b/apps/desktop/src/app/accounts/login.component.ts @@ -1,4 +1,5 @@ import { Component, NgZone, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; import { Router } from "@angular/router"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component"; @@ -7,6 +8,7 @@ import { AuthService } from "@bitwarden/common/abstractions/auth.service"; import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service"; import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; +import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; @@ -47,7 +49,9 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { private broadcasterService: BroadcasterService, ngZone: NgZone, private messagingService: MessagingService, - logService: LogService + logService: LogService, + formBuilder: FormBuilder, + formValidationErrorService: FormValidationErrorsService ) { super( authService, @@ -59,7 +63,9 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { passwordGenerationService, cryptoFunctionService, logService, - ngZone + ngZone, + formBuilder, + formValidationErrorService ); super.onSuccessfulLogin = () => { return syncService.fullSync(true); diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index 420ee20a39e..a1ecc0732f9 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -309,6 +309,23 @@
{{ "enableBrowserIntegrationDesc" | i18n }}
+
+
+ +
+ {{ + "enableDuckDuckGoBrowserIntegrationDesc" | i18n + }} +