diff --git a/.eslintrc.json b/.eslintrc.json index 3a1c197326d4..fae64d869e85 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,7 +4,7 @@ "browser": true, "webextensions": true }, - "plugins": ["@typescript-eslint", "rxjs", "rxjs-angular"], + "plugins": ["@typescript-eslint", "rxjs", "rxjs-angular", "import"], "parser": "@typescript-eslint/parser", "parserOptions": { "project": ["./tsconfig.eslint.json"], @@ -18,6 +18,16 @@ "prettier", "plugin:rxjs/recommended" ], + "settings": { + "import/parsers": { + "@typescript-eslint/parser": [".ts"] + }, + "import/resolver": { + "typescript": { + "alwaysTryTypes": true + } + } + }, "rules": { "@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], @@ -65,6 +75,27 @@ "selector": "CallExpression[callee.name='svgIcon']" } ], - "curly": ["error", "all"] + "curly": ["error", "all"], + "import/namespace": ["off"], // This doesn't resolve namespace imports correctly, but TS will throw for this anyway + "import/no-restricted-paths": [ + "error", + { + "zones": [ + // Do not allow angular/node/electron code to be imported into common + { + "target": "./libs/common/**/*", + "from": "./libs/angular/**/*" + }, + { + "target": "./libs/common/**/*", + "from": "./libs/node/**/*" + }, + { + "target": "./libs/common/**/*", + "from": "./libs/electron/**/*" + } + ] + } + ] } } diff --git a/.github/workflows/build-web-ee.yml b/.github/workflows/build-web-ee.yml new file mode 100644 index 000000000000..678ccd83200e --- /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 a5fc5f6185b7..94396b757097 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 80d15ac653de..1e31c95cbebb 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 8790a1e73ff0..b1c9359fa1ee 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 34abd9594f5f..a6fb87d23055 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -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,13 +531,13 @@ } }, "newUri": { - "message": "New URI" + "message": "رابط جديد" }, "addedItem": { - "message": "Added item" + "message": "تمت إضافة العنصر" }, "editedItem": { - "message": "Edited item" + "message": "تم تعديل العنصر" }, "deleteItemConfirmation": { "message": "هل تريد حقاً أن ترسل إلى سلة المهملات؟" diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index b132e0c464a4..e7f0f7a8015d 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 f4574ce9c6d4..fe9945100468 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/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index ea449c6cc97d..a85bcbf7e6d2 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 9297eac5ae31..cc5e8dcff1c8 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/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 38d6a28475a6..3e79a9e586d7 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": "Üçüncü taraf sunucu uygulamasına bağlandı, $SERVERNAME$. Lütfen resmi sunucuyu kullanarak hataları doğrulayın veya üçüncü taraf sunucuya bildirin.", + "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 c82f2700213c..b177d118f824 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 f2df60ad7a4b..5286cece1bb3 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 0c97b983f75b..c757a44c7f2d 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 e225db61967f..2b3f4715d460 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 da65be04903e..5cd869a5b679 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 4c18805f452a..1b32f63819a6 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 83c654d67372..5028af06bfb8 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 ca2a5ced7fa2..2f0112ff64f7 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 de88b6d8b2f0..f7101ddae2ed 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 cdc7d4cf154b..dea2e75a5edf 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 60813c229316..f3b6c74a5e35 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 6685f495e065..78bc721031a0 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 76c10a400e4a..f0f63d5394ae 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 aaabe81d590e..0bd454addb21 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/tr/copy.resx b/apps/browser/store/locales/tr/copy.resx index 2763c3ec91b3..1fc3e2a34b30 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/stores/snap/snapcraft.yaml b/apps/cli/stores/snap/snapcraft.yaml index d76e30ef1043..19e6897faa3f 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 3c93018e65f5..b587e9ecfb92 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 b04d1531a2f4..56f193413049 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 000000000000..d5ba8f9d9ca8 --- /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 000000000000..34b1bb540680 --- /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 000000000000..85d4a1ab6a63 --- /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 000000000000..a017b4c72cbf --- /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 000000000000..17244623b55c --- /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 000000000000..ecfbd3f5bb8c --- /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 000000000000..34bb41abb95e --- /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 000000000000..f3098062c461 --- /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 000000000000..7782e203cb89 --- /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 000000000000..b6478bcf2689 --- /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 000000000000..eadc69a35136 --- /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 000000000000..0e7c39742e30 --- /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 000000000000..ada09064e3c3 --- /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 000000000000..7aba3aa41f99 --- /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 000000000000..973da2c224b9 --- /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 000000000000..a34554a264fc --- /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 314b05506219..c11ed881b000 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 959c8a45650b..33eefbd57ec4 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 420ee20a39e6..a1ecc0732f95 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 + }} +