# 关于

这里是对 Bitwarden 官方 [Bitwarden Contributing Docs](https://contributing.bitwarden.com/) 的中文翻译，并参考了 Github 上的 [bitwarden / contributing-docs](https://github.com/bitwarden/contributing-docs) 仓库。

译者：[@wcjxixi](mailto:wcjxixi@gmail.com)

致谢 [Google Translate](https://translate.google.com/) 以及 [DeepL](https://www.deepl.com/)！

{% hint style="danger" %}
个人能力有限，具体请以官方 [Bitwarden Contributing Docs](https://contributing.bitwarden.com/) 页面为准。使用本内容所产生的一切后果，与 @wcjxixi 无关。Use at your own risk！！！
{% endhint %}

{% hint style="info" %}
**备注：**&#x6807;题前有 \* 的表示官方曾经有但现已移除的页面和/或分类，我将其保留仅作为参考之用。
{% endhint %}


# 概述

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/)
{% endhint %}

欢迎！Bitwarden 贡献文档包含社区开发人员入门所需要的所有信息。

## 入门 <a href="#getting-started" id="getting-started"></a>

您需要做的第一件事就是建立一个本地开发环境；为此，我们建议按以下顺序浏览此文档：

1. 安装所有推荐的用于开发的[工具和库](/getting-started/tools)
2. 遵循[服务器设置指南](/getting-started/server/guide)设置您的本地服务器和相关服务
3. 从源代码构建和运行各个[客户端](/getting-started/clients)并将它们连接到您的本地服务器
4. 阅读我们的[贡献指南](/contributing/contributing)和[代码样式](/contributing/code-style)

## 获取帮助 <a href="#help" id="help"></a>

如果您在遵循这些说明操作时遇到问题，请不要惊慌。

1. [Bitwarden 帮助中心](https://help.ppgg.in/)是了解软件和配置选项的不同功能的绝佳资源
2. 在[社区论坛](https://community.bitwarden.com/)中向社区寻求帮助
3. 在官方 [Gitter 聊天](https://gitter.im/bitwarden/Lobby)中询问开发团队

## 帮助完善本文档 <a href="#help-make-this-documentation-better" id="help-make-this-documentation-better"></a>

我们想要一种分享知识的文化，而文档是实现这一目标的最佳方式之一。

* 如果您在遵循某些说明时遇到问题，请改进它们。
* 如果您添加或更改需要设置开发的某个功能，请更新此文档。
* 如果您正在审查需要更改此文档的 PR，请要求作者将其作为您审查的一部分。

要为此文档做出贡献，请遵循 [Contributing Docs Github 存储库](https://github.com/bitwarden/contributing-docs/)中的说明。


# 工具

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/tools/)
{% endhint %}

## 操作系统 <a href="#operating-system" id="operating-system"></a>

所有 Bitwarden 开发人员都建议配备 Macbook。本文档中的工具建议和说明假设您使用的是 macOS。如果您使用不同的操作系统，这可能需要一些调整。

## 推荐工具 <a href="#recommended-tools" id="recommended-tools"></a>

强烈推荐将以下工具作为「标准」开发人员设置的一部分。我们建议所有新的 Bitwarden 开发人员都安装它们，以作为设置本地开发环境的一部分。

### IDE <a href="#ides" id="ides"></a>

* [Visual Studio Code](https://code.visualstudio.com/) - 用于所有 Typescript 项目，也适用于 C#。一定要安装[扩展](#visual-studio-code-extensions)
* [JetBrains Rider](https://www.jetbrains.com/rider/download/) - 用于 C#、.NET 及更多功能的全功能集成开发环境。Bitwarden 开发人员应联系 IT 部门获取许可证。
* [Xcode](https://developer.apple.com/xcode/) - 用于 iOS 移动端和 Safari 网页扩展开发

### 本地环境 <a href="#local-environment" id="local-environment"></a>

* [Homebrew](https://brew.sh/) - macOS 的包管理器
* [Iterm2](https://iterm2.com/)（可通过 Homebrew 获得）- 更好的终端模拟器
* 各种浏览器 - 值得庆幸有大量浏览器可用于在许多场景中测试扩展。您也可以使用多个浏览器安装不同版本的浏览器扩展来对这些扩展进行比较
* [Docker](https://docs.docker.com/get-docker/) - 仅服务器开发需要
* [.NET SDK](https://dotnet.microsoft.com/download) - 服务器和其他后端开发环境所需
* [PowerShell](https://learn.microsoft.com/zh-cn/powershell/scripting/install/installing-powershell-on-macos)（可通过 Homebrew 获得：`brew install powershell`）
* [NodeJS](https://nodejs.org/) v20（最好使用[节点版本管理器](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)）
* [NPM](https://www.npmjs.com/) v10（包含在 Node 中）
* [Rust](https://www.rust-lang.org/tools/install) - 用于本地桌面组件
* [Git](https://git-scm.com/)
  * 强烈建议[提交签名](/contributing/commit-signing)

### 移动端 <a href="#mobile" id="mobile"></a>

* [Android Studio](https://developer.android.com/studio/) - 非常适合设置和运行 Android 模拟器
* [abd](https://developer.android.com/studio/command-line/adb) - 用于与 Android 模拟人生交互
* [Apple Icons Generator Gist](https://gist.github.com/brutella/0bcd671a9e4f63edc12e) - 用于从图像生成 Apple 图标的脚本

### 数据库 <a href="#databases" id="databases"></a>

* [Azure Data Studio](https://docs.microsoft.com/zh-cn/sql/azure-data-studio/download-azure-data-studio) - 用于与本地 SQL Server 一起使用
* [PgAdmin4](https://www.pgadmin.org/) - 用于 PostgreSQL 数据库的基准测试
* [MySQLWorkbench](https://www.mysql.com/products/workbench/) - 用于 MySQL 数据库的基准测试
* [SQLiteStudio](https://www.sqlitestudio.pl/) - 用于操作 SQLite 数据库

### Visual Studio Code 扩展 <a href="#visual-studio-code-extensions" id="visual-studio-code-extensions"></a>

有一些 VS Code 扩展可以节约我们工作中的时间。强烈推荐下面列表中的这些扩展：

* 通用
  * [Back & Forth](https://marketplace.visualstudio.com/items?itemName=nick-rudenko.back-n-forth) - 在编辑器的右上角添加前进和后退按钮。很简单，但我喜欢它。
  * [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - 可能很烦人，但为我节省了很多 `tmes form writting oragnizations`。
  * [LiveShare](https://marketplace.visualstudio.com/items?itemName=MS-vsliveshare.vsliveshare) - 用于结对编程
* C#
  * [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) - Omnisharp 集成
  * [.NET Core Test Explorer](https://marketplace.visualstudio.com/items?itemName=formulahendry.dotnet-test-explorer) - 用于 .NET 测试的测试资源管理器
  * [.NET Core User Secrets](https://marketplace.visualstudio.com/items?itemName=adrianwilczynski.user-secrets) - 通过右键点击 `.proj` 并选择编辑用户机密来编辑机密文件
* Git
  * [Git Graph](https://marketplace.visualstudio.com/items?itemName=mhutchie.git-graph) - 出色的 git 可视化工具
  * [Git History](https://marketplace.visualstudio.com/items?itemName=donjayamanne.githistory) - 更多的 Git 历史
  * [Git Lens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) - 更多的 Git 选项
* Typescript / Angular
  * [Angular Language Service](https://marketplace.visualstudio.com/items?itemName=Angular.ng-template) - 了解 Angular 模板
  * [Jest](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest) - Jest 测试运行器
  * [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - 与更漂亮的代码格式集成
  * [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - 用于 ESLint 集成
* Rust
  * [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=matklad.rust-analyzer) - 强大的 rust 语言服务器
  * [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml) - 用于处理 TOML（cargo 配置）
  * [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) - 用于 rust 调试
* 数据库
  * [MySQL Syntax](https://marketplace.visualstudio.com/items?itemName=jakebathman.mysql-syntax) - 用于 MySQL 的语法高亮显示
  * [PostgreSQL](https://marketplace.visualstudio.com/items?itemName=ckolkman.vscode-postgres) - 用于 PostgreSQL 的语法高亮显示

## 可选工具 <a href="#optional-tools" id="optional-tools"></a>

根据您的偏好或您正在开发的内容，以下工具可能会很有用：

* [Microsoft Azure Storage Explorer](https://azure.microsoft.com/zh-cn/features/storage-explorer/) - 用于连接本地 Azure 表存储和队列，或与本地 Azure 表存储和队列一起使用
* [Parallels](https://www.parallels.com/) - 用于运行 Windows VM（虚拟机）
* [Sourcetree](https://www.sourcetreeapp.com/) -Git GUI。注意：在 macOS 上使用 nvm 时，要使 git hooks 正常运行，请遵循[这些说明](https://typicode.github.io/husky/#/?id=command-not-found)。


# 服务器


# 设置指南

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/guide)
{% endhint %}

此页面将向您展示如何为开发目的设置本地 Bitwarden 服务器。

Bitwarden 服务器由多个可以独立运行的服务组成。对于基本的开发设置，需要 **Api** 和 **Identity** 服务。

{% hint style="info" %}
开始之前，请确保您已经安装好了推荐的[工具和库](/getting-started/tools)，包括：

* Docker Desktop
* Visual Studio 2022
* Powershell
* [NET 8.0 SDK](https://dotnet.microsoft.com/download)
* Azure Data Studio
  {% endhint %}

## 克隆存储库 <a href="#clone-the-repository" id="clone-the-repository"></a>

克隆 Bitwarden 服务器项目：

```bash
git clone https://github.com/bitwarden/server.git
cd server
```

## 配置 Git <a href="#configure-git" id="configure-git"></a>

1、配置 Git 以忽略 Prettier 修订：

```bash
git config blame.ignoreRevsFile .git-blame-ignore-revs
```

2、*（可选）*&#x8BBE;置预提交 `dotnet format` 钩子：

```bash
git config --local core.hooksPath .git-hooks
```

格式化需要一次完整的构建，而每次提交都这样做可能会太慢。作为替代方案，您可以在方便的时候（例如在请求 PR 审核之前）从命令行运行 `dotnet format`。

## 配置 Docker <a href="#configure-docker" id="configure-docker"></a>

我们提供了一个 Docker Compose 配置，用于在开发过程提供所需的依赖。这被分成多个服务配置文件，以方便自定义。

1、一些 Docker 设置被配置在环境文件 `dev/.env` 中。复制示例环境文件：

```bash
cd dev
cp .env.example .env
```

2、使用您喜欢的编辑器打开 `.env`。

3、设置 `MSSQL_PASSWORD` 变量。这将是用于您的 MSSQL 数据库服务器的密码。

{% hint style="danger" %}
您的 MSSQL 密码必须符合以下[密码复杂性准则](https://learn.microsoft.com/zh-cn/sql/relational-databases/security/password-policy?view=sql-server-ver15#password-complexity)：

* 它必须至少有八个字符。
* 它必须包含来自以下四个类别中的三个类别的字符：
  * 拉丁文大写字母 (A-Z)
  * 拉丁文小写字母 (a-z)
  * 基本的 10 位数数字 (0-9)
  * 非字母数字字符，例如：感叹号 (!)、美元符号 ($)、数字符号 (#) 或百分比 (%)。
    {% endhint %}

4、您可以更改其他变量，也可以使用它们的默认值。保存并退出此文件。

5、启动 Docker 容器。

使用 PowerShell 导航到已克隆的服务器存储库位置，进入 `dev` 文件夹然后运行下面的 docker 命令。

```bash
docker compose --profile mssql --profile mail up -d
```

这将启动 MSSQL 和本地邮件服务器容器，这应该适合大多数社区贡献。

运行 `docker compose` 命令后，您可以使用 [Docker Dashboard](https://docs.docker.com/desktop/dashboard/) 来管理您的容器。您应该能看到您的容器运行在 `bitwardenserver` 组下了。

{% hint style="danger" %}
首次运行 docker compose 后更改 `MSSQL_PASSWORD` 变量将需要重新创建存储卷。

**警告：这将删除您的开发数据库。**

为此，请运行

`docker compose --profile mssql down docker volume rm`

`bitwardenserver_edgesql_dev_data`

之后，重新运行步骤 5 中的 docker compose 命令。
{% endhint %}

### SQL Server <a href="#sql-server" id="sql-server"></a>

为了支持基于 ARM 的开发环境，例如 M1 Mac，我们使用 [Azure SQL Edge](https://hub.docker.com/_/microsoft-azure-sql-edge) docker 容器而不是普通的 [Microsoft SQL Server](https://hub.docker.com/_/microsoft-mssql-server) 容器。它的行为与常规 SQL Server 实例基本相同，并运行在 1433 端口上。

您可以使用 Azure Data Studio 并通过以下凭据连接到它：

* 服务器：localhost
* 端口：1433
* 用户名：sa
* 密码：（您在 `dev/.env` 中设置的密码）

### Mailcatcher <a href="#mailcatcher" id="mailcatcher"></a>

此服务器使用电子邮件进行许多用户交互。我们提供了一个预配置的 [MailCatcher](https://mailcatcher.me/) 实例，它可以捕获任何出站电子邮件并防止将其被发送到真实的电子邮件地址。您可以通过 `http://localhost:1080` 打开其 Web 界面。

### Azurite <a href="#azurite" id="azurite"></a>

{% hint style="info" %}
本部分仅适用于 Bitwarden 开发人员。
{% endhint %}

[Azurite](https://github.com/Azure/Azurite) 是一个 Azure 存储 API 的模拟器，支持 Blob、队列和表存储。我们使用它来最小化在云环境中开发所需的在线依赖。

要引导本地 Azurite 实例：

1、安装 Az 模块。在不提供任何用户反馈的情况下，这可能需要几分钟才能完成（它可能会显示为冻结）。

```bash
pwsh -Command "Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -Force"
```

2、运行安装脚本：

```bash
pwsh setup_azurite.ps1
```

## 配置用户机密 <a href="#configure-user-secrets" id="configure-user-secrets"></a>

[用户机密](https://learn.microsoft.com/zh-cn/aspnet/core/security/app-secrets?view=aspnetcore-6.0)是一种开发人员管理应用程序设置的方法。它们覆盖每一个项目的 `appsettings.json` 中的设置。您的用户机密文件应与您打算覆盖的设置的 `appsettings.json` 文件的结构相匹配。

我们提供了一个帮助脚本，可以简化为服务器存储库中的所有项目设置用户机密的步骤。

1、获取 `secrets.json` 模板。我们需要获取 `secrets.json` 的初始版本，根据您自己的机密值对其进行修改。

导航到服务器仓库中的 `dev` 文件夹，复制 `secrets.json` 示例文件。

```bash
cp secrets.json.example secrets.json
```

* 将用户机密文件从共享的 Development 集合（您的 Bitwarden Vault）复制到 `dev` 文件夹中。
* 如果您无权访问 Development 集合，请联系我们的 IT 经理来安排访问权限。确保您已经使用公司电子邮件地址设置了 Bitwarden 账户。
* 此 `Secrets.json` 被配置为使用 dockerized Azurite 和 MailCatcher 实例，建议在本指南中使用。

2、使用您自己的值更新 `secrets.json`：

* `sqlServer` > `connectionString`：在指示的地方插入您的密码
* `installation` > `id` 和 `key`：[申请一个主机安装 ID 和密钥](https://bitwarden.com/host/)，然后在此处插入
* `licenseDirectory`：将其设为空目录，这是用于存储上传的许可证文件的位置

3、完成 `secrets.json` 后，运行下面的命令将机密添加到每个 Bitwarden 服务器项目中：

```bash
pwsh setup_secrets.ps1
```

此帮助脚本还支持一个可选标志，该标志用于在重新应用它们之前删除所有现有的设置：

```bash
pwsh setup_secrets.ps1 -clear
```

## 创建数据库 <a href="#create-database" id="create-database"></a>

现在 MSSQL 服务器已经运行了。下一步是创建 Bitwarden 服务器所使用的数据库。

我们提供了一个帮助脚本用于创建开发数据库 `vault_dev` 并同时运行所有迁移。

导航至服务器 repo 中的 `dev` 文件夹，然后执行以下步骤：

1、创建数据库并运行所有迁移：

```bash
pwsh migrate.ps1
```

2、您应该会收到各个迁移脚本已成功运行的确认信息：

```
info: Bit.Migrator.DbMigrator[12482444]
      Migrating database.
info: Bit.Migrator.DbMigrator[12482444]
      Migration successful.
```

{% hint style="info" %}
您需要定期重新运行迁移帮助程序脚本，以使您的本地开发数据库保持最新。有关详细信息，请参阅 [MSSQL 数据库](/getting-started/server/database/mssql)。
{% endhint %}

## 构建并运行服务器 <a href="#build-and-run-the-server" id="build-and-run-the-server"></a>

您现在已准备好构建和运行您的开发服务器了。

1、在存储库的根目录中打开一个新的终端窗口。

2、恢复 Identity 服务所需的 nuget 包：

```bash
cd src/Identity
dotnet restore
```

3、启动 Identity 服务：

```bash
dotnet run
```

4、通过导航到 <http://localhost:33656/.well-known/openid-configuration> 来测试 Identity 服务是否处于活动状态。

5、在另一个终端窗口中，恢复 API 服务所需的 nuget 包：

```bash
cd src/Api
dotnet restore
```

6、启动 API 服务：

```bash
dotnet run
```

7、通过导航到 <http://localhost:4000/alive> 来测试 API 服务是否处于活动状态。

8、通过配置客户端的 API 和 Identity 端点将客户端连接到您的本地服务器。请参阅[更改客户端环境](https://help.ppgg.in/on-premises-hosting/connect-clients-to-your-instance)以及贡献文档中各客户端的说明。

{% hint style="warning" %}
如果您无法连接到 API 或 Identity 项目，请检查终端输出以确认它们所运行的端口。
{% endhint %}

{% hint style="info" %}
我们建议之后继续使用 [Web Vault](/getting-started/clients/web-vault)，因为许多管理操作只能在其中执行。
{% endhint %}

## 调试 <a href="#debugging" id="debugging"></a>

{% hint style="info" %}
在 macOS 上，您必须先为每个项目运行 `dotnet restore`，然后才能在调试器中启动它。
{% endhint %}

### Visual Studio <a href="#visual-studio" id="visual-studio"></a>

调试：

* 在 Windows 上，右键点击每个项目 → 点击 **Debug** → 点击 **Start New Instance**
* 在 macOS 上，右键点击每个项目 → 点击 **Start Debugging Project**

### Rider <a href="#rider" id="rider"></a>

通过分别点击每一个项目的「play」按钮来启动 Api 项目和 Identity 项目。


# 高级服务器设置

{% hint style="info" %}
任意对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/advanced-setup)
{% endhint %}

您的服务器启动并运行后，可能还需要一些额外的配置才能激活 Bitwarden 的所有功能。

## 高级功能​ <a href="#premium-features" id="premium-features"></a>

用户机密文件包括一个 Stripe 测试密钥，它允许您「购买」高级功能而无需实际支付费用。

1. 使用 Web 客户端连接到本地服务器
2. 购买订阅或高级功能时，请使用 Stripe 文档中的测试数据所使用的信用卡付款方式：

   | 品牌         | 卡号                    | CVC      | Date    |
   | ---------- | --------------------- | -------- | ------- |
   | Visa       | `4111 1111 1111 1111` | 任意 3 位数字 | 任意将来的日期 |
   | Mastercard | `5555 5555 5555 4444` | 任意 3 位数字 | 任意将来的日期 |
3. 您的信用卡详细信息只需通过前端表单验证（例如，您的信用卡号必须是正确的字符数量）。
4. 购买您想要的任意高级功能

{% hint style="info" %}
Stripe 的[策略](https://support.stripe.com/questions/test-mode-subscription-data-retention)是在 90 天后**自动取消**测试订阅，然后在 30 天后**删除**已取消的测试订阅。这会导致本地服务器上长期存在的高级用户和组织出现异常的计费行为。

要纠正这种情况，您必须将组织/用户重新订阅到高级计划，以创建新的测试订阅。

1. 从 Bitwarden 门户，删除组织/用户网关信息，然后将其计划设置为「免费」
2. 从 Web 客户端为组织/用户添加新的测试支付方式
3. 像之前一样重新购买所需的高级功能
   {% endhint %}

## 电子邮件​ <a href="#emails" id="emails"></a>

Docker compose 将启动一个可以使用的本地 SMTP 服务器，但也可以使用 Mailtrap 或 Amazon 等其他服务来调试 amazon 集成。

* Amazon Simple Email Service - 用户机密密码库项目包含一个名为 `additional-keys-for-cloud-services.json` 的单独附件。将 `amazon` 密钥添加到您的用户机密中以使用 Amazon 的邮件服务。注意：这会将电子邮件发送到真实的电子邮件地址！
* [bytemark/docker-smtp](https://github.com/BytemarkHosting/docker-smtp) - 在 Docker 上运行的本地 SMTP 服务器。

## 文件上传（文件 Send 和附件） <a href="#file-uploads-file-sends-and-attachments" id="file-uploads-file-sends-and-attachments"></a>

使用两种方式之一来存储上传的文件：

* 使用我们的生产环境云实例所使用的 Azure 存储。
  * Docker 将创建一个模拟 Azure 存储 API 的本地 [Azurite](https://github.com/Azure/Azurite) 实例。并用于基础测试。
  * 我们还有一个测试 Azure 存储账户供开发使用。此用户机密附加到“服务器用户机密”共享密码库项目。您需要将 `send` 密钥和 `attachment` 密钥复制到您自己的用户机密中。
* 使用自托管实例所使用的直接上传用功能，将文件直接存储在服务器上。以下设置将允许您上传文件，但不允许下载它们。将以下设置放在 `globalSettings` 下（根据需要更新路径）：

```json
"send": {
    "baseDirectory": "/Users/<your name>/Projects/localStorageDev",
    "baseUrl": "file:///Users/<your name>/Projects/localStorageDev"
},
"attachment": {
    "baseDirectory": "/Users/<your name>/Projects/localStorageDev",
    "baseUrl": "file:///Users/<your name>/Projects/localStorageDev"
}
```

{% hint style="info" %}
要正确测试直接上传所使用的上传和下载文件功能，您需要设置本地文件服务器。如果您这样做，请在此处添加说明:)
{% endhint %}

## PayPal <a href="#paypal" id="paypal"></a>

如果您只需要高级功能，则通过卡支付会更容易（请参阅上面的说明）。但是，您可能需要专门测试 PayPal 集成。

1. 确保您使用的是y已共享的工程集合中的 `secrets.json`。这些机密提供对 PayPal 沙盒账户的访问，在本地运行服务器时将自动使用该账户。
2. 您需要 2 个 PayPal 沙盒账户来进行测试：&#x20;
   * 卖家 - 这将是我们的沙盒账户。登录详细信息可在共已享的工程集合中找到。您可以登录此账户查看 Bitwarden 在您处理的任何交易中收到的（假）资金记录。
   * 买家 - 这将是您的沙盒账户。
3. 使用您的工作电子邮件地址[创建一个新的 PayPal 沙盒账户](https://www.sandbox.paypal.com/)。然后，您可以使用您想要的任何特定个人信息（例如国家/地区、付款方式）[创建虚假买家账户](https://developer.paypal.com/docs/api-basics/sandbox/accounts/)。这在测试销售税时特别有用。如果需要，您可以使用[信用卡生成器](https://developer.paypal.com/developer/creditCardGenerator/)生成「有效」的虚假信用卡详细信息。
4. 登录您的 Web 密码库然后导航至付款页面。所有 PayPal 付款功能都应使用您的沙盒账户或您已创建的虚假买家账户来运行。

注意：如果您正在测试销售税，您首先必须通过「管理员门户」创建销售税率。

## YubiKey 2FA <a href="#yubikey-2fa" id="yubikey-2fa"></a>

要在本地测试 YubiKey 2FA，您必须首先使用 Yubico 的 ClientId 和 Key 配置本地用户机密。这用于验证对 Yubico 的 API 调用，以验证所提供的 OTP。

设置本地服务器进行 YubiKey 验证的步骤如下：

1. 通过[这里](https://upgrade.yubico.com/getapikey/)从 Yubico 获取一组 ClientId 和 Key。请注意，这要求您拥有一个 YubiKey 才能提供 OTP。如果您没有 YubiKey，请联系您的经理。
2. 更新 `Identity` 项目中的 `globalSettings:yubico:key` 和 `globalSettings:yubico:clientid` 用户机密。您可以使用[更新脚本](/getting-started/server/secrets)或手动更新：

```bash
   dotnet user-secrets set globalSettings:yubico:key [Key]
   dotnet user-secrets set globalSettings:yubico:clientid [ClientId]
```

## 反向代理设置​ <a href="#reverse-proxy-setup" id="reverse-proxy-setup"></a>

运行反向代理可用于模拟以分布式方式运行多个服务器服务。`/dev` 文件夹中的 [Docker Compose](https://docs.docker.com/compose/) 配置已经为 Api 和 Identity 服务准备好了配置（可为其他服务扩展）。

1、反向代理容器被设置为使用位于 `dev/reverse-proxy.conf` 的 [nginx](https://nginx.org/en/docs/beginners_guide.html#conf_structure) 配置文件。复制反向代理配置示例：

```bash
cd dev
cp reverse-proxy.conf.example reverse-proxy.conf
```

2、（可选）修改 reverse-proxy.conf 以支持所需的服务数量及其端口。默认情况下，它支持分别在 **4000/4002** 和 **33656/33658** 端口上运行的两个 **Api** 和两个 **Identity** 服务。

3、确保环境变量 `API_PROXY_PORT` 和 `IDENTITY_PROXY_PORT` 存在于 `dev/.env` 中。有关其默认值，请参阅 `dev/.env.example`。

4、使用 docker compose 启动反向代理。

```bash
docker compose --profile proxy up -d
```

5、为每个正在运行的实例使用特定的端口，在本地启动所需的服务。**这些端口必须与 `upstream` 配置块中的 `reverse-proxy.conf` 中的端口匹配。**

* **命令行**（*在单独的终端中*）

```bash
# 1st instance
cd src/Api
dotnet run --urls=http://localhost:4000/
```

```bash
# 2nd instance: --no-build can avoid conflicts with the first instance
cd src/Api
dotnet run --urls=http://localhost:4002/ --no-build
```

* **Rider** - 为所需服务创建新的启动配置，每个配置在 `ASPNETCORE_URLS` 环境变量中使用不同的端口。然后同时运行/调试每个配置。
* **Visual Studio** - 您可以为每个服务添加额外的运行配置以使用特定的端口，这类似于 Rider。*您可能需要运行多个 Visual Studio 实例才能运行/调试同一个项目*。

> *特别小心由于启动配置更改而导致的意外提交*

6、更新所有客户端以使用反向代理而不是直接使用服务。这些端口在 `dev/.env` 和 `dev/reverse-proxy.conf` 中定义。

* **Api** - `http://localhost:4100`
* **Identity** - `http://localhost:33756`

如果您需要添加其他服务（除 Api 和 Identity 外），请将它们添加到 `dev/reverse-proxy.conf` 中，并确保在 `dev/docker-compose.yml` 文件中为反向代理容器暴露必要的端口。

## 使用 GitHub 软件包的 NuGet <a href="#nuget-with-github-packages" id="nuget-with-github-packages"></a>

服务器端项目和解决方案可使用 [Bitwarden 共享的 .NET 扩展库](https://github.com/orgs/bitwarden/packages?repo_name=dotnet-extensions)和 [GitHub Packages](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-nuget-registry) 提供的需要通过身份验证才能访问的预发布软件包。

首先，[生成](https://github.com/settings/tokens/new)一个 GitHub 个人访问令牌（classic），其范围仅限于 `packages:read`。您可以设置过期日期，但考虑到范围，不设置过期日期可能更方便。复制令牌值并运行：

```bash
IFS= read -rs GITHUB_PAT < /dev/tty
```

粘贴数值并按 Enter 键。接下来，运行：

```bash
dotnet nuget add source --username bitwarden --password $GITHUB_PAT --store-password-in-clear-text --name github --configfile ~/.nuget/NuGet/NuGet.Config "https://nuget.pkg.github.com/bitwarden/index.json"
```

这将设置必要的全局源代码和凭据。任何 NuGet 还原现在也将利用我们为 NuGet 设置的 GitHub 包。

共享库的完整版本发布请访问我们的 [NuGet.org presence](https://www.nuget.org/profiles/Bitwarden)。


# 数据库


# MSSQL

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/mssql/)
{% endhint %}

Bitwarden 主要将数据存储在 MSSQL (Microsoft SQL Server) 中。服务器中的数据访问层使用 [Dapper](https://github.com/DapperLib/Dapper) 编写，Dapper 是 .NET 的轻量级对象映射器。

## 创建数据库 <a href="#creating-the-database" id="creating-the-database"></a>

请参阅[服务器设置指南](/getting-started/server/guide)。

## 更新数据库 <a href="#updating-the-database" id="updating-the-database"></a>

`dev/migrate.ps1` 辅助脚本使用我们的 [MsSql Migrator Utility](https://github.com/bitwarden/server/tree/main/util/MsSqlMigratorUtility) 来运行迁移。每次与 `main` 分支同步或创建新的迁移脚本时，都应运行此辅助脚本。已经运行的迁移会在数据库的 `Migration` 表中进行跟踪。

## 修改数据库 <a href="#modifying-the-database" id="modifying-the-database"></a>

修改数据库的过程描述在[迁移](/contributing/database-migrations)中。


# 实体框架

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/ef/)
{% endhint %}

{% hint style="danger" %}
实体框架 (EF) 支持仍处于测试阶段，不适合生产型数据库。
{% endhint %}

{% hint style="info" %}
本页面指的是建立一个 Bitwarden 实例来进行开发，如需了解如何测试个人使用的 EF 部署（例如 Bitwarden Unified），请参阅[帮助文档](https://help.ppgg.in/self-hosting/install-and-deploy-guides/install-and-deploy-unified-beta)。
{% endhint %}

## 背景 <a href="#background" id="background"></a>

实体框架 (EF) 是一个 ORM 框架，充当数据库的包装器。它允许我们支持多个（非 MSSQL）数据库，而无需为每个数据库维护迁移和查询脚本。

我们的 EF 实现目前支持 Postgres、MySQL 和 SQLite3。

## 设置 EF 数据库 <a href="#setting-up-ef-databases" id="setting-up-ef-databases"></a>

这里的工作流程与普通的 MSSQL 实现大致相同：设置 docker 容器、配置用户机密，并按时间顺序针对相关数据库运行脚本文件夹中的脚本。

### 要求 <a href="#requirements" id="requirements"></a>

* 一个正常运行的本地开发服务器
* Docker
* 一种在服务器项目中管理用户机密的方法 - 请参阅[用户机密参考](/contributing/user-secrets)
* 数据库管理软件（参阅[工具推荐](/getting-started/tools)）
* `dotnet` cli
* `dotnet` cli [实体框架核心工具](https://learn.microsoft.com/zh-cn/ef/core/cli/dotnet)

您可以配置多个数据库，并通过改变 `globalSettings:databaseProvider` 用户机密的值以在它们之间切换。您不需要删除您的连接字符串。

### 数据库设置 <a href="#database-setup" id="database-setup"></a>

{% tabs %}
{% tab title="PostgreSQL" %}
在您的服务器存储库的 `dev` 文件夹中，运行：

```bash
docker compose --profile postgres up
```

{% endtab %}

{% tab title="MySQL" %}
在您的服务器存储库的 `dev` 文件夹中，运行：

```bash
docker compose --profile mysql up
```

{% endtab %}

{% tab title="SQLite" %}
选择数据库文件的位置。您可以使用服务器存储库的 `dev` 文件夹，为此目的，git 配置为忽略此存储库中的 `.db` 文件。
{% endtab %}
{% endtabs %}

### 用户机密 <a href="#user-secrets" id="user-secrets"></a>

将以下值添加到您的 API、身份和管理员用户机密中。

{% tabs %}
{% tab title="PostgreSQL" %}
请务必根据需要更改 root 密码等信息。如果您已经拥有这些机密，请确保更新现有的值，而不是创建一个新的值：

```json
"globalSettings:databaseProvider": "postgres",
"globalSettings:postgreSql:connectionString": "Host=localhost;Username=postgres;Password=example;Database=vault_dev;Include Error Detail=true",
```

{% endtab %}

{% tab title="MySQL" %}
请务必根据需要更改 root 密码等信息。如果您已经拥有这些机密，请确保更新现有的值，而不是创建一个新的值：

```json
"globalSettings:databaseProvider": "mysql",
"globalSettings:mySql:connectionString": "server=localhost;uid=root;pwd=example;database=vault_dev",
```

{% endtab %}

{% tab title="SQLite" %}
将以下值添加到您的 API、身份和管理员用户机密中。注意，您必须设置数据源路径。使用在[数据库设置](#database-setup)中选择的文件位置：

```json
"globalSettings:databaseProvider": "sqlite",
"globalSettings:sqlite:connectionString": "Data Source=/path/to/your/server/repo/dev/db/bitwarden.db",
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
更改 `secrets.json` 文件后，记得运行 `pwsh setup_secrets.ps1 -clear` 使更改生效。
{% endhint %}

### 迁移 <a href="#migrations" id="migrations"></a>

{% tabs %}
{% tab title="PostgreSQL" %}
在 `dev` 文件夹中运行以下命令将数据库更新到最新的迁移：

```bash
pwsh migrate.ps1 -postgres
```

`migrate.ps1` 中的 `-postgres` 标志将用于运行 `dotnet ef` 命令以执行迁移。

您还可以使用以下命令同时为所有数据库提供程序运行迁移：

```bash
pwsh migrate.ps1 -all
```

{% endtab %}

{% tab title="MySQL" %}
在 `dev` 文件夹中运行以下命令将数据库更新到最新的迁移：

```bash
pwsh migrate.ps1 -mysql
```

`migrate.ps1` 中的 `-mysql` 标志将用于运行 `dotnet ef` 命令以执行迁移。

您还可以使用以下命令同时为所有数据库提供程序运行迁移：

```bash
pwsh migrate.ps1 -all
```

{% endtab %}

{% tab title="SQLite" %}
在 `dev` 文件夹中运行以下命令将数据库更新到最新的迁移：

```bash
pwsh migrate.ps1 -sqlite
```

`migrate.ps1` 中的 `-sqlite` 标志将用于运行 `dotnet ef` 命令以执行迁移。

您还可以使用以下命令同时为所有数据库提供程序运行迁移：

```bash
pwsh migrate.ps1 -all
```

{% endtab %}
{% endtabs %}

### 验证（可选） <a href="#optional-verify" id="optional-verify"></a>

如果您想验证一切工作是否正常：

* 检查数据库表以确保所有内容均已创建
* 使用 `dotnet test` 从您的服务器项目的根部运行集成测试。注意：这需要一个已配置好的 MSSQL 数据库。您可能还需要设置其他 EF 提供程序才能通过测试。

## 测试 EF 更改 <a href="#testing-ef-changes" id="testing-ef-changes"></a>

在您的 `server/dev/secrets.json` 文件中查找，或在 json 结构的根部添加此机密块：

```
"databases:0:type": "Postgres",
"databases:0:connectionString": "Host=localhost;Username=postgres;Password=_________;Database=ef_test",
"databases:0:enabled": "true",
"databases:1:type": "Sqlite",
"databases:1:enabled": "true",
"databases:1:connectionString": "Data Source=_________",
"databases:2:type": "MySql",
"databases:2:connectionString": "server=localhost;uid=root;pwd=_________;database=ef_test",
"databases:2:enabled": "true",
"databases:3:type": "SqlServer",
"databases:3:connectionString": "Server=localhost;Database=ef_test;User Id=SA;Password=_________;Encrypt=True;TrustServerCertificate=True;",
"databases:3:enabled": "true"
```

{% hint style="info" %}
示例数据库索引 + 类型组合是工具运行所必需的，并可支持同一数据库的多个版本同时运行测试。
{% endhint %}

此块用于为每个支持的提供程序类型测试数据库。集成测试将连接到这些数据库。如果尚未更新，则应更新这些连接字符串的密码，以使其与现有数据库相匹配。如果您的 `server/dev/secrets.json` 文件中根本没有这些设置，只需将其添加到底部即可。这些设置不会出现在 `globalSettings` 中。然后运行 `pwsh setup_secrets.ps1 -clear` 将其应用到本地项目。

将连接字符串应用到项目后：使用 `pwsh server/dev/migrate.ps1 --all` 确保数据库已全部迁移。然后就可以使用 `dotnet test 从 test/Infrastructure.IntegrationTest` 文件夹运行 EF 测试了。


# 事件日志

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/events)
{% endhint %}

## 要求 <a href="#requirements" id="requirements"></a>

* 完成了[服务器设置指南](/getting-started/server/guide)
* 一个[企业组织](https://help.ppgg.in/organizations/organizations#types-of-organizations)

## Azure Queue（云） <a href="#azure-queue-cloud" id="azure-queue-cloud"></a>

Bitwarden 云实例使用 Azure 队列和表存储来处理事件。以下是它的工作原理：

1. 由用户执行需要被记录的动作
2. 如果事件发生在客户端（例如查看了密码），客户端会将事件的详细信息发送到事件服务器项目，然后事件服务器项目调用 `EventService`。如果事件发生在服务器端，则相关项目调用 `EventService` 本身。
3. 事件暂时存储在 Azure Queue Storage（专为处理大量消息而设计）中
4. EventsProcessor 服务器项目运行常规批处理作业以从队列存储中检索事件并将它们保存到表存储中
5. 从表存储中检索事件以供查看

要在本地模拟：

1. 确保您已安装并设置 Azurite，如[服务器设置指南](/getting-started/server/guide#azurite)中所述
2. 确保未设置 `globalSettings:events:connectionString` 用户机密，或具有默认值 `UseDevelopmentStorage=true`
3. 使用 `dotnet run` 或您的 IDE 启动 Events 和 EventsProcessor 项目。（还要确保您的 Api、Identity 和你的 Web Vault 处于运行状态。）

您现在应该可以观察到您的企业组织正在记录事件（例如，当创建项目或邀请用户时）。这些将出现在组织密码库的事件日志部分。

[Azure Storage Exporer](https://learn.microsoft.com/zh-cn/azure/vs-azure-tools-storage-manage-with-storage-explorer) 允许您检查本地队列和表存储的内容，对调试非常有用。

## 数据库存储（自托管） <a href="#database-storage-self-hosted" id="database-storage-self-hosted"></a>

Bitwarden 的自托管实例使用替代的 `EventService` 实现将事件日志直接写入其数据库的 `Event` 表中。

要为事件使用数据库存储：

1. 在[自托管配置](/getting-started/server/self-hosted)中运行您的本地开发服务器（Api、Identity 和 Web 密码库）
2. 使用 `dotnet run` 或您的 IDE 启动 Events 项目（注意：自托管不需要 EventsProcessor）

## 分布式事件（可选）[​](https://contributing.bitwarden.com/getting-started/server/events#distributed-events-optional) <a href="#distributed-events-optional" id="distributed-events-optional"></a>

事件可以通过 AMQP 消息系统进行分发。该消息系统允许新的集成订阅事件。系统支持 RabbitMQ 或 Azure Service Bus。

### 监听器/处理器模式[​](https://contributing.bitwarden.com/getting-started/server/events#listener--handler-pattern) <a href="#listener--handler-pattern" id="listener--handler-pattern"></a>

将事件迁移到分布式的主要目的是构建额外的服务集成，以消费事件。为了方便支持多个 AMQP 服务（RabbitMQ 和 Azure Service Bus），监听事件流的行为与响应事件的行为是分离的。

#### 监听器 <a href="#listeners" id="listeners"></a>

* 每个通信平台（例如，一个用于 RabbitMQ，一个用于 Azure Service Bus）都有一个监听器。
* 可以配置多个实例独立运行，每个实例都有自己的处理程序和订阅/队列。
* 执行消息平台的设置/拆卸、订阅等所有方面，但不会直接处理任何事件。相反，它们将任务委托给已配置的处理程序。

#### 处理程序 <a href="#handlers" id="handlers"></a>

* 每个集成（例如 HTTP POST 或事件数据库存储库）一个处理程序。
* 完全独立于并不知道所使用的消息平台。这使它们可以在不同的通信平台中自由重用。
* 执行处理事件的所有方面。
* 由于它们与更复杂的消息传递相隔离和分离，因此具有很强的可测试性。

此组合允许在 `Startup.cs` 中进行配置，将当前运行的消息平台监听服务的实例与任何数量的处理器配对。它还允许快速开发新的处理器，因为它们只专注于处理特定事件的任务。

### RabbitMQ 实现[​](https://contributing.bitwarden.com/getting-started/server/events#rabbitmq-implementation) <a href="#rabbitmq-implementation" id="rabbitmq-implementation"></a>

RabbitMQ 实现增加了一个步骤，重构了在本地或自托管运行时处理事件的方式。每个事件都会广播到 RabbitMQ 交换，而不是直接通过 `EventsRepository` 写入 `Events` 表。使用 `EventRepositoryHandler` 配置的新 `RabbitMqEventListenerService` 实例会订阅 RabbitMQ 交换，并通过 `EventsRepository` 写入 `Events` 表。最终结果是相同的（事件存储在数据库中），但 RabbitMQ 交换的添加允许其他集成进行订阅。

为了说明广播事件的性能，一个配置了 `WebhookEventHandler` 的 `RabbitMqEventListenerService` 实例订阅了 RabbitMQ 事件交换机并将每个事件通过 `POST` 发送到可配置的 URL。这是一个简单具体的示例，说明了通过分布式事件如何启用多个集成。

#### 运行 RabbitMQ 容器[​](https://contributing.bitwarden.com/getting-started/server/events#running-the-rabbitmq-container) <a href="#running-the-rabbitmq-container" id="running-the-rabbitmq-container"></a>

1、验证您是否在 `.env` 文件中设置了用户名和密码（请参阅 `.env.example` 以获取示例）

2、使用 Docker Compose 以当前设置运行容器：

```bash
docker compose --profile rabbitmq up -d
```

* Compose 配置使用来自 `env` 文件的用户名和密码。
* 它配置为在 localhost 上以 RabbitMQ 的标准端口运行，但可以在 Docker 配置中进行自定义。

3、要验证其正在运行，请在浏览器中打开 `http://localhost:15672`，并使用您的 `.env` 文件中的用户名和密码登录。

#### 配置服务器以使用 RabbitMQ 处理事件[​](https://contributing.bitwarden.com/getting-started/server/events#configuring-the-server-to-use-rabbitmq-for-events) <a href="#configuring-the-server-to-use-rabbitmq-for-events" id="configuring-the-server-to-use-rabbitmq-for-events"></a>

1、将以下内容添加到您的 `secrets.json` 文件中，将默认值更改为与您的 `.env` 文件匹配：

```yaml
"eventLogging": {
  "rabbitMq": {
    "hostName": "localhost",
    "username": "bitwarden",
    "password": "SET_A_PASSWORD_HERE_123",
    "exchangeName": "events-exchange",
    "eventRepositoryQueueName": "events-write-queue",
    "webhookQueueName": "events-webhook-queue",
  }
  "webhookUrl": "<HTTP POST URL>",
}
```

2、（可选）上面指定的 `webhookQueueName` 和 `webhookUrl` 是可选的。如果它们被定义，则将添加一个 `WebhookEventHandler` 到一个 `RabbitMqEventListenerService` 实例中，该实例将 `POST` 事件到已配置的 URL。

{% hint style="success" %}
[RequestBin](http://requestbin.com/) 提供了一个易于设置的接收这些请求并允许您检查它们的简单服务器。
{% endhint %}

3、重新运行 PowerShell 脚本，将这些秘密添加到每个 Bitwarden 项目中：

```bash
pwsh setup_secrets.ps1
```

4、启动（或重新启动）所有项目以应用新设置

随着这些更改的实施，您应该会看到数据库事件按之前的方式写入，同时您也会在 RabbitMQ 管理界面中看到消息正在通过配置的交换/队列流动。

### Azure Service Bus 实现[​](https://contributing.bitwarden.com/getting-started/server/events#azure-service-bus-implementation) <a href="#azure-service-bus-implementation" id="azure-service-bus-implementation"></a>

Azure Service Bus 实现是 Azure Queue 的可配置替代。不是将事件写入队列等待提取，而是将事件发送到配置的 Service Bus 主题。然后使用 `AzureTableStorageEventHandler` 配置一个实例为 `AzureServiceBusEventListenerService` ，以订阅该主题并将事件写入 Azure 表存储。类似于上面的 RabbitMQ，最终结果相同（事件存储在 Azure 表存储中），但添加 Service Bus 主题允许其他集成进行订阅。

与上面的 RabbitMQ 实现一样，可以配置一个 `WebhookEventHandler` 来运行并通过单独的订阅将事件 POST 到 URL。

#### 运行 Azure Service Bus 模拟器[**​**](https://contributing.bitwarden.com/getting-started/server/events#running-the-azure-service-bus-emulator) <a href="#running-the-azure-service-bus-emulator" id="running-the-azure-service-bus-emulator"></a>

1. 确保您已在本地设置了 Azurite（如之前的用于将事件写入 Azure 表存储的[一般说明](/getting-started/server/guide#azurite)）。此外，这还假设您正在使用 `mssql` 默认配置文件并通过 `.env` 设置了 `${MSSQL_PASSWORD}`&#x20;
2. 使用 Docker Compose 添加/启动本地模拟器：

```bash
docker compose --profile servicebus up -d
```

{% hint style="success" %}
Service Bus 模拟器启动前会等待 15 秒。您可以在 Docker 桌面中查看控制台或运行 `docker logs service-bus` 来验证服务是否启动，然后再启动服务器。
{% endhint %}

#### 配置服务器以使用 Azure Service Bus 处理事件[**​**](https://contributing.bitwarden.com/getting-started/server/events#configuring-the-server-to-use-azure-service-bus-for-events) <a href="#configuring-the-server-to-use-azure-service-bus-for-events" id="configuring-the-server-to-use-azure-service-bus-for-events"></a>

1. 在 `dev` 中的 `secrets.json` 中添加以下内容以配置 Service Bus：

```yaml
	"eventLogging": {
	  "azureServiceBus": {
		"connectionString": "\"Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;\"",
		"topicName": "event-logging",
		"eventRepositorySubscriptionName": "events-write-subscription",
	  }
	},
```

2. 重新运行机密脚本以发布新的机密

```bash
pwsh setup_secrets.ps1 -clear
```

3. 启动或重新启动所有服务，包括 `EventsProcessor`。

### 配置 webhook（可选）[​](https://contributing.bitwarden.com/getting-started/server/events#configuring-the-webhook-optional) <a href="#configuring-the-webhook-optional" id="configuring-the-webhook-optional"></a>

1. 编辑 `servicebusemulator_config.json` 文件以添加对主 `event-logging` 主题的订阅 topic:

```yaml
{
  "Name": "events-webhook-subscription"
}
```

2. 重新启动 server-bus 容器以应用这些更改
3. 将 webhook 订阅名称和 URL 配置添加到 `secrets.json`

```yaml
  "eventLogging": {
    "azureServiceBus": {
    "connectionString": "\"Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;\"",
    "topicName": "event-logging",
    "eventRepositorySubscriptionName": "events-write-subscription",
    "webhookSubscriptionName": "events-webhook-subscription"
    },
    "webhookUrl": "<Optional URL here>"
  },
```

4. 发布新密钥到 App：

   ```bash
   pwsh setup_secrets.ps1 -clear
   ```
5. 重启所有服务，包括 `EventsProcessor`


# Ingress 隧道

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/tunnel)
{% endhint %}

有时，允许其他团队成员访问本地运行的 Bitwarden 实例也是有用处的。通常这需要在防火墙中打开端口，即便如此，您通常也只能通过 IP 地址进行连接。

## 配置网络 <a href="#configure-web" id="configure-web"></a>

如果目标是暴露本地网络密码库（包括对大多数服务的访问），那么网络密码库就需要配置为不使用 `https`，而是以未加密的方式提供内容。

打开 `webpack.config.js` 并注释掉 `const devServer = {` 中的以下几行：

```typescript
https: {
  key: fs.readFileSync('dev-server' + certSuffix + '.pem'),
  cert: fs.readFileSync('dev-server' + certSuffix + '.pem'),
},
```

并将域添加到 `local.json` 中的 `allowedHosts`：

```json
{
  "allowedHosts": ["<super-secret-tunnel>"]
}
```

## Cloudflare Argo 隧道 <a href="#cloudflare-argo-tunnels" id="cloudflare-argo-tunnels"></a>

另一种方法是使用 [Cloudflare Argo Tunnels](https://www.cloudflare.com/products/tunnel/)，这种方法有一些好处。它的工作原理是在 Cloudflare 和本地计算机之间建立一个本地隧道，以便访问本地运行的服务。此外，该隧道还可以放置在提供有效 SSL 证书的 Cloudflare 代理后面，因此非常适合移动应用程序的测试。

### 设置 <a href="#setup" id="setup"></a>

1. [下载](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/)并安装 `cloudflared`
2. 启动本地网络服务器，并注意其运行的 `$PORT`
3. 使用 `cloudflared tunnel --url http://127.0.0.1:$PORT` 启动隧道

Cloudflare 将为您建立一个隧道，并提供其 URL：`*.trycloudflare.com`。在尝试访问之前，请等待 DNS 解析生效。

**注意**：任何拥有此 URL 的人都可以在您的机器上访问转发的 URL。

## Ngrok <a href="#ngrok" id="ngrok"></a>

1、注册一个免费的 ngrok 帐户。

2、请按照[官方说明](https://dashboard.ngrok.com/get-started/setup)下载。或使用 [brew](https://formulae.brew.sh/cask/ngrok) 安装，它支持一个账户多个实例。

3、使用 ngrok 暴露本地端口：

```bash
ngrok http <port>
```

4、ngrok 的界面将显示「转发中」URL，例如：

```
https://abcd-123-456-789.au.ngrok.io -> http://localhost:<port>
```

5、通过导航到转发 URL，并在末尾添加 `/alive` 来验证转发 URL 是否有效。例如，`https://abcd-123-456-789.au.ngrok.io/alive` ..

{% hint style="info" %}
任何拥有此 URL 的人都可以在你的机器上访问转发的 URL。
{% endhint %}


# SCIM

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/scim)
{% endhint %}

SCIM 是跨域身份管理系统 (System for Cross-domain Identity Management) 的缩写。SCIM 使用 Bitwarden 服务器和身份提供商 (IdP) 之间的直接链接，将用户和群组从身份提供商同步到 Bitwarden 服务器。

{% hint style="info" %}
在某些方面，SCIM 与目录连接器 (Directory Connector) 相似。不过，Directory Connector 的工作方式是轮询更改（例如执行计划同步），而 SCIM 的工作方式是在发生更改时将更改直接推送到 Bitwarden。
{% endhint %}

## 要求​ <a href="#requirements" id="requirements"></a>

* 一个[本地开发服务器](/getting-started/server/guide)
* [Web 密码库](/getting-started/clients/web-vault)
* 一个企业组织
* Mailcatcher 或类似的本地邮件服务，这样您就不会在测试邀请时向真实电子邮件地址发送垃圾邮件（这包含在服务器设置指南中）

## 步骤​ <a href="#steps" id="steps"></a>

### 为您的组织启用 SCIM ​ <a href="#enable-scim-for-your-organization" id="enable-scim-for-your-organization"></a>

1、登录到 Web  Vault 然后导航到「{您的组织}」->「设置」->「SCIM 配置」

2、勾选「启用 SCIM」然后点击「保存」。此时您的 SCIM URL 和 API 密钥应该会显示出来。保留此窗口打开以供后面参考

### 启动 SCIM 项目​ <a href="#start-the-scim-project" id="start-the-scim-project"></a>

3、在您的本地服务器存储库中启动 SCIM 项目：

```bash
cd bitwarden_license/src/Scim
dotnet run
```

4、通过导航到 `http://localhost:44559/alive` 来验证 SCIM 项目是否已成功启动

### 公开您的本地端口​ <a href="#expose-your-local-port" id="expose-your-local-port"></a>

SCIM 要求 SCIM 项目和 IdP 之间的直接连接。因此，您需要将本地端口暴露到互联网。请遵循 [Ingress Tunnels](/getting-started/server/tunnel) 上的任一个指南来执行此操作。默认暴露的端口是 `44559`。

### 配置 IdP ​ <a href="#configure-idp" id="configure-idp"></a>

本指南使用 JumpCloud 作为测试 IdP。Okta 同样也适合用于测试，您可以使用任何支持 SCIM 的 IdP。

如果需要，您还可以参考 [JumpCloud SCIM 帮助文档](https://support.jumpcloud.com/support/s/article/Custom-SCIM-Identity-Management)。

1. 创建一个 JumpCloud 账户然后登录 [JumpCloud 管理界面](https://console.jumpcloud.com/login/admin)
2. 点击左侧的「SSO」，然后点击加号按钮以创建一个新应用程序
3. 在应用程序列表中搜索「Bitwarden」，然后点击「配置」
4. 在「常规信息」选项卡中，添加一个「显示名称」
5. 在「身份管理」选项卡中，向下滚动到「配置设置」部分然后完成如下内容：
   * **API 类型**：SCIM API
   * **SCIM 版本**：SCIM 2.0
   * **基本 URL**：使用 Web 密码库中的 SCIM URL，但要将 `localhost` 替换为您的 ngrok 转发 URL。例如，`https://abcd-123-456-789.au.ngrok.io/v2/d24f1dcd-d3fb-4810-977e-adf00009f0ca`
   * **令牌密钥**：使用 Web 密码库中的 SCIM API 密钥
   * **测试用户电子邮件**：使用还没有关联用户账户的任何电子邮件地址。当您测试连接时，JumpCloud 将使用它来执行测试操作
6. &#x20;点击「测试连接」并等待 JumpCloud 完成测试。您应该在 ngrok 窗口中看到 HTTP 请求。
7. 测试通过后点击「激活」。
8. 在「用户群组」选项卡中，将此连接与「所有用户」群组链接。

### 测试​ <a href="#test" id="test"></a>

您应该已经设置好并准备就绪！您可以通过在 JumpCloud 中添加和移除用户来测试 SCIM 集成。确保您的用户属于「所有用户」群组。您应该能看到您的更改几乎立即反映在 Bitwarden 中。

您还可以在 JumpCloud 中挂起和激活用户，这对应于 Bitwarden 中的撤销和恢复操作。


# 自托管指南

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/self-hosted/)
{% endhint %}

{% hint style="info" %}
此页面仅在您需要测试自托管功能时才与您相关。大多数开发工作都**不**需要它。如果您只有本地服务器，请转至[服务器设置指南](/getting-started/server/guide)部分。
{% endhint %}

本页面将阐述如何配置并运行自托管开发服务器以及云端开发服务器。这在以下情况下很有用：

* 您需要测试自托管实例如何与云端通信时
* 您需要在不扰乱正常开发环境的情况下开发自托管功能时

在最常见的配置中，云端和自托管开发服务器的运行都使用相同的基础架构配置：

* 服务运行在 `http://localhost:{port}` 上（云端与自托管使用不同的端口号）
* 本地 SQL 数据库（云端与自托管使用不同的数据库名称）

## 要求 <a href="#requirements" id="requirements"></a>

本指南假定您已完成并熟悉了[服务器设置指南](/getting-started/server/guide)中的技术细节。请确保您已经拥有一个运行中的云端配置服务器开发环境，然后再将自托管实例连接到它。

## 设置 <a href="#setup" id="setup"></a>

将服务器存储库克隆到一个名为 `server-selfhost` 的新文件夹中：

```bash
git clone git@github.com:bitwarden/server.git server-selfhost
```

接下来，我们将致力于让自托管服务器与本地云端配置服务器同时运行。

### 生成安装 ID 和安装 Key <a href="#generate-installation-id-and-key" id="generate-installation-id-and-key"></a>

每一个自托管实例都由安装 ID 和安装 Key 定义。它们被存储在这两个地方：

* 作为用户机密存储在自托管实例中，以及
* 在您的云端配置实例的 `Installations` 表中，以便它可以验证来自您的自托管服务器的请求

要获取 ID 和 Key，可以选择：

* 请求[主机安装 ID](https://bitwarden.com/host/)，或者
* 生成一个 Guid (ID) 和随机字母数字字符串 (Key)

记录下记录这些信息，以便在接下来的步骤中使用。

### 从云端实例复制文件 <a href="#copy-files-from-cloud-instance" id="copy-files-from-cloud-instance"></a>

您需要从云配置的存储库中复制两个文件（这俩个文件您应该已经设置好了）。它们将被复制到您的自托管存储库。它们都位于 `dev` 文件夹中。我们将它们复制过来以节省时间和精力；既然它们的值几乎都一样，就没必要重新生成了。复制到自托管存储库后，我们将在下面的后续步骤中修改它们。

#### `secrets.json` 文件 <a href="#secretsjson-file" id="secretsjson-file"></a>

将 `secrets.json` 文件复制到您刚刚克隆的自托管存储库的 `dev` 文件夹中。

{% hint style="success" %}
`.gitignore` 配置将阻止将 `dev` 文件夹中的 `secrets.json` 签入源代码管理。在任何情况下都不应将 `secrets.json` 推送到源。
{% endhint %}

#### `.env` 文件 <a href="#env-file" id="env-file"></a>

将 `.env` 文件复制到您刚刚克隆的自托管存储库的 `dev` 文件夹中。

### 自托管机密配置 <a href="#self-hosted-secrets-configuration" id="self-hosted-secrets-configuration"></a>

在您的自托管存储库中，导航到 `dev` 文件夹并打开您复制来的 `secrets.json` 文件。

我们必须配置用户机密的 `Dev:SelfHostOverride:GlobalSettings` 部分。本节指定的设置将覆盖本地自托管开发实例的设置。被覆盖部分中的任何内容都将被应用，而不是 `GlobalSettings` 中给出的值。

{% hint style="success" %}
如果您忘记了什么是用户机密，请回顾[用户机密](/contributing/user-secrets)。
{% endhint %}

我们需要在此处执行此操作，因为我们需要能够定义在 Docker 容器中的环境变量中指定的真实的自托管实例的设置值。我们使用机密文件来执行此操作，而不是在我们的机器上设置环境变量以及让 .NET Core 配置中的构建为我们构建我们的设置。

目前，我们只覆盖 `GlobalSettings`。任何其他需要覆盖的用户机密都需要更改代码才能这样做。检查服务器存储库中的 `ServiceCollectionExtension.AddGlobalSettingsServices`，看看我们现在是怎么做的（[代码的脆弱链接](https://github.com/bitwarden/server/blob/master/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs#L448-L463)）。

[内部用户机密](/getting-started/server/secrets)包含一个最小覆盖示例。您需要更新 `Dev:SelfHostOverride:GlobalSettings` 部分中的以下值：

* 安装 ID 和 Key，使用您刚刚生成的值
* 我们将在下面创建的新 SQL 数据库的 SQL Server 密码。它可以从您的 `secrets.json` 中已有的云配置设置中复制（即使用与您的云端配置服务器相同的密码），或者生成一个新的密码。
* 任何其他空白值

在自托管存储库中更新了 `secrets.json` 文件后，通过运行以下命令应用更改：

```bash
pwsh setup_secrets.ps1 -clear:$True
```

现在您已经完成更新自托管实例的用户机密的步骤。

## 数据库配置 <a href="#database-configuration" id="database-configuration"></a>

{% hint style="success" %}
在设置数据库之前，请确保您的 Docker 容器正在运行。
{% endhint %}

导航到自托管服务器存储库。我们将为我们的自托管配置创建第二个数据库，以便云端配置实例可以拥有用于开发的独立的数据集。

如果您在创建云端配置数据库时遵循了[服务器设置指南](/getting-started/server/guide)，则您只需运行带有 `-s` 参数的相同 PowerShell 迁移脚本即可：

```bash
pwsh migrate.ps1 -s
```

这将创建一个名为 `vault_dev_self_host` 的新数据库和/或对其运行未知迁移。

为了使您的自托管数据库保持最新，将来调用带有 `-s` 参数的脚本时将对 `vault_dev_self_host` 执行新的迁移。

### 为您的云数据库定义安装 ID 和密钥 <a href="#define-installation-id-and-key-for-your-cloud-database" id="define-installation-id-and-key-for-your-cloud-database"></a>

您需要手动将安装 Key 添加到您的云端配置实例，以便它知道您的自托管实例，并在需要在两者之间进行 API 调用时允许访问。随意使用您喜欢的任何工具，如 Azure Data Studio、sqlcmd、以及下面的脚本，执行此操作。

```sql
/opt/mssql-tools/bin/sqlcmd -S mssql -d vault_dev -U sa -P <<SA_PASSWORD>> -I -i <<SCRIPT_FILE>>
```

其中 `<<SA_PASSWORD>>` 是云数据库的 SQL SA 密码，`<<SCRIPT_FILE>>` 指向包含以下内容的文件：

```plsql
INSERT INTO [vault_dev].[dbo].[Installation]
(
    [Id]
    ,[Email]
    ,[Key]
    ,[Enabled]
    ,[CreationDate]
)
VALUES
(
    '<<YOUR_ID>>'
    ,'<<YOUR_DEV_EMAIL>>'
    ,'<<YOUR_KEY>>'
    ,1
    ,GETUTCDATE()
)
```

其中 `<<YOUR_ID>>` 是您的安装 ID，`<<YOUR_DEV_EMAIL>>` 是您的电子邮件地址，`<<YOUR_KEY>>` 是您的安装 Key。

## 客户端设置 <a href="#client-setup" id="client-setup"></a>

如果我们没有 Web 门户来与 API 交互，则自托管服务器的用途有限。同样，克隆一个新的存储库实例是最简单的：

```bash
git clone git@github.com:bitwarden/clients.git clients-selfhost
```

安装依赖并初始化 jslib：

```bash
cd apps/web
npm ci
```

## 运行 <a href="#running" id="running"></a>

### 服务器 <a href="#server" id="server"></a>

当服务在自托管配置中运行时，它将默认使用它们在云端配置实例中运行的端口号 +1 的端口号。

上面，我们设置了一系列用户机密覆盖，使我们能够运行我们的自托管实例。我们需要确保启动服务器以使用这些设置。如果满足以下两个条件，服务器代码将使用这些设置：

* 环境用于开发
* `developSelfHosted` 为 `true`

我们基于您运行服务器的方式以不同方式执行此操作。在您的环境中，自托管启动配置（例如「Api-SelfHost」）将为您设置环境和 `developSelfHosted` 标志。

### VS Code <a href="#vs-code" id="vs-code"></a>

我们有多种启动配置以及组合配置用以轻松启动服务。默认情况下，个人自托管启动是隐藏的。导航到 `launch.json` 以取消隐藏。

{% embed url="<https://contributing.bitwarden.com/assets/images/vs-code-a8f7bc4080c82a45457833355fb8c7bd.png>" %}

### Visual Studio <a href="#visual-studio" id="visual-studio"></a>

已运行的配置用于在自托管模式下启动一个给定服务。

{% embed url="<https://contributing.bitwarden.com/assets/images/visual-studio-dd6ac01d0f9a22b112ae3122a7d79a09.png>" %}

### CLI

要从 CLI 运行自托管，您需要：

1、在自托管存储库的根目录中打开一个新的终端窗口。

2、恢复 Identity 服务所需的 nuget 包：

```bash
cd src/Identity
dotnet restore
```

3、启动 Identity 服务：

```bash
dotnet run --launch-profile Identity-SelfHost
```

4、通过导航到 <http://localhost:33657/.well-known/openid-configuration> 测试 Identity 服务是否处于活动状态

5、在另一个终端窗口中，恢复 Api 服务所需的 nuget 包：

```bash
cd src/Api
dotnet restore
```

6、启动 API 服务：

```bash
dotnet run --launch-profile Api-SelfHost
```

7、通过导航到 <http://localhost:4001/alive> 测试 Api 服务是否处于活动状态

{% hint style="info" %}
如果您无法连接到 Api 或 Identity 项目，请检查终端输出信息以确认它们运行时所使用的端口。
{% endhint %}

要启动其他服务，请遵循相同的格式，记得跟随合适的自托管启动配置 `--launch-profile`。

## Web 客户端 <a href="#web-client" id="web-client"></a>

从 `clients-selfhost` 目录中，您可以执行以下命令来启动 Bitwarden 许可服务器或 OSS Web 服务器：

* `npm run build:bit:selfhost:watch`
* `npm run build:oss:selfhost:watch`

默认端口是 `8081`，因此您可以同时运行云端配置和自托管配置的 Web 客户端。它还被配置为指向各种服务器项目的默认 `*-SelfHost` 端口。

<details>

<summary>sausage 配置是如何生成的</summary>

我们的 Web 配置位于 `config/` 中。每一个都有一个名为 `dev` 的子对象。为此，config 对象将 `dev` 对象重新定义为 `dev: {cloud: {}, selfHosted: {}}`。在我们的 webpack 配置文件中，我们根据这些值更新代理和端口设置。

`dev` 对象同时包含云端和自托管开发环境的配置。

</details>

## 许可证功能 <a href="#licensed-features" id="licensed-features"></a>

如果您需要在自托管实例上重新实现[许可证功能](https://help.ppgg.in/self-hosting/licensing-for-paid-features)，则需要使用在云端配置实例中已注册的许可证文件来解锁这些功能。

为此，您应该首先启动本地云端配置和自托管 Web 客户端，要获取和应用许可证，这两个环境都是必需的。

您现在可以选择要申请的许可证类型。每个指令的说明各不相同，但最好的资源是 Bitwarden 帮助中心文档：

* [个人许可证](https://help.ppgg.in/self-hosting/licensing-for-paid-features#individual-license)
* [通过网络密码库获得的组织许可证](https://help.ppgg.in/self-hosting/licensing-for-paid-features#organization-license)
* [通过提供商门户获得的组织许可证](https://help.ppgg.in/provider-portal/get-started-with-provider-portal#enabling-the-self-hosted-instances)


# 系统管理门户

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/portal)
{% endhint %}

{% hint style="info" %}
**门户命名**

本文档涉及我们的 `server` 存储库中 `admin` 应用程序的部署。为了将该应用程序与 Bitwarden 中的其他应用程序区分开来，我们将其分别如下命名：

* 对于**云托管实例**（Bitwarden 内部）→ **Bitwarden 门户**
* 对于**自托管实例** → **系统管理门户**
  {% endhint %}

## 设置 <a href="#setup" id="setup"></a>

1、导航到 `server/src/admin` 目录。

2、恢复 nuget 包：

```bash
dotnet restore
```

3、安装 npm 包：

```bash
npm ci
```

4、构建管理员项目：

```bash
dotnet build
```

5、使用必要的样式表和库构建 `wwwroot` 目录：

```bash
npx gulp build
```

6、启动服务器：

```bash
dotnet run
```

7、使用您喜欢的浏览器导航到您的管理页面（默认情况下是 <http://localhost:62911>），以确认它正在工作。

## 配置访问权限 <a href="#configuring-access" id="configuring-access"></a>

### 身份验证 <a href="#authentication" id="authentication"></a>

门户身份验证通过使用电子邮件发送的链接完全无密码流程进行。要获得授权，电子邮件地址必须列在 adminSettings:admins 用户机密中。

如果您已遵循[服务器设置指南](/getting-started/server/guide)，则应该已经配置好，下列账户应该已经拥有了访问权限：

* `owner@localhost`
* `admin@localhost`
* `cs@localhost`
* `billing@localhost`
* `sales@localhost`

如果还没有，请立即返回然后配置它。

{% hint style="success" %}
有关如何配置用户机密的信息，请参阅[用户机密](broken://pages/NAIJ4ojaHjQInt4zM9PH)。
{% endhint %}

## 登录 <a href="#logging-in" id="logging-in"></a>

1. 导航到您的控制台 URL。默认情况下是 <http://localhost:62911>。
2. 输入 `admin@localhost` 作为电子邮件（或您在用户机密中配置的任何电子邮件）
3. 打开 MailCatcher（默认为 <http://localhost:1080>），然后点击登录链接。


# 单点登录 (SSO)

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/sso/)
{% endhint %}

## 设置和配置 <a href="#setup-and-configuration" id="setup-and-configuration"></a>

您可以使用以下方式为开发设置 SSO：

* [本地 IdP](/getting-started/server/sso/local)（推荐）
* [Okta](/getting-started/server/sso/okta)

## 桌面客户端 <a href="#desktop-client" id="desktop-client"></a>

{% hint style="info" %}
可能不需要这种变通方法 - 即使在开发版本中，SSO 也能正常工作。先试试看！
{% endhint %}

桌面客户端会打开浏览器以完成 SSO 身份验证流程。通过 IdP 验证后，浏览器将重定向到 `bitwarden://` URI。该 URI 通常会打开桌面客户端，但如果你的桌面客户端没有正确安装（例如，因为你是从源代码运行的），这可能不起作用。它可能只会打开一个空的 Electron 窗口（如果安装了正式版客户端，也可能会打开）。

您可以按以下方法解决这个问题：

1. 通过 SSO 流程导航，直到浏览器窗口打开
2. 打开开发工具，点击「网络」选项卡
3. 使用 IDP 完成登录
4. 当 Bitwarden 客户端无法启动时，请返回浏览器并点击最后一个网络请求。该请求应该是向 `localhost` 发送的，并以 `callback?client_id=desktop` 开始...
5. 复制 `location` 响应头中的 URI。它应以 `bitwarden://sso-callback?code=` 开头。

下面是一个示例：

{% embed url="<https://contributing.bitwarden.com/assets/images/devtools-a7381ad4c1c96a9bfac9f42e7a285236.png>" %}

1. 返回桌面客户端，打开开发工具
2. 在控制台中粘贴以下命令并按回车键：`window.location.href = '<paste the URI here>'`
3. 桌面客户端现在应该已完成了 SSO 登录


# 本地 IdP

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/sso/local)
{% endhint %}

本文将向您展示如何为测试目的设置本地 SSO 身份提供程序 (IdP)。

这里使用 [Docker Test SAML 2.0 Identity Provider。](https://github.com/kenchan0130/docker-simplesamlphp)

## 先决条件 <a href="#prerequisites" id="prerequisites"></a>

1. Bitwarden 服务器已安装和配置，并运行以下服务器项目：
   * Identity
   * API
   * SSO（位于 `server/bitwarden_license/src/Sso`)
2. 本地网络客户端正在运行

## 配置 IdP <a href="#configure-idp" id="configure-idp"></a>

1、打开本地网络密码库然后导航至「组织」→「设置」→「单点登录」

2、勾选「允许 SSO 身份验证」复选框

3、编写并输入一个 SSO 标识符

4、选择「SAML 2.0」作为 SSO 类型。先不要保存或退出此页面，稍后再回来查看

5、打开一个新的终端，导航至服务器存储库中的 `dev` 文件夹，例如

```bash
cd ~/Projects/server/dev
```

6、打开 `.env` 文件，根据网络密码库 SSO 配置页面上的「SP 实体 ID」和「断言消费者服务 (ACS) URL」值设置以下环境变量：

```bash
IDP_SP_ENTITY_ID=http://localhost:51822/saml2
IDP_SP_ACS_URL=http://localhost:51822/saml2/yourOrgIdHere/Acs
```

{% hint style="info" %}
您应该已经在初始服务器设置期间创建了此 `.env` 文件。如果需要，您可以参考 `.env.example` 文件。
{% endhint %}

7、（可选）您可以生成证书来签署 SSO 请求。您可以使用为您选择的操作系统制作的脚本来完成此操作。

```bash
# Mac
./create_certificates_mac.sh

# Windows
.\create_certificates_windows.ps1

# Linux
./create_certificates_linux.sh
```

将指纹（例如 `0BE8A0072214AB37C6928968752F698EEC3A68B5`）粘贴到 `globalSettings` > `identityServer` > `certificateThumbprint` 下的 `Secrets.json` 文件中。按[此处所示](/getting-started/server/guide#configure-user-secrets)更新您的机密。

8、复制提供的 `authsources.php.example` 文件，其中包含您的 IdP 用户配置。

```bash
cp authsources.php.example authsources.php
```

默认情况下，该文件配置了两个用户：`user1` 和 `user2`，并且都有密码`password`。您可以按照此格式添加或修改用户，或者仅使用默认值。有关自定义此文件的更多信息，请参阅[此处](https://github.com/kenchan0130/docker-simplesamlphp#advanced-usage)。

9、启动 docker 容器：

```bash
docker-compose --profile idp up -d
```

10、您可以通过导航至 <http://localhost:8090/simplesaml>，然后「身份验证」→「测试已配置的身份验证源」→「`example-userpass`」来测试用户配置。您应该可以使用配置的用户登录了。

## 配置 Bitwarden <a href="#configure-bitwarden" id="configure-bitwarden"></a>

1、回到打开 SSO 配置页面的窗口

2、在「SAML 身份提供程序配置」(SAML Identity Provider Configuration) 部分填写以下值：

* 实体 ID：

```
http://localhost:8090/simplesaml/saml2/idp/metadata.php
```

* 单点登录服务 URL：

```
http://localhost:8090/simplesaml/saml2/idp/SSOService.php
```

* X509 公共证书：打开一个新标签页，导航到上述实体 ID URL，即可获得该证书。它将打开（或下载）一个 XML 文件。复制并粘贴 `<ds:X509Certificate>` 标记之间的值（它看起来应该像一个 B64 编码的字符串）

3、保存 SSO 配置

现在，您的 SSO 已准备就绪！

## 更新 IdP 配置 <a href="#updating-the-idp-configuration" id="updating-the-idp-configuration"></a>

### 用户 <a href="#users" id="users"></a>

要添加或更改用户，只需编辑 `authsources.php`。您的更改将立即生效，但当前已通过身份验证的用户必须退出登录，其账户更改才能生效。

要注销用户身份，请访问 <http://localhost:8090/simplesaml/module.php/core/authenticate.php?as=example-userpass> 然后点击「注销」。或者，您也可以使用私密浏览会话。

### SAML 配置 <a href="#saml-configuration" id="saml-configuration"></a>

要更改实体 ID 或 ACS URL，请编辑 `.env` 文件，然后重启 Docker 容器：

```bash
docker-compose --profile idp up -d
```

## 故障排除 <a href="#troubleshooting" id="troubleshooting"></a>

### Bitwarden 服务器显示 "unknown userId" 错误 <a href="#bitwarden-server-thows-unknown-userid-error" id="bitwarden-server-thows-unknown-userid-error"></a>

`authsources.php` 中缺少用户的 `uid` 声明。


# Okta

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/sso/okta)
{% endhint %}

本指南将使用 Okta 设置基本的 SSO 验证。这仅用于测试，不应用于任何生产环境。

## 先决条件 <a href="#prerequisites" id="prerequisites"></a>

1. Bitwarden 服务器已安装和配置，并且以下服务器项目正运行：
   * Identity
   * API
   * SSO（位于 `server/bitwarden_license/src/Sso`)
2. 访问 Bitwarden Vault 中的开发集合

## 步骤 <a href="#steps" id="steps"></a>

在浏览器中执行以下步骤访问 Okta：

1. 在开发集合中启动「[oktapreview.com](http://oktapreview.com/)」登录项目
2. 使用这些凭据登录
3. 展开左侧菜单面板上的「目录」部分
4. 点击「人员」
5. 点击「添加人员」，为自己创建个人档案
6. 点击顶部菜单栏中的「应用程序」
7. 现在您应该会看到我们的测试应用程序列表。为了进行本地测试，请点击「Bitwarden Test 2」应用程序。该应用程序的客户端凭证和其他信息将被列出，您可以在后续步骤中使用。

打开一个单独的浏览器标签，在本地 Bitwarden 网络密码库中配置 SSO：

1. 登录网络密码库并导航到要为其启用 SSO 的组织
2. 点击 `Settings`，输入组织的标识符。该标识符应是唯一的，可以只是组织名称。点击「保存」。
3. 转到 `Manage > Single Sign-On` 并输入以下信息：

| 类型                                 | OpenId Connect                                                            |
| ---------------------------------- | ------------------------------------------------------------------------- |
| Authority                          | [https://dev-836655.oktapreview.com](https://dev-836655.oktapreview.com/) |
| Client ID                          | 从 Okta 复制而来                                                               |
| Client Secret                      | 从 Okta 复制而来                                                               |
| OIDCS Redirect Behavior            | Redirect GET                                                              |
| Get Claims From User Info Endpoint | ✅                                                                         |

现在您可以使用 SSO 登录了。

{% hint style="info" %}
您必须在客户端端点中设置密码库 URL
{% endhint %}

## 示例配置 <a href="#example-configuration" id="example-configuration"></a>

未显示的字段应为空。

{% embed url="<https://contributing.bitwarden.com/assets/images/config-7c9523f3f9b7edbf6d5651c0ae739346.png>" %}


# 故障排除

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/troubleshooting)
{% endhint %}

## macOS

### AppleCFErrorCryptographicExceptio <a href="#applecferrorcryptographicexception" id="applecferrorcryptographicexception"></a>

错误信息：`Interop.AppleCrypto.AppleCFErrorCryptographicException: The operation couldn't be completed.`

此问题有好几种可能的修复方法，最有可能的是您需要重新启动设备。

如果这不起作用，您可以尝试使用以下方法强制解锁您的登录钥匙串：

```bash
security -v unlock-keychain /Users/$USER/Library/Keychains/login.keychain
```

如果这也不起作用，Mac 有时可以以管理员身份（而不是用户身份）为您的证书设置信任设置。如果您没有看到您的证书，请运行：

```bash
security dump-trust-settings
```

然后请尝试运行：

```bash
security dump-trust-settings -d
```

如果您的证书显示在此处，则您必须使用 [security 命令](https://ss64.com/osx/security.html)将信任设置导出给用户，因为无法在钥匙串访问应用程序中指定此设置。

为此，请运行 `security trust-settings-export -d <filename>` 以导出管理员证书。然后使用 `security trust-settings-import <filename>` 将它们导入用户。

请参阅相关的 [Github 话题](https://github.com/dotnet/runtime/issues/59703)了解更多信息。

### Error NU1403: Package content hash validation failed <a href="#error-nu1403-package-content-hash-validation-failed" id="error-nu1403-package-content-hash-validation-failed"></a>

以下命令应该可以解决问题：

```bash
dotnet nuget locals all --clear  
git clean -xfd  
git rm \*\*/packages.lock.json -f  
dotnet restore
```

有关更多详细信息，请阅读 <https://github.com/NuGet/Home/issues/7921#issuecomment-478152479>

## Windows

### An attempt was made to access a socket in a way forbidden by its access permissions <a href="#an-attempt-was-made-to-access-a-socket-in-a-way-forbidden-by-its-access-permissions" id="an-attempt-was-made-to-access-a-socket-in-a-way-forbidden-by-its-access-permissions"></a>

当应用程序尝试使用已被使用或保留的端口时，通常会发生此错误。带有 Hyper-V 的较新 Windows 保留了许多 50000+ 端口。

幸运的是，可以手动将端口标记为反向，以防止 Hyper-V 保留它们。以特权模式启动 CMD 会话并运行以下命令，然后重新启动计算机。

```bash
net stop winnat

netsh int ipv4 add excludedportrange protocol=tcp startport=<port> numberofports=1 store=persistent

net start winnat
```


# 用户机密

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/secrets/)
{% endhint %}

服务器存储库带有它自己的 `dev/secrets.json` 文件，供社区贡献者使用。Bitwarden 内部开发人员将需要不同的用户机密文件，以便正确模拟云端环境。

我们用于开发的用户机密文件可以在 Bitwarden 应用程序的开发集合中找到。如果您无权访问此集合，请联系您的管理员。

<div align="left"><figure><img src="https://3064160133-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fl6V5M2C6mA47hOsWBwcA%2Fuploads%2FVYgvjQlUunfnsgmaEcbJ%2Fserver-user-secrets.png?alt=media&amp;token=c8f62afc-8b00-47e3-ba67-74e211228cf1" alt=""><figcaption></figcaption></figure></div>

此安全说明拥有 2 个附件：

1. `secrets.json` - 用于开发的默认推荐用户机密。这将使用[服务器设置指南](/getting-started/server/guide)中概述的本地 Docker 服务（例如 MailCatcher 和 Azurite）。
2. `additional-keys-for-cloud-services.json` - 如果您想启用测试云端服务而不是本地 Docker 服务，这些是可选的 Key，您可以将其添加到您的 `secrets.json` 中。它不是一个完整的机密文件，不能单独使用。

## 更新共享的用户机密

如果您需要更新共享的用户机密，请遵循以下规则：

* 新的用户机密应该只放在一个 `.json` 文件中。我们需要避免跨两个文件的重复 Key。
* 避免创建新的 `.json` 文件作为版本控制的手段。如果我们需要回滚，通常可以从原始来源检索信息（例如认证密钥）。如果您觉得必须创建备份，请将其标记为「DEPRECATED」并注明日期。`secrets.json` 和 `additional-keys-for-cloud-services.json` 应该始终包含最新的机密。
* 在 `#team-eng` Slack 频道中通告任何更新，以便每个人都知道更新他们的本地实例。


# 公共 API

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/public-api)
{% endhint %}

Bitwarden 公共 API 为组织提供了一套管理成员、集合、群组、事件日志和策略的工具。有关公共 API 的更多信息，请参阅[帮助中心 ](https://bitwarden.com/help/public-api/)。

## 与私有 API 的区别

大多数开发者可能更熟悉我们客户端应用程序所使用的私有 API。公共 API 与其在几个关键方面有所不同：

| 私有 API                                                                              | 公共 API                                |
| ----------------------------------------------------------------------------------- | ------------------------------------- |
| 位于 [https://api.bitwarden.com](https://api.bitwarden.com/)                          | 位于 <https://api.bitwarden.com/public> |
| 由官方 Bitwarden 客户端应用程序使用                                                             | 由第三方使用，通常用于自定义集成                      |
| 广泛范围 -- 可用于任何事物                                                                     | 狭窄范围 -- 只能用于管理组织                      |
| 可随时更改（但受官方[支持周期](https://bitwarden.com/help/bitwarden-software-release-support/)约束） | 必须提前通知（如弃用警告）某些更改                     |
| 使用用户凭据进行身份验证                                                                        | 使用组织 API 密钥进行身份验证                     |

## 开发指引

1. 避免进行破坏性更改 -- 这些更改会要求现有 API 用户更新他们的集成以避免错误或意外行为 -- 例如，使新属性可选，这样现有集成就不必提供一个值
2. 如果必须进行破坏性更改，请考虑如何提前通知现有用户。与工程、产品和客户成功集成团队沟通，协调所需的通知并最小化影响
3. 不要使用与私有 API 相同的请求/响应模型
4. 使用 [xml 文档注释](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/xmldoc/)为您的端点和模型添加文档。这些将包含在 SwaggerUI 输出中

## 本地开发 <a href="#developing-locally" id="developing-locally"></a>

当以开发模式运行时，Bitwarden 服务器包含一个 [SwaggerUI](https://swagger.io/tools/swagger-ui/) 实例，类似于我们在[帮助中心](https://bitwarden.com/help/api/)的这一个。

SwaggerUI 可以帮助您测试对公共 API 所做的任何更改，而无需编写自己的 HTTP 请求。您还可以检查当帮助中心更新时，SwaggerUI 将如何呈现您的 xmldoc 注释。

要使用 SwaggerUI：

1. 启动您的本地开发服务器（API 和身份项目）以及 Web Vault
2. 导航到 <http://localhost:4000/docs>
3. 点击「授权」
4. 在另一个标签页中打开 Web Vault 并导航到您的组织设置页面。点击「查看 API 密钥」
5. 将您的组织中的 `client_id` 和 `client_secret` 从 Web Vault 中输入到 Swagger 中。现在您可以关闭 Web Vault 并在 Swagger 中继续操作
6. 在作用域部分，点击「全选」
7. 点击「授权」以关闭对话框
8. 您应该会收到一个确认对话框。点击「关闭」

您现在可以通过展开任何部分，点击「试一试」，编辑请求并点击执行来测试公共 API。响应将显示在下方。您还可以通过手动检查 Web Vault 中的组织来验证您的请求是否成功。


# 网页客户端

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/clients/)
{% endhint %}

{% hint style="info" %}
对于移动应用程序，请访问[移动端](/getting-started/mobile)章节。此页面涵盖了其他客户端。
{% endhint %}

本章节涵盖了各个 Bitwarden Typescript 客户端应用程序的开发信息：

* [Web Vault](/getting-started/clients/web-vault)
* [浏览器端](/getting-started/clients/browser)
* [桌面端](/getting-started/clients/desktop)
* [CLI](/getting-started/clients/cli)

在这里，「客户端」通常指的是 Typescript 客户端，它们位于 `client` 单一存储库中。

## 要求 <a href="#requirements" id="requirements"></a>

在开始之前，您应该已经安装了 Node 和 npm。有关详细信息，请参阅[工具和库](/getting-started/tools)页面。

## 设置说明 <a href="#setup-instructions" id="setup-instructions"></a>

在对任何客户端进行操作之前，您需要克隆和设置 `client` 单一存储库。

1、克隆存储库：

```bash
git clone https://github.com/bitwarden/clients.git
```

2、安装依赖：

```bash
cd clients
npm ci
```

{% hint style="info" %}
您应该只从存储库的根目录安装依赖。不要尝试为单个客户端应用程序安装依赖。
{% endhint %}

3、配置 git blame 以忽略某些提交（一般是管理性修改，如格式化）：

```bash
git config blame.ignoreRevsFile .git-blame-ignore-revs
```

4、在 Visual Studio Code 中打开 `client.code-workspace` 文件。这已经被配置为使用[多 root 工作区](https://code.visualstudio.com/docs/editor/multi-root-workspaces)来改善您的开发体验。每个客户端将在左侧的资源管理器面板中显示为自己的工作区。

现在您已准备好继续为您要处理的特定客户提供任何其他说明了。


# 网页密码库

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/clients/web-vault/)
{% endhint %}

## 要求 <a href="#requirements" id="requirements"></a>

* 在开始之前，您必须已完成[客户端存储库设置说明](/getting-started/clients)。
* 如果您想针对本地服务器实例进行开发，请参阅[服务器设置指南](/getting-started/server/guide)。

### SSL 证书 <a href="#ssl-certificate" id="ssl-certificate"></a>

建议为本地开发生成有效的 SSL 证书，而不是使用共享的开发证书。这将防止浏览器发出无效证书的警告，并正确支持 WebAuthn，因为在证书出错的网站上，WebAuthn 会被阻止。请遵循 [mkcert 安装说明](https://github.com/FiloSottile/mkcert#installation)。

```bash
# 生成并安装根 CA
mkcert --install

# 生成对 localhost、127.0.0.1 和 bitwarden.test 有效的证书
mkcert -cert-file dev-server.local.pem -key-file dev-server.local.pem localhost 127.0.0.1 bitwarden.test
```

## 构建说明 <a href="#build-instructions" id="build-instructions"></a>

1、构建并运行 Web Vault。

```bash
cd apps/web
npm run build:oss:watch
```

以上针对本地 bitwarden 实例。关于如何针对官方服务器的信息，请参阅[官方服务器](#official-server)。

2、打开浏览器并导航到 `https://localhost:8080`。您应该会看到 Bitwarden 登录界面。

3、如果您想更进一步，请启动本地服务器并设置好 MailCatcher。现在让我们创建一个账户并验证它。

* 创建一个新账户（这可以是一个假的邮箱）
* 登录新账户
* 点击「验证电子邮件地址」
* 导航至 `http://localhost:1080/`
* 打开无回复电子邮件然后验证您的电子邮件地址

{% hint style="info" %}
您也可以使用 `build:bit:selfhost:watch` 和 `build:os:selfhost:watch` 命令在自托管模式下运行 Web Vault。
{% endhint %}

## 配置 API 端点 <a href="#configuring-api-endpoints" id="configuring-api-endpoints"></a>

默认情况下，Web Vault 将使用您的本地开发服务器（运行在默认端口的 `localhost` 上）。您也可以改用官方的 Bitwarden 服务器或配置自定义端点。

### 官方服务器 <a href="#official-server" id="official-server"></a>

要使用官方的 Bitwarden 服务器，请按照上面的构建说明进行操作，但使用以下命令运行 Web Vault：

```bash
ENV=cloud npm run build:oss:watch
```

### 自定义端点 <a href="#custom-endpoints" id="custom-endpoints"></a>

您可以通过创建具有以下结构的 `config/local.json` 文件来手动设置 API 端点设置：

```json
{
    "dev": {
        "proxyApi": "<http://your-api-url>",
        "proxyIdentity": "<http://your-identity-url>",
        "proxyEvents": "<http://your-events-url>",
        "proxyNotifications": "<http://your-notifications-url>",
        "allowedHosts": ["hostnames-to-allow-in-webpack"],
    },
    "urls": {

    }
}
```

* `dev`：来自前面的 `/api -> <http://your-api-url>` 的代理流量。
* `urls`：直接调用远程服务 [<mark style="background-color:green;">注1</mark>](#user-content-fn-1)[^1]。注意：这可能会导致 CORS 标头出现问题。

***

<mark style="background-color:green;">注1</mark>：`urls`：遵守 [`EnvironmentService` 中的类型定义](https://github.com/bitwarden/clients/blob/master/libs/common/src/abstractions/environment.service.ts)。

[^1]: `urls`：遵守 [`EnvironmentService` 中的类型定义](https://github.com/bitwarden/clients/blob/master/libs/common/src/abstractions/environment.service.ts)。


# WebAuthn

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/clients/web-vault/webauthn)
{% endhint %}

{% hint style="info" %}
如果您需要在本地测试 WebAuthn 身份验证，此页面包含额外的设置说明。
{% endhint %}

[WebAuthn](https://webauthn.guide/) 规范要求使用一个有效的域名。由于 `localhost` 不满足这个要求，因此您需要将本地实例配置为使用域名。

有多种方法可以做到这一点。但是，最简单的方法是修改操作系统的主机文件，将其环回到 `127.0.0.1`。

## 配置 <a href="#configuration" id="configuration"></a>

Webpack 默认通过阻止主机名来防止 DNS 重新绑定攻击。但是，我们可以在 Web 环境配置 JSON 文件中指定被允许的特定主机名。

1、在 `web/config/` 文件夹中创建一个 `local.json` 文件

2、将「bitwarden.test」添加为 `allowedHosts` 实体：

```json
{
  "dev": {
    "allowedHosts": ["bitwarden.test"]
  }
}
```

{% hint style="info" %}
如果您正在运行此应用程序，则必须重新启动它才能使配置更改生效。
{% endhint %}

### 主机文件 <a href="#hosts-file" id="hosts-file"></a>

{% hint style="info" %}
您需要管理员权限才能编辑此文件。
{% endhint %}

不同操作系统的主机文件的位置略有不同。

{% tabs %}
{% tab title="Windows" %}

```
C:\Windows\System32\drivers\etc\hosts
```

{% endtab %}

{% tab title="macOS" %}

```
/etc/hosts
```

{% endtab %}
{% endtabs %}

使用您选择的文本编辑器打开文件。并附加以下行。

```
127.0.0.1 bitwarden.test
```

### 用户机密 <a href="#user-secrets" id="user-secrets"></a>

除了修改主机文件外，还需要创建或更新服务器中 API 和 Identity 项目的[用户机密](/contributing/user-secrets) `globalSettings:baseServiceUri:vault` 以映射域名。例如：

```json
{
  ...
   "globalSettings":{
      "baseServiceUri":{
         "vault":"https://bitwarden.test:8080"
      }
   },
   ...
}
```

### 测试 <a href="#testing" id="testing"></a>

您现在应该可以通过访问 <https://bitwarden.test:8080> 在本地实例上测试 WebAuthn 了。


# 浏览器端

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/clients/browser/)
{% endhint %}

## 要求 <a href="#requirements" id="requirements"></a>

在开始之前，您必须先完成[客户端存储库设置说明](/getting-started/clients)。

## 构建说明 <a href="#build-instructions" id="build-instructions"></a>

1、构建并运行扩展：

```bash
cd apps/browser
npm run build:watch
```

2、使用下一节中的说明在浏览器中加载已解压的浏览器扩展。

## 环境设置 <a href="#environment-setup" id="environment-setup"></a>

默认情况下，浏览器扩展将运行指向生产服务器的端点。要覆盖此设置以进行本地开发和测试，有多种选项。

### Using `managedEnvironment` <a href="#using-managedenvironment" id="using-managedenvironment"></a>

浏览器扩展具有「代管式环境」的概念，它是存储在 `devFlags` 对象内的 [`development.json`](https://github.com/bitwarden/clients/blob/master/apps/browser/config/development.json) 中的 JSON 配置。

`managedEnvironment` 设置允许贡献者覆盖服务器的任何或所有 URL。 `managedEnvironment` 在 [`BrowserEnvironmentService`](https://github.com/bitwarden/clients/blob/master/apps/browser/src/services/browser-environment.service.ts) 中被读取，并为任何提供的 URL 覆盖默认的（生产）设置。

有两种使用 `managedEnvironment` 的方法，具体取决于您是否同时运行 Web Vault。

#### 运行 Web Vault 的 **`managedEnvironment`** <a href="#managedenvironment-with-web-vault-running" id="managedenvironment-with-web-vault-running"></a>

如果您还同时运行 Web Vault，则只需在 `managedEnvironment` 中设置 `base` URL：

```json
{
   "devFlags":{
      "managedEnvironment":{
         "base":"https://localhost:8080"
      }
      ...
   }
   ...
}
```

这是因为 Web Vault 在其 [`webpack.config.js`](https://github.com/bitwarden/clients/blob/master/apps/web/webpack.config.js) 中包含了 `webpack-dev-server` 软件包。当它运行时，它根据自己的 [`development.json`](https://github.com/bitwarden/clients/blob/master/apps/web/config/development.json) 配置文件中的配置设置代理每一个端点：

```json
  "dev": {
    "proxyApi": "http://localhost:4000",
    "proxyIdentity": "http://localhost:33656",
    "proxyEvents": "http://localhost:46273",
    "proxyNotifications": "http://localhost:61840"
  },
```

这意味着当 Web Vault 运行时，浏览器 `managedEnvironment` **无需**逐个覆盖每一个 URL。浏览器会将每个 URL 格式化为 `{base}/{endpoint}`，例如 <http://localhost:8080/api，但> webpack DevServer 会将该 URL 代理到正确的端口，例如 [http://localhost:4000。](https://dev.ppgg.in/getting-started/clients/http:/localhost:4000。)

#### 未运行 Web Vault 的 **`managedEnvironment`** <a href="#managedenvironment-without-web-vault-running" id="managedenvironment-without-web-vault-running"></a>

如果您在没有运行 Web Vault 的情况下测试浏览器扩展，您将无法利用 webpack DevServer 来代理 URL。这意味着您的 `managedEnvironment` 设置必须显式覆盖您要在本地进行通信的所有 URL。

```json
{
    "devFlags": {
        "managedEnvironment": {
            "webVault": "http://localhost:8080",
            "api": "http://localhost:4000",
            "identity": "http://localhost:33656",
            "notifications": "http://localhost:61840",
            "icons": "http://localhost:50024"
        }
        ...
    }
    ...
}
```

### 手动设置自定义环境 URL <a href="#manually-setting-the-custom-environment-urls" id="manually-setting-the-custom-environment-urls"></a>

加载扩展后，您可能需要调整服务器 URL 以指向本地服务器，而不是在 `managedEnvironment` 中覆盖它们。您可以通过浏览器设置更改。有关如何配置 URL 的说明，请点击[此处](https://help.ppgg.in/self-hosting/connect-clients-to-your-instance)。

配置完成后，您的本地自定义环境看上去应该像这样：

{% embed url="<https://contributing.bitwarden.com/assets/images/custom-local-environment-2c78fa111af8dc5186b8da9d12b7fdea.png>" %}

## 测试和调试 <a href="#testing-and-debugging" id="testing-and-debugging"></a>

### Chrome 和基于 Chromium 的浏览器 <a href="#chrome-and-chromium-based-browsers" id="chrome-and-chromium-based-browsers"></a>

要加载浏览器扩展构建：

1. 在地址栏中导航到 `chrome://extensions`，这将打开扩展页面
2. 开启「开发者模式」（切换开关）
3. 点击「加载已解压的扩展程序」按钮
4. 打开您本地存储库的 `build` 文件夹然后确认您的选择

现在您已拥有了已安装的浏览器扩展程序的本地构建。

您可以通过点击 `chrome://extensions` 中 Bitwarden 标题下方的「background.html」来调试浏览器扩展的背景页面。您可以在弹出窗口打开时右键点击它并点击「检查」来调试它。

### Firefox

要加载浏览器扩展构建：

1. 在地址栏中导航到 `about:debugging`，这将打开附加组件页面
2. 点击「此 Firefox」
3. 点击「载入临时附加组件」
4. 打开您本地存储库的 `build` 文件夹然后打开 `manifest.json` 文件

现在您已拥有了已安装的浏览器扩展程序的本地构建。

临时附加组件仅安装在当前会话中。如果您关闭然后重新打开 Firefox，您必须再次加载临时附加组件。

您可以通过点击临时附加组件页面中 Bitwarden 标题旁边的「检查」按钮来调试浏览器扩展的背景页面。要调试弹出窗口：

1. 使用上面的说明检查背景页面
2. 点击调试器右上角的「三点」，然后点击「禁用弹出式自动隐藏」
3. 打开扩展弹出窗口
4. 点击「iframe」按钮（「三点」旁边）然后选择「/popup/index.html」

### Safari

Safari WebExtensions 必须通过 Mac App Store 分发，并与常规 Mac App Store 应用程序捆绑在一起。因此，与其他浏览器相比，构建和调试过程略有不同。

#### 卸载以前的版本 <a href="#uninstall-previous-versions" id="uninstall-previous-versions"></a>

如果您已构建、已安装或运行过桌面客户端（包括官方版本），Safari 很可能会继续加载官方浏览器扩展，而不是加载从源代码构建的版本。

要避免这种情况，请按照以下说明卸载 Safari 扩展：

1. 打开 Safari
2. 点击「设置」，然后点击「扩展程序」选项卡
3. 点击 Bitwarden 扩展旁边的「卸载」
4. 使用扩展删除此应用程序
5. 重新打开 Safari 并检查「设置」以确认没有安装 Bitwarden 浏览器扩展。如果仍有 Bitwarden 扩展，请重复步骤 3-4
6. 退出并完全关闭 Safari 浏览器

如果您要从不同来源加载浏览器扩展（例如，在本地构建和官方版本之间切换），您可能需要定期执行此操作。

#### 在 Xcode 中开发 <a href="#developing-in-xcode" id="developing-in-xcode"></a>

开发扩展的最简单方法是使用 Xcode 构建和调试它。

1、构建扩展：

```bash
npm run build
```

2、编辑 `build/manifest.json`。将 `nativeMessaging` 权限从 `optional_permissions` 部分移至 `permissions` 部分。

3、编辑 `build/index.html` 文件，将 `<html class="__BROWSER__">` 替换为 `<html class="browser_safari">`。

3、在 Xcode 中打开 `src/safari/desktop.xcodeproj`

4、运行「桌面」目标。

{% hint style="info" %}
每当对源文件进行任何更改时，请记住通过 Xcode 重新运行它。它不会自动重新加载。
{% endhint %}

#### 生产构建 <a href="#production-build" id="production-build"></a>

另一种方法是通过 gulp 使用「正确的」构建流程。这种方法不需要对输出进行任何手动处理，因为 gulp 会为我们完成这些工作。不过，我们必须为每次更改完全重建扩展，这会比较慢。

要构建并加载浏览器扩展：

1、为 Safari 构建扩展

```bash
npm run dist:safari:dmg
```

2、打开 Safari 并检「设置」以确认扩展已安装且已启用

{% hint style="warning" %}
您可能需要[在 macOS 中配置 Safari 以运行未签名的扩展](https://developer.apple.com/documentation/safariservices/safari_web_extensions/running_your_safari_web_extension#3744467)。
{% endhint %}

要启用调试：

1. 点击「设置」，然后点击「高级」选项卡
2. 启用「在菜单栏中显示开发菜单」

您可以通过点击 `Develop -> Web Extension Background Pages`，然后选择 Bitwarden 来调试浏览器扩展的背景页面。您可以在弹出窗口打开时右键点击它并点击「检查元素」来调试它。

对于大多数调试和测试来说，这应该足够了，除非您使用的是本机代码。


# 生物识别解锁

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/clients/browser/biometric)
{% endhint %}

当前，移动端、桌面端和浏览器扩展支持生物识别解锁。Bitwarden 的生物识别解锁功能与本机消息传递 API 集成以发挥其功能。

## 哪些设备支持生物识别解锁？ <a href="#which-devices-support-biometric-unlock" id="which-devices-support-biometric-unlock"></a>

有关最新的信息，请参阅[帮助文章](https://help.ppgg.in/your-vault/unlocking-with-biometrics)。截止撰写本文时，以下是概要：

*支持：*

* 基于 Chromium 的浏览器
* Firefox 87 及更高版本
* Safari 14 及更高版本
* 从 [download](https://bitwarden.com/download) 获取的侧面加载的 Windows 桌面应用程序
* 从 Mac Apple Store 下载 Mac 应用程序
* 使用 Windows Hello 的 Windows 桌面应用程序

*不支持：*

* Firefox 86 及更早版本
* 侧面加载的 macOS 桌面应用程序
* Linux OS

## 一般设置步骤 <a href="#general-setup-steps" id="general-setup-steps"></a>

{% hint style="warning" %}
如果您之前已经为 Safari 安装了本地构建的浏览器扩展，请按照[此处](/getting-started/clients/browser)所述重置扩展引用路径。
{% endhint %}

本机消息传递的工作方式是让浏览器启动一个轻量级的代理，并将其植入我们的桌面应用程序。

开箱即用，桌面应用程序只能与生产型浏览器扩展通信。当您在桌面应用程序中启用浏览器集成时，应用程序会生成包含浏览器扩展的生产 ID 的清单。要启用桌面应用程序与开发版浏览器扩展之间的通信，我们需要将您的浏览器扩展开发 ID 添加到此清单中。

### 构建并运行浏览器扩展 <a href="#build-and-run-the-browser-extension" id="build-and-run-the-browser-extension"></a>

* 在本地浏览器项目中，运行 `npm ci`。
* 要在 Safari 上使用本地浏览器扩展，请使用以下命令：`npm run dist:safari:dmg`。构建完成后，您应该会在 Safari 的「偏好设置」中的「扩展」菜单下看到 Bitwarden 扩展。如果没有，打开并构建相关的 Xcode 项目（通常位于 `$HOME/browser/dist/Safari/dmg/desktop.xcodeproj`）。然后它会出现在「设置」菜单中，您就可以启用它了。
* 对于其他浏览器，使用 `npm run build:watch` 然后使用[此处](/getting-started/clients/browser#testing-and-debugging)描述的方法加载本地构建的扩展。

### 为本机消息添加扩展 ID <a href="#add-the-extension-id-for-native-messaging" id="add-the-extension-id-for-native-messaging"></a>

运行扩展后，确认扩展 ID 已添加到您选择的浏览器的 `NativeMessagingHost` JSON 文件中。

* 在 `chrome://extensions` 或 `about:debugging` 中找到扩展 ID。

{% embed url="<https://contributing.bitwarden.com/assets/images/extension-id-2d5d9c8d9954b5ebda77216ba436fe23.png>" %}

* 使用您的 IDE 将 ID 添加到 `NativeMessageHost` JSON 文件中。此文件嵌套在应用程序支持目录下。例如，对于 Chrome 浏览器，该文件位于：

{% tabs %}
{% tab title="Windows" %}

```bash
%APPDATA%\Bitwarden\browsers\chrome.json
```

{% endtab %}

{% tab title="macOS" %}

```bash
~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.8bit.bitwarden.jso
```

{% endtab %}

{% tab title="Linux" %}

```bash
~/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json
```

{% endtab %}
{% endtabs %}

### 构建并运行桌面应用程序 <a href="#build-and-run-the-desktop-app" id="build-and-run-the-desktop-app"></a>

按照[桌面端](/getting-started/clients/desktop)设置文档进行操作。

{% hint style="danger" %}
在 macOS 上，您需要构建一个 Mac App Store Development Build，或者关闭 Gatekeeper。
{% endhint %}

我的项目已经在运行了，然后呢？

* 在桌面应用程序的首选项菜单中，开启 `Enable browser integration`。
* 在浏览器扩展中，访问设置菜单，然后启用 `Unlock with biometrics` 选项。
  * 浏览器会询问您要求您允许该操作，但随后会锁定扩展。当您解锁密码库并再次启用生物识别解锁时，系统会要求您在桌面应用程序中确认此选择，并开启本地生物识别解锁功能。


# Firefox 隐私模式

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/clients/browser/ff-private)
{% endhint %}

您不能使用通常的侧面加载技术在 Firefox 隐私模式下运行附加组件。这是因为它们仅出现在 `about:debugging` 页面中，该页面并没有为您提供在隐私模式下启用扩展的选项。

作为替代方案，您可以使用下面的步骤将开发版本安装为未签名的附加组件。

## 先决条件 <a href="#prerequisites" id="prerequisites"></a>

1. 安装 Firefox 开发者版
2. 配置 Firefox 开发者版：
   * 打开 Firefox 开发者版
   * 导航到 `about:config`
   * 将 `xpinstall.signatures.required` 设置为 `false`（如果该设置项不在列表中，请添加该设置）

## 步骤 <a href="#steps" id="steps"></a>

1. 在命令行上打开本地浏览器存储库
2. 使用 `npm run dist:firefox` 构建并打包浏览器扩展
3. 打开 Firefox 开发者版然后导航到 `about:addons`
4. 点击右上角「管理您的扩展」旁边的齿轮
5. 点击「从文件安装附加组件」
6. 打开本地存储库中的 `dist/dist-firefox.zip`
7. 此扩展现在将出现在 `about:addons` 页面。点击扩展名称将其展开，然后切换「在隐私窗口中运行」。


# 桌面端

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/clients/desktop/)
{% endhint %}

## 要求 <a href="#requirements" id="requirements"></a>

在开始之前，您必须完成[客户端存储库设置说明](/getting-started/clients)。

{% tabs %}
{% tab title="Windows" %}
在 Visual Studio 安装程序中，这些都作为附加依赖项提供。

* Visual C++ 构建工具
* [Rust](https://www.rust-lang.org/tools/install)
  {% endtab %}

{% tab title="macOS" %}

* Xcode 命令行工具
* [Rust](https://www.rust-lang.org/tools/install)
  {% endtab %}

{% tab title="Linux" %}

* 以下软件包
  * `build-essential`
  * `libsecret-1-dev`
  * `libglib2.0-dev`
* [Rust](https://www.rust-lang.org/tools/install)
  {% endtab %}
  {% endtabs %}

## 构建本机模块 <a href="#build-native-module" id="build-native-module"></a>

桌面应用程序依赖一个使用 rust 编写的本机模块，需要单独编译。这已包含在 `npm run electron` 的构建过程中，但您也可以手动编译它。

```bash
cd apps/desktop/desktop_native
npm run build
```

**注意**：如果本机代码发生了变化，需要重新构建这个模块。

### 交叉编译 <a href="#cross-compile" id="cross-compile"></a>

在某些环境中，例如 WSL（Windows Subsystem for Linux - Linux 的 Windows 子系统），可能需要交叉编译本机模块。为此，首先确保您已安装相关的 Rust 目标。更多信息，请参阅 [`rustup` 文档](https://rust-lang.github.io/rustup/cross-compilation.html)。

```
# 确保 cargo 环境文件的来源。
source "$HOME/.cargo/env"

cd apps/desktop/desktop_native
export PKG_CONFIG_ALL_STATIC=1
export PKG_CONFIG_ALLOW_CROSS=1
npm run build -- --target x86_64-unknown-linux-musl # 替换为相关目标
```

## 构建说明 <a href="#build-instructions" id="build-instructions"></a>

构建并运行：

```bash
cd apps/desktop
npm run electron
```

## 调试和测试 <a href="#debugging-and-testing" id="debugging-and-testing"></a>

Electron 应用程序有一个在 Electron 窗口中运行的渲染器进程，以及一个在后台运行的主进程。

渲染器进程可以使用 Chromium 调试器进行检查。它应该在桌面应用程序打开时自动打开，或者您可以从「视图」菜单中打开它。

主进程可以通过从 Visual Studio Code 中的 [Javascript 调试终端](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_javascript-debug-terminal)运行此应用程序，然后在 `build/main.js` 中放置断点进行调试。

## 生物识别解锁（本机消息传递） <a href="#biometric-unlock-native-messaging" id="biometric-unlock-native-messaging"></a>

配置本机消息传递（桌面应用程序和浏览器扩展之间的通信）的说明位于[浏览器章节](/getting-started/clients/browser/biometric)。

## 故障排除 <a href="#troubleshooting" id="troubleshooting"></a>

### 构建故障 <a href="#trouble-building" id="trouble-building"></a>

如果您看到这样的错误：

```bash
[Main] Error: Cannot find module '@bitwarden/desktop-native-darwin-arm64'
```

您可能还没有构建本机模块，请参阅[构建本机模块](#build-native-module)。

### 桌面 Electron 应用程序窗口未打开 <a href="#desktop-electron-app-window-doesnt-open" id="desktop-electron-app-window-doesnt-open"></a>

如果运行 `npm run Electron` 会显示类似以下的错误：

```bash
[Main] npm ERR! Error: Missing script: "build-native"
```

或 electron 窗口无法渲染，您可能需要更新 node 和/或 npm。从旧版本升级后，这个问题将得到解决：

* Node：`16.18.1`
* npm：`8.19.2`


# Mac App Store Dev

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/clients/desktop/mac/)
{% endhint %}

{% hint style="danger" %}
只有在测试 Mac App Store (MAS) 独有的某些功能时，才需要使用 Mac App Store (MAS) Dev 构建。一般来说，除非有特殊原因需要使用 MAS 构建，否则应使用主构建说明（使用 `npm run electron`）。
{% endhint %}

## 设置 <a href="#setup" id="setup"></a>

这些步骤可能比较复杂。如果遇到任何困难，请在 `#team-eng` Slack 频道中发帖寻求帮助。

### Xcode

1. 安装[来自 App Store 的 Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12)
2. 使用您的 AppleID（8bit solutions LLC 组织的成员）登录。这可以从 `Xcode > Preferences ... > Accounts` 完成
3. 点击 `Bitwarden Inc` 团队并点击 `Manage Certificates...` ，以确保您拥有分配给 Bitwarden Inc 的个人代码签名证书。
4. 如果未列出证书，请点击加号 ( `+` ) 创建一个。

### 钥匙串 <a href="#keychain" id="keychain"></a>

验证您的 Apple 钥匙串是否包含 `AC_PASSWORD` 的值，如果没有，我们需要生成一个。

1、使用您的 Apple 账户在 [AppleID 网站](https://appleid.apple.com/)登录

2、点击「应用程序专用密码」

<div align="left"><figure><img src="https://github.com/bitwarden/contributing-docs/blob/master/docs/getting-started/clients/desktop/mac/app-specific-passwords.png?raw=true" alt=""><figcaption></figcaption></figure></div>

3、然后点击 `Passwords` 旁边的 `+` 图标以添加新的应用程序专用密码

{% embed url="<https://contributing.bitwarden.com/assets/images/app-specific-passwords2-bad7e0cf20855adc23e15b91dbc27031.png>" %}

4、使用已保存的新应用程序专用密码

```bash
security add-generic-password -a "<apple_id>" -w "<app_specific_password>" -s "AC_PASSWORD"
```

### 配置配置文件 <a href="#provisioning-profile" id="provisioning-profile"></a>

1. 请求 DevOps (@DevOps in slack) 将您的 `Apple Development` 签名证书添加到配置文件中，以及将您的 Mac `Provisioning UDID` 添加到白名单中。通过转到 `About This Mac > System Report...` 可以找到 `Provisioning UDID`，然后复制 `Provisioning UDID:` 行&#x20;
2. 添加完所有内容后，从 <https://developer.apple.com/account/resources/profiles/list> 下载 `Bitwarden Desktop Development (2021)` 配置文件
3. 将配置文件安装到您的设备，并将其放置在 `clients/apps/desktop` 存储库根目录中。

## 测试 <a href="#testing" id="testing"></a>

1、运行以下命令来识别您的个人开发证书的名称：

```bash
security find-identity -v | grep 'Apple Development'
```

2、运行 `export CSC_NAME=""`，确保设置了 `CSC_NAME` 环境变量，其值应该是 `find-identity` 的输出，不带 `Apple Development:` 部分。

3、运行 `npm run dist:mac:masdev`。

{% hint style="info" %}
如果这是您第一次在本地运行桌面，请确保在运行 `npm run dist:mac:masdev` 之前先运行 `npm ci`。
{% endhint %}

## 故障排除 <a href="#troubleshoot" id="troubleshoot"></a>

如果收到错误消息 `You do not have permission to open the application "Bitwarden".`（您没有打开应用程序 "Bitwarden" 的权限），请确保将正确的配置文件放置在桌面存储库根目录中。


# Microsoft Store

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/clients/desktop/microsoft-store)
{% endhint %}

要调试 Microsoft Store 应用程序，需要生成代码签名证书，可使用以下 Powershell 命令生成：

```powershell
New-SelfSignedCertificate -Type Custom `
  -Subject "CN=ElectronSign, 0=Your Corporation, C=US" `
  -TextExtension @("2.5.29.19={text}false") `
  -KeyUsage DigitalSignature `
  -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}") `
  -FriendlyName ElectronSign `
  -CertStoreLocation "Cert:\CurrentUser\My"
```

生成的证书需要复制到 `Cert:\CurrentUser\Trusted People` 中，以告知操作系统信任该证书。使用 `certmgr` 工具最方便简单。

要访问签名工具，需要使用 [Windows SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/)。

```powershell
npm run dist:win

cd dist
"C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\signtool.exe" sign /v /fd sha256 /n "14D52771-DE3C-4886-B8BF-825BA7690418" .\Bitwarden-2022.<version>.appx
```


# Native Messaging Test Runner

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/clients/desktop/native-messaging-test-runner)
{% endhint %}

Native Messaging Test Runner 是一个 Node 应用程序，用于测试桌面端中的 Native Messaging 功能，特别是从 DuckDuckGo 浏览器接收到的命令。它使用进程间通信 (IPC) 与桌面应用程序进行通信。它的创建是为了支持对本机消息传递本身进行开发，并使 QA 能够测试这些命令。它被放置于 `bitwarden/clients` 存储库中桌面应用程序的根目录的[此处](https://github.com/bitwarden/clients/tree/master/apps/desktop/native-messaging-test-runner)。

## 入门 <a href="#getting-started" id="getting-started"></a>

1、克隆 [bitwarden/clients](https://github.com/bitwarden/clients) 存储库

2、按照[这些](/getting-started/clients/desktop)说明在本地运行桌面应用程序

3、在正在运行的桌面应用程序中，转到 `Preferences` 然后打开 `Allow DuckDuckGo browser integration` 设置：

{% embed url="<https://user-images.githubusercontent.com/8926729/191100543-208fa2f4-8859-4bbf-86f6-5ea0795f504c.png>" %}

4、在另一个终端中，导航到 `apps/desktop/native-messaging-test-runner`

5、运行 `npm ci`

6、选择一个命令然后运行它！一个好的开始是 `status`。完整的命令列表可以在本文档的 `Commands` 部分中看到。有的命令带有参数，例如 `create`。运行这些命令时，通过在所有参数前加上两个破折号的方式传递参数：`npm run create -- --name NewLogin!`。**注意**，您需要在每个命令之前接受桌面应用程序中的提示。这肯定是一个需要改进的地方。

{% embed url="<https://user-images.githubusercontent.com/8926729/191779284-dfce1764-c92a-4728-8aa5-994607741eef.png>" %}

{% hint style="warning" %}
这些命令针对您本地正在运行的桌面实例以及您在那里拥有的任何帐户运行的。您需要预先设置您的帐户和密码库，以正确测试这些命令。
{% endhint %}

## 架构和结构 <a href="#architecture-and-structure" id="architecture-and-structure"></a>

### 命令 <a href="#commands" id="commands"></a>

命令文件夹包含可运行的节点脚本/命令。当前，每个本机消息传递命令都有一个文件用于测试。

1. **`handshake`** 发送 `bw-handshake` 命令并与桌面应用程序中的本机消息服务建立通信
   * **参数**：无
   * **示例用法**：`npm run handshake`
2. **`status`** 发送 `bw-status` 命令并返回桌面应用程序中配置的帐户数组。
   * **参数**：无
   * **示例用法**：`npm run status`
3. **`create`** 发送 `bw-credential-create` 命令并使用提供的名称和其他字段的测试数据创建新的登录
   * **参数**：`--name`
   * **示例用法**：`npm run create -- --name NewLoginFromTestRunner`
4. **`update`** 发送 `bw-credential-update` 命令并使用提供的字段更新凭证
   * **参数**：`--name`，`--username`，`--password`，`--uri`，`--credentialId`
   * **示例用法**：`npm run update -- --name UpdateLoginFromTestRunner --username rmaccallum --password dolphin123 --uri google.com --credentialId 8fdd5921-4b10-4c47-9f92-af2b0106d63a`
5. **`retrieve`** 发送 `bw-credential-retrieval` 命令并返回与提供的 uri 匹配的凭据列表
   * **参数**：`--uri`
   * **示例用法**：`npm run retrieve -- --uri google.com`
6. **`generate`** 发送 `bw-generate-password` 命令并使用传递给它的 userId 的设置返回密码/密码短语
   * **参数**：`--userId`
   * **示例用法**：`npm run generate -- --userId fe2af956-a6a6-468c-bc8c-ae6600e48bdd`

### IPCService

该服务管理与套接字的连接以及在该套接字上的消息发送和消息接收。

### NativeMessageService

该服务使用 IPCService 连接到本地运行的 Bitwarden 桌面应用程序的 IPC 代理服务。它使用 Bitwarden 的加密服务和功能来处理消息的加密和解密。它使用位于 `/native-messaging-test-runner/src/variables.ts` 文件中的测试公钥/私钥对。

### 其他 <a href="#other" id="other"></a>

#### 我以为 Node 使用了 JavaScript？我们如何使用 TypeScript 类，包括来自 libs 的类？ <a href="#i-thought-node-used-javascript-how-are-we-using-typescript-classes-including-the-ones-from-libs" id="i-thought-node-used-javascript-how-are-we-using-typescript-classes-including-the-ones-from-libs"></a>

好问题！用一点魔法✨我们将这些 TypeSript 文件编译成 Node 熟悉和喜爱的 JavaScript。它通过以下几个机制来完成：

* **`tsconfig.json`**：在该文件的 `paths` 节点中，将提供的 `paths` 中的文件编译成纯 JavaScript，然后放在 `dist` 文件夹中。
* **`package.json`**：安装了 `module_alias`，它允许我们将 TypeScript 文件中使用的任何服务映射到它们编译的 JavaScript 等价物。在任何命令文件中直接使用的文件的路径被定义在 `package.json` 的 `"_moduleAliases"` 节点中。

#### **Deferred** <a href="#deferred" id="deferred"></a>

覆盖 JavaScript `Promise` 接口的 `resolve` 和 `reject` 方法的默认实现的类，以允许我们等待对通过 IPC 发送的消息的响应。

#### **logUtils** <a href="#logutils" id="logutils"></a>

在整个应用程序中为 `console.log()` 添加漂亮的颜色格式的 Utils 类。

#### race

`IPCService` 在创建允许在等待消息时使用超时的承诺时使用的实用程序。我们不能保证我们会从桌面应用程序中获得响应，因此如果没有及时收到响应，我们可以优雅地取消。

## 故障排除 <a href="#troubleshooting" id="troubleshooting"></a>

* 如果您在测试运行程序使用的服务或编辑命令时看到意外行为，请删除 `native-messaging-test-runner` 顶层的 `dist` 文件夹并重新运行该命令。
* 在运行命令时，如果您正在添加/编辑命令文件时收到 `MODULE_NOT_FOUND` 错误，请确保您已在你的命令文件中 `import "module-alias/register";`。这会将已编译的 JavaScript 类映射到 Typescript 文件中使用的类。

{% embed url="<https://user-images.githubusercontent.com/8926729/191111302-8ab7e795-4cd9-4494-965d-67ae7b92cbba.png>" %}


# 更新测试

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/clients/desktop/update)
{% endhint %}

有时可能需要测试桌面应用程序的更新流程。本文档将尝试详细描述如何做到这一点。

对于开发目的，我们不能真的将桌面应用程序发布到我们的 GitHub 页面。为了提供一个尽可能相似的环境，我们运行了一个本地 S3 模拟器，它可以让我们模拟一个 S3 提供程序环境。

虽然这与我们的场景不完全相同，但它仍然允许我们测试 UI 以及更新过程，而不会把 GitHub 存储库弄得乱七八糟。

## 准备 <a href="#preparation" id="preparation"></a>

1、在 `scripts/dev` 中使用 `docker compose up` 启动 minio docker 容器。

2、添加一个名为 `update` 的只读存储桶，通过访问 `http://localhost:9001` 并使用 `minioadmin/minioadmin` 登录，点击 `Create bucket` 填写详细信息。然后点击 `Manage`、`Access Rules` 并填写以下详细信息。

{% embed url="<https://contributing.bitwarden.com/assets/images/minio-access-rule-1e1cc705d067202f7e4edbafbb681955.png>" %}

3、使用以下的 `publish` 设置修改 `package.json`

```json
"publish" : {
    "provider": "s3",
    "endpoint": "http://127.0.0.1:9000",
    "bucket": "update"
},
```

4、使用以下内容在用户主目录（Windows：C:\Users\username，Linux：\~/）中创建 `.aws/credentials`

```systemd
[default]
aws_access_key_id=minioadmin
aws_secret_access_key=minioadmin
```

## 更新 <a href="#update" id="update"></a>

1. 使用 `npm run publish:win:dev` 生成本地构建
2. 在 `dist/nsis-web/Bitwarden-Installer-1.32.0.exe` 中安装构建
3. 更新 `src/package.json` 中的版本号
4. 使用 `npm run publish:win:dev` 发布新的版本
5. 该 App 现在应该会提示更新

备注：这也可以在 Linux 上完成，只需在步骤 1 和 4 中使用 `npm run publish:lin`。

相关内容：<https://www.electron.build/tutorials/test-update-on-s3-locally>


# CLI

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/clients/cli/)
{% endhint %}

{% hint style="success" %}
如果您不熟悉 CLI 客户端，Bitwarden 帮助中心有很多[很棒的文档](https://help.ppgg.in/getting-started/bitwarden-cli)可以帮助您熟悉。
{% endhint %}

## 要求 <a href="#requirements" id="requirements"></a>

在开始之前，您必须完成[客户端存储库设置说明](/getting-started/clients)。

## 构建说明 <a href="#build-instructions" id="build-instructions"></a>

构建并运行：

```bash
cd apps/cli
npm run build:watch
```

默认情况下，这将使用官方的 Bitwarden 服务器。您可以使用 [config 命令](https://help.ppgg.in/getting-started/bitwarden-cli#confirm)定位本地服务器。您可能需要[配置节点以使用您的自签名证书](https://help.ppgg.in/getting-started/bitwarden-cli#using-self-signed-certificates)。

## 测试和调试 <a href="#testing-and-debugging" id="testing-and-debugging"></a>

构建位于 `build/bw.js`。您可以使用 node 运行它，例如：

```bash
node build/bw.js login
```

首先让文件执行尽可能更方便：

```bash
cd build
chmod +x bw.js
./bw.js login
```

要调试 CLI 客户端，请从 [Javascript 调试终端](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_javascript-debug-terminal)运行它并将断点放置在您的 Typescript 代码中。


# 故障排除

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/clients/troubleshooting)
{% endhint %}

## 构建错误 / npm 依赖错误 <a href="#build-errors-npm-dependency-errors" id="build-errors-npm-dependency-errors"></a>

如果您在尝试构建客户端 App 时收到 npm 依赖错误，请确保您仅在存储库的根目录中运行了 `npm ci`。您不应该在任何其他目录（例如客户端 App 目录）中运行 `npm i` 或 `npm ci`。

您可以通过在 `libs` 和 `apps` 目录中搜索 `node_modules` 来仔细检查：

```bash
find apps -name node_modules
find libs -name node_modules
```

删除这些搜索到的所有结果，然后再次尝试构建 App。

如果您仍然遇到构建错误，您可以尝试删除存储库根目录中的 `node_modules` 文件夹，重新运行 `npm ci`，然后再次构建 App。


# 移动端

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/mobile/)
{% endhint %}

## 安卓开发 <a href="#android-development" id="android-development"></a>

请参阅 [Android 移动 App](/getting-started/mobile/net-maui-legacy/android) 页面以设置 Kotlin 开发环境。

## iOS 开发 <a href="#ios-development" id="ios-development"></a>

请参阅 [iOS 移动 App](/getting-started/mobile/net-maui-legacy/ios) 页面以设置 Swift 开发环境。

## .NET MAUI 开发 (legacy) <a href="#net-maui-development-legacy" id="net-maui-development-legacy"></a>

请参阅 [.NET MAUI App](/getting-started/mobile/net-maui-legacy) 页面以设置 Swift 开发环境。


# Android

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/mobile/android/)
{% endhint %}

## 要求 <a href="#requirements" id="requirements"></a>

* Android Studio - 最新稳定版本
* Android SDK - 对于 SDK 平台版本，请参考 [libs.versions.toml](https://github.com/bitwarden/android/blob/main/gradle/libs.versions.toml) 文件中的 `compileSdk` 版本

## 设置 <a href="#setup" id="setup"></a>

1、克隆仓库：

```sh
$ git clone https://github.com/bitwarden/android
```

2、在项目的根目录下创建一个 `user.properties` 文件，并添加以下属性：

* `gitHubToken`：一个具有 `read:packages` 权限的「经典」 GitHub 个人访问令牌 (PAT)（例如：`gitHubToken=gph_xx...xx`）。这些令牌可以通过访问 [GitHub 令牌页面](https://github.com/settings/tokens)生成。参阅[有关身份验证的 GitHub 包用户文档](https://docs.github.com/zh/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry)了解更多详情。
* `localSdk`：一个布尔值，用于确定是否应从本地 Maven 仓库加载 SDK（例如：`localSdk=true`）。这在开发新 SDK 时特别有用。查看[将 SDK 链接到客户端](/getting-started/sdk/internal-sdk#linking-the-sdk-to-clients)了解更多详情。

3、设置代码风格格式化工具：

所有代码必须遵循以下指南中描述的[代码样式指南文档](/contributing/code-style/android-and-kotlin) 。为了帮助遵守这些规则，所有贡献者应将 `docs/bitwarden-style.xml` 作为他们的代码样式方案应用。在 IntelliJ / Android Studio 中：

* 导航到 `Preferences > Editor > Code Style` 。
* 点击 `Scheme` 旁边的 `Manage` 按钮。
* 选择 `Import`。
* 在项目的 `docs/` 目录中找到 `bitwarden-style.xml` 文件。
* 从 `BitwardenStyle` 导入到 `BitwardenStyle`。
* 勾选 `Enable EditorConfig support`。
* 点击 `Apply` 和 `OK` 保存更改然后退出 `Preferences`。

注意，在某些情况下，您可能需要重新启动 Android Studio 才能使更改生效。

所有代码在提交拉取请求之前都应格式化。这可以手动完成，但创建一个带有自定义键盘绑定的宏以在保存时自动格式化也是有帮助的。在 OS X 的 Android Studio 中：

* 选择 `Edit > Macros > Start Macro Recording`
* 选择 `Code > Optimize Imports`
* 选择 `Code > Reformat Code`
* 选择 `File > Save All`
* 选择 `Edit > Macros > Stop Macro Recording`

这可以通过导航到 `Android Studio > Preferences` 并编辑 `Keymap` 下的宏来实现一组键（例如：shift + command + s）。

请避免在同一个提交/PR 中混合格式化和逻辑更改。如果可能，在提交逻辑更改之前，先在单独的 PR 中修复任何大的格式化问题。这有助于他人专注于代码审查时的有意义代码更改。

## 依赖 <a href="#dependencies" id="dependencies"></a>

对于应用程序、开发环境和 CI/CD 依赖，请参阅 [libs.versions.toml](https://github.com/bitwarden/android/blob/main/gradle/libs.versions.toml) 文件。


# F-Droid

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/)
{% endhint %}

## 概述[​](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/#overview) <a href="#overview" id="overview"></a>

Bitwarden F-Droid 存储库托管在 [GitHub](https://github.com/bitwarden/f-droid) 上。它包含所有在 F-Droid 上可用的 Bitwarden App。

Bitwarden F-Droid 存储库会定期自动更新，以确保存储库中托管的 App 与 Google Play Store 发布的最新版本保持同步。

## 设置[​](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/#setup) <a href="#setup" id="setup"></a>

### Go <a href="#go" id="go"></a>

构建和运行 `metascoop` 应用程序需要 Go 语言。

使用 Homebrew 下载和安装 Go 的命令如下：

```bash
brew install go
```

其他下载和安装选项请参阅 [Go 安装文档](https://go.dev/doc/install)。

### F-Droid 服务器和存储库工具[​](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/#f-droid-server-and-repo-tools) <a href="#f-droid-server-and-repo-tools" id="f-droid-server-and-repo-tools"></a>

要手动更新 F-Droid 存储库，需要使用 F-Droid 服务器和存储库工具。安装说明请参阅[官方 F-Droid 服务器和存储库工具文档](https://f-droid.org/zh_Hans/docs/Installing_the_Server_and_Repo_Tools/)。

### 安卓 SDK[​](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/#android-sdk) <a href="#android-sdk" id="android-sdk"></a>

F-Droid 服务器和存储库工具需要 `apksigner`，它是安卓 SDK 的一部分。

要使用 Homebrew 安装所需的安卓 SDK 工具，请运行以下命令：

```bash
brew install android-sdk
android update sdk --no-ui --all --filter tools,platform-tools,build-tools-25.0.0
```

可以在[此处](https://developer.android.com/tools/releases/platform-tools?hl=zh-cn)找到下载和安装所需 Android SDK 工具的替代说明。

## 文件结构[​](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/#file-structure) <a href="#file-structure" id="file-structure"></a>

该存储库的结构如下：

* `fdroid/`：存放应用程序的 F-Droid 存储库。
* `metascoop/`：用于在 Bitwarden App 发布新版本时更新 F-Droid 存储库的 Go App。
* `repos.yml`：定义源存储库和可托管 F-Droid App 的文件。

### `repos.yml`[​](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/#reposyml) <a href="#reposyml" id="reposyml"></a>

此文件包含有关 `MetasCoop` 将搜索新的 F-Droid 版本的源存储库的详细信息。它使用以下结构来声明存储库及其应用程序：

```yaml
my-repository:
  git: "https://github.com/bitwarden/android"
  applications:
    - filename: "com.x8bit.bitwarden-fdroid.apk"
      id: "bitwarden"
      name: "Bitwarden"
      categories:
        - Security
      description: |
        My awesome app description.
```

* `my-repository`：存储库的名称。用于在索引中识别存储库。
* `git`：源存储库的 URL。
* `applications`：存储库中可用的应用程序。可以通过向 `applications` 列表中添加新条目来向存储库添加多个应用程序。
  * `filename`：将要从源存储库下载的 APK 文件的名称。
  * `id`：应用程序 ID。这必须是唯一的，用于在 F-Droid 存储库中识别应用程序。
  * `name`：在 F-Droid 中查看应用程序时显示给用户的名称。
  * `categories`：应用程序所属的分类。用于在 F-Droid 中对应用程序进行分类。
  * `description`：在 F-Droid 中查看应用程序时显示给用户的描述。

### `metascoop/`[​](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/#metascoop) <a href="#metascoop" id="metascoop"></a>

Bitwarden 的 F-Droid 存储库配置为在源仓库中检测到新版本时自动更新应用程序，这些源存储库在 `repos.yml` 中定义。

这通过使用 `metascoop` 应用程序来获取源存储库的最新版本，然后更新存储库索引来完成。

`metascoop` 应用程序由 CI/CD 管道定期运行，以确保存储库索引保持最新。

当检测到源存储库中的更改时，任何新的发布都将添加到 F-Droid 存储库中，并执行 `fdroid update` 命令以更新 F-Droid 服务器和存储库元数据。CI/CD 管道将自动创建一个拉取请求以更新存储库中的这些更改。

### `fdroid/`[​](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/#fdroid) <a href="#fdroid" id="fdroid"></a>

此目录中的大多数文件由 `metascoop` App 和 `fdroid` 工具生成。一些文件无法自动生成，必须手动编辑。

#### F-Droid 存储库配置[**​**](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/#f-droid-repo-configuration) <a href="#f-droid-repo-configuration" id="f-droid-repo-configuration"></a>

F-Droid 存储库配置在 `config.yml` 文件中。这包括名称、描述和存档设置等详细信息。出于安全考虑，此文件不会被跟踪。

#### 存储库图标[**​**](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/#repository-icon) <a href="#repository-icon" id="repository-icon"></a>

F-Droid 存储库图标存储在 `fdroid/icon.png` 中。

#### 应用程序图片[**​**](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/#application-images) <a href="#application-images" id="application-images"></a>

一些应用程序元数据，如应用程序图标、功能图形和截图，未在 `repos.yml` 文件中定义，必须放置在 F-Droid 存储库的正确位置。

以下目录结构用于存储应用程序图像：

* `fdroid/repo/<app-id>/<locale>/icon.png` ：应用程序图标。
* `fdroid/repo/<app-id>/<locale>/feature-graphic.png` ：功能图形。
* 各种设备的截图。例如 `fdroid/repo/com.x8bit.bitwarden/en-US/phoneScreenshots/login-screenshot.png` 。

元数据文件结构详细信息请参阅[官方 F-Droid 文档](https://f-droid.org/zh_Hans/docs/All_About_Descriptions_Graphics_and_Screenshots/#%E4%BD%8D%E4%BA%8E-f-droid-%E5%AD%98%E5%82%A8%E5%BA%93%E4%B8%AD)。

### 测试[​](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/#testing) <a href="#testing" id="testing"></a>

#### 本地测试[**​**](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/#local-testing) <a href="#local-testing" id="local-testing"></a>

运行 `run_metascoop.sh` 和 `update_repo.sh` 脚本可以手动检查新发布并更新 F-Droid 存储库。这在进行测试时特别有帮助。

当在本地执行 `run_metascoop.sh` 时，需要存储库密钥库，因为 `fdroid update` 作为过程的一部分被执行。

可以通过在 `fdroid` 目录下运行 `fdroid init` 命令并按照提示操作来生成一个临时密钥库。这将生成具有默认值的 `config.yml` 和 `keystore.p12` 文件。

运行以下命令以生成新的密钥库和配置：

```bash
cd fdroid
fdroid init
```

{% hint style="info" %}
如果您需要访问生产环境的 F-Droid 密钥库和配置，请联系您的管理员。
{% endhint %}

{% hint style="danger" %}
请勿推送由本地生成的密钥库或配置签名的更改。

使用本地生成的密钥库或配置将强制重新生成所有元数据和重新签名仓库。这些更改仅应用于本地测试。
{% endhint %}

可以运行本地 F-Droid 服务器进行端到端测试。此类测试需要将您的计算机设置为 Web 服务器，并将整个存储库复制到 Web 根目录中。有关设置本地演示存储库的说明，请参阅[官方 F-Droid 文档](https://f-droid.org/zh_Hans/docs/Setup_an_F-Droid_App_Repo/#%E6%9C%AC%E5%9C%B0%E6%BC%94%E7%A4%BA%E5%AD%98%E5%82%A8%E5%BA%93-howto)。

{% hint style="info" %}
要从 Android 模拟器连接到您的本地存储库，请使用 `10.0.2.2` 而不是 `localhost`。
{% endhint %}

{% hint style="success" %}
对于 macOS 用户

如果使用 *nginx* 作为 Web 服务器，并且使用 Homebrew 安装，Web 根目录位于 `/opt/homebrew/var/www/`。

要启动/停止 *nginx*，请运行：

```bash
brew services start nginx
brew services stop nginx
```

默认情况下，从 *homebrew* 启动时，*nginx* 将监听端口 `8080`。从模拟器进入时，本地服务器 URL 的示例应如下所示： `http://10.0.2.2:8080/fdroid/repo`
{% endhint %}

#### 远程测试[**​**](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/#remote-testing) <a href="#remote-testing" id="remote-testing"></a>

可以从 GitHub Actions 标签页触发 `fdroid.yml` 工作流。勾选「Dry run」复选框可以在不发布更改的情况下运行工作流。默认情况下，工作流会将更改发布到 F-Droid 存储库。

## 安全[​](https://contributing.bitwarden.com/getting-started/mobile/android/f-droid/#security) <a href="#security" id="security"></a>

F-Droid 存储库使用 Bitwarden 拥有的证书进行签名。用户可将签名与 `README.md` 文件中提供的指纹进行比较，以验证存储库的有效性。


# iOS

{% hint style="info" %}
对应的[官方文档地址](https://contributing.bitwarden.com/getting-started/mobile/ios/)
{% endhint %}

## 要求[​](https://contributing.bitwarden.com/getting-started/mobile/ios/#requirements) <a href="#requirements" id="requirements"></a>

1. [Xcode](https://developer.apple.com/cn/xcode/)（版本 15.4）
2. 设置一个 iPhone 15 Pro 模拟器（iOS 17.0.1）

## 设置[​](https://contributing.bitwarden.com/getting-started/mobile/ios/#setup) <a href="#setup" id="setup"></a>

1、克隆存储库：

```sh
$ git clone https://github.com/bitwarden/ios
```

2、安装 [Mint](https://github.com/yonaskolb/mint)：

```sh
$ brew install mint
```

或者，如果您想不使用 `brew` 安装 Mint，请将 Mint 存储库克隆到临时目录中并运行 `make`：

```sh
$ git clone https://github.com/yonaskolb/Mint.git
$ cd Mint
$ make
```

3、引导项目：

```sh
$ Scripts/bootstrap.sh
```

> **注意**：因为 `Scripts/bootstrap.sh` 用于生成项目，因此每次项目配置或文件结构发生变化（例如添加、删除或移动文件）时，都需要运行 `bootstrap.sh`。 通常情况下，最佳做法是在切换分支或拉取更改时运行 `bootstrap.sh`。

或者，您可以创建 git 钩子，以便在每次 git 钩子事件发生时自动执行 `bootstrap.sh` 脚本。要使用 `Scripts` 目录中已定义的 git 钩子脚本，请将脚本复制到 `.git/hooks` 目录：

```sh
$ cp Scripts/post-merge .git/hooks/
$ cp Scripts/post-checkout .git/hooks/
```

### 运行 App <a href="#run-the-app" id="run-the-app"></a>

1. 在 Xcode 15.4+ 中打开项目。
2. 在模拟器中运行带有 `Bitwarden` 目标的 App。

### 运行测试[​](https://contributing.bitwarden.com/getting-started/mobile/ios/#running-tests) <a href="#running-tests" id="running-tests"></a>

由于 iOS 版本之间快照测试的细微差异，测试目标需要在 iPhone 15 Pro 模拟器（iOS 17.0.1）中运行。

1. 在 Xcode 的工具栏中，选择项目以及连接的设备或模拟器。
   * 用于构建的 `Generic iOS Device` 将无法用于测试。
2. 在 Xcode 的菜单栏中，选择 `Product > Test`。
   * 测试结果将出现在调试区域。如果调试区域未显示，您可以通过以下方式访问： `View > Debug Area > Show Debug Area` 。

### Linting[​](https://contributing.bitwarden.com/getting-started/mobile/ios/#linting) <a href="#linting" id="linting"></a>

本项目使用 [SwiftLint](https://github.com/realm/SwiftLint) 和 [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) 进行着色。在每次构建 `Bitwarden` 目标时，这两个工具都会以着色模式运行。不过，如果您想让 SwiftFormat 自动纠正在着色过程中发现的任何问题，可以手动运行修复命令 `mint run swiftformat`。

此外，如果您希望 SwiftFormat 在每次提交前自动更正任何问题，可以使用 git 钩子脚本。要使用 `Scripts` 目录中已定义的 git 钩子脚本，请将脚本复制到 `.git/hooks` 目录：

```sh
$ cp Scripts/pre-commit .git/hooks/
```


# .NET MAUI (legacy)

{% hint style="info" %}
对应的[官方文档地址](https://contributing.bitwarden.com/getting-started/mobile/net-maui-legacy/)
{% endhint %}

{% hint style="warning" %}
**Legacy**

在 .NET MAUI 中完成的**旧版**移动 App 入门。
{% endhint %}

## 配置 Git blame <a href="#configure-git-blame" id="configure-git-blame"></a>

我们建议您配置 Git 以忽略 Prettier 修订版本：

```bash
git config blame.ignoreRevsFile .git-blame-ignore-revs
```

## Android 开发[​](https://contributing.bitwarden.com/getting-started/mobile/net-maui-legacy/#android-development) <a href="#android-development" id="android-development"></a>

请参阅 [Android 移动 App](/getting-started/mobile/net-maui-legacy/android) 页面以设置 Android 开发环境。

## iOS 开发[​](https://contributing.bitwarden.com/getting-started/mobile/net-maui-legacy/#ios-development) <a href="#ios-development" id="ios-development"></a>

请参阅 [iOS 移动 App](/getting-started/mobile/net-maui-legacy/ios) 页面以设置 iOS 开发环境。

## watchOS 开发[​](https://contributing.bitwarden.com/getting-started/mobile/net-maui-legacy/#watchos-development) <a href="#watchos-development" id="watchos-development"></a>

请参阅  [watchOS App](/getting-started/mobile/net-maui-legacy/watchos) 页面以设置 watchOS 开发环境。

## 单元测试[​](https://contributing.bitwarden.com/getting-started/mobile/net-maui-legacy/#unit-tests) <a href="#unit-tests" id="unit-tests"></a>

{% hint style="success" %}
TL;DR;

为了运行单元测试，请在构建/运行时在 `dotnet` 命令中添加参数 `/p:CustomConstants=UT`。要进行单元测试或使用测试运行程序，请在 `Directory.Build.props` 中取消对 `CustomConstants` 行的注释。
{% endhint %}

鉴于 `Core.csproj` 是一个 MAUI 项目，其目标框架为 `net8.0-android;net8.0-ios`，而我们需要在测试中使用 `net8.0`，因此我们需要一种方法来添加该框架。`Core.Test.csproj` 将 `net8.0` 作为目标框架，因此通过添加参数 `/p:CustomConstants=UT`，我们可以将 `UT` 作为常量添加到项目中。这样，接下来会发生以下事情：

* `UT` 被添加为常量，供预编译器指令使用
* `Core.csproj` 被修改为添加 `net8.0` 作为单元测试的目标框架
* `FFImageLoading` 被移除作为参考，因为它不支持 `net8.0`。因此，现在我们有一个封装的 `CachedImage`，如果不是 `UT`，则使用库中的 `CachedImage`，如果是 `UT`，则使用带有 NOOP 实现的自定义 `CachedImage`

如果想构建测试项目，需要前往 `test/Core.Test` 并运行：

```bash
dotnet build -f net8.0 /p:CustomConstants=UT
```

要运行测试，请进入相同的文件夹并运行：

```bash
dotnet test -f net8.0 /p:CustomConstants=UT
```

最后，当工作于 `Core.Test` 项目或想要使用测试运行器时，请转到 `Directory.Build.props`（位于根目录），取消对引用 `CustomConstants` 的行的注释，以便所有内容都将相应地加载到项目中。 由于某些问题，只有在 `UT` 常量就位时，引用的项目（如 `Core`）才会包含在内。取消这行注释后，项目将被引用，可以在该项目上工作或通过测试运行器运行测试。

## 自定义常量[​](https://contributing.bitwarden.com/getting-started/mobile/net-maui-legacy/#custom-constants) <a href="#custom-constants" id="custom-constants"></a>

在构建/运行/发布时，可通过参数 `/p:CustomConstants={Value}` 使用自定义产量：

* `FDROID`：用于指示这是 F-Droid 的构建/发布版本（ [了解更多](/getting-started/mobile/net-maui-legacy/android#f-droid)）
* `UT`：在构建/运行测试项目或在这些项目之一上工作时使用（ [了解更多](#unit-tests)）

这些常量被添加到已定义的常量中，因此任何人都可以通过预编译指令在代码中使用它们。


# Android

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/mobile/net-maui-legacy/android/)
{% endhint %}

{% hint style="warning" %}
**Legacy**

在 .NET MAUI 中完成的**旧版** Android App 入门。
{% endhint %}

## 要求 <a href="#requirements" id="requirements"></a>

在开始之前，您应该已经安装了推荐的[工具和库](/getting-started/tools)。您还需要安装：

1. Visual Studio 2022 / VS Code
2. [.NET 8（最新版本）](https://dotnet.microsoft.com/zh-cn/download/dotnet/8.0)
   * 注意：即使您使用的是基于 ARM64 的 Mac（M1、M2、M3 等），也可以安装所有 x64 SDK 来运行 Android
   * 在 Mac 版 Visual Studio 中，您可能需要通过 Visual Studio > Preferences > Preview Features > Use the .NET 8 SDK 来打开 .NET 8 功能
3. .NET MAUI Workload
   * 您可以通过运行 `dotnet workload install maui` 来安装它
4. Android SDK 34
   * 您可以使用 [Visual Studio](https://learn.microsoft.com/zh-cn/previous-versions/xamarin/android/get-started/installation/android-sdk) 或 [Android Studio](https://developer.android.com/tools/releases/platforms?hl=zh-cn) 中的 SDK 管理器来安装它

要确保您是否已安装 Android SDK 和模拟器：

1. 打开 Visual Studio
2. 点击 Tools > SDK Manager（在 Android 子标题下）
3. 点击 Tools 选项卡
4. 确保已安装以下项目：
   * Android SDK 工具（至少一个版本的命令行工具）
   * Android SDK Platform-Tools
   * Android SDK 构建工具（至少一个版本）
   * Android 模拟器
5. 如果您已标记所有要安装的内容，请单击 Apply Changes

如果您遗漏了任何内容，Visual Studio 应该都会提示您。

## 安卓开发设置 <a href="#android-development-setup" id="android-development-setup"></a>

要设置新的虚拟 Android 设备用于调试：

1. 点击 Tools > Device Manager（在 Android 子标题下）
2. 点击 New Device
3. 设置您要模拟的设备 - 如果您不确定，您可以选择 Base Device 并保留默认设置
4. 然后，Visual Studio 将下载该设备的镜像。下载进度显示在 Android 设备管理器对话框的进度中。
5. 完成后，模拟的 Android 设备将在 App > Debug > {设备名称} 下作为构建目标使用

### ARM64 Mac <a href="#arm64-macs" id="arm64-macs"></a>

1. 安装并打开 Android Studio
2. 在顶部导航栏中，点击 Android Studio > Settings > Appearance & Behavior (tab) > System Settings > Android SDK
3. 在 SDK 平台选项卡中，确保选中 Show Package Details 复选框（位于右下角）
4. 在每一个 Android API 的下方，您会看到一系列系统镜像，选其中的 `ARM 64 v8a` 并等待它下载
5. 转到 View > Tool Windows > Device Manager
6. 在 Device Manager 中，使用之前下载的系统镜像创建一个设备

{% embed url="<https://contributing.bitwarden.com/assets/images/android-sdk-529574bf6a67ae6500b23196a6843ec5.png>" %}

## F-Droid[​](https://contributing.bitwarden.com/getting-started/mobile/net-maui-legacy/android/#f-droid) <a href="#f-droid" id="f-droid"></a>

在 `App.csproj` 和 `Core.csproj` 中，我们现在可以在构建/发布时传递 `/p:CustomConstants=FDROID`，以便将 `FDROID` 常量添加到项目级别的已定义常量中，我们可以使用它与预编译指令一起，例如：

```c
#if FDROID
    // perform operation only for FDROID.
#endif
```

## 构建[​](https://contributing.bitwarden.com/getting-started/mobile/net-maui-legacy/android/#building) <a href="#building" id="building"></a>

目前在 Mac 版 Visual Studio 中构建项目时存在一些问题，因此如果您遇到一些错误，请使用 CLI 构建（之前移除的 `bin/obj` 文件夹）：

```vbnet
dotnet build -f net8.0-android -c Debug
```

## 测试与调试 <a href="#testing-and-debugging" id="testing-and-debugging"></a>

### 使用 Android 模拟器 <a href="#using-the-android-emulator" id="using-the-android-emulator"></a>

在 Mac 上使用 Visual Studio 进行原生调试时，要访问 Android 模拟器中的 `localhost:<port>` 资源，您需要使用 `<http://10.0.2.2:<port>` 配置端点地址，以便访问 `localhost`，`localhost` 被设计为映射 Android 代理。

### 使用服务器隧道 <a href="#using-server-tunneling" id="using-server-tunneling"></a>

您可以使用一个[到本地服务器的代理隧道](/getting-started/server/tunnel)并让您的 App 直接连接到它，而不是配置设备或模拟器。

### 推送通知 <a href="#push-notifications" id="push-notifications"></a>

Android App 的默认配置是将自身注册到与 Bitwarden 的 QA Cloud 相同的环境中。这意味着，如果您尝试使用生产端点调试 App，您将无法接收实时同步更新或无密码登录请求。

因此，为了在调试时接收通知，您有两个选项：

* 为 Api 和身份使用 QA Cloud 端点，或
* 使用本地服务器设置，其中 Api 连接到 QA Azure Notification Hub

### 本地测试无密码 <a href="#testing-passwordless-locally" id="testing-passwordless-locally"></a>

在开始测试和调试无密码登录之前，请确保本地服务器设置运行正常（[服务器设置](/getting-started/server/guide)）。您还应该能够将 Android App 部署到您的设备或模拟器上。

{% hint style="info" %}
调试和测试无密码身份验证受到[推送通知](#push-notifications)的限制。
{% endhint %}

测试无密码通知：

1. 启动本地服务器（`Api`、`Identity`、`Notifications`）
2. 确保您的移动设备可以[连接到您的本地服务器](#using-server-tunneling)
3. 启动[网页客户端](/getting-started/clients/web-vault)，因为您需要它来发出登录请求
4. 将 Android App 部署到您的设备或模拟器
5. 部署后，打开 App，登录您的 QA 账户然后在设置中激活无密码登录请求
6. 使用您喜欢的浏览器打开网页密码库（例如：[http://localhost:8080）](https://dev.ppgg.in/getting-started/mobile/net-maui-legacy/http:/localhost:8080）)
7. 输入之前已在该设备（即「已知设备」）上进行了身份验证的账户的电子邮箱地址，然后点击「继续」。当出现登录选项时，点击「设备登录」。
8. 检查移动设备是否有通知

## AndroidX 凭据[​](https://contributing.bitwarden.com/getting-started/mobile/net-maui-legacy/android/#androidx-credentials) <a href="#androidx-credentials" id="androidx-credentials"></a>

目前， [androidx.credentials](https://developer.android.com/jetpack/androidx/releases/credentials) 官方绑定存在一些错误，我们还不能使用它。因此，我们自己实现了一个绑定，位于此处：[AndroidX.Credentials](https://github.com/bitwarden/xamarin.androidx.credentials)

目前，我们使用的是 1.2.0 版本。

在项目中，该包被添加为本地 NuGet 包，位于 `lib/android/Xamarin.AndroidX.Credentials`，该源已在 `nuget.config` 文件中配置。

如果需要更改绑定，请创建一个新的本地 NuGet 包并将其替换到上述源中。

{% hint style="danger" %}
请勿将项目添加到解决方案中，也不要将其作为项目引用添加到 `App.csproj`  /  `Core.csproj` 中，这将导致 iOS App 在启动时因解决方案配置而崩溃。尽管我们无法找到根本原因，但这就是该操作造成的影响。
{% endhint %}


# iOS

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/mobile/net-maui-legacy/ios/)
{% endhint %}

{% hint style="warning" %}
**Legacy**

在 .NET MAUI 中完成的**旧版** iOS App 入门。
{% endhint %}

## 要求 <a href="#requirements" id="requirements"></a>

1. Visual Studio 2022 / VS Code
2. [.NET 8（最新版本）](https://dotnet.microsoft.com/zh-cn/download/dotnet/8.0)
   * 在 Mac 版 Visual Studio for Mac 中，您可能需要通过 Visual Studio > Preferences > Preview Features > Use the .NET 8 SDK 来打开 .NET 8 功能
3. .NET MAUI Workload
   * 您可以通过运行 `dotnet workload install maui` 来安装它
4. 安装了 Xcode 15.0 的 Mac

## Apple 开发者账户设置 <a href="#apple-developer-account-setup" id="apple-developer-account-setup"></a>

1. 接受邀请加入 Bitwarden Apple Developer 团队。您的电子邮件中应该会收到一个请求，主题是「您被邀请加入开发团队」。点击链接「接受邀请」，系统会提示您为自己的 Bitwarden 电子邮件地址创建一个 Apple ID。如果您没有收到这封邮件，请联系 IT 部门 (@IT in slack)。接受条款和条件并完成注册流程
2. 访问 [Apple ID Online](https://appleid.apple.com/)，使用新的 Apple ID 登录。设置因素身份验证（使用手机和/或受信任的设备）-- 这一点至关重要，因为苹果不再允许没有 MFA 的「开发者」账户，但在本地构建失败时它不会告诉你这一点
3. 访问 [App Store Connect](https://appstoreconnect.apple.com/) 并接受条款和条件
4. 确保您有权访问 Bitwarden 团队和团队应用程序配置文件
5. 访问 [Apple Developer Account](https://developer.apple.com/account/)，然后转到「证书、ID 和配置文件」菜单项。检查是否能在「证书」部分看到 8bit Solutions LLC 证书，以及在「配置文件」部分看到 Bitwarden 配置文件。如果缺少其中任何一项，请向 IT 部门 (@IT #tech-support in slack) 询问附加角色/权限

## macOS 设置 <a href="#macos-setup" id="macos-setup"></a>

接下来，您需要为构建和运行 Bitwarden iOS 移动项目设置 Mac 环境。这需要创建必要的开发人员配置文件，以便在 Mac 上通过 Xcode 进行代码签名和执行。Visual Studio 有一个获取所有供应配置文件的简单过程，但在没有太多反馈的情况下很容易失败。请先尝试 Visual Studio 的说明（「简单方式」），如果需要，再使用 Xcode 的说明（「复杂方式」）。

### Visual Studio：简单方式 <a href="#visual-studio-the-easy-way" id="visual-studio-the-easy-way"></a>

1、打开 Visual Studio for Mac

2、转到 Preferences > Publishing > Apple Developer Accounts

3、点击「Add」，选择「Enterprise Account」，然后使用之前配置的 Apple 开发者账户登录

{% hint style="info" %}
如果您收到「Failed to synchronize with Apple Developer Portal」（无法与 Apple Developer Portal 同步）错误，则表示您缺少附加角色/权限。
{% endhint %}

成功登录后，您应该在列表中看到您的账户，并在账户团队列表中看到「Bitwarden Inc」

4、点击「View Details…」

5、如果您没有有效的 Apple 开发证书，请点击 Create certificate > Apple Development

6、点击「Download All Profiles」

7、您现在应该可以通过设置左上角的 `iOS > Debug | iPhone Simulator > [pick any iOS Simulator]` 然后按「Play」来运行 App 了

<figure><img src="https://contributing.bitwarden.com/assets/images/run-debug-81ba8d56bdba21767e27f03a980585ee.png" alt=""><figcaption></figcaption></figure>

如果可以运行，您可以跳到下一部分。

如果只有「Generic Simulator」（通用模拟器）选项，并提示降低「Deployment Target」（部署目标），则您的 MAUI 版本可能尚不支持您正在使用的 Xcode 版本（如[此处](https://github.com/xamarin/xamarin-macios/issues/15954#issuecomment-1246025735)所讨论）。

{% embed url="<https://contributing.bitwarden.com/assets/images/troubleshoot-generic-simulator-54e1581c5f70b5930d86347d2e940eb0.png>" %}

要解决这个问题，请尝试从 Apple [下载](https://developer.apple.com/download/all/)并安装旧版本的 Xcode（您可以从 Xamarin.iOS [发行说明](https://github.com/xamarin/xamarin-macios/releases)中查找有关使用哪个 Xcode 版本的指导）。安装新版本的 Xcode 后，重启 Visual Studio 并加载项目以验证可用的模拟器选项。

{% hint style="info" %}
如果您需要在您的开发机器上安装多个版本的 Xcode，您可以将从下载中提取的 `Xcode.app` 文件重命名为其他名称（例如「Xcode\_14\_2.app」），然后将其放入您的应用程序文件夹中。然后，您可以在命令行中使用 `xcode-select` 以在 Xcode 版本之间进行切换：

```bash
sudo xcode-select -s /Applications/Xcode_14_2.app
```

您可以使用 Xcodes.app 等工具获得类似的结果
{% endhint %}

### Xcode：复杂方式 <a href="#xcode-the-hard-way" id="xcode-the-hard-way"></a>

{% hint style="info" %}
如果您是下一个按照这些说明操作的人，请提交并上传您创建的 Xcode 项目文件，以便我们简化这一流程。
{% endhint %}

仅当上述 Visual Studio 说明不适合您时才尝试这些说明。

1、打开 Xcode

2、接受所有默认设置，确保已安装所有扩展/附加组件等

3、Create new project... > iOS > App

4、对您的新项目使用以下选项：

* 产品名称：「bitwarden」
* 团队：Bitwarden Inc（如果缺少，请仔细检查上面您的 Apple 开发者账户设置）
* 组织标识符：「com.8bit」
* 绑定标识符（自动生成）：「com.8bit.bitwarden」
* 语言：「Objective-C」
* 用户界面：「Storyboard」
* 保留所有其他复选框未选中（或取消选中它们）

<div align="left"><figure><img src="https://contributing.bitwarden.com/assets/images/new-project-options-03e83d1de2942e190f3d992d846409df.png" alt=""><figcaption></figcaption></figure></div>

5、点击「Next」，保存到默认位置，然后点击「Create」

6、在项目配置页面上，点击「Signing & Capabilities」选项卡

7、确保您具有以下默认值：

* 自动管理签名：（已选中）
* 团队：Bitwarden Inc
* 配置配置文件：Xcode 托管配置文件
* 签名证书：您的 Apple ID/Name

{% embed url="<https://contributing.bitwarden.com/assets/images/signing-and-capabilities-241bc2966623419ab15ef49a36c2a153.png>" %}

8、从菜单栏中，点击 Product > Build

9、重复步骤 3-8，并在步骤 4 中进行以下更改：

* 产品名称：「find-login-action-extension」
* 组织标识符：「com.8bit.bitwarden」
* 捆绑定标识符（自动生成）：「com.8bit.bitwarden.find-login-action-extension」

10、重复步骤 3-8，并在步骤 4 中进行以下更改：

* 产品名称：「autofill」
* 组织标识符：「com.8bit.bitwarden」
* 捆绑定标识符（自动生成）：「com.8bit.bitwarden.autofill」

11、重复步骤 3-8，并在步骤 4 中进行以下更改：

* 产品名称：「share-extension」
* 组织标识符：「com.8bit.bitwarden」
* 绑定标识符（自动生成）：「com.8bit.bitwarden.share-extension」

12、如果您有想要用于测试的物理设备（例如 iPhone 或 iPad），您还需要对刚刚创建的每个 Xcode 项目执行以下操作：

* 用电缆连接设备
* 在 Xcode 中选择您的设备作为构建目标
* 从菜单栏中，点击 Product > Build
* 如果询问，请同意注册您的设备

{% hint style="info" %}
有时这些配置文件可能会比较混乱。如果您在物理设备（或模拟器）上运行时遇到问题，请尝试运行 `rm -r ~/Library/MobileDevice/Provisioning\ Profiles` 来清除它们。再次构建每一个 Xcode 项目以重新生成它们。
{% endhint %}

## Visual Studio <a href="#visual-studio" id="visual-studio"></a>

接下来，我们需要配置您的 Visual Studio 开发环境。

{% tabs %}
{% tab title="Windows" %}

1. 连接到您刚刚完成上述步骤的 Mac
2. 打开 Visual Studio 然后点击 Tools > iOS > Pair to Mac
3. 扫描并选择您的机器。如果看不到，请点击「Add Mac...」按钮并输入 Mac 名称或 IP 地址。如果不知道 Mac 名称（或 Mac 上有 Windows 虚拟机），请进入 Mac，打开 Preferences > Sharing，查找机器的「.local」地址。
4. 出现提示时提供您的 MacOS 用户名和密码
5. 配对后，关闭「Pair Mac」窗口
6. 将活动的构建配置文件更改为 Debug > iPhoneSimulator > iOS
7. 从解决方案资源管理器重新构建 iOS 项目
8. 您现在可以使用 iOS 模拟器进行调试了
   {% endtab %}

{% tab title="macOS" %}

1. 检查是否安装了命令行工具：
   * 打开 Xcode
   * 从菜单栏中，点击 Xcode > Preferences > Locations
   * 确保在「Command Line Tools」下选择了 Xcode 版本
2. 打开 Visual Studio for Mac
3. 打开本地移动存储库根目录中的移动解决方案文件 (`bitwarden-mobile.sln`)
4. 在顶部栏中，您应该能够选择 App > Debug > 选择您的模型然后点击「Run」（如果您设置了物理设备，则选择您的物理设备）
   {% endtab %}
   {% endtabs %}

## 构建 <a href="#building" id="building"></a>

要从 CLI 构建，请导航到应用程序目录：

对于设备：

```vbnet
cd src/App
dotnet build -f net8.0-ios -c Debug -r ios-arm64
```

对于模拟器：

```vbnet
cd src/App
dotnet build -f net8.0-ios -c Debug -r iossimulator-x64
```

您也可以使用 IDE，但请注意：

{% hint style="success" %}
**Visual Studio for Mac**

目前在 Visual Studio for Mac 上构建项目时存在一些问题，因此如果您遇到一些错误，请使用 CLI 在 `src/App` 文件夹中构建（请先移除 bin/obj 文件夹）。
{% endhint %}

{% hint style="success" %}
**Argon2Id**

如果在为模拟器构建时发现有关 argon2Id 库的任何错误，请确保您正在为运行时标识符 `iossimulator-x64` 构建，因为目前该库不支持 `iossimulator-arm64`。
{% endhint %}

{% hint style="success" %}
**常见错误的故障排除**

如果您发现以下错误：

> `error NETSDK1134`：不支持使用特定 RuntimeIdentifier 构建解决方案。如果您想发布单个 RID，请在单个项目级别指定 RID

您几乎肯定是在尝试从根目录构建 App。 请转到 `src/App`，然后重新尝试构建。
{% endhint %}

### Argon2Id 库加载[​](https://contributing.bitwarden.com/getting-started/mobile/net-maui-legacy/ios/#argon2id-library-loading) <a href="#argon2id-library-loading" id="argon2id-library-loading"></a>

几乎所有解决方案项目都使用 `MTouchExtraArgs` 加载 Argon2Id 库 (`libargon2.a`)。为了简化这一过程，我们在 **Directory.Build.props** 中添加了一个名为 `Argon2IdLoadMtouchExtraArgs` 的属性，其中包含填写额外 args 参数的代码。每个项目都配置了该属性，因此只需在正确的运行时标识符上添加该属性，我们就能在每种情况下成功构建 App。

### 忽略扩展/watchOS App[​](https://contributing.bitwarden.com/getting-started/mobile/net-maui-legacy/ios/#ignoring-extensions--watchos-app) <a href="#ignoring-extensions--watchos-app" id="ignoring-extensions--watchos-app"></a>

有时，我们需要快速构建 App，或者 iOS 扩展或 watchOS App 上的一些配置会妨碍我们。为了让我们能快速只关注主 App，我们在 **Directory.Build.Props** 中添加了两个属性来帮助解决这个问题：

* `IncludeBitwardeniOSExtensions`：如果为 `True`，则在构建主 App 时将包含所有 iOS 扩展，否则将跳过。
* `IncludeBitwardenWatchOSApp`：如果为 `True`，则在构建主 App 时将包含 watchOS App，否则将跳过。

{% hint style="warning" %}
**共享代码**

关闭这些选项可以提供更快的开发者体验，这在很多情况下都非常有用，但请始终牢记，主 App 和扩展程序之间共享了很多内容，因此在推送您的工作之前，请在启用所有选项的情况下再次进行测试，以防万一。
{% endhint %}

### 本地发布模式[​](https://contributing.bitwarden.com/getting-started/mobile/net-maui-legacy/ios/#release-mode-locally) <a href="#release-mode-locally" id="release-mode-locally"></a>

有些问题需要我们在 **Release** 配置上构建 App，但不通过 CI/CD 管道，而是在本地构建。问题是我们没有本地分发的代码签名详细信息。为了解决这个问题，我们可以在 **Release** 配置上使用与 **Debug** 相同的 `CodesignProvision` 和 `CodesignKey`。但要在每个项目上都更改这两个属性有点麻烦，因此我们在 **Directory.Build.Props** 中添加了两个属性来帮助解决这个问题：

* `ReleaseCodesignProvision`：`CodesignProvision` 在所有项目中的 Release 配置
* `ReleaseCodesignKey`：`CodesignKey` 在所有项目中的 Release 配置

通过替换它们的值，所有项目都将应用这些值，这样在本地以 **Release** 模式构建 App 就更容易了。

## 调试 <a href="#debugging" id="debugging"></a>

### iPhone 模拟器 <a href="#iphone-simulator" id="iphone-simulator"></a>

iPhone 模拟器可以访问 localhost，您可以像往常一样将客户端指向本地开发服务器。不过，App 默认需要使用 https。要允许使用 http 进行测试，请按以下步骤操作。

1、在 Visual Studio Code 或其他编辑器中打开 `src/iOS/Info.plist` ，以便可以编辑原始 XML。（不要使用 Visual Studio 中的「Property List Editor」）

2、在顶层 `<dict>` 元素中添加以下代码：

```xml
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <false/>
    <key>NSExceptionDomains</key>
    <dict>
        <key>localhost</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSIncludesSubdomains</key>
            <true/>
        </dict>
    </dict>
</dict>
```

3、保存并退出 `Info.plist`

4、在启动之前按 `Command` + `B` 强制运行新的构建

5、不要推送这些更改 :)

### iPhone 设备 <a href="#iphone-device" id="iphone-device"></a>

该设备无法直接访问 Mac 的 localhost，因此您可以遵循[本指南](https://ymoondhra.medium.com/how-to-run-localhost-on-your-iphone-4110a54d1896)来连接它们。

完成此操作后，您还必须修改 `Info.plist` 以允许 http 用于测试，如之前在模拟器测试中所解释的那样。

您也很可能需要更改服务器上每个项目的 `Properties` 上的 `launchSettings.json`。在那里，您需要更改 `iisSettings -> iisExpress` 和 `profiles -> Identify` 的 `applicationUrl` ，以便它显示 `name.local` 而不是 `localhost`，其中 `name` 是您在 Mac 共享配置中设置的计算机名称。

在实际测试 App 之前，请打开浏览器并尝试通过访问 `http://name.local:4000/alive` 以连接到 `Api` 。如果这不起作用，请查看指南或服务器配置中的步骤。确保您的 `User secrets` 也是最新的。

最后，您必须在手机上配置 `Api` 和 `Identity` URL 才能使用 `http://name.local:4000` 和 `http://name.local:33656` ，其中 `name` 是您在 Mac 共享配置中设置的计算机名称。

### iOS 扩展 <a href="#ios-extensions" id="ios-extensions"></a>

1. 将 iOS 扩展项目设置为启动项目
2. 您将收到一个弹出窗口，显示「Waiting for the debugger to connect...」（正在等待调试器连接...）
3. 不要打开 Bitwarden App（否则调试器将连接到它而不是连接到扩展）。相反，触发扩展
4. 您的扩展断点现在应该被命中

例如：如果您想调试 **iOS.Autofill** 扩展，您需要完成步骤 1-3，然后转到您的 iOS 设备，打开浏览器，登录，点击钥匙图标并从底部弹出窗口打开 Bitwarden。

### 使用服务器隧道​ <a href="#using-server-tunneling" id="using-server-tunneling"></a>

您应该使用一个[到本地服务器的代理隧道](/getting-started/server/tunnel)，让您的 App 直接与之连接，而不是将设备或模拟器配置为忽略 SSL 证书。

### 推送通知（实时同步和无密码）​ <a href="#push-notifications-live-sync--passwordless" id="push-notifications-live-sync--passwordless"></a>

推送通知当前不可用于调试部署。它们仅在 TestFlight 和生产版本上受支持。


# watchOS

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/mobile/net-maui-legacy/watchos/)
{% endhint %}

{% hint style="warning" %}
**Legacy**

在 .NET MAUI 中完成的**旧版** watchOS App 入门。
{% endhint %}

## 要求​ <a href="#requirements" id="requirements"></a>

按照 [iOS 设置](/getting-started/mobile/net-maui-legacy/ios)进行操作。

为了使一切正常工作，需要使用**设备**。在模拟器上，同步将无法正常工作（某些其他部分可能也无法正常工作）。此外，尽可能启用蓝牙功能，以简化设备间的同步和调试通信。

建议同时阅读 [watchOS 架构](/architecture/mobile-clients/net-maui-legacy/watchos)。

## macOS 设置​ <a href="#macos-setup" id="macos-setup"></a>

按照 iOS 的 macOS 设置，在 XCode 中打开项目时，它将自动设置配置文件，因此无需额外配置。

## 调试​ <a href="#debugging" id="debugging"></a>

可以从两个位置进行调试：

* 从 Visual Studio for Mac（iOS App）
* 从 XCode（watchOS App）

目前，无法同时调试两个 App（iOS 和 watchOS），因为从 Xamarin 中无法访问 watchOS App 的调试信息，并且从 XCode 中，iPhone 上安装了一个 iOS 存根 App 来对其进行调试。因此，在调试时需要选择要了解哪一部分的信息，从而确定是从 VS4M 还是从 XCode 进行调试。

{% hint style="warning" %}
从 XCode 进行调试时，Xamarin iOS App 将被替换为来自 XCode 的存根 App。因此 iOS App 上的任何配置都将丢失（例如服务器 URL）

从 VS4M 进行调试时，请在两次构建之间从 Apple Watch 上卸载之前的 watchOS App（如果有的话），以使其始终保持最新（如果不卸载以前的 watchOS App，有时它就不会更新）
{% endhint %}

### 构建 <a href="#building" id="building"></a>

鉴于 Xamarin iOS App 需要 Xcode 构建的输出，因此需要先从 XCode 构建 watchOS App，然后再从 VS4M 构建 iOS App，以便在设备上运行。

XCode 构建的输出存储在与 `iOS.csproj` 中配置的附近位置非常相似的位置：

```xml
<PropertyGroup>
    <WatchAppBuildPath Condition=" '$(Configuration)' == 'Debug' ">$(Home)/Library/Developer/Xcode/DerivedData/bitwarden-cbtqsueryycvflfzbsoteofskiyr/Build/Products</WatchAppBuildPath>
```

每一台 Mac 上的文件夹 `bitwarden-cbtqsueryycvflfzbsoteofskiyr` 很可能都不相同。因此，我们需要将 `iOS.csproj` 中的这一部分更改为 XCode 在本地自动创建的部分。

要知道路径确切是什么：在 XCode 中打开项目 -> 转到 Product -> 在 Finder 中 Show Build Folder。

*这一点需要改进，以便有一个固定的位置，或者有一个更简单的方法来自动获取它。*

{% hint style="warning" %}
需要特别注意两个 IDE 上的目标平台相同。因此，在设备上运行时，在 XCode 和 VS4M 上构建时都要以设备为目标，这样才能正确绑定 watchOS App。此外，还需要确保选择了「bitwarden WatchKit app」方案。
{% endhint %}

### 同步​ <a href="#synchronization" id="synchronization"></a>

由于上述原因，无法同时完全调试同步。

因此，我们只能调试一端 (iOS) 或另一端 (watchOS)。

如果需要「同时」检查两端的某些内容（如检查消息未发送/到达的原因），则需要使用控制台日志或将 Xamarin 代码的一部分调整到 Xcode 上的 iOS 存根 App，并从 XCode 调试同步。


# SDK

{% hint style="info" %}
对应的[官方文档地址](https://contributing.bitwarden.com/getting-started/sdk/)
{% endhint %}

Bitwarden 为 [Secrets Manager](https://bitwarden.com/products/secrets-manager/) 提供了公共软件开发工具包 (SDK)，并为 Bitwarden [Password Manager](https://bitwarden.com/) 提供了内部 SDK。这些 SDK 使用 Rust 编写，并为多种语言提供绑定。

* [内部 SDK](/getting-started/sdk/internal-sdk)
* [Secrets Manager SDK](/getting-started/sdk/secrets-manager)


# 内部 SDK

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/sdk/internal/)
{% endhint %}

有关更深入的文档，请查阅 [SDK 架构](/architecture/sdk)和内部 SDK 项目的 [`README`](https://github.com/bitwarden/sdk-internal)。

## 要求[​](https://contributing.bitwarden.com/getting-started/sdk/internal/#requirements) <a href="#requirements" id="requirements"></a>

* [Rust](https://www.rust-lang.org/tools/install) 最新稳定版本 -（建议通过 [rustup](https://rustup.rs/) 安装）
* NodeJS 和 NPM。

更多信息请参阅[工具和库](/getting-started/tools)页面。

## 设置说明[​](https://contributing.bitwarden.com/getting-started/sdk/internal/#setup-instructions) <a href="#setup-instructions" id="setup-instructions"></a>

1. 克隆存储库：

   ```bash
   git clone https://github.com/bitwarden/sdk-internal.git
   cd sdk
   ```
2. 安装依赖：

   ```bash
   npm ci
   ```

## 构建 SDK[​](https://contributing.bitwarden.com/getting-started/sdk/internal/#building-the-sdk) <a href="#building-the-sdk" id="building-the-sdk"></a>

SDK 为不同的平台构建，每个平台都有自己的构建说明。有关如何为特定平台构建的更多信息，请参阅不同 crate 的 readme：

* **Web** ： [`crates/bitwarden-wasm-internal`](https://github.com/bitwarden/sdk-internal/tree/main/crates/bitwarden-wasm-internal)
* **iOS**: [`crates/bitwarden-uniffi/swift`](https://github.com/bitwarden/sdk-internal/tree/main/crates/bitwarden-uniffi/swift)
* **Android** : [`crates/bitwarden-uniffi/kotlin`](https://github.com/bitwarden/sdk-internal/tree/main/crates/bitwarden-uniffi/kotlin)

请注意，每个平台都有自己的依赖项，它们需要在构建之前安装。如果遇到任何问题，请务必再次检查 readme。

## 链接 SDK 到客户端[​](https://contributing.bitwarden.com/getting-started/sdk/internal/#linking-the-sdk-to-clients) <a href="#linking-the-sdk-to-clients" id="linking-the-sdk-to-clients"></a>

在修改 SDK 之后，测试客户端应用程序中的更改可能是有益的。为此，您需要更新客户端应用程序中的 SDK 引用。

这些说明假设您有一个类似以下目录结构：

```
sdk/
clients/
ios/
android/
```

### 网页客户端[​](https://contributing.bitwarden.com/getting-started/sdk/internal/#web-clients) <a href="#web-clients" id="web-clients"></a>

网页客户端使用 NPM 将 SDK 作为依赖项安装。NPM 提供了专门的 [`link`](https://docs.npmjs.com/cli/v9/commands/npm-link) 命令，用于将软件包临时替换为本地版本。

```bash
npm link ../sdk-internal/crates/bitwarden-wasm-internal/npm
```

{% hint style="danger" %}
运行 `npm ci` 或 `npm install` 将使用已发布的版本替换链接的包。
{% endhint %}

### 移动端[​](https://contributing.bitwarden.com/getting-started/sdk/internal/#mobile) <a href="#mobile" id="mobile"></a>

#### **Android**

1. 在本地 Maven 仓库中构建和发布 SDK：

   ```
   ../sdk-internal/crates/bitwarden-uniffi/kotlin/publish-local.sh
   ```
2. 在 `user.properties` 文件中设置用户属性 `localSdk=true`。

#### iOS[​](https://contributing.bitwarden.com/getting-started/sdk/internal/#ios)

使用设置为 true 的 `LOCAL_SDK` 环境变量运行 bootstrap 脚本，以使用本地 SDK 构建：

```bash
LOCAL_SDK=true ./Scripts/bootstrap.sh
```


# Secrets Manager

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/)
{% endhint %}

本章节包含 Bitwarden Secrets Manager CLI、语言包装器和基于 SDK 集成的开发信息。

有关更深入的文档，请查阅 [SDK 架构](/architecture/sdk)和 Secrets Manager SDK 项目的 [`README`](https://github.com/bitwarden/sdk-sm)。

## 要求[​](https://contributing.bitwarden.com/getting-started/sdk/internal/#requirements) <a href="#requirements" id="requirements"></a>

* [Rust](https://www.rust-lang.org/tools/install) 最新稳定版本 -（建议通过 [rustup](https://rustup.rs/) 安装）
* NodeJS 和 NPM。

更多信息请参阅[工具和库](/getting-started/tools)页面。

## 设置说明[​](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/#setup-instructions) <a href="#setup-instructions" id="setup-instructions"></a>

1. 克隆存储库：

   ```bash
   git clone https://github.com/bitwarden/sdk-sm.git
   cd sdk
   ```
2. 安装依赖：

   ```bash
   npm ci
   ```

## 构建 SDK[​](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/#building-the-sdk) <a href="#building-the-sdk" id="building-the-sdk"></a>

要构建 SDK，请运行以下命令：

```bash
cargo build
```

## 网页客户端[​](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/#web-client) <a href="#web-client" id="web-client"></a>

要启动网页客户端，请按照[网页密码库设置说明](/getting-started/clients/web-vault)，创建一个启用了 Secrets Manager 的组织。然后使用组织切换器进入 Secrets Manager。

{% embed url="<https://contributing.bitwarden.com/assets/images/sm-product-switcher-7b3de19f3d4c8cacc95558f947ced76f.png>" %}

{% hint style="info" %}
如果您已经为您的组织启用了 Secrets Manager，但在产品切换器中看不到 Secrets Manager，您可能需要为您的用户手动启用它，方法是进入**管理控制台** -> **成员** -> 为您的用户选中「此用户可以访问 Secrets Manager」。

<img src="https://contributing.bitwarden.com/assets/images/enable-sm-4f772badfe704c8e7998c61042ff3e3c.png" alt="" data-size="original">
{% endhint %}


# Integrations


# Kubernetes

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/integrations/kubernetes)
{% endhint %}

Bitwarden Secrets Manager Kubernetes Operator (`sm-operator`) 是一个工具，用于帮助团队无缝地将 Bitwarden Secrets Manager 集成到他们的 Kubernetes 工作流中。

sm-operator 使用[控制器](https://github.com/bitwarden/sm-kubernetes/blob/main/internal/controller/bitwardensecret_controller.go)将 Bitwarden Secrets 同步到 Kubernetes secrets 中。它的做法是将 BitwardenSecret 的自定义资源定义注册到集群中。它会监听群集上已注册的新 BitwardenSecrets，然后按可配置的时间间隔进行同步。

{% hint style="success" %}
如果您是 Secrets Manager 的新手，您应该首先[阅读帮助中心文档](https://bitwarden.com/help/secrets-manager-overview/)，以了解其工作原理。
{% endhint %}

## 要求[​](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/integrations/kubernetes#requirements) <a href="#requirements" id="requirements"></a>

{% hint style="success" %}
大部分所需的设置都可以使用 Visual Studio Code Dev Containers 来完成。这是推荐的方案，尤其是对于 macOS 和 Windows 用户。
{% endhint %}

{% tabs %}
{% tab title="Dev 容器" %}

* [Visual Studio Code](https://code.visualstudio.com/)
* [Docker](https://www.docker.com/)
* [Visual Studio Code Dev Containers Extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)

{% hint style="warning" %}
如果您使用的是除 Docker 以外的容器引擎，开发容器将无法正常工作。
{% endhint %}
{% endtab %}

{% tab title="直接设置" %}

* [Visual Studio Code](https://code.visualstudio.com/)
* [Visual Studio Code Go Extension](https://marketplace.visualstudio.com/items?itemName=golang.go)
* [Go](https://go.dev/dl/) 版本 1.21
* [musl-gcc](https://wiki.musl-libc.org/getting-started.html)
* [Make](https://www.gnu.org/software/make/)
* [kubectl](https://kubernetes.io/docs/tasks/tools/)
* [Docker](https://www.docker.com/) 或 [Podman](https://podman.io/) 或其他容器引擎
* [Kind Cluster](https://kind.sigs.k8s.io/docs/user/quick-start/) 或将 Kubectl 指向它作为本地开发的当前上下文的其他 Kubernetes 集群。
  {% endtab %}
  {% endtabs %}

您还需要：

* 一个[具有 Secrets Manager 的 Bitwarden 组织](https://bitwarden.com/help/sign-up-for-secrets-manager/) 。需要您的组织 ID GUID。
* Secrets Manager 机器账户（以前称为服务账户）的[访问令牌](https://bitwarden.com/help/access-tokens/)，与您要提取的项目绑定。

## 设置和配置[​](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/integrations/kubernetes#setup-and-configuration) <a href="#setup-and-configuration" id="setup-and-configuration"></a>

克隆存储库：

```
git clone https://github.com/bitwarden/sm-kubernetes.git
```

在存储库根目录下打开 Visual Studio Code。

{% tabs %}
{% tab title="Dev 容器" %}
开发容器的设置是自动化的。这将创建一个 Kind 集群并设置所有必要的软件。

* 打开命令调板（根据用户设置，使用 `Cmd/Ctrl`+`Shift`+`P` 或 `F1`）
* 输入 `Dev Containers: Reopen in Container` 以开始
  {% endtab %}

{% tab title="直接设置" %}
{% hint style="success" %}
请注意，如果通过路径中任意位置的符号链接打开工作区，Visual Studio Code 的 Go 调试器将无法正常工作。要使调试正常工作，应从软件源的完整路径打开。
{% endhint %}

* 安装直接设置要求中所列出的所有要求
* 使用 `kind create cluster` 创建 Kind 群集
* 运行 `make setup` 创建默认的 .env 文件。
  {% endtab %}
  {% endtabs %}

### 配置设置[​](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/integrations/kubernetes#configuration-settings) <a href="#configuration-settings" id="configuration-settings"></a>

创建了 Dev 容器或运行了 `make setup` 后，就会在存储库根目录下创建一个 `.env` 文件。可以更新以下环境变量设置来改变 operator 的行为：

* **BW\_API\_URL** - 设置 Secrets Manager SDK 使用的 Bitwarden API URL。这对于自托管场景以及访问欧洲服务器非常有用。
* **BW\_IDENTITY\_API\_URL** - 设置 Secrets Manager SDK 使用的 Bitwarden Identity 服务 URL。这对于自托管场景以及访问欧洲服务器非常有用。
* **BW\_SECRETS\_MANAGER\_STATE\_PATH** - 设置 Secrets Manager SDK 存储其状态文件的基本路径。
* **BW\_SECRETS\_MANAGER\_REFRESH\_INTERVAL** - 指定 Secrets Manager 和 K8s secrets 之间同步秘密的刷新间隔（以秒为单位）。最小值为 180。

## 运行和调试[​](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/integrations/kubernetes#running-and-debugging) <a href="#running-and-debugging" id="running-and-debugging"></a>

1. 使用 `make install` 或通过在命令面板中使用来自「Tasks: Run Task」的被称为「apply-crd」的 Visual Studio 任务，将自定义资源定义安装到群集中。
2. 要调试代码，只需按一下 F5。您还可以在命令行中使用 `make run` 来运行，而无需调试。，

{% hint style="success" %}
您也可以通过运行以下命令（不使用调试器）一步到位：`make install run`
{% endhint %}

### 卸载自定义资源定义[​](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/integrations/kubernetes#uninstall-custom-resource-definition) <a href="#uninstall-custom-resource-definition" id="uninstall-custom-resource-definition"></a>

要从集群中删除 CRD：

```bash
make uninstall
```

{% hint style="success" %}
运行 `make --help` 获取所有潜在 `make` 目标的更多信息。
{% endhint %}

### 创建 BitwardenSecret 用于测试[​](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/integrations/kubernetes#create-a-bitwardensecret-to-test) <a href="#create-a-bitwardensecret-to-test" id="create-a-bitwardensecret-to-test"></a>

调试器运行中，我们现在将创建一个 BitwardenSecret 对象，以将 Secret Manager 机密同步到 K8s 机密中：

{% hint style="warning" %}
通过 kubectl 创建下面的授权令牌机密后，您的授权令牌就会出现在机器终端历史记录中。对于生产系统，请考虑使用 CSI 机密驱动程序或通过一个短暂的构建代理应用该机密。
{% endhint %}

{% hint style="success" %}
确保在 VS Code 的 Dev 容器终端中运行以下命令。
{% endhint %}

1. 在创建 BitwardenSecret 对象的命名空间中创建一个秘密，用于存放 Secrets Manager 身份验证令牌：`kubectl create secret generic bw-auth-token -n <some-namespace> --from-literal=token="<Auth-Token-Here>"`

{% hint style="success" %}
要获取命名空间列表，请运行 `kubectl get namespaces`
{% endhint %}

2. 安装 BitwardenSecret 实例。[config/samples/k8s\_v1\_bitwardensecret.yaml](https://github.com/bitwarden/sm-kubernetes/blob/main/config/samples/k8s_v1_bitwardensecret.yaml) 中有一个示例。您需要复制这个示例并根据自己的需要进行更新。然后按照这个方式应用：`kubectl apply -n <some-namespace> -f k8s_v1_bitwardensecret.yaml`
3. 在调试控制台窗口中，您应该看到表示机密已启动并完成同步的的消息
4. 运行以下命令查看是否已创建了机密：`kubectl get secrets -n <some-namespace>`
5. 运行以下命令查看已同步机密的结构和数据：`kubectl get secret -n <some-namespace> <secret-name> -o yaml`

{% hint style="success" %}
机密值以 Base64 编码的字符串形式存储。
{% endhint %}

#### BitwardenSecret 清单[**​**](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/integrations/kubernetes#bitwardensecret-manifest) <a href="#bitwardensecret-manifest" id="bitwardensecret-manifest"></a>

将 BitwardenSecret 对象视为 operator 用来创建和同步 Kubernetes 机密的同步设置。这个 Kubernetes 机密将位于命名空间内，并将注入 Secrets Manager 机器账户（以前称为服务账户）可用的数据。生成的 Kubernetes 机密将包括特定机器账户可以访问的所有机密。示例文件 ([config/samples/k8s\_v1\_bitwardensecret.yml](https://github.com/bitwarden/sm-kubernetes/blob/main/config/samples/k8s_v1_bitwardensecret.yaml)) 提供了 BitwardenSecret 清单的基本结构。下面列出了需要更新的关键设置：

* **metadata.name**：您要部署的 BitwardenSecret 对象的名称
* **spec.organizationId**：您要从其中提取 Secrets Manager 数据的 Bitwarden 组织 ID
* **spec.secretName**：将创建并注入 Secrets Manager 数据的 Kubernetes 机密的名称。
* **spec.authToken**：BitwardenSecrets 对象部署到的 Kubernetes 名称空间中的机密名称，其中包含用于访问机密的 Secrets Manager 机器账户授权令牌。

Secrets Manager 不保证跨项目机密名称的唯一性，因此默认情况下，机密将以 Secrets Manager 机密 UUID 作为键创建。为了使生成的机密更容易使用，您可以创建一个 Bitwarden 机密 ID 到 Kubernetes 机密键的映射。生成的机密将用您提供的映射友好名称替换 Bitwarden 机密 ID。以下是可以使用的映射设置：

* **bwSecretId**：这是 Secrets Manager 中机密的 UUID。可以在 Secrets Manager 门户网站或使用 [Bitwarden Secrets Manager CLI](https://github.com/bitwarden/sdk-sm/releases) 在机密名称下找到
* **secretKeyName**：在 Kubernetes 机密中生成的键，用于替换 UUID

注意，自定义映射仅作为信息目的在已生成的机密中提供，可在 `k8s.bitwarden.com/custom-map` 注解中找到。

## 测试 Docker 镜像[​](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/integrations/kubernetes#testing-the-docker-image) <a href="#testing-the-docker-image" id="testing-the-docker-image"></a>

Windows 上的 Kind 很难从本地注册表中提取数据，因此我们提供了两种不同的路径来部署映像。

{% tabs %}
{% tab title="使用注册表" %}

1. 构建并推送您的镜像到由 `IMG` 指定的注册表位置（本地或其他位置）： `make docker-build docker-push IMG=<some-registry>/sm-operator:tag`
2. 使用 `IMG` 指定的镜像将控制器部署到集群中：`make deploy IMG=<some-registry>/sm-operator:tag`
   {% endtab %}

{% tab title="推送到 Kind" %}

1. 直接使用 Visual Studio Code 命令面板构建并推送镜像到 Kind。打开面板（按 F1），选择「Tasks: Run Task」，然后选择「docker-build」和「kind-push」。
2. 使用 Visual Studio Code 命令面板部署 Kubernetes 对象到 Kind。打开面板（按 F1），选择「Tasks: Run Task」，然后选择「deploy」。
   {% endtab %}
   {% endtabs %}

{% hint style="success" %}
在使用容器时，可通过更新 [config/manager/manager.yaml](https://github.com/bitwarden/sm-kubernetes/blob/main/config/manager/manager.yaml) 中的环境变量对 URL、刷新间隔和状态路径进行自定义配置。
{% endhint %}

安装后，根据本文档中之前所述创建您的 K8s 授权令牌机密和 BitwardenSecret 以进行测试。

### 查看 Pod 日志[​](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/integrations/kubernetes#viewing-pod-logs) <a href="#viewing-pod-logs" id="viewing-pod-logs"></a>

要查看通过上述步骤部署的 operator 日志：

1. 运行 `kubectl get pods -n sm-operator-system` 。这将获取已安装的 operator pod 的名称。
2. 运行 `kubectl logs -n sm-operator-system <name-of-pod-from-previous-step>`

### 卸载控制器[​](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/integrations/kubernetes#undeploy-controller) <a href="#undeploy-controller" id="undeploy-controller"></a>

要从集群中移除已安装的控制器 pod，请运行：

```bash
make undeploy
```

## 单元测试[​](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/integrations/kubernetes#unit-testing) <a href="#unit-testing" id="unit-testing"></a>

单元测试目前位于以下文件中：

* internal/controller/suite\_test.go
* cmd/suite\_test.go

要运行单元测试，请运行 `make test`。要调试单元测试，请打开您想调试的文件。在 Visual Studio Code 的 「Run and Debug」选项卡中，将启动配置从「Debug」更改为「Test current file」，然后按 F5。

{% hint style="success" %}
在调试测试之前，您需要运行 `make test`，这将设置一些必要的软件以进行 operator 测试。
{% endhint %}

{% hint style="warning" %}
目前不支持使用 Visual Studio Code 的「Testing」选项卡。
{% endhint %}

## 修改 API 定义[​](https://contributing.bitwarden.com/getting-started/sdk/secrets-manager/integrations/kubernetes#modifying-the-api-definitions) <a href="#modifying-the-api-definitions" id="modifying-the-api-definitions"></a>

如果您正在通过 [api/v1/bitwardensecret\_types.go](https://github.com/bitwarden/sm-kubernetes/blob/main/api/v1/bitwardensecret_types.go) 编辑 API 定义，请使用以下命令重新生成清单：

```bash
make manifests
```

更多信息请参阅 [Kubebuilder 文档](https://book.kubebuilder.io/introduction.html)。


# 业务 App


# 目录连接器

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/business/directory-connector/)
{% endhint %}

Bitwarden 目录连接器是一个桌面应用程序，用于将您的 Bitwarden 企业组织同步到现有的用户和群组目录。

目录连接器接收的更新少于主客户端。为了降低维护成本，它拥有自己的共享 Javascript 库（以前称为 jslib）副本，位于 `jslib` 子目录中。

## 要求 <a href="#requirements" id="requirements"></a>

* [Node.js](https://nodejs.org/) v18 (LTS)
* Windows 用户：要编译应用程序中使用的本机节点模块，您需要 Visual C++ 工具集，可通过标准 Visual Studio 安装程序（推荐）或通过 `npm` 安装 `windows-build-tools` 获得。在[编译本机组件模块](https://github.com/Microsoft/nodejs-guidelines/blob/master/windows-environment.md#compiling-native-addon-modules)中查看更多信息。

## 构建说明 <a href="#build-instructions" id="build-instructions"></a>

1、克隆存储库：

```bash
git clone https://github.com/bitwarden/directory-connector.git
```

2、安装依赖项：

```bash
cd directory-connector
npm ci
```

3、运行应用程序：

{% tabs %}
{% tab title="GUI" %}

```bash
npm run electron
```

{% endtab %}

{% tab title="CLI" %}

```bash
npm run build:cli:watch
```

然后，从 `./build-cli` 文件夹运行命令：

```bash
cd ./build-cli

node ./bwdc.js --help

# Test sync
node ./bwdc.js test

# Real sync
node bwdc.js sync
```

{% endtab %}
{% endtabs %}

## 从目录服务同步 <a href="#syncing-from-a-directory-service" id="syncing-from-a-directory-service"></a>

要正确测试目录连接器，您需要一个目录来同步。我们有相关的设置说明：

* 一个 [Open LDAP Docker 镜像](/getting-started/business/directory-connector/open-ldap)（推荐）
* [JumpCloud](/getting-started/business/directory-connector/jumpcloud)

这些都是 LDAP 目录服务。如果您需要测试另一种类型，您应该能够找到提供该免费层级的服务的平台。


# JumpCloud

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/business/directory-connector/jumpcloud)
{% endhint %}

[JumpCloud](https://jumpcloud.com/) 提供了带有免费层级的 LDAP 即服务，可用于测试。

{% hint style="info" %}
JumpCloud 免费层仅限于 10 个用户，而且您不会得到 [OpenLDAP](/getting-started/business/directory-connector/open-ldap) 设置中的那种漂亮的预生成数据。
{% endhint %}

## 设置 JumpCloud <a href="#setup-jumpcloud" id="setup-jumpcloud"></a>

1. 创建一个 JumpCloud 账户并登录。
2. 创建一个用户并将该用户绑定到一个目录。应该有一个可以用于此的默认目录，称为 JumpCloud LDAP。有关说明，请参阅 [JumpCloud 帮助文档](https://support.jumpcloud.com/support/s/article/using-jumpclouds-ldap-as-a-service1#createuser)。
3. 创建一个管理员用户并将该用户绑定到同一目录。您将使用此用户验证 JumpCloud 目录连接器。

## 配置目录连接器 <a href="#configure-directory-connector" id="configure-directory-connector"></a>

1. 运行 Directory Connector Electron 应用程序（请参阅[构建说明](/getting-started/business/directory-connector#build-instructions)）。
2. 使[用组织 API 密钥](https://help.ppgg.in/organizations/bitwarden-public-api#authentication)登录。
3. 使用下面的配置设置。

### 目录设置 <a href="#directory-settings" id="directory-settings"></a>

对于这些设置，需要您的 JumpCloud 组织 ID。您可以在 JumpCloud Admin Console → User Authentication → LDAP → \[your LDAP server] 中找到它。

* **Type**: Active Directory / LDAP
* **Server Hostname**: ldap.jumpcloud.com
* **Server Port**: 636
* **Root Path**: o=\[Your JumpCloud Organization ID],dc=jumpcloud,dc=com
* **This server uses Active Directory:** \[unchecked]
* **This server pages search results:** \[unchecked]
* **This server uses an encrypted connection:** \[checked]
  * **Use SSL** \[checked]
  * **Do not verify server certificates** \[checked]
* **Username**: uid=\[Admin User],ou=Users,o=\[Your JumpCloud organization ID],dc=JumpCloud,dc=com
* **Password**: \[Admin User's password]

### 同步设置 <a href="#sync-settings" id="sync-settings"></a>

* **Sync Users**: \[checked]
* **User Path**: ou=Users,o=\[Your JumpCloud Organization ID]
* **User Object Class**: inetOrgPerson
* **User Email Attribute**: mail
* **Sync Groups**: \[checked]
* **Group Path**: o=\[Your JumpCloud Organization ID]
* **Group Object Class**: groupOfNames
* **Group Name Attribute**: memberOf

## 同步 <a href="#sync" id="sync"></a>

{% hint style="warning" %}
当您进行真正的同步时，邀请电子邮件将发送给所有已同步的用户。确保您使用的是 [Mailcatcher](/getting-started/server/guide#mailcatcher)，这样您就不会发送实时电子邮件。
{% endhint %}

1. 点击目录连接器中的「Test Now」按钮。你应该会得到一个用户列表。
2. 准备好后，点击「Sync Now」以执行真正的同步。您应该会在 Directory Connector 中收到确认消息，并在 Web 密码库中看到新的被邀请的用户。


# OpenLDAP Docker 服务器

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/business/directory-connector/open-ldap)
{% endhint %}

此方法使用 [OpenLDAP Docker 镜像](https://github.com/osixia/docker-openldap)来运行可用于开发的本地目录服务。

这也是用于在 CI 工作流程中运行集成测试的方法。

## 要求 <a href="#requirements" id="requirements"></a>

* [mkcert](https://github.com/FiloSottile/mkcert)（通过 Homebrew 提供）
* [Web Vault](/getting-started/clients/web-vault)
* [服务器](/getting-started/server)
* [目录连接器](/getting-started/business/directory-connector)
* 一个付费组织

## 快速开始 <a href="#ldif-filequick-start" id="ldif-filequick-start"></a>

### 启动目录服务 <a href="#start-directory-service" id="start-directory-service"></a>

1、在 Directory Connector 存储库中打开一个终端。

2、配置 TLS 证书：

```bash
npm run test:integration:setup
```

3、启动 OpenLDAP Docker 容器：

```bash
docker compose up -d
```

### 配置目录连接器 <a href="#configure-directory-connector" id="configure-directory-connector"></a>

1. 运行 Directory Connector Electron App（请参阅[构建说明](/getting-started/business/directory-connector#build-instructions)）。
2. 使用[组织 API 密钥](https://help.ppgg.in/organizations/bitwarden-public-api#authentication)登录。
3. 使用下面的配置设置。

**目录设置：**

* **Type**: Active Directory / LDAP
* **Server Hostname**: localhost
* **Server Port**: 389
* **Root Path**: dc=bitwarden,dc=com
* **This server uses Active Directory:** \[unchecked]
* **This server pages search results:** \[unchecked]
* **This server uses an encrypted connection:** \[unchecked]
* **Username**: cn=admin,dc=bitwarden,dc=com
* **Password**: admin

**同步设置：**

* **User Path**: \[blank]
* **User Object Class**: person
* **User Email Attribute**: mail
* **Group Path**: \[blank]
* **Group Object Class**: organizationalUnit
* **Group Name Attribute**: ou

### 同步 <a href="#sync" id="sync"></a>

{% hint style="warning" %}
当您进行真正的同步时，邀请电子邮件将发送给所有已同步的用户。确保您使用的是 [Mailcatcher](/getting-started/server/guide#mailcatcher)，这样您就不会发送实时电子邮件。
{% endhint %}

1. 点击 Directory Connector 中的「Test Now」按钮。您应该会得到一个用户列表。
2. 准备好后，点击「Sync Now」以执行真正的同步。您应该会在 Directory Connector 中收到确认消息，并在网页密码库中看到新的被邀请的用户。

### 集成测试 <a href="#integration-tests" id="integration-tests"></a>

您也可以针对 Docker 容器运行集成测试：

```bash
npm run test:integration
```

{% hint style="danger" %}
集成测试断言收到的同步数据与一组静态测试数据匹配。对 OpenLDAP 目录数据的任何更改都会导致这些测试失败。
{% endhint %}

## 其他数据集 <a href="#other-datasets" id="other-datasets"></a>

LDIF 文件包含您的目录的配置（例如用户、群组等）。您可以修改或使用自定义 LDIF 文件来自定义您的测试数据。

LDIF 文件可以放置在您的 Directory Connector 存储库中的 `openldap/ldifs` 位置。您可能需要删除并重新创建您的 Docker 容器，以便更改生效（例如 `docker compose up -d --force-recreate` ）。

### 使用示例 LDIF 文件 <a href="#use-example-ldif-file" id="use-example-ldif-file"></a>

不同大小的示例 LDIF 文件包含在 Directory Connector 存储库的 `openldap/examples` 文件夹中。

### 生成您自己的 LDIF 文件 <a href="#generate-your-own-ldif-file" id="generate-your-own-ldif-file"></a>

或者，您可以使用以下说明生成您自己的 LDIF 文件。除非您有特殊要求，否则您不需要这样做。

1. 下载 [LDIF 生成器](https://ldapwiki.com/wiki/LDIF%20Generator)。
2. 将 `Data/mail-hosts.txt` 文件替换为我们自己的 [mail-hosts.txt](https://contributing.bitwarden.com/enterprise/directory-connector/mail-hosts.txt) 文件。这包含大量唯一主机名，以避免生成重复的电子邮件地址。
3. 运行 `java -jar LDIFGen.jar`。
4. 使用以下设置：
   * Base Added: dc=bitwarden, dc=com
   * Generate OUs: Generic
   * Generate People: add
5. 点击「Run」。
6. LDIF 输出可能在电子邮件地址中包含非法字符（例如空格和撇号） - 您应该在使用前手动检查。


# Key Connector

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/business/key-connector)
{% endhint %}

{% hint style="info" %}
如果您是 Key Connector 的新手，您应该首先阅读[帮助中心文档](https://help.ppgg.in/login-with-sso/about-key-connector)以了解其工作原理。
{% endhint %}

## 要求 <a href="#requirements" id="requirements"></a>

* 运行在自托管配置中的[本地开发服务器](/getting-started/server/guide)
* 配置了 SSO 的企业组织
* 本地运行的 [Web 密码库](/getting-started/clients/web-vault)
* [.NET Core 5.0 SDK](https://www.microsoft.com/net/download/core)

### macOS <a href="#macos" id="macos"></a>

macOS 需要更新 SSL 库，否则您将收到「No usable version of libssl was found」的错误。

{% tabs %}
{% tab title="Intel" %}
1、安装 [Homebrew](https://brew.sh/)

2、安装 OpenSSL 软件包：

```bash
brew install openssl
```

3、将所需的环境变量设置为指向 OpenSSL 库：

```bash
echo 'DYLD_LIBRARY_PATH="/usr/local/opt/openssl@1.1/lib"' >> ~/.zshrc
```

4、如果您是从终端运行的 Key Connector，请重新启动终端以确保更新的 `.zshrc` 设置被应用
{% endtab %}

{% tab title="ARM" %}
鉴于 Key Connector 项目基于 NET 5，那么我们需要使用 x86\_64 版本的 OpenSSL，从而使用 Homebrew 安装 x86\_64 软件包（可以在[此处](https://www.wisdomgeek.com/development/installing-intel-based-packages-using-homebrew-on-the-m1-mac/)找到包含多种方法的指南）。

1、安装 Rosetta：

```bash
softwareupdate --install-rosetta
```

2、将终端设置为使用 Rosetta 打开（创建终端应用程序的副本 -> 转到「获取信息」 -> 选中「使用 Rosetta 打开」）。

3、安装 [Homebrew](https://brew.sh/)

4、使用 x86\_64 Homebrew 安装 OpenSSL 包：

```bash
arch -x86_64 /usr/local/homebrew/bin/brew install openssl
```

5、设置所需的环境变量以指向 OpenSSL 库：

```bash
echo 'export DYLD_LIBRARY_PATH="/usr/local/opt/openssl@1.1/lib"' >> ~/.zshrc
```

6、如果您从终端运行 Key Connector，请重新启动终端以确保应用或运行已更新的 `.zshrc` 设置：

```bash
source ~/.zshrc
```

{% endtab %}
{% endtabs %}

## 设置和配置 <a href="#setup-and-configuration" id="setup-and-configuration"></a>

克隆存储库：

```bash
git clone https://github.com/bitwarden/key-connector.git
```

### 配置密钥和用户机密 <a href="#configure-keys-and-user-secrets" id="configure-keys-and-user-secrets"></a>

{% hint style="danger" %}
这些是推荐的开发设置，不适合生产使用。如果需要，[README](https://github.com/bitwarden/key-connector/blob/master/README.md) 中提供了更多配置选项。
{% endhint %}

1、打开终端并导航到本地 Key Connector 存储库中的 `dev` 文件夹

2、生成一个新的 RSA 密钥对（如果它们位于 `dev` 文件夹中，git 将忽略它们）：

```bash
openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout bwkc.key -out bwkc.crt -subj "/CN=Bitwarden Key Connector" -days 36500

openssl pkcs12 -export -out ./bwkc.pfx -inkey bwkc.key -in bwkc.crt -passout pass:{Password}}
```

3、创建您自己的示例用户机密副本：

```bash
cp secrets.json.example secrets.json
```

4、编辑 `secrets.json` 并插入缺失的信息，包括本地存储库的路径和数据库文件的密码。

5、（可选）默认情况下，Key Connector 将使用本地自托管端点 - `https://localhost:8081` 用于 Web 密码库，`http://localhost:33657` 用于身份验证。如果您遵循本文档，则无需进行任何更改。但是，如果您的设置需要不同的端点，您可以在您的用户机密中设置它们，如下所示：

```json
  "keyConnectorSettings": {
    "webVaultUri": "https://localhost:8081",
    "identityServerUri": "http://localhost:33657"
  }
```

6、保存并应用用户机密：

```bash
pwsh setup_secrets.ps1
```

{% hint style="info" %}
如果您在设置用户机密时需要帮助，请参阅[用户机密参考](/contributing/user-secrets)。
{% endhint %}

### 配置组织 <a href="#configure-organization" id="configure-organization"></a>

打开您的本地 Web 密码库并将您的企业组织配置为使用以下设置：

* 策略：单一组织和单一登录身份验证
* 单点登录：
  * 成员解密选项：Key Connector
  * Key Connector URL：`http://localhost:5000`

## 运行和调试 <a href="#running-and-debugging" id="running-and-debugging"></a>

您现在已准备好开始在您的开发环境中使用 Key Connector 了！

{% tabs %}
{% tab title="Visual Studio" %}
使用 Visual Studio 打开解决方案文件 (`bitwarden-key-connector.sln`)，然后点击「Play」按钮。

启动 Key Connector 后，使用非管理员或所有者的账户使用 SSO 登录。新用户将自动加入 Key Connector，现有用户将被提示删除其主密码。
{% endtab %}

{% tab title="CLI" %}
从存储库根目录运行以下命令：

```bash
dotnet run --project src/KeyConnector --configuration Development
```

{% hint style="info" %}
如果在基于 ARM 的 Mac 上运行，您可能需要使用 `/usr/local/share/dotnet/x64/dotnet`

```bash
/usr/local/share/dotnet/x64/dotnet run --project src/KeyConnector --configuration Development
```

{% endhint %}

macOS 需要 `--configuration` 标志才能使用正确的 SSL 库。

启动 Key Connector 后，使用非管理员或所有者的帐户使用 SSO 登录。新用户将自动加入 Key Connector，现有用户将被提示删除其主密码。
{% endtab %}
{% endtabs %}


# Splunk App

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/business/splunk-app)
{% endhint %}

Bitwarden Splunk App 从 Bitwarden 公共 API 获取事件日志数据，并将其提供给 Splunk。

## 要求[​](https://contributing.bitwarden.com/getting-started/business/splunk-app#requirements) <a href="#requirements" id="requirements"></a>

* Docker。如果您使用的是 Apple Silicon Mac，请启用 *Docker Desktop* -> *Settings* -> *General* -> *Use Rosetta for x86\_64/amd64 emulation on Apple Silicon*
* Python 3.7 - 3.10
* [Poetry](https://python-poetry.org/docs/#installation)
  * 还要使用 `poetry self add poetry-plugin-export` 安装 Poetry 导出插件
* libmagic（仅限 macOS），可通过 homebrew 安装：`brew install libmagic`
* Bitwarden 团队或企业组织
* 如果使用本地开发服务器 - 确保事件和 EventsProcessor 项目正在运行，并且[事件记录](/getting-started/server/events)功能正常

## 设置和配置[​](https://contributing.bitwarden.com/getting-started/business/splunk-app#set-up-and-configuration) <a href="#set-up-and-configuration" id="set-up-and-configuration"></a>

### 配置您的环境[​](https://contributing.bitwarden.com/getting-started/business/splunk-app#configure-your-environment) <a href="#configure-your-environment" id="configure-your-environment"></a>

1. 克隆 Github 存储库：

   ```bash
   git clone https://github.com/bitwarden/splunk.git
   ```
2. 导航到存储库的根目录：

   ```bash
   cd splunk
   ```
3. 告诉 poetry 要使用的 Python 版本：

   ```bash
   poetry env use <executable>
   ```

   其中 `<executable>` 是 Python 的可执行文件。如果它在您的 PATH 变量中，则无需指定完整路径。例如 `poetry env use python3.9`。
4. 安装依赖：

   ```bash
   poetry install --with dev
   ```

### 设置 Splunk Enterprise[​](https://contributing.bitwarden.com/getting-started/business/splunk-app#set-up-splunk-enterprise) <a href="#set-up-splunk-enterprise" id="set-up-splunk-enterprise"></a>

1. 运行 Splunk Enterprise：

   ```bash
   docker compose -f dev/docker-compose.yml up -d
   ```

{% hint style="warning" %}
如果您使用的是 Apple Silicon Mac，则必须使用至高是版本 9.3 的 Splunk。从版本 9.4 开始，Splunk 依赖于 AVX 指令集来使用其 KVStore，而 Apple Silicon 不支持该指令集。
{% endhint %}

请注意这将设置管理员密码为 `password`。仅限开发使用。

2. 通过访问 [http://localhost:8001](http://localhost:8001/) 确认 Splunk 正在运行

### 部署 App <a href="#deploy-the-app" id="deploy-the-app"></a>

1. 打包 App：

   ```bash
   ./package.sh
   ```

   这将生成一个已打包的 Splunk App 到 `output/bitwarden_event_logs.tar.gz` 。
2. 将 App 部署到 Splunk：

   ```
   ./deploy.sh
   ```

   这将重新启动 Splunk，脚本完成后可能需要几秒钟才能再次可用。
3. （可选）检查日志以查找错误或用于后续调试：

   ```bash
   docker exec -u splunk -it splunk tail -f /opt/splunk/var/log/splunk/bitwarden_event_logs.log
   ```

### 在 Splunk 中配置 App <a href="#configure-the-app-in-splunk" id="configure-the-app-in-splunk"></a>

1. 访问 Splunk Web App：<http://localhost:8001>。
2. 使用用户名 `admin` 和密码 `password` 登录。
3. 点击 *Apps* -> *Bitwarden Event Logs*。
4. 完成设置。有关配置的更多信息，请参阅 [Bitwarden 帮助中心 ](https://bitwarden.com/help/splunk-siem/)。

{% hint style="warning" %}
Splunk 使用 https，需要额外配置才能与本地开发服务器一起工作。我们还没有这方面的说明。同时，我们建议将 Splunk 配置为使用 Bitwarden 云部署（如生产或内部 QA 环境）。
{% endhint %}

您现在应该可以在 *Apps* -> *Bitwarden Event Logs* -> *Dashboards* 中看到您的组织事件。如果事件日志没有出现，请检查 Splunk 日志（见上文）。


# 贡献

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/)
{% endhint %}

## 如何贡献 <a href="#how-to-contribute" id="how-to-contribute"></a>

我们欢迎各种类型的贡献！

请访问我们的[社区论坛](https://community.bitwarden.com/)，了解一般的社区讨论和开发路线图。

以下是您可以参与的方式：

* **请求新功能**：转到社区论坛的 [Feature Requests category](https://community.bitwarden.com/c/feature-requests/)。请在创建新功能请求之前搜索现有功能请求
* **为新功能编写代码**：在社区论坛的 [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) 中发布新帖子。包括对您提议的贡献的描述、截图和任何相关功能请求的链接。这有助于在您开始编写代码之前从社区和 Bitwarden 团队成员那里获得反馈
* **报告错误或提交错误修复**：使用 Github 话题和拉取请求
* **编写文档**：向 [Bitwarden 帮助存储库](https://github.com/bitwarden/help)提交拉取请求
* **帮助其他用户**：转到社区论坛上的 [Ask the Bitwarden Community category](https://community.bitwarden.com/c/support/)&#x20;
* **翻译**：请参阅下面的本地化 (i10n) 部分
* **报告安全问题或漏洞**：欢迎安全审核和反馈。如果问题很敏感，请[私下联系我们](https://bitwarden.com/contact)或通过我们的 [HackerOne 计划](https://hackerone.com/bitwarden/)提交报告。您可以在下面阅读我们的安全政策。

### 贡献者协议 <a href="#contributor-agreement" id="contributor-agreement"></a>

如果您打算为任何 Github 存储库做出贡献，请签署[贡献者协议](https://cla-assistant.io/bitwarden/clients)。除非作者签署了贡献者协议，否则拉取请求不会被接受和合并。

### 拉取请求指南 <a href="#pull-request-guidelines" id="pull-request-guidelines"></a>

* 在提交拉取请求之前使用 `npm run lint` 并修复任何 linting 建议
* 向 `master` 分支提交任何拉取请求
* 包含指向您的社区论坛帖子的链接

## 本地化 (l10n) <a href="#localization-l10n" id="localization-l10n"></a>

我们使用一个名为 [Crowdin](https://crowdin.com/) 的翻译工具来帮助管理我们跨多种不同语言的本地化工作。

为了在所有平台和语言之间实现一致的翻译，请访问 [bitwarden.com/translate](https://bitwarden.com/translate) 了解如何使用本地化术语表。

如果您有兴趣帮助将 Bitwarden 应用程序翻译成另一种语言（或进行翻译更正），请在 Crowdin 注册一个账户并在此处加入我们的项目：

* <https://crowdin.com/project/bitwarden-browser>
* <https://crowdin.com/project/bitwarden-desktop>
* <https://crowdin.com/project/bitwarden-mobile>
* <https://crowdin.com/project/bitwarden-web>

如果您有兴趣翻译的语言尚未列出，请在 Crowdin 上创建一个新账户，加入项目并[联系项目的所有者](https://crowdin.com/profile/dwbit)。

您可以在此处阅读 Crowdin 的译者入门指南：<https://support.crowdin.com/crowdin-intro/>。

## 安全政策 <a href="#security-policy" id="security-policy"></a>

Bitwarden 认为，与全球的安全研究人员合作对于确保我们的用户安全至关重要。如果您认为您在我们的产品或服务中发现了安全问题，我们鼓励您通过我们的 [HackerOne 计划](https://hackerone.com/bitwarden/)提交报告。我们欢迎与您合作，尽快解决问题。提前致谢！

### 披露政策 <a href="#disclosure-policy" id="disclosure-policy"></a>

* 一旦发现潜在的安全问题，请尽快通知我们，我们将尽一切努力快速解决问题。
* 在向公众或第三方披露任何信息之前，请为我们提供合理的时间来解决问题。如果合适，我们可能会在解决问题之前公开披露该问题。
* 真诚地努力避免侵犯隐私、破坏数据以及中断或降低我们的服务。仅与您拥有的帐户或经帐户持有人的明确许进行交互。
* 如果您想加密您的报告，请使用长 ID 为 `0xDE6887086F892325FEC04CC0D847525B6931381F` 的 PGP 密钥（在公共密钥服务器池中可用）。

在研究过程中，我们希望您不要：

* 拒绝服务
* 垃圾邮件
* Bitwarden 员工或承包商的社会工程（包括网络钓鱼）
* 对 Bitwarden 财产或数据中心的任何物理尝试

### 我们想帮助你！ <a href="#we-want-to-help-you" id="we-want-to-help-you"></a>

如果您有一些您认为接近被利用的东西，或者如果您想了解有关内部 API 的一些信息，或者通常对您想努力提供帮助的应用程序有任何疑问，请[联系我们](https://bitwarden.com/contact)并询问该信息。如上所述，Bitwarden 希望帮助您发现问题，并且非常愿意提供帮助。

感谢您帮助保护 Bitwarden 和我们的用户的安全！


# 代码样式

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/code-style/)
{% endhint %}

本章节包含用于 Bitwarden 代码库的通用代码样式指南。建议通读本章节，因为它包含许多避免常见陷阱的好建议。

## 通用 <a href="#general" id="general"></a>

### 列表 <a href="#lists" id="lists"></a>

* 当语言支持时，包括多行列表的尾随逗号。内联列表不必遵循此标准。

### 代码区域 <a href="#code-regions" id="code-regions"></a>

* 我们不在代码库中使用代码区域。如果您发现自己需要使用代码区域，请考虑重构代码以使其更具可读性。


# =Android & Kotlin

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/code-style/android-kotlin)
{% endhint %}


# Angular & TypeScript

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/code-style/angular)
{% endhint %}

## HTML

请确保每个输入字段和按钮都有一个描述性 ID。这将方便 QA 更有效地编写测试自动化。

ID 应具有以下三个组件：

* **组件名称**：为确保 ID 的唯一性，我们在它们前面加上组件名称。用很少的改变，同时避免我们多次重复使用相同的组件名称，以保持唯一性。
* **HTML 元素**：这使您可以快速了解我们正在访问的内容。
* **可读名称**：我们正在访问的内容的描述性名称。

请在组件内使用破折号，并使用下划线分隔*组件*。

```bash
<component name>_<html element>_<readable name>

register_button_submit
register-form_input_email
```

在为组件库编写组件时，有时需要确保 ID 存在，以便正确处理对其他元素的引用的可访问性。可以考虑使用自动生成的 ID，但要确保它可以被覆盖。对自动 ID 使用以下命名约定：

```bash
<component selector>-<incrementing number>

bit-input-0
```

请确保选择器中的单词使用破折号分隔，并且不要使用 camelCase 格式。

> \[**译者注**]：camelCase - 驼峰命名法。[骆峰式命名](https://zh.wikipedia.org/zh-my/%E9%A7%9D%E5%B3%B0%E5%BC%8F%E5%A4%A7%E5%B0%8F%E5%AF%AB)法是电脑编程时的一套命名规则。当变量名或函数名是由两个或多个单词连结在一起，利用驼峰式命名法来表示，以增加变量和函数的可读性。单词之间不以空格、连接号或下划线等隔开。第一个单词的首字母小写，第二个单词的首字母大写（小驼峰），或者每一个单词的首字母均大写（大驼峰。也被称为 **Pascal 命名法**）。

## JavaScript / TypeScript <a href="#javascript-typescript" id="javascript-typescript"></a>

我们使用 [Prettier](https://prettier.io/) 和 [ESLint](https://eslint.org/) 来自动格式化和 lint 代码库。每次创建提交时，`npm ci` 都会自动安装 pre-commit 钩子以在您的更改上运行 Prettier 和 ESLint。

或者，您可以手动运行它们：

```bash
npm run prettier
npm run lint:fix
```

### 角度样式指南 <a href="#angular-style-guide" id="angular-style-guide"></a>

我们通常遵循 [Angular 样式指南](https://angular.io/guide/styleguide)。

### 变量命名 <a href="#variable-naming" id="variable-naming"></a>

* 对于 `boolean` 变量，使用基本词，**不要**包含前缀，如 `is`、`has` 等，除非没有它就无法传达含义，例如避免与其他属性混淆。

## RxJS

在编写 RxJS 代码时，我们有一些准则，这些准则是使用 [`eslint-plugin-rxjs`](https://github.com/cartant/eslint-plugin-rxjs) 和 [`eslint-plugin-rxjs-angular`](https://github.com/cartant/eslint-plugin-rxjs-angular) 强制执行。这些规则旨在帮助避免常见的 RxJS 陷阱，这些陷阱可能导致 Observables 无法清理或出现意外行为。

### 避免订阅 <a href="#avoid-subscriptions" id="avoid-subscriptions"></a>

只要有可能，我们应该避免显式订阅，而是使用模板中的 `| async` 管道。这将确保在没有任何样板的情况下销毁组件时清理订阅。

为此，我们可以使用 `.pipe` 操作和 rxjs 操作符将输入的 observable 修改为我们可以显示的内容。

研究以下示例，很容易忘记取消订阅 observable，我们也有比我们想要的更多的样板。

```javascript
private destroy$ = new Subject();
public transformed = [];

observable$
  .pipe(takeUntil(this.destroy$))
  .subscribe((v) => {
    transformed = transform(v);
  });

ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}

// Template
<div *ngFor="let t of transformed">
  {{ t }}
</div>
```

现在另外研究以下示例，其中我们将订阅替换为 `| async`。

```javascript
transformed$ = observable$.pipe(map(transform));

// Template
<div *ngFor="let t of transformed$ | async">
  {{ t }}
</div>
```

### 使用 `takeUntil` 取消订阅 <a href="#unsubscribe-using-takeuntil" id="unsubscribe-using-takeuntil"></a>

悬空订阅是内存泄漏的常见原因。为了避免这种情况，我们使用了 `prefer-takeUntil` 规则。这要求任何订阅首先通过 `takeUntil` 操作符进行管道传输。

`takeUntil` 模式的主要好处是审查者可以快速确认订阅是否已清理。

```javascript
private destroy = new Subject<void>();

ngOnInit() {
  this.observable$
    .pipe(takeUntil(this.destroy$))
    // This subscription will automatically be cleaned up when `this.destroy$` emits.
    .subscribe(value => console.log);
}

ngOnDestroy() {
  this.destroy.next();
  this.destroy.complete();
}
```

### 无异步订阅 <a href="#no-async-subscribes" id="no-async-subscribes"></a>

异步订阅很少如您期望的那样工作。它们不是按顺序执行，而是有可能并行执行。这很容易导致意外行为。为避免这种情况，我们的代码库中禁止异步订阅，您需要选择正确的操作。

一些合适的操作符如下：

* [`switchMap`](https://www.learnrxjs.io/learn-rxjs/operators/transformation/switchmap)：取消之前的操作，使其适用于我们在收到新的输入后不关心旧结果的场景。
* [`concatMap`](https://www.learnrxjs.io/learn-rxjs/operators/transformation/concatmap)：按顺序运行异步操作，防止并行和乱序执行。如果我们关心每一个事件的处理，请使用它。
* [`mergeMap`](https://www.learnrxjs.io/learn-rxjs/operators/transformation/mergemap)：请仔细考虑这是否适合您的用例。mergeMap 将展平可观察对象，但不关心顺序。如果排序很重要，请使用 `concatMap`。如果您只关心最新值，请使用 `switchMap`。


# C\#

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/code-style/csharp)
{% endhint %}

我们使用 [dotnet-format](https://github.com/dotnet/format) 工具来格式化我们的 C# 代码。像如下运行：

```bash
dotnet format
```

然而，由于它相当初级，我们还遵循一些额外的代码样式指南。

我们尝试遵循 C# 社区标准（有一些例外）。查看以下文章获取总体概述。

1. <https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/inside-a-program/coding-conventions>
2. [https://stackoverflow.com/a/310967/1090359](<https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/inside-a-program/coding-conventions&#xD;&#xA;https://stackoverflow.com/a/310967/1090359>)

## 私有字段 <a href="#private-fields" id="private-fields"></a>

私有字段应使用 camelCase 格式并以 `_` 作为前缀。

示例：

```bash
private readonly IUserService _userService;
```

请参阅以下文章，了解如何配置 Visual Studio 代码生成快捷方式以协助此命名约定：<https://stackoverflow.com/q/45736659/1090359>

## 公共属性 <a href="#public-properties" id="public-properties"></a>

* 属性应使用 PascalCase 格式并且没有前缀
* 属性应拼写出来，而不要使用缩写或简称，例如「OrganizationConfiguration」（正确）与「OrgConfig」（错误）
* 属性应在属性组和以下方法之间包含空行

> \[**译者注**]：PascalCase - 大驼峰命名法。[骆峰式命名法](https://zh.wikipedia.org/zh-my/%E9%A7%9D%E5%B3%B0%E5%BC%8F%E5%A4%A7%E5%B0%8F%E5%AF%AB)是电脑编程时的一套命名规则。当变量名或函数名是由两个或多个单词连结在一起，利用驼峰式命名法来表示，以增加变量和函数的可读性。单词之间不以空格、连接号或下划线等隔开。第一个单词的首字母小写，第二个单词的首字母大写（小驼峰），或者每一个单词的首字母均大写（大驼峰。也被称为 **Pascal 命名法**）。

## 空白区 <a href="#whitespace" id="whitespace"></a>

* 我们对所有代码文件使用空格（不是制表符），包括 C#。缩进应该是标准的 4 个空格。
* 代码文件应在最后的 `}` 后以换行符结尾
* 空行用于分隔每组代码组合类型（字段、构造函数、属性、公共方法、私有方法、子类）

## 构造函数 <a href="#constructors" id="constructors"></a>

* 多个**构造函数**应使用换行符分隔（之间有空行）
* 具有多个参数的构造函数应每行列出 1 个参数
* 必要时，空的构造函数应全位于为 1 行，即 `public ClassName() { }`

## 控制块 <a href="#control-blocks" id="control-blocks"></a>

* 控制块应始终使用大括号（即使是 1 行大括号）
* `using` 和 `foreach` 块应该用 `var` 声明上下文变量
* 在控制关键字和 `()` 之间始终包含一个空格

## 条件句 <a href="#conditionals" id="conditionals"></a>

当跨多行分隔时，长条件应使用尾随运算符。

```json
// Good example
if (someBooleanExpression &&
    someVariable != null &&
    someVariable.IsTrue)
{
}

// Bad examples (don't do)
if (someBooleanExpression
    && someVariable != null
    && someVariable.IsTrue)
{
}
// Too long, separate
if (someBooleanExpression && someVariable != null && someVariable.IsTrue)
{
}
```


# =Rust

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/code-style/rust)
{% endhint %}


# T-SQL

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/code-style/sql)
{% endhint %}

## 存储库 <a href="#repositories" id="repositories"></a>

我们使用存储库模式，并使用 Dapper 编写 MSSQL 存储库。每个存储库方法依次调用一个存储过程，该过程主要从 *Views*（视图）中获取数据。

## 部署脚本 <a href="#deployment-scripts" id="deployment-scripts"></a>

有一些特定的方式来构建部署脚本。这些标准的目标是确保脚本可以重新运行。我们从不打算在一个环境中多次运行脚本，但脚本应该支持它。

### 表 <a href="#tables" id="tables"></a>

#### 创建表 <a href="#creating-a-table" id="creating-a-table"></a>

建表时，首先要检查此表是否存在：

```plsql
IF OBJECT_ID('[dbo].[{table_name}]') IS NULL
BEGIN
    CREATE TABLE [dbo].[{table_name}] (
        [Id]                UNIQUEIDENTIFIER NOT NULL,
        ...
        CONSTRAINT [PK_{table_name}] PRIMARY KEY CLUSTERED ([Id] ASC)
    );
END
GO
```

#### 删除表 <a href="#deleting-a-table" id="deleting-a-table"></a>

删除表时，使用 `IF EXISTS` 以避免当表不存在时出现错误。

```sql
DROP IF EXISTS [dbo].[{table_name}]
GO
```

#### 添加列 <a href="#adding-a-column-to-a-table" id="adding-a-column-to-a-table"></a>

您必须先检查该列是否存在，然后才能将其添加到表中：

```sql
IF COL_LENGTH('[dbo].[{table_name}]', '{column_name}') IS NULL
BEGIN
    ALTER TABLE [dbo].[{table_name}]
        ADD [{column_name}] {DATATYPE} {NULL|NOT NULL};
END
GO
```

向现有表添加新的 `NOT NULL` 列时，请重新评估是否需要它。不要害怕在 C# 和应用层中使用 Nullable \<T> 原语，这总比对 DB 的每一行使用默认值导致占用不必要的空间要好，特别是对于新功能或新特性，需要占用很长时间，才能对大多数行级数据（如果有的话）有用。

如果您决定添加 `NOT NULL` 列，请**使用 DEFAULT 约束**，而不是创建列、更新行以及更改列。这对于像 `dbo.User` 和 `dbo.Cipher` 这样的大型表尤其重要。我们在 Azure 中的 SQL Server 版本使用元数据作为默认约束。这意味着我们可以更新默认的列值，而**无需**更新表中的每一行（这将使用大量的 DB I/O）。

这个很慢：

```sql
IF COL_LENGTH('[dbo].[Table]', 'Column') IS NULL
BEGIN
    ALTER TABLE
        [dbo].[Table]
    ADD
        [Column] INT NULL
END
GO

UPDATE
    [dbo].[Table]
SET
    [Column] = 0
WHERE
    [Column] IS NULL
GO

ALTER TABLE
    [dbo].[Column]
ALTER COLUMN
    [Column] INT NOT NULL
GO
```

这个更好：

```sql
IF COL_LENGTH('[dbo].[Table]', 'Column' IS NULL
BEGIN
    ALTER TABLE
        [dbo].[Column]
    ADD
        [Column] INT NOT NULL CONSTRAINT D_Table_Column DEFAULT 0
END
GO
```

#### 更改列数据类型 <a href="#changing-a-column-data-type" id="changing-a-column-data-type"></a>

您必须将 `ALTER TABLE` 语句包装在条件块中，以便脚本的后续运行不会再次修改数据类型。

```sql
IF EXISTS (
    SELECT *
    FROM INFORMATION_SCHEMA.COLUMNS
    WHERE COLUMN_NAME = '{column_name}' AND
        DATA_TYPE = '{datatype}' AND
        TABLE_NAME = '{table_name}')
BEGIN
    ALTER TABLE [dbo].[{table_name}]
    ALTER COLUMN [{column_name}] {NEW_TYPE} {NULL|NOT NULL}
END
GO
```

#### 调整元数据 <a href="#adjusting-metadata" id="adjusting-metadata"></a>

调整表时，您还应该检查该表是否被引用在任何视图中。如果视图中的基础表已被修改，则应运行 `sp_refreshview` 以重新生成视图元数据。

```sql
EXECUTE sp_refreshview N'[dbo].[{view_name}]'
GO
```

### 视图 <a href="#views" id="views"></a>

#### 创建或修改视图 <a href="#create-or-modify-a-view" id="create-or-modify-a-view"></a>

我们建议使用 `CREATE OR ALTER` 语法来添加或修改视图。

```sql
CREATE OR ALTER VIEW [dbo].[{view_name}]
AS
SELECT
    *
FROM
    [dbo].[{table_name}]
GO
```

#### 删除视图 <a href="#deleting-a-view" id="deleting-a-view"></a>

删除视图时，使用 `IF EXISTS` 以避免当表不存在时出现错误。

```sql
DROP IF EXISTS [dbo].[{view_name}]
GO
```

#### 调整元数据 <a href="#adjusting-metadata" id="adjusting-metadata"></a>

更改视图时，您可能还需要刷新引用该视图或函数的模块（存储了过程或函数），以便 SQL Server 更新其统计信息并编译对它的引用。

```sql
IF OBJECT_ID('[dbo].[{procedure_or_function}]') IS NOT NULL
BEGIN
    EXECUTE sp_refreshsqlmodule N'[dbo].[{procedure_or_function}]';
END
GO
```

### 函数和存储过程 <a href="#functions-and-stored-procedures" id="functions-and-stored-procedures"></a>

#### 创建或修改函数或存储过程 <a href="#create-or-modify-a-function-or-stored-procedure" id="create-or-modify-a-function-or-stored-procedure"></a>

我们建议使用 `CREATE OR ALTER` 语法来添加或修改函数或存储过程。

```sql
CREATE OR ALTER {PROCEDURE|FUNCTION} [dbo].[{sproc_or_func_name}]
...
GO
```

#### 删除函数或存储过程 <a href="#deleting-a-function-or-stored-procedure" id="deleting-a-function-or-stored-procedure"></a>

删除函数或存储过程时，请使用 I`F EXISTS` 以避免其不存在时出现错误。

```sql
DROP IF EXISTS [dbo].[{sproc_or_func_name}]
GO
```

### 创建或修改索引 <a href="#create-or-modify-an-index" id="create-or-modify-an-index"></a>

在创建索引时，尤其是在频繁使用的表上，我们的生产数据库很容易出现脱机、无法使用、CPU 达到 100% 以及许多其他不良行为。通常最好使用在线索引构建来执行此操作，以免锁定底层表。这可能会导致索引操作花费更长的时间，但您不会创建一个底层架构表锁来阻止所有读取和连接到该表，而只会在操作过程中锁定表的更新。

一个很好的例子是在 `dbo.Cipher` 或 `dbo.OrganizationUser` 上创建索引时，由于这些表是重读表，锁定会导致 Azure SQL 中异常高的 CPU、等待时间和 Worker 耗尽。

```sql
CREATE NONCLUSTERED INDEX [IX_OrganizationUser_UserIdOrganizationIdStatus]
   ON [dbo].[OrganizationUser]([UserId] ASC, [OrganizationId] ASC, [Status] ASC)
   INCLUDE ([AccessAll])
   WITH (ONLINE = ON); -- ** THIS ENSURES ONLINE **
```


# =Swift

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/code-style/swift)
{% endhint %}


# Tailwind

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/code-style/tailwind)
{% endhint %}

我们目前正在将基于 Web 的客户端迁移到 Tailwind。目前涉及 web vault，但长期计划是迁移桌面端和浏览器。

在开始使用 Tailwind 之前，我们建议您熟悉 [Utility-First Fundamentals](https://tailwindcss.com/docs/utility-first)。博客文章 [CSS Utility Classes and "Separation of Concerns"](https://adamwathan.me/css-utility-classes-and-separation-of-concerns/) 也是一篇很好的读物，可以更好地理解 Utility first CSS 框架背后的动机和目标。

我们还建议使用 [tailwind 文档](https://tailwindcss.com/)的搜索功能来查找类和示例。

## Bitwarden 的 Tailwind <a href="#tailwind-at-bitwarden" id="tailwind-at-bitwarden"></a>

我们已经定义了我们自己的 Tailwind 配置，它严格限制了颜色的使用，以支持多个主题。为了实现这一点，我们将 CSS 变量与 tailwind 配置结合使用。这使我们能够支持比 Tailwind 中内置的深色/浅色更多。

为此，我们不鼓励使用任意值，唯一的例外是支持现有的 Bootstrap 样式。在这种情况下，它应该被记录并添加为技术债务，作为从 Bootstrap 迁移的一部分来解决。

{% hint style="warning" %}
所有 Tailwind 类都需要以 tailwind 配置中定义的 `tw-` 为前缀。用法示例：`<div class="tw-bg-background-alt2"> ... </div>`
{% endhint %}

### 组件 <a href="#components" id="components"></a>

由于 Tailwind 是一个实用优先的 CSS 框架，以避免代码重复，这会使设计难以维护，我们大量使用 Angular 组件来封装孤立的设计块。在大多数情况下，这些块是[表示组件](https://angular-training-guide.rangle.io/state-management/ngrx/component_architecture)。

### 组件库 <a href="#component-library" id="component-library"></a>

Bitwarden 的一项工程计划是[组件库](https://github.com/bitwarden/clients/tree/master/libs/components)，旨在封装最常用的核心组件。

#### Storybook

我们使用 [Storybook](https://storybook.js.org/) 来独立开发组件。要启动 Storybook 开发独立的组件，请在 `client` 存储库的根目录中运行 `npm run storybook` 命令。


# 数据库迁移

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/database-migrations/)
{% endhint %}

## 应用迁移 <a href="#applying-migrations" id="applying-migrations"></a>

我们使用 `migrate.ps1` PowerShell 脚本将迁移应用到本地开发数据库。此脚本可以处理我们支持的不同数据库提供程序。

有关如何使用 `migrate.ps1` 的说明，请参阅 [MSSQL](/getting-started/server/database/mssql#updating-the-database) 和[实体框架](/getting-started/server/database/ef#migrations)的入门部分。

## 为新的更改创建迁移 <a href="#creating-migrations-for-new-changes" id="creating-migrations-for-new-changes"></a>

任何数据库的更改都必须编写为我们的主 DBMS - MSSQL 以及实体框架的迁移脚本。针对每个提供商，请按照以下说明进行操作。

### MSSQL 迁移 <a href="#mssql-migrations" id="mssql-migrations"></a>

{% hint style="success" %}
我们建议首先阅读[进化的数据库设计](/contributing/database-migrations/edd)和 [T-SQL 代码样式](/contributing/code-style/t-sql)，因为它们对我们如何编写迁移的有很大的影响。
{% endhint %}

根据[进化数据库设计](/contributing/database-migrations/edd)的原则，每个更改都需要考虑分为两部分：

1. 向后兼容的过渡迁移
2. 非向后兼容的最终迁移

更改可能无需非向后兼容的结束阶段（即所有更改的最终形式可能是向后兼容的）。在这种情况下，只需要进行一个阶段的更改即可。

#### 向后兼容迁移 <a href="#backwards-compatible-migration" id="backwards-compatible-migration"></a>

1. 修改 `src/Sql/dbo` 中的源 `.sql` 文件。
2. 编写迁移脚本，并将其放在 `util/Migrator/DbScripts` 中。每个脚本必须以当前日期为前缀。

#### 非向后兼容迁移 <a href="#non-backwards-compatible-migration" id="non-backwards-compatible-migration"></a>

1. 将相关的 `.sql` 文件从 `src/Sql/dbo` 复制到 `src/Sql/dbo_finalization`。
2. 移除不再需要的向后兼容性。
3. 编写一个新的 Migration 并将其放置在 `src/Migrator/DbScripts_finalization` 中。将其命名为 `YYYY-0M-FinalizationMigration.sql`。
   * 通常情况下，迁移是被设计为按顺序运行的。然而，由于 DbScripts\_finalization中 的迁移可以不按顺序运行，因此必须注意确保它们与 DbScripts 的变更保持兼容。为了做到这一点，我们只保留了一个迁移，用于执行所有向后不兼容的架构变更。

### EF 迁移 <a href="#ef-migrations" id="ef-migrations"></a>

如果要更改数据库架构，则必须创建 EF 迁移脚本，以确保 EF 数据库与这些更改保持同步。开发人员必须这样做，并将迁移包含在他们的 PR 中。

要创建这些脚本，您必须首先根据需要更新 `Core/Entities` 中的数据模型。这将用于为我们的每个 EF 目标生成迁移。

模型更新后，导航到 `server` 存储库中的 `dev` 目录并执行 `ef_migrate.ps1` PowerShell 命令。您应该提供迁移的名称，将其作为唯一的参数：

```bash
pwsh ef_migrate.ps1 [NAME_OF_MIGRATION]
```

这将生成迁移，然后应将其包含在您的 PR 中。

### \[未实现] 手动 MSSQL 迁移 <a href="#not-yet-implemented-manual-mssql-migrations" id="not-yet-implemented-manual-mssql-migrations"></a>

可能需要在正常更新过程之外运行迁移。这些类型的迁移应该出于非常特殊的目的而保留。其中一个原因可能是索引重建。

1. 编写一个带有当前日期前缀的新迁移并将其放置在 `src/Migrator/DbScripts_manual` 中
2. 在我们的云环境中运行它并且我们对结果感到满意后，创建一个 PR 将其移动到 `DbScripts`。这将使其能够由我们的迁移器进程在全新安装的云和自托管环境，以及自托管中运行


# 进化数据库设计

{% hint style="info" %}
开始对应的[官方页面地址](https://contributing.bitwarden.com/contributing/database-migrations/edd)
{% endhint %}

在 Bitwarden，我们遵循[进化数据库设计 (EDD)](https://en.wikipedia.org/wiki/Evolutionary_database_design)。EDD 描述了一个过程，在这个过程中，数据库架构被持续更新，同时通过使用数据库过渡阶段仍然确保与旧版本的兼容性。

Bitwarden 还需要支持：

* **零停机部署**：这意味着应用程序的多个版本将在部署窗口期间同时运行。
* **代码回滚**：代码中的严重缺陷应该能够回滚到以前的版本。

为了满足这些附加要求，数据库架构必须支持以前版本的服务器。

## 设计 <a href="#design" id="design"></a>

数据库更改可以分为两类：破坏性更改和非破坏性更改\[1]。如果没有相应的代码更改，破坏性更改会阻止现有功能按预期工作。非破坏性更改则相反：数据库更改不需要更改代码即可允许非应用程序继续按预期工作。

### 非破坏性改更改 <a href="#non-destructive-changes" id="non-destructive-changes"></a>

通过在数据库表、视图和存储过程中混合使用可空字段和默认值，可以以向后兼容的方式设计许多数据库更改。这确保了可以在没有新列的情况下调用存储过程，并允许它们同时使用旧代码和新代码运行。

### 破坏性更改 <a href="#destructive-changes" id="destructive-changes"></a>

任何不能以非破坏性方式完成的更改都是破坏性更改。这可以像添加一个不可为空的列一样简单，其中需要从现有字段计算值，或者重命名现有列。为了处理破坏性更改，有必要将它们分为三个阶段：开始、过渡和结束，如下图所示。

{% embed url="<https://contributing.bitwarden.com/assets/images/transitions-b5d691da2f06e34d8a4e13a3ab25a4b8.png>" %}

值得注意的是，*重构阶段*通常是滚动的，一个重构的*结束阶段*是另一个重构的*过渡阶段*。下表详细说明了在哪个数据库阶段需要支持哪些应用程序版本。

| 数据库阶段 | Release X | Release X+1 | Release X+2 |
| ----- | --------- | ----------- | ----------- |
| 开始    | ✅         | ❌           | ❌           |
| 过渡    | ✅         | ✅           | ❌           |
| 结束    | ❌         | ✅           | ✅           |

### 迁移 <a href="#migrations" id="migrations"></a>

上图中描述的三种不同的迁移是初始迁移、过渡迁移和最终迁移。

#### 初始 (**Initial**) 迁移 <a href="#initial-migration" id="initial-migration"></a>

初始迁移在代码部署之前运行，其目的是在不中断对版本 X 的支持的情况下添加对版本 X+1 的支持。迁移应快速执行且不包含任何昂贵的操作，以确保零停机时间。

#### 过渡 (**Transition**) 迁移 <a href="#transition-migration" id="transition-migration"></a>

过渡迁移在过渡阶段的某个时候运行，并提供可选的数据迁移，以防数据迁移太慢或对数据库造成太大负载，或者使其不适合初始迁移。

* 与版本 X 和版本 X+1 应用程序兼容。
* 如果需要，此时只能运行数据群体迁移
  * 必须在过渡阶段作为后台任务运行。
  * 操作被批处理或以其他方式优化，以确保数据库保持响应。
* 在此阶段不会运行架构更改。

#### 最终 (**Finalization**) 迁移 <a href="#finalization-migration" id="finalization-migration"></a>

最终迁移删除了保留与版本 X 的向后兼容性所需的临时测量，并且数据库架构从此仅支持版本 X+1。这些迁移作为版本 X+2 部署的一部分运行。

### 示例 <a href="#example" id="example"></a>

让我们看一个例子，重命名列的重构如下图所示。

{% embed url="<https://contributing.bitwarden.com/assets/images/rename-column-1f4999a32d438f4a75649089e85b4c18.gif>" %}

在这个重构中，我们将 `Customer` 表中的 `Fname` 列重命名为 `FirstName`。这可以很容易地使用常规的 `Alter Table` 语句来实现，但这将破坏与现有运行代码的兼容性。相反，让我们来看看如何逐步重构这个表。

我们将首先创建一个迁移，将 `FirstName` 列添加到 `Customer` 表中。同时，我们还将更新存储过程，以同步 `FName` 和 `FirstName` 之间的内容，以确保新旧服务器版本可以同时运行。同步代码在下面的代码片断中突出显示。

之后将部署新的服务器版本，一切检查完毕后，现有数据将使用*数据迁移*脚本进行迁移。这实际上是将 `FName` 复制到 `FirstName` 列。

最后，将运行*第二次迁移*，删除旧列并更新存储过程以删除同步逻辑。

#### 迁移 <a href="#migrations" id="migrations"></a>

{% hint style="info" %}
所有的数据库迁移都应该支持多次运行；即使随后的运行不执行任何操作。
{% endhint %}

{% tabs %}
{% tab title="初始迁移" %}

```sql
-- Add Column
IF COL_LENGTH('[dbo].[Customer]', 'FirstName') IS NULL
BEGIN
    ALTER TABLE
        [dbo].[Customer]
    ADD
        [FirstName] NVARCHAR(MAX) NULL
END
GO

-- Drop existing SPROC
IF OBJECT_ID('[dbo].[Customer_Create]') IS NOT NULL
BEGIN
    DROP PROCEDURE [dbo].[Customer_Create]
END
GO

-- Create the new SPROC
CREATE PROCEDURE [dbo].[Customer_Create]
    @CustomerId UNIQUEIDENTIFIER OUTPUT,
    @FName NVARCHAR(MAX) = NULL, -- Deprecated as of YYYY-MM-DD
    @FirstName NVARCHAR(MAX) = NULL
AS
BEGIN
    SET NOCOUNT ON

    SET @FirstName = COALESCE(@FirstName, @FName);

    INSERT INTO [dbo].[Customer]
    (
        [CustomerId],
        [FName],
        [FirstName]
    )
    VALUES
    (
        @CustomerId,
        @FirstName,
        @FirstName
    )
END
```

{% endtab %}

{% tab title="过渡迁移" %}

```sql
UPDATE [dbo].Customer SET
    FirstName=FName
WHERE FirstName IS NULL
```

{% endtab %}

{% tab title="最终迁移" %}

```sql
-- Remove Column
IF COL_LENGTH('[dbo].[Customer]', 'FName') IS NOT NULL
BEGIN
    ALTER TABLE
        [dbo].[Customer]
    DROP COLUMN
        [FName]
END
GO

-- Drop existing SPROC
IF OBJECT_ID('[dbo].[Customer_Create]') IS NOT NULL
BEGIN
    DROP PROCEDURE [dbo].[Customer_Create]
END
GO

-- Create the new SPROC
CREATE PROCEDURE [dbo].[Customer_Create]
    @CustomerId UNIQUEIDENTIFIER OUTPUT,
    @FirstName NVARCHAR(MAX) = NULL
AS
BEGIN
    SET NOCOUNT ON

    INSERT INTO [dbo].[Customer]
    (
        [CustomerId],
        [FirstName]
    )
    VALUES
    (
        @CustomerId,
        @FirstName
    )
END
```

{% endtab %}
{% endtabs %}

## 部署编排 <a href="#deployment-orchestration" id="deployment-orchestration"></a>

该流程的实施有一些重要的约束条件：

* Bitwarden 生产环境需要始终处于在线状态
* 自托管实例必须支持相同的数据库更改流程；但是，它们没有相同的始终在线应用程序限制
* 最大限度地减少流程中的手动步骤

支持所有这些约束条件的过程是一个复杂的流程。下面是状态机的图像，希望有助于可视化该过程及其支持的内容。它假设所有数据库更改都遵循[迁移](/contributing/database-migrations)中规定的标准。

***

{% embed url="<https://contributing.bitwarden.com/assets/images/edd_state_machine-76cf9f0a9e72188bf7f8fb71f5d53c19.jpg>" %}

***

### 在线环境 <a href="#online-environments" id="online-environments"></a>

架构迁移和数据迁移只是迁移。底层的实现问题是协调迁移的运行时约束。最终，所有迁移都将以 `DbScripts` 结束。但是，为了协调 *Transition* 和相关 *Finalization* 迁移的运行，它们将保留在 `DbScripts` 之外，直到正确的时间。

在具有始终在线应用程序的环境中，必须在推出新代码后运行 *Transition* 脚本。要执行完整部署，将运行 `DbScripts` 中的所有新迁移，推出新代码，然后在所有新代码服务上线后立即运行 `DbScripts_transition` 目录中的所有转换迁移。如果新代码推出后出现严重故障，将进行回滚（请参阅下面的回滚）。*Finalization* 迁移将在下一次部署开始时才会运行，并将其移至 `DbScripts` 中。

在此部署之后，为了准备下一个版本，`DbScripts_transition` 中的所有迁移都会移至 `DbScripts` ，然后 `DbScripts_finalization` 中的所有迁移都会移至 `DbScripts`，保留其执行顺序以进行全新安装。对于当前的分支策略，当 `rc` 被削减以准备此版本时，PR 将针对 `master` 开放。此 PR 自动化还将处理重命名迁移文件并将 `[dbo_finalization]` 的任何引用更新为 `[dbo]`。

下一次部署将选取 `DbScripts` 中新添加的迁移，并将之前可重复的 *Transition* 迁移设置为不再可重复，执行 *Finalization* 迁移，然后执行与以下代码更改相关的任何新迁移出去。

任何时候不同目录中的迁移状态都会保存在迁移器实用程序中并进行版本控制，该实用程序支持两种类型环境中的分阶段迁移过程。

### 离线环境 <a href="#offline-environments" id="offline-environments"></a>

离线环境的过程与始终在线的过程类似。但是，由于它们没有始终在线的约束，因此 *Initial* 迁移和 *Transition* 迁移将相继运行：

* 像今天一样停止 Bitwarden 堆栈
* 启动数据库
* 运行 `DbScripts` 中的所有新迁移（包括上次部署的 *Finalization* 迁移和当前部署的任何 *Initial* 迁移）
* 运行所有 *Transition* 迁移
* 重新启动 Bitwarden 堆栈

## 回滚 <a href="#rollbacks" id="rollbacks"></a>

如果服务器发布失败，需要回滚，这应该很简单，只要重新部署以前的版本就可以了。数据库将**停留**在过渡阶段，直到发布修补程序并且可以更新服务器。修补程序准备好发布后，就会对其进行部署，并重新运行 *Transition* 迁移以验证数据库是否处于所需的状态。

如果需要完全拉取某个功能，则需要编写新的迁移来撤消数据库更改，并且还需要更新未来的迁移以适应数据库更改。通常不建议这样做，因为需要重新访问挂起的迁移（对于其他版本）。

## 测试 <a href="#testing" id="testing"></a>

在合并 PR 之前，请确保数据库更改在当前发布的版本上运行良好。我们目前没有为此提供自动化测试套件，开发人员需要确保他们的数据库更改针对当前发布的版本正确运行。

## 延伸阅读 <a href="#further-reading" id="further-reading"></a>

1. [进化数据库设计](https://martinfowler.com/articles/evodb.html)（特别是[所有数据库更改都是数据库重构](https://martinfowler.com/articles/evodb.html#AllDatabaseChangesAreMigrations)）
2. [敏捷数据 (AD) 方法](http://agiledata.org/)（特别是[数据库目录重构](http://agiledata.org/essays/databaseRefactoringCatalog.html)）
3. [重构数据库：进化数据库](https://databaserefactoring.com/)
4. 重构数据库：进化数据库设计（Addison-Wesley 签名系列 (Fowler)）ISBN-10: 0321774515


# 提交签名

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/commit-signing)
{% endhint %}

可以使用任何名称和电子邮件配置 git，从而使不良行为者能够欺骗提交并冒充他们想要的任何人。 GitHub 支持多种对 git 提交进行数字签名的方法，验证它们是否来自有权访问先前配置的私钥的人。

例如，2022 年 8 月 3 日，Stephen Lacy [在 Twitter 上分享了](https://twitter.com/stephenlacy/status/1554697080718823424)他如何通过注意到未经验证的提交（即未经数字签名的提交）来发现 GitHub 上的大规模恶意软件攻击。

为了防止提交欺骗，我们鼓励所有 Bitwarden 贡献者对他们的提交进行数字签名。

## 设置提交签名 <a href="#setting-up-commit-signing" id="setting-up-commit-signing"></a>

Github 支持使用 GPG、SSH 和 S/MIME 方式的提交签名。如果您不确定要使用哪种方式，我们推荐 GPG。

1、安装 GnuPG：

{% tabs %}
{% tab title="macOS" %}

```bash
brew install gnupg
echo "export GPG_TTY=$(tty)" >> ~/.zshrc
```

重新启动打开的终端以使其生效。
{% endtab %}
{% endtabs %}

2、按照 [Github 文档](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification)配置提交签名

3、在下面配置您喜欢的 git 工具

4、将测试提交推送到 Github 并确保「Verified」标记出现在提交描述旁边：

<figure><img src="https://contributing.bitwarden.com/assets/images/commit-signing-bd1537917a2ce059f7bdff988017b829.png" alt=""><figcaption></figcaption></figure>

### 命令行 <a href="#command-line" id="command-line"></a>

配置提交签名后，您可以使用 `-S` 标志对提交进行签名：

```bash
git commit -S
```

为了避免每次都使用 `-S` 标志，您可以默认签署所有提交：

```bash
git config --global commit.gpgSign true 
```

（移除 `--global` 标志以仅将此设置应用于当前存储库）

### Visual Studio 代码 <a href="#visual-studio-code" id="visual-studio-code"></a>

在 Preferences -> Settings -> 搜索「commit signing」以启用提交签名。

#### macOS：GPG 密钥密码短语提示故障 <a href="#macos-gpg-key-passphrase-prompt-issue" id="macos-gpg-key-passphrase-prompt-issue"></a>

一些 macOS 用户在使用 VS Code 时遇到问题，并且 gpg-agent 在使用 VS Code git GUI 时没有提示输入 GPG 密钥密码短语以签署提交。VS Code 显示的错误提示消息：`Git: gpg failed to sign the data` 表明了此故障。

此问题的[解决方法](https://github.com/microsoft/vscode/issues/43809#issuecomment-828773909)是将您的 gpg-agent 配置为使用 macOS 的 [pinentry](https://www.gnupg.org/related_software/pinentry/index.html) 以强制安全提示。在您选择的终端中运行以下命令：

1. `brew install pinentry-mac`
2. `echo "pinentry-program $(which pinentry-mac)" >> ~/.gnupg/gpg-agent.conf`
3. `killall gpg-agent`

**注意**：您可能需要重新启动 VS Code 才能使其生效，但现在应该会根据需要提示您输入 GPG 密钥密码短语。如果这不能解决您的问题，请按照下面的[故障排除](#troubleshooting)指南进行操作。

### SourceTree <a href="#sourcetree" id="sourcetree"></a>

请参阅 [Setup GPG to sign commits within SourceTree](https://confluence.atlassian.com/sourcetreekb/setup-gpg-to-sign-commits-within-sourcetree-765397791.html)。

## 故障排除 <a href="#troubleshooting" id="troubleshooting"></a>

* 如果您收到此错误消息「error: gpg failed to sign the data」，请确保将 `export GPG_TTY=$(tty)` 添加到您的 `~/.zshrc`（或 `~/.bashrc`，如果您使用 bash）并重新启动您的终端。有关此错误的更多帮助，请参阅[此故障排除文档](https://gist.github.com/paolocarrasco/18ca8fe6e63490ae1be23e84a7039374)。


# 拉取请求

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/pull-requests/)
{% endhint %}

Pull Requests（拉取请求）是我们用来编写软件的主要机制。GitHub 有一些关于使用 Pull Request 功能的精彩[文档](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests)。

## 分支 <a href="#branch" id="branch"></a>

每个新功能或错误修复都应该在单独的分支上开发。分支允许您同时处理多个功能。在大多数情况下，您应该从 `master` 分支。但是，如果您与其他贡献者合作，我们通常会分支出一个长期存在的功能分支。长期存在的功能分支允许我们将单个功能分解为多个 PR，这些 PR 可以单独审查，但可以一起测试和发布。

作为 Bitwarden 贡献者，您应该分支 `origin/master`，这确保分支始终基于最新的上游 `master` 即使本地 `master` 已过时。

```bash
git checkout -b <team>/<issue-number>/<brief-description> -t origin/master
```

[这里](/contributing/pull-requests/branching)详细描述了我们的分支策略。

## 提交 <a href="#commit" id="commit"></a>

我们建议将相关更改分组到单个提交中。这可以使审阅者更容易理解和评估所提议的更改，同时还可以为贡献者提供检查点，以便在出现问题时可以恢复。

我们没有关于如何构建提交消息（例如语义提交消息）的标准。我们鼓励提交消息应在 50 个字符的限制内，以便可以轻松使用 `git log`。如果提交消息需要超过 50 个字符，最好将其分解为更小的原子更改，以提高 git 历史记录的可读性和可延展性（还原、挑选等）。

更高级的贡献者可能会发现[重写历史](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History)很有用。这允许贡献者在推送到远程存储库之前修改其本地历史记录。一个常见的用例是压缩多个半工作提交。请务必遵循强制推送建议。

{% hint style="danger" %}
PR 被审查后，就应**避免强制推送**。

影响现有 git 提交的 Git 操作会阻止 GitHub 正确识别 PR 的「新的更改」，迫使审阅者重新开始。
{% endhint %}

## 创建拉取请求 <a href="#creating-a-pull-request" id="creating-a-pull-request"></a>

Bitwarden 存储库有一个应遵循的 *Pull Request* 模板。这将确保 PR 审核顺利进行，因为它将为审核者提供背景信息。创建社区 PR 后，它们将自动链接到内部 Jira 票证。内部票证用于确定优先级和跟踪目的。将 `@dept-design` 标记为任何 UI 更改的审阅者。

## 审查流程 <a href="#review-process" id="review-process"></a>

虽然我们主要使用异步审查流程，但请随时安排与审查者/贡献者的会议来讨论更改。虽然异步通信很有用，但它会带来时间损失，从而拖延审查过程。有时，召开简短的电话会议来讨论更改可能会节省大量时间。

我们编写了一些[代码审查指南](/contributing/pull-requests/code-review)，建议您在执行第一次代码审查之前阅读这些指南。


# =贡献审查程序

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/pull-requests/community-pr-process)
{% endhint %}


# 分支

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/pull-requests/branching/)
{% endhint %}

## 命名约定 <a href="#naming-convention" id="naming-convention"></a>

要保持分支有组织性，我们需要遵守分支的命名约定。

这种命名约定使我们能够轻松识别分支上正在完成的工作类型，并将有助于识别和跟踪过时的分支。

### 特定 Jira 议题的分支 <a href="#branches-for-a-specific-jira-issue" id="branches-for-a-specific-jira-issue"></a>

为了将分支上的工作链接到我们的 Jira 议题，分支名称应由三部分组成，并用斜杠分隔：

* 团队名称或缩写（例如 `vault`），以及
* Jira 议题标签（例如 `pm-1234`）
* 正在完成的工作的简短描述（例如 `update-csp-hashes`）

在此示例中，完整的分支名称为 `vault/pm-1234/update-csp-hashes` 。

* 分支名称中仅使用小写字母。
* 使用 `-` 分隔单词，使用 `/` 分隔分支名称部分。仅使用这些字符来分隔单词或部分。
* 将工作描述部分限制为约 50 个字符。总体分支名称最多应为 80 个字符。
* 团队名称必须一致。要么总是缩写，要么不缩写。

### 多个 Jira 议题的分支​ <a href="#branches-for-multiple-jira-issues" id="branches-for-multiple-jira-issues"></a>

如果分支将包含多个 Jira 议题（很可能是因为它是[长期功能分支](#long-lived-feature-branch)）的工作，则名称应该是功能的描述性名称，用破折号分隔（例如 `my-long-lived-feature`）。尽可能考虑简洁，因为我们的 QA 团队在对功能执行 QA 测试时需要使用此分支名称。

## 分支开发 <a href="#branching-for-development" id="branching-for-development"></a>

分支策略的开发取决于是否使用我们所谓的「长期功能分支」来管理工作。

{% hint style="info" %}
我们在本文档中使用术语「功能」来指代对代码库的任何更改，无论是一组新功能、错误修复还是重构现有代码。这并不意味着要排除本身不是「功能」的开发类型。
{% endhint %}

### 选择哪种分支模型？​ <a href="#which-branching-model-to-choose" id="which-branching-model-to-choose"></a>

分支模型的开发选择取决于您计划如何处理**整体可测试、可发布的功能**。因此，在开始处理一组新的 Jira 故事或任务时应该提前计划，并且需要整个团队（开发、QA 和产品）之间的协调。

如果该功能将满足以下条件，请选择**长期功能分支**：

* 需要多个拉取请求来生成可测试、可发布的更改，并且
* 无法将更改放在功能标志后面。

如果该功能将满足以下条件，请选择**短期功能分支**：

* 需要一个拉取请求来生成可测试、可发布的更改，或者
* 当多个 PR 正在运行时，可以将更改放在功能标志后面。

{% hint style="success" %}
如果有疑问，请倾向于直接从 `master` 为每个开发人员的工作创建短期功能分支，因为长期功能分支会产生内置开销。
{% endhint %}

### 长期功能分支​ <a href="#long-lived-feature-branch" id="long-lived-feature-branch"></a>

当产生最小的独立可测试、可发布变更的工作主体太大而无法封装在单个 PR 中，或者需要多个开发人员的贡献时，长期功能分支是必要的。

长期功能分支应**仅仅**包含：

* 个人对该功能的贡献已批准更改的 PR，以及
* 合并来自 `master` 的提交

任何其他直接提交到长期功能分支都会使 `master` 的最终 PR 的最终审查变得复杂，应该避免。

#### **开发** <a href="#development" id="development"></a>

要开始使用长期功能分支开发某个功能，贡献者应该为该功能创建长期功能分支，并从该分支创建一个草稿 PR 到 `master` 中。如果适用，此名称应包含 Jira Epic 名称。

然后，每个开发人员都应该从该功能分支中分支出来，创建一个以发起 Jira 议题命名的「议题分支」，如[特定 Jira 议题的分支](#branches-for-a-specific-jira-issue)中所述。我们将其称为议题分支，因为 Jira 将故事和任务称为议题，并与上面的长期功能分支区分开来。

{% hint style="info" %}
值得注意的是，在这种情况下，我们决定不能（或不会）独立测试或发布每个问题分支。我们引入了一个中间功能分支来收集所有这些相关更改，以允许作为一个整体进行测试和发布。
{% endhint %}

当每个开发人员完成他们的工作时，他们应该在长期功能分支中打开一个 PR。对长期功能分支的每项更改都必须有经过批准的 PR，否则在最终合并到 `master` 之前，所有这些单独的提交都需要经过审查。开发人员应该标记适当的开发组来审查 PR。

当开发人员批准 PR 时，PR 应完成，并将整体更改的一部分合并到长期功能分支中。

合并所有议题分支后，`needs-qa` 标签应应用于长期功能分支。

#### **同步长期功能分支​** <a href="#syncing-the-long-lived-feature-branch" id="syncing-the-long-lived-feature-branch"></a>

我们通常指定一个人负责使长期功能分支保持最新状态。在大多数情况下，这将是团队的技术主管，但可以是任何人。此人负责使功能分支与 master 保持合理的最新状态，为合并等做好准备。

由于 GitHub 处理审查的方式，此人也无法批准长期功能分支的最终 PR。审查者可以是团队中的任何人。然而，由于该分支的工作通常较大，因此具有代码库的一些资历可能是有益的。

#### **QA** <a href="#qa" id="qa"></a>

QA 团队在长期功能分支上进行测试（请注意，QA 在 PR 完成后进行）。这是因为该流程的前提是功能的各个部分无法单独测试。

如果 QA 发现缺陷，则应在长期功能分支之外的另一个议题分支中修复该缺陷，并在长期功能分支中使用另一个经过审查的 PR 来解决该议题。

#### **最终审查​** <a href="#final-review" id="final-review"></a>

由于每个拉取请求在合并到长期功能分支之前已经经过审查，因此功能分支审查更多的是健全性检查。审查者应确保以下内容：

* 该功能分支已准备好合并到 `master` 中。
  * 确保工作已经过 QA 测试，包括在需要时在 Jira 中编写 QA 注释，或者
  * 验证该功能位于功能标志后面，并且交叉边界经过测试。
* 直接在分支上审查任何未经审查的提交。这些可以是功能工作或合并提交。

{% hint style="danger" %}
由于功能分支没有与主分支相同的保护，因此在技术上可以直接提交到分支或合并拉取请求，而无需进行最新的审查。然而，这不应该鼓励，并且应尽可能避免，唯一的例外是合并提交。
{% endhint %}

当功能分支上的所有开发和功能测试完成后，`master` 中的原始 PR 应移出草稿状态，并用适当的开发组标记它以供审查。

可以使用 GitHub 的 UI 打开 Pull 请求并单击 `Commits` 选项卡来执行最终审查。然后可以单独检查每个提交。

* 通过点击拉取请求链接并验证提交 SHA 哈希匹配，验证提交是否具有现有审查。寻找 `Author merged commit {hash} into branch`。
* 如果不执行提交的定期代码审查。

合并提交也应该进行审查，GitHub UI 将自动简化合并提交并仅显示所做的更改。如果从命令行或通过其他工具进行审查，请使用命令 `git show <hash>`。有关一些背景和更多信息，请阅读[如何审查合并提交](https://haacked.com/archive/2014/02/21/reviewing-merge-commits/)。

### 短期功能分支​ <a href="#short-lived-feature-branch" id="short-lived-feature-branch"></a>

短期功能分支非常适合以下工作主体：

* 可由单个贡献者开发
* 以在单个拉取请求中进行审查
* 可以独立测试，并且
* 可独立发布

对于小部分新功能和大多数错误修复来说，通常都是这种情况。

#### **开发** <a href="#development" id="development"></a>

开发人员应创建一个以发起 Jira 议题命名的分支（例如 `PM-1234`），并从该分支创建草稿 PR 到 `master` 中。

{% hint style="info" %}
分支名称应尽可能短，最好只是议题名称。这使得 QA 团队可以更轻松地将环境切换到各个分支，因为他们在执行此操作时必须多次输入分支名称。这对于短期功能分支尤其重要，其中测试可能要简短得多。
{% endhint %}

开发过程中，应定期将 `master` 分支合并到分支中，以避免冲突。

开发完成后，开发人员应准备 PR 供审查：

* 从 PR 中删除草稿状态
* 将 `needs-qa` 标签添加到 PR
* 如果需要设计批准，则标记适当的开发组以供审查和 `@dept-design`

当团队成员批准 PR 时，它应该保持开放状态以供测试。这很重要，因为此时完成 PR 会向 `master` 引入未经测试的更改。

#### **QA** <a href="#qa" id="qa"></a>

QA 团队应该在短期功能分支上进行测试。如果发现任何缺陷，则应通过直接提交到短期功能分支来解决这些缺陷，从而在重新引入 QA 的新更改之前触发开发人员团队的重新审查。

在 QA 测试了功能并且开发人员解决了所有缺陷后，PR 所有者应完成 PR 并将更改合并到 `master` 分支中。

## **发布分支** <a href="#branching-for-release" id="branching-for-release"></a>

在给定版本的开发完成日期后的第一个工作日，将在 `master` 基础上创建 `rc` 分支。这是 `master` 中正在进行的工作的快照，它将代表即将发布的版本中发布代码。

`rc` 分支用于回归测试。然后将其用作每个已部署实体的生产发布和部署的源。每次发布时，都会创建一个 `vYYYY.MM.#-{component}` 格式的标签。这可用于稍后修复此版本。

发布完成后，`rc` 分支将被删除。

### 修补程序版本​ <a href="#hotfix-releases" id="hotfix-releases"></a>

对于修补程序版本，会根据应应用修补程序的版本的版本标记创建修补程序分支。分支命名取决于存储库。对于除 `clients` 之外的所有存储库，分支名称为 `hotfix-rc`。但是，由于我们可以单独发布各个客户端，因此 `clients` 存储库中的每个客户端都有自己的已命名修补程序分支：

* 网页端： `hofix-rc-web`
* 桌面端： `hotfix-rc-desktop`
* 浏览器端： `hotfix-rc-browser`
* CLI： `hotfix-rc-cli`

创建修补程序分支后，`master` 中的各个提交将被精心挑选到修补程序分支中。对于客户端修复，这可能需要挑选多个修补程序分支。

部署修补程序后，修补程序分支将被删除。

{% hint style="success" %}
**修补程序 QA 测试**

对于修补程序，在合并到 `master` 之前，我们不会对功能分支执行 QA 测试。这是我们承认的风险，为了加快修补程序过程并避免必须切换所有 QA 测试环境以引用我们的修补程序分支。

相反，一旦 PR 获得批准，修补程序更改就会合并到 `master` 中，然后精心挑选到适当的修补程序分支。然后在修补程序分支上对它们进行测试。
{% endhint %}


# 代码审查

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/pull-requests/code-review)
{% endhint %}

在 Bitwarden，我们鼓励每个人都参与代码审查。团队将主要关注他们自己的代码审查，但如果您看到一些有趣的内容，请随时加入并讨论。

要进行高效的代码审查，需要记住以下几点（来自 [Best Practices for Code Review | SmartBear](https://smartbear.com/learn/code-review/best-practices-for-peer-code-review/)）：

* 拉取请求应该是可管理的。如果 PR 太大（明显超过几百行），请询问贡献者是否可以在审查之前将其拆分为多个 PR。
  * 这在我们的代码库中可能很棘手，因为许多事情都是紧密耦合的。
* 审查时请花些时间 - 预计每小时代码量少于 500 行。
* 休息一下——审查时间不要超过 60 分钟。

不要因为花时间进行代码审查而感到难过！它们通常花费的时间比您想象的要长，我们应该根据需要花费尽可能多的时间。

{% hint style="success" %}
在开发周期早期发现的错误或缺陷，其修复成本要小得多。
{% endhint %}

## 审查 <a href="#reviewing" id="reviewing"></a>

如果您认为其他人对您正在审查的代码非常了解，请随时与他们联系或将他们添加为审查者。

请不要批准您不理解其含义的代码。随时欢迎提出意见和关注！例如，可以留下一些一般性评论或反馈，同时也表示您没有足够的知识来批准更改。作者可以要求其他人再次审查，并且 PR 上有两个审查者并没有什么问题。

{% hint style="info" %}
在审查代码时，请记住所有软件都是为了符合一组假设而构建的。功能、错误修复和其他需求更改代表了这些假设的变化。合并后的代码应该代表满足新需求集的最佳解决方案，这可能不一定与以前的解决方案一致。
{% endhint %}

### 审查状态 <a href="#review-statuses" id="review-statuses"></a>

请正确使用审查状态。

#### 评论 <a href="#comment" id="comment"></a>

评论是讨论事物而无需明确批准或请求更改的好方法。

#### 请求更改 <a href="#request-changes" id="request-changes"></a>

当您认为在合并 PR 之前需要更改某些内容时，应使用请求更改，因为这会阻止其他人在解决您的问题之前批准 PR。

我们应该毫不犹豫地使用此状态，但是我们应该就 PR 获得批准需要进行哪些更改提供明确的反馈。同样，PR 作者不应因更改请求而气馁，这只是表明应在合并 PR 之前进行更改。这很常见。

{% hint style="warning" %}
虽然可以放弃审查，但应谨慎使用。只要有可能，请先联系审查者，以确保他们的疑虑得到解决。以下是一些通常认为放弃审查的情况。

* 审查者离开办公室的时间较长，他们最初的反馈已得到解决。新审查者有责任确保原始反馈得到解决。
* PR 是一个需要快速部署的修补程序，并且审查者处于不同的时区。如果反馈尚未得到解决，则可以在后续 PR 中解决。
  {% endhint %}

#### 批准 <a href="#approve" id="approve"></a>

批准 PR 意味着您对代码的工作原理以及它执行 PR 所声称的功能有信心。这可以基于测试更改或以前的领域知识。

* PR 针对正确的分支。
* 您已验证链接的 Jira 议题描述是否与 PR 中所做的更改匹配。
* 您已阅读并理解 PR 建议的更改的全部影响。
* 您证明这些变化
  * 解决了明显的问题，
  * 以最好的方式解决了需求，
  * 代码结构良好，
  * 遵循了我们最新的、公认的模式，
  * 并且没有意外的副作用。

如果您对上述任何内容不确定，请考虑使用不同的状态或先与作者联系以做讨论。另外，请毫不犹豫地请求其他人进行第二次审查。

如果 PR 影响多个团队，则需要所有受影响团队的批准。生成 PR 的团队（或者管理 PR 的团队（如果它源自社区））的审查者负责批准整个更改，而受影响的团队仅负责他们的代码库部分。

## 审查技术 <a href="#reviewing-techniques" id="reviewing-techniques"></a>

没有一种放之四海而皆准的代码审查技术。然而，有一些技术、工具和其他资源可以帮助您更有效地审查代码。

### 多个重点领域 <a href="#multiple-focus-areas" id="multiple-focus-areas"></a>

将代码审查分为多个重点领域会很有帮助。并一次专注于一个视角。

* 宏观视角——关注整个 PR。
  * 问题解决了吗？
  * 通过在适当的地方使用适当的抽象进行更改，是否可以有效地解决该问题？
  * PR 是否改变了您期望改变的领域？
    * 有缺失的吗？
    * 有没有意想不到的收获？
* 微观视角 - 专注于单个文件。
  * 是否遵守代码样式？
  * 代码可读吗？
  * 是否遵循了以前的模式？
  * 以前的模式仍然是正确的选择吗？

### GitHub 功能 <a href="#github-features" id="github-features"></a>

GitHub 界面有一些方便的工具可以帮助您审查代码。有关更多信息，请参阅下面的内容。

* [评论拉取请求 - GitHub 文档](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/commenting-on-a-pull-request)。如何评论 PR，包括：
  * 对多行进行评论
  * 建议作者通过 GitHub 界面对代码更改立即接受并合并
    * 小心这个！您没有享受到 IDE 的好处。这很容易破坏语法或格式。
* [关于比较拉取请求中的分支 - GitHub Docs](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-comparing-branches-in-pull-requests)。查看差异的不同方式，包括：
  * 隐藏空格（如果有大量缩进更改，则非常有用）
  * 分别显示旧代码和新代码（这样您就可以在没有任何干扰的情况下阅读新代码） - 或将它们组合起来（这样您可以准确地看到发生了哪些更改）

### 本地运行 <a href="#running-locally" id="running-locally"></a>

许多更改可以在 GitHub 上在线查看。然而，有时在本地运行代码有助于提高您的理解 - 例如：

* 使用 IDE 功能（例如跳转到定义或查找引用）
* 重现您认为在代码中发现的错误
* 运行解决方案以了解它们如何组合在一起（宏观视角）。

要在本地运行代码，我们建议使用 GitHub CLI。这使您可以直接签出 PR，而无需管理远程分支 - 例如：

```bash
// From within the repo:
gh pr checkout <GitHub PR number>
```


# UI 审查 - Chromatic

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/pull-requests/chromatic)
{% endhint %}

我们使用一种名为 [Chromatic](https://www.chromatic.com/) 的工具，对所有 Storybook Story 进行自动快照测试。这使我们能够以自动化的方式快速捕捉设计回归。作为其中的一部分，我们还使用 Chromatic 来审查和批准可视化更改。Bitwarden GitHub 组织的成员可以使用他们的 GitHub 账户登录 Chromatic。

## 检查​ <a href="#checks" id="checks"></a>

Chromatic 将审核流程分为两个部分：用户界面测试 (UI Tests) 和用户界面审核 (UI Review)。这些在 PR 上表现为两种不同的检查，需要以不同的方式处理。下面详细介绍了处理每一项的期望。

### UI 测试​ <a href="#ui-tests" id="ui-tests"></a>

> UI 测试捕获云浏览器环境中每个 Story 的视觉快照。每当您推送代码时，Chromatic 都会生成一组新的快照并将它们与基线进行比较。如果有视觉变化，您需要验证它们是否是故意的。
>
> *<https://www.chromatic.com/docs/test>*

### UI 审查​ <a href="#ui-review" id="ui-review"></a>

> UI 测试可以保护您免受意外回归的影响。但是，在发布之前，您需要邀请开发人员、设计师和产品经理检查 UI 以确保其正确。
>
> UI 审查创建由 PR 引入的精确视觉更改的变更集。您指定审阅者，他们可以对不太正确的更改发表评论并请求调整。可以将其视为代码审查，但针对的是您的 UI。
>
> *<https://www.chromatic.com/docs/review>*

## 审查​ <a href="#reviewing" id="reviewing"></a>

如果存在视觉变化，Chromatic 会将拉取请求标记为待处理。每个拉取请求作者都有责任审查 Chromatic 中的 UI 测试结果，并批准更改是否是有意的。

通过单击拉取请求中的 **UI 测试**检查，可以轻松访问测试。

<figure><img src="https://raw.githubusercontent.com/bitwarden/contributing-docs/master/docs/contributing/pull-requests/ui-tests.png" alt=""><figcaption></figcaption></figure>

UI 审核所需的操作取决于失败的案例：

* 组件库应由设计部门审查，这是通过在 GitHub 中请求审查 `bitwarden/dept-design` 来完成的。
* 其他更改应由审查开发人员审查。

可以通过单击 **UI 审查**检查来访问审核。

<figure><img src="https://raw.githubusercontent.com/bitwarden/contributing-docs/master/docs/contributing/pull-requests/publish-review.png" alt=""><figcaption></figcaption></figure>

还可以通过单击 **Storybook Publish** 检查来浏览 Storybook 中的拉取请求。


# 无障碍

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/accessibility/)
{% endhint %}

Bitwarden 严格遵守 [WCAG AA 要求](https://www.w3.org/WAI/WCAG2AA-Conformance)。以下介绍了在实现功能和内容时应考虑的无障碍的常见方面。此列表并不全面，但它提供了一个起点，让您了解可能会限制无障碍新功能的常见「陷阱」。有关无障碍的全面指南，请访问 [WCAG 官方网站](https://www.w3.org/WAI/WCAG21/Understanding/)。

{% hint style="info" %}
有关 Bitwarden 无障碍的其他资源可以在 [Confluence](https://bitwarden.atlassian.net/wiki/spaces/EN/pages/193953973) 上找到。
{% endhint %}

## 视觉呈现 <a href="#visual-presentation" id="visual-presentation"></a>

| WCAG 成功规范                                                                                                                                                                                                                      | 描述                                                                                                                                                                                                                                                                                                                              |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <p><a href="https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html">1.4.3 Contrast minimum</a><br><br><a href="https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html">1.4.11 Non-text contrast</a></p> | <p>内容的对比度应达到或超过 WCAG AA 准则：<br>  - 对于普通文本：4.5:1<br>  - 大型文本和图形：3:1<br>  - 使用  <a href="https://webaim.org/resources/contrastchecker/">WebAIM contrast checker</a> 了解更多信息<br>  - 使用组件库中的变量时， <code>text-main</code> 变量会在 <code>background</code> 和 <code>background-alt</code> 变量上进行测试。所有其他背景一般使用 <code>text-contrast</code></p> |
| [1.4.13 Content on Hover of focus](https://www.w3.org/WAI/WCAG21/Understanding/content-on-hover-or-focus.html)                                                                                                                 | **Bitwarden 设计最佳实践**：交互式元素应始终可见，用户无需悬停或聚焦该元素。这样可以使内容更容易被发现，从而便于用户访问，同时也消除了为满足 WCAG 标准而实施这种模式所需的复杂性。                                                                                                                                                                                                                             |
| [2.4.7 Focus visible](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible.html)                                                                                                                                          | **Bitwarden 设计最佳实践**：所有交互式元素都应具有可见焦点、悬停状态和正确的光标样式。                                                                                                                                                                                                                                                                              |
|                                                                                                                                                                                                                                | **Bitwarden 设计最佳实践**：缩短的图标和文本应始终带有 `title` 属性，以提供额外的上下文。                                                                                                                                                                                                                                                                        |

## 键盘导航 <a href="#keyboard-navigation" id="keyboard-navigation"></a>

| WCAG 成功规范                                                                                                                                                                                              | 描述                                                  |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------- |
| [2.1.1 Keyboard](https://www.w3.org/WAI/WCAG21/Understanding/keyboard.html)                                                                                                                            | 页面上的所有鼠标交互元素都应可通过键盘导航访问。                            |
| <p><a href="https://www.w3.org/WAI/WCAG21/Understanding/keyboard.html">2.1.1 Keyboard</a><br><br><a href="https://www.w3.org/WAI/WCAG21/Understanding/no-keyboard-trap">2.1.2 No keyboard trap</a></p> | 如果打开了弹出窗口（尤其是多个对话框），请务必测试焦点是否能正确进出每个对话框，以及是否存在键盘陷阱。 |
| [2.4.7 Focus visible](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible.html)                                                                                                                  | 每个交互式元素都应有一个可见的焦点指示器，以帮助用户通过键盘导航页面。                 |

## 屏幕阅读器 <a href="#screen-reader" id="screen-reader"></a>

弱视或失明用户通常会使用键盘导航和屏幕阅读器来浏览产品。这将导致页面内容的语音朗读，通常还会根据关注的语义元素提供额外的指导。

要进一步了解屏幕阅读器和键盘导航，请在 macOS 上打开 [VoiceOver](https://support.apple.com/en-ca/guide/voiceover/vo2682/mac) 或在 Windows 上下载 [NVDA](https://www.nvaccess.org/download/)，并尝试使用键盘和屏幕阅读器导航设备。

| WCAG 成功规范                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | 描述                                                                                                                                                                                                                               |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [1.1 Text alternatives](https://www.w3.org/WAI/WCAG21/Understanding/text-alternatives)                                                                                                                                                                                                                                                                                                                                                                                    | 对于传递其他地方未提供的信息的任何非文本元素，请使用 `alt-text` 或 aria-labels 标记。如果元素不具有信息性，则可标记为 `decorative`。                                                                                                                                            |
| <p><a href="https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships">1.3.1 Info and relationships</a><br><br><a href="https://www.w3.org/WAI/WCAG21/Understanding/meaningful-sequence">1.3.2 Meaningful sequence</a><br><br><a href="https://www.w3.org/WAI/WCAG21/Understanding/error-identification">3.3.1 Error identification</a><br><br><a href="https://www.w3.org/WAI/WCAG21/Understanding/labels-or-instructions">3.3.2 Labels or instructions</a></p> | <p>确保相关内容一起呈现，并可通过编程确定。<br><br>使用 <code>aria-describedby</code> 将一个元素中的内容与另一个元素关联起来。这通常用于表单错误或辅助文本。</p>                                                                                                                          |
| [4.1.2 Name, role, value](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value)                                                                                                                                                                                                                                                                                                                                                                                    | 在触发菜单或对话框等其他元素的元素上使用 `aria-haspopup`。这将告诉屏幕阅读器（以及用户），另一个元素正在打开，并将有额外的操作。更多信息，请参阅 [aria-haspopup - Accessibility MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-haspopup)。                  |
| [4.1.2 Name, role, value](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value)                                                                                                                                                                                                                                                                                                                                                                                    | 在展开或折叠的元素上使用 `aria-expanded` 可显示或隐藏更多信息。注意：`aria-haspopup` 和 `aria-expanded` 不应同时用于同一元素。菜单默认使用 `aria-haspopup`，手风琴或其他显示/隐藏切换器等交互方式则使用 `aria-expanded` 。                                                                          |
| [4.1.2 Name, role, value](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value)                                                                                                                                                                                                                                                                                                                                                                                    | 当标签页、表格行或网格单元格等多个元素可被选中或取消选中时，请使用 `aria-selected`。更多信息，请参阅 [WAI-ARIA Roles - Accessibility MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles)。                                                          |
| [4.1.2 Name, role, value](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value)                                                                                                                                                                                                                                                                                                                                                                                    | 大多数 HTML 元素都已分配了角色。但在某些情况下，您可能需要指定元素的角色。有关角色的更多信息，请参阅 [WAI-ARIA Roles - Accessibility MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles)。大多数 Bitwarden 组件库组件都已应用了适用的角色。但在某些情况下，如果您需要创建一个新的组件，则应考虑元素的角色。 |
| [4.1.3 Status messages](https://www.w3.org/WAI/WCAG21/Understanding/status-messages)                                                                                                                                                                                                                                                                                                                                                                                      | 有时，页面上的交互会触发附加内容的添加或更改。例如：点击 「发送验证码」按钮会触发一条确认信息。通过屏幕阅读器公布这些内容非常重要。这可以通过 `aria-live` 区域或使用 `role=alert` 属性来实现。                                                                                                                    |

## 语义结构 <a href="#semantic-structure" id="semantic-structure"></a>

确保在每个页面上使用适当的语义结构。用于创建页面的元素可为屏幕阅读器提供更多上下文信息，使其了解该元素包含的内容类型。正确使用地标和标题，还能让用户仅通过地标或标题级别来浏览页面，而不必总是以标签方式浏览整个页面的内容。

| WCAG 成功规范                                                                                          | 描述                                                                                  |
| -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| [3.1.1 Language of page](https://www.w3.org/WAI/WCAG21/Understanding/language-of-page)             | 在每个页面的 `html` 元素中加入 `lang` 属性。                                                      |
| [2.4.2 Page titled](https://www.w3.org/WAI/WCAG21/Understanding/page-titled)                       | 使用一个 `title` 元素和一个 `h1` 在每个页面上显示标题。                                                 |
| [1.3.1 Info and relationships](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) | 用与内容目的相匹配的、具有语义意义的元素来包装内容。注意：`div` 和 `span` 没有语义意义，因此最好避免使用它们来包转文本。取而代之的是使用 `p` 元素。 |
| [1.3.1 Info and relationships](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) | 标题层次使用逻辑顺序。`h4` 不应出现在第一个 `h3` 元素之前。此外，不要跳过标题层，应使用下一个降序标题来创建结构。                      |
| [1.3.1 Info and relationships](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) | 使用地标区域传达预期内容；确保在页面导航周围包含 `nav` 元素，在页面内容周围包含 `main` 元素。                              |
| [1.3.1 Info and relationships](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) | 使用 `a` 元素和 `hrefs` 作为链接；当用户导航到新页面时使用链接。                                             |
| [1.3.1 Info and relationships](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) | 使用 `button` 元素提交数据或执行不移动键盘焦点的屏幕操作。                                                  |
| [3.3.2 Labels or instructions](https://www.w3.org/WAI/WCAG21/Understanding/labels-or-instructions) | 使用适当的表单语义，每个输入都有相应的 `label` 元素，并在适用时使用 `fieldset` 元素。                               |


# 依赖管理

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/dependencies/)
{% endhint %}

Bitwarden 使用 [Renovate](https://www.mend.io/renovate/) 来自动更新依赖项。Renovate 会每周自动为依赖项创建拉取请求。安全更新会立即生成拉取请求。

## 所有权​ <a href="#ownership" id="ownership"></a>

Bitwarden 的存储库分为两类：团队拥有的和共享的。

### 团队拥有的存储库​ <a href="#team-owned-repositories" id="team-owned-repositories"></a>

从依赖性的角度来看，团队拥有的存储库由单个团队「拥有」。指定的团队负责审查、批准和合并依赖项更新。一个存储库可能由团队拥有的一些原因是该存储库主要由该团队开发，或者是为了平衡团队需要管理的依赖项数量。

团队拥有的存储库的一些示例是 [`directory-connector`](https://github.com/bitwarden/directory-connector)（由管理控制台团队拥有）和 [`key-connector`](https://github.com/bitwarden/key-connector/)（由身份验证团队拥有）。

### 共享的存储库​ <a href="#shared-repositories" id="shared-repositories"></a>

共享存储库没有任何直接拥有者。相反，每个依赖项都分配给一个团队。分配给某个依赖项的团队负责审查、批准和合并该依赖项。对于重大升级，该团队负责与其他团队协调升级。

共享的存储库的示例是 [`server`](https://github.com/bitwarden/server/) 和 [`clients`](https://github.com/bitwarden/clients/)。

## PR 示例​ <a href="#example-pr" id="example-pr"></a>

{% embed url="<https://contributing.bitwarden.com/assets/images/renovate-pr-50085e99a2b40ed978266a67c808e53a.png>" %}
Renovate PR 示例
{% endembed %}

Renovate PR 包含多个相关领域。上面的 PR 示例包含了两个分组的依赖项。PR 提议将依赖项从 `6.0.21` 升级到 `7.0.12`。该版本的存在时间为 **13 天**，**13%** 的存储库已采用该版本。Renovate 在 Renovate 管理的存储库中的测试成功率为 **74%**，以及对这一更改的置信度较低。有关更多详细信息，请参阅[合并置信度的 Renovate 文档](https://docs.renovatebot.com/merge-confidence/)。

## 工作流程 <a href="#workflow" id="workflow"></a>

Renovate 会在周末自动创建拉取请求，这自然与每个团队在下周一分配一些时间来处理各自团队中的拉取请求相吻合。团队应共同努力在一周内解决未完成的拉取请求，以避免工作停滞。

{% hint style="info" %}
主要的例外是重大升级，有时可能需要更长的时间来协调。理想情况下，团队会提前协调并解决弃用问题。
{% endhint %}

Renovate PR 可能包含单个依赖项或一组相关依赖项。在 Bitwarden，我们通常会将已知相关且应同时升级的依赖项进行分组。我们尽量使分组尽可能小，以最大程度地减少影响并增加批准和合并的信心。

### 审查 <a href="#review" id="review"></a>

典型的依赖关系工作流包括以下步骤：

1. 阅读提议的变更。
2. 审查当前升级和建议升级之间每个已发布版本的每个依赖项的发行说明。确定是否有影响我们代码的任何弃用或破坏性变更。
   * 对于破坏性变更，要么自己解决，要么与其他团队协调解决重大变更。
   * 对于弃用，在受影响团队的积压工作中创建高优先级的 Jira 票证，到期日期至少要比下一个计划的主要依赖项版本早一个冲刺。
3. 验证 CI 状态。
4. 如果测试覆盖率不足，请在本地检查并手动确认几个关键区域。
5. 审查提议的代码变更并批准 PR。
6. 编写包含 QA 测试说明的 Jira 票证。
7. 合并 PR。将 Jira 票证分配给 QA。

### Jira 票证 <a href="#jira-ticket" id="jira-ticket"></a>

开发人员和 QA 之间的交接将通过 Jira 票证进行。该票证应包含受影响的依赖项、要测试部分的任何相关发行说明，以及受影响区域的一些测试说明。

### QA 测试​ <a href="#qa-testing" id="qa-testing"></a>

虽然开发人员负责编写带有测试说明的 Jira 票证，但 QA 工程师应该进行尽职尽责，同时考虑依赖项变更的影响，并在需必要时与工程师讨论可能增加或减少测试范围的问题。

### 回归 <a href="#reverting" id="reverting"></a>

如果 QA 发现了回归，开发人员应负责评估影响并立即还原更新或在新 PR 中解决回归问题。

### 关闭无关的 PR <a href="#closing-irrelevant-prs" id="closing-irrelevant-prs"></a>

有时，由于各种原因，Renovate 会为我们目前无法升级的依赖项创建 PR。例如， `contributing-docs` 依赖于 `docusaurus` ，而后者支持特定版本的 `react`。在 `docusaurus` 支持它之前，我们无法升级 `react`。

在这些情况下，团队可以对 PR 注释不升级的原因，然后关闭 PR 或推迟到以后再升级。如果团队关闭了 PR，则希望其成员监控其依赖性，并在未来重新考虑升级问题。

## 更新配置 <a href="#renovate-configuration" id="renovate-configuration"></a>

Renovate 通过每个存储库中的 `.github/renovate.json` 文件进行配置。为了保持一致性，我们遵循一个内部模板。该模板可在[模板库](https://github.com/bitwarden/template/blob/main/.github/renovate.json)中获取。

Renovate 使用一个名为 [`PackageRules`](https://docs.renovatebot.com/configuration-options/#packagerules) 的概念，它允许我们指定依赖项的所有权，并确保将适当的团队添加为审查者。下面是将 `@angular/core` 指派给 Platform 团队的示例。

```json
{
  "matchPackageNames": ["@angular/core"],
  "description": "Platform owned dependencies",
  "commitMessagePrefix": "[deps] Platform:",
  "reviewers": ["team:team-platform-dev"]
}
```

对于由单个团队维护的存储库，无需使用 `packageRules` 来分配所有权。相反，请确保设置了适当的代码所有者。


# 功能标记

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/feature-flags)
{% endhint %}

## 背景​ <a href="#background" id="background"></a>

基于 [ADR 0018](/architecture/adr/0018-feature-management) 添加了对功能标记的支持。一些亮点：

* 当请求标记的状态时提供[上下文](https://github.com/bitwarden/server/blob/master/src/Core/Context/ICurrentContext.cs)。我们目前允许针对用户、组织和服务账户进行定位。仅将 ID 发送到 LaunchDarkly，以避免 PII 共享。
* 所有可用的功能标记状态都提供给调用[配置 API](https://github.com/bitwarden/server/blob/master/src/Api/Models/Response/ConfigResponseModel.cs) 的客户端。
* 环境（目前是生产、质量保证 (QA) 和开发）的存在可以进一步细分标记状态。这将根据代码运行的位置自动进行。

## 标记数据源​ <a href="#flag-data-sources" id="flag-data-sources"></a>

在客户端或服务器代码中使用功能标记时，了解标记的来源非常重要。

标志的来源取决于正在使用的 Bitwarden 服务器实例，对于客户端开发，标记由 Bitwarden API 提供。

| 服务器配置            | 标记来源                    |
| ---------------- | ----------------------- |
| 本地开发             | 本地应用程序设置、JSON 文件或代码修改   |
| 自托管              | 除非提供上述本地配置，否则标记为「off」   |
| QA Cloud         | LaunchDarkly QA         |
| Production Cloud | LaunchDarkly Production |

{% hint style="warning" %}
**自托管支持**

自托管客户未正式支持功能标记。在 Bitwarden 内部测试之外，不支持使用应用程序设置或 JSON 文件来获取功能标记值。请参阅[自托管注意事项](#self-hosted-considerations)，了解功能标记如何应用于自托管。
{% endhint %}

本地开发服务器实例不会查询 LaunchDarkly 的功能标记值。

如果您需要在本地开发期间更改任何功能标记值的默认值，则需要设置本地应用程序设置或基于文件的数据源。**如果没有本地数据存储，所有标记值都将解析为其默认（「off」）值**。

### 本地配置：用户机密​ <a href="#local-configuration-user-secrets" id="local-configuration-user-secrets"></a>

要通过应用程序设置设置数据源，请将以下内容放入您的用户机密中：

```json
{
  "globalSettings": {
    "launchDarkly": {
      "flagValues": {
        "example-boolean-key": true,
        "example-string-key": "value"
      }
    }
  }
}
```

将 `example-boolean-key` 和 `example-string-key` 替换为您的标记名称，并相应地更新标记值。

请记住运行 `dev/setup_secrets.ps1` 并重新启动服务器以使新的机密生效。

环境变量也可以像其他应用程序设置覆盖一样使用。

### 本地配置：JSON 文件​ <a href="#local-configuration-json-file" id="local-configuration-json-file"></a>

要通过本地文件设置数据源，请创建一个 `flags.json` 文件，如下所示：

```json
{
  "flagValues": {
    "example-boolean-key": true,
    "example-string-key": "value"
  }
}
```

将 `example-boolean-key` 和 `example-string-key` 替换为您的标记名称，并相应地更新标记值。

默认情况下，LaunchDarkly 启动将在根项目目录中查找此文件（例如 `Api` 项目的 `/src/Api/`），并将其部署到构建输出目录。但是，如果您希望将文件存储在其他位置，则可以使用 `FlagDataFilePath` 配置设置来覆盖它。该文件必须在构建解决方案之前存在，但一旦存在，您就可以更改文件内容并在运行/调试代码时立即查看结果。

### 本地配置：代码修改​ <a href="#local-configuration-code-modification" id="local-configuration-code-modification"></a>

在某些情况下，可能需要在清理活动完全完成之前将功能标记值更改为默认状态以外的值，特别是当已部署的客户端仍然依赖于返回的标记值来确保某些功能时。在服务器代码库中，除了功能标记[常量定义](#server)之外，还存在一个方法 `GetLocalOverrideFlagValues()` ，其中可以将覆盖作为字典键值对放置：

```csharp
return new Dictionary<string, string>()
{
    { ExampleBooleanKey, "true" }
};
```

这只能暂时使用，并作为功能标记清理过程的一部分，以及为不使用或不了解替代配置方法的安装启用快速功能可用性。

{% hint style="success" %}
**客户端中使用的标记的本地数据源**

为了在客户端中使用功能标记，上述设置应在 `Api` 项目中定义 - 这是因为客户端用来查询功能标记的 `/config` 端点位于 `Api`。这样做将确保检索到正确的标记值并将其发送到客户端。
{% endhint %}

## 创建一个新标记​ <a href="#creating-a-new-flag" id="creating-a-new-flag"></a>

当开始开发新功能时，请与您的团队讨论是否应将其放在功能标记后面。团队应该就标记内容的范围以及应该应用标记的位置（客户端和服务器端）达成一致。虽然对于“功能”的构成没有明确的规则，但应共同努力实现标记及其各自用途的最佳平衡。

一旦您确定需要一个功能标记，第一步就是确定一个名称。命名的建议是：

* 使用短横线命名该标记（小写字母和破折号分隔，例如 `enable-feature`）。
* 对于布尔标记，不必包含 `enable` 动词，因为它暗示它是一个功能标记。例如，建议使用 `new-feature` 而不是 `enable-new-feature`。
* 保持键名简洁。

名称确定后，将功能标记添加到服务器上的 [`FeatureFlagKeys`](https://github.com/bitwarden/server/blob/master/src/Core/Constants.cs) 常量文件中。这将允许通过您在下面配置的任何数据源从 LaunchDarkly 检索标记。

### 本地开发 <a href="#local-development" id="local-development"></a>

当您开始使用该功能时，请使用本地配置选项之一将标记显示给您的使用代码，以确保所有支持的标记值的行为都是正确的。由于初始开发的功能标记不必存在于 LaunchDarkly 中，因此**在确定最终实现之前不要在线创建它们**。

{% hint style="success" %}
**本地客户端开发**

请记住，对于客户端本地开发，功能标记的来源取决于您正在使用的服务器实例。例如，如果您正在开发客户端代码并引用 QA Cloud Bitwarden API，则必须在此处配置该标记，而不是在本地数据存储中。
{% endhint %}

### LaunchDarkly 中的定义​ <a href="#definition-in-launchdarkly" id="definition-in-launchdarkly"></a>

为了在任何部署的环境中测试功能标记，必须首先在 LaunchDarkly Web 应用程序中定义它。为此，请向您的工程经理请求标记- 他们将拥有适当的访问权限。你们应该讨论：

* 标记的数据类型。
* 标记的默认值。
* 标记的可能值（对于非布尔类型）。
* 任何应驱动标记行为的基于上下文的规则。

{% hint style="success" %}
**我什么时候应该在 LAUNCHDARKLY 中请求标记？**

作为一般规则，应请求在 LaunchDarkly 中创建功能标记，作为使用该标记将代码合并到主线分支的一部分。由于使用自托管实例进行本地开发和 QA 测试将使用本地数据源，因此第一次引用 LaunchDarkly 中的标记是在代码部署到云环境时。
{% endhint %}

## 在代码中使用功能标记​ <a href="#consuming-feature-flags-in-code" id="consuming-feature-flags-in-code"></a>

当针对功能标记进行编码时，尽可能默认为「off」状态 - 进行防御性编码，以便在标记完全不可用时维护现有功能。当接口支持它时，还应向功能标记访问器提供暗示「off」的默认值。

离线模式使默认值变得更加重要，本地开发以及自托管安装意味着离线。不仅在 LaunchDarkly 的在线标记定义中设置安全默认值，而且还要在代码中设置。

### 客户​端 <a href="#clients" id="clients"></a>

所有客户端都通过查询 Bitwarden API 上的 `/config` 端点来检索其功能标记。客户端不直接引用LaunchDarkly客户端SDK。

为了优化功能标记的使用，不会在每次请求标记值时从服务器检索它们。相反，按照以下时间间隔从服务器检索标记：

* 在应用程序启动时。
* 应用程序启动后每小时一次。
* 同步（自动和手动）。
* 在环境变化时。

从下面定义的服务请求标记值将为使用组件提供来自这些检索事件之一的最新值。

#### 网络​端 <a href="#web" id="web"></a>

通过 [`ConfigService`](https://github.com/bitwarden/clients/blob/master/libs/common/src/services/config/config.service.ts) 上的 `fetchServerConfig()` 方法检索功能标记值。

要使用功能标记，您应该首先将新功能标记定义为 [`FeatureFlags`](https://github.com/bitwarden/clients/blob/master/libs/common/src/enums/feature-flag.enum.ts) 枚举中的枚举值。

定义后，可以通过注入 `ConfigService` 并使用其中一种检索方法来检索该值：

* `getFeatureFlagBool()`
* `getFeatureFlagString()`
* `getFeatureFlagNumber()`

#### 移动端 <a href="#mobile" id="mobile"></a>

通过 `ConfigService` 上的 `GetAsync()` 方法检索功能标记值。

要使用功能标记，您应该首先在 `Constants` 文件中将新功能标记定义为字符串常量值。

定义后，可以通过注入 `IConfigService` 并使用其中一种检索方法来检索该值：

* `GetFeatureFlagBoolAsync()`
* `GetFeatureFlagStringAsync()`
* `GetFeatureFlagNumberAsync()`

### 服务器​ <a href="#server" id="server"></a>

1. 在需要功能标记的地方注入 `IFeatureService`。请注意，访问功能状态时您还需要 `ICurrentContext`。
2. 在 [`FeatureFlagKeys`](https://github.com/bitwarden/server/blob/master/src/Core/Constants.cs) 列表中查找您计划使用的密钥的常量。它应该在[创建新标记时](https://contributing.bitwarden.com/contributing/feature-flags/#creating-a-new-flag)添加。
3. 在要素服务上通过适当的方法使用上述关键常量：
   * `IsEnabled` 用于布尔值，`false` 为假定的默认值。
   * `GetIntVariation` 用于整数，`0` 为假定的默认值。
   * `GetStringVariation` 用于字符串，`null` 为假定的默认值。

## 功能标志生命周期​ <a href="#feature-flag-lifecycle" id="feature-flag-lifecycle"></a>

当您需要更改 LaunchDarkly 内的在线功能时，请让您的管理层知道。只有少数用户拥有 LaunchDarkly 账户，以节省许可成本。

功能标记不一定需要从 LaunchDarkly 中删除，只是未使用即可。将它们链接到 Jira 有助于创建该功能的历史记录，并且可以保留大量的在线日志和审计记录。长时间未访问的功能标记将自动转至「非活动」状态，这也有助于识别需要清理的技术债务。

在定义故事的子任务时，请务必包含一个清理任务，用于从代码中删除功能标记- 重要的是这些任务不要保留太久并假设永久存在。一旦功能成功启动，请在稍后阶段解决该任务。

### 自托管注意事项​ <a href="#self-hosted-considerations" id="self-hosted-considerations"></a>

自托管实例将无法访问 LaunchDarkly，因此从 API 检索的服务器配置会将所有功能标记评估为其默认状态，除非服务器进行了其他配置。这在实践中意味着，在该功能可用于自托管实例之前，必须从代码中删除该功能标记。这意味着分阶段的功能发布周期，如下所示：

1. 发布云并自托管，功能已关闭
2. 打开功能标记，**仅**针对云实例启用该功能
3. 发布云和移除功能标记的自托管，从而为自托管实例启用该功能

自托管安装可以选择配置替代[数据源](#flag-data-sources)以更快地采用功能。


# 模板存储库

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/template-repository)
{% endhint %}

## 位置和用途​ <a href="#location-and-usage" id="location-and-usage"></a>

私有模板库是新项目的基础文件集和整体设置，可在 GitHub 仓库创建界面中选择。它包含了拉取请求模板、inting、持续集成入门等所需的内容。一般适用于所有版本库的核心概念都应在这里创建和审查，然后再发布。该模板代表了整个公司的最佳实践，但也应被视为根据版本库需求进一步设置的起点；在许多情况下，都需要进行定制，详见下文。

## 内容和许可​ <a href="#content-and-licensing" id="content-and-licensing"></a>

文本许可证文件声明了 GPL 和 Bitwarden 专有许可证的使用途。带有 `README` 的 `bitwarden_license` 目录将用于放置 Bitwarden 许可的代码和内容，否则将指定使用 GPL。这些文件及其位置/结构不得修改。

安全和贡献文件阐明了公司的政策和方法。如果有特殊情况，可以对这些文件进行修改，但极有可能保持原样。

根目录下的 `README` 预期可以定制，但没有明确的内容规定。由于文档可以保存在其他地方，如本网站！因此建议简短地写上标题和几句话的简单描述。

## 编辑器配置​ <a href="#editor-configuration" id="editor-configuration"></a>

有一组文件为 Git 定义了预期属性和忽略属性。后者有望根据版本库的需求进行扩展，但在可能的情况下，模板本身也应针对其他用例进行扩展。流行的操作系统和 IDE 特定的忽略项已经存在。

编辑器配置为文件格式设置了规则。与上述忽略类似，模板也应根据新语言和公司标准进行更新。Linters 在执行规则时将遵从编辑器配置。

## 本地 linting <a href="#local-linting" id="local-linting"></a>

[Husky](https://github.com/typicode/husky) 和通过 NPM 进行的 [lint-staged](https://github.com/okonet/lint-staged) 用于本地 lint 和格式化已更改的文件。运行：

```bash
npm install
```

克隆您的新存储库以安装必要的 Git hook 后。

在[包配置](https://github.com/bitwarden/template/blob/main/package.json)的 `lint-staged` 部分中，存在针对特定文件类型的 linter 配置，并使用 [Prettier](https://github.com/prettier/prettier) 作为所有文件的默认格式化程序。扩展这些文件类型，以使其适用于具有可用格式化程序的相关文件类型，例如使用 .NET 应用程序：

```json
"*.cs": "dotnet format --include"
```

或 TypeScript：

```json
"*.ts": "eslint --cache --cache-strategy content --fix"
```

上面使用的编辑器配置可由许多 linter 访问，以驱动结果。

## 依赖管理​ <a href="#dependency-management" id="dependency-management"></a>

为可管理依赖更新配置。它：

* 每个包管理器将次要更改和补丁更改合并到一个汇总拉取请求中。
* 使用依赖性仪表板，我们可以看到哪些拉取请求尚未创建，但仍然可以管理工作负载。
* 通过重建、语义版本控制和锁定文件更新来管理更新。
* 以较小的拉取请求限制作为起点。
* 包括了作为单独拉取请求的主要更新（最新）。
* 将计划安排在周末，此时组织可能有更多的 Actions 工作人员。

如果存储库随着时间的推移而扩展以包含新的包管理器，建议将所有包管理器保持启用状态。更新计划以及单个存储库有多少拉取请求。可能需要例外、其他包管理器和特定于依赖项的配置。

考虑固定依赖项的[最佳实践](https://docs.renovatebot.com/dependency-pinning/#so-whats-best)（尤其是在 root），就像上面用于本地 linting 的最佳实践一样。开发依赖项（例如格式化程序和 linter）需要在所有团队之间进行沟通和协调部署，以便代码风格根据我们的标准和模板存储库本身中看到的编辑器配置保持一致。

## 议题模板​ <a href="#issue-templates" id="issue-templates"></a>

针对集中和相关链接议题的配置，以及用于创建拉取请求的模板，该模板使用几乎所有更改所需的公共部分。说明存在于拉取请求模板中，但一般来说：

* 预期会有跟踪链接，例如 GitHub 或 Jira 议题。
* 如果存储库没有用户界面，则可以移除「Screenshots」部分。
* 根据存储库上下文的需要调整提醒。

您不太可能需要在目标存储库中对模板进行大量修改，并且此处的原始模板始终可以通过组织内的其他改进进行扩展。

## 代码所有权​ <a href="#code-ownership" id="code-ownership"></a>

要定义的 CODEOWNERS 条目指示「拥有」相关路径上的代码的团队。


# 测试


# =数据库集成测试

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/testing/database/)
{% endhint %}


# 负载测试

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/testing/load)
{% endhint %}

负载测试以 k6 脚本的形式提供，可在本地或云中执行，重点是对平台进行大规模演练。

## 配置和参数​ <a href="#configuration-and-parameters" id="configuration-and-parameters"></a>

所有测试都使用两个环境变量：

* `IDENTITY_URL`：用于身份验证的 [Identity](https://github.com/bitwarden/server/tree/master/src/Identity) 实例的 URL。
* `API_URL`：经过身份验证后用于负载测试操作的 [API](https://github.com/bitwarden/server/tree/master/src/Api) 实例的 URL。
* `CLIENT_ID`：所有请求的 `X-ClientId` 标头值，用于跟踪唯一客户端并管理速率限制。

根据所测试的 API，使用密码或客户端凭据授权。

对于密码：

* `AUTH_USER_EMAIL`：用户电子邮件地址。
* `AUTH_USER_PASSWORD_HASH`：用户主密码的哈希值。

对于客户端凭据：

* `AUTH_CLIENT_ID`：OAuth 客户端 ID。
* `AUTH_CLIENT_SECRET`：OAuth 客户端密钥。

Grafana 的在线状态用于在云中托管脚本，并配置了上述所有内容。对于本地测试，您可能需要[生成](https://help.ppgg.in/admin-console/bitwarden-public-api#authentication)一组 ID 和密钥。

## 入门 <a href="#getting-started" id="getting-started"></a>

1. 在本地[安装](https://k6.io/docs/get-started/installation/)并配置 k6。
2. （可选）使用令牌[登录](https://k6.io/docs/cloud/creating-and-running-a-test/cloud-tests-from-the-cli/)您的云账户。
3. 运行您的脚本！

对于本地运行，这很简单：

```bash
k6 run script.js
```

如果您想将结果传输到云端，请添加 `--out=cloud` 参数。要传递环境变量，请使用 `-e` 参数，例如 `-e IDENTITY_URL="http://localhost:4000"`。

对于云，直接运行：

```bash
k6 cloud script.js
```

计划的运行会在云端自动进行。

## 创建新脚本​ <a href="#creating-new-scripts" id="creating-new-scripts"></a>

有些示例已经存在，可以复制。要进行简单的 `GET` 操作，请查看 `/config` 测试。要了解更全面的 CRUD 套件，请查看 `/public/groups` 测试。

### 最佳实践​ <a href="#best-practices" id="best-practices"></a>

检查应简单明了，查找状态代码和继续操作所需的元素（例如 ID）。避免功能测试，因为在大多数情况下，单元测试和自动化测试已经涵盖了功能测试；负载测试的目的是对系统施加压力，而不是确保其正确性。

每个脚本顶部的 `options` 应至少包含在云中使用的 `ext` 信息。选择一个好的 `name`，并在 `params` 的 `tags` 元素中设置相同的名称；这将用于对请求进行分组和收集准确的指标。

k6 提供了已包含的[实用程序脚本](https://k6.io/docs/javascript-api/jslib/utils/)，并且常用的 [`uuidv4`](https://k6.io/docs/javascript-api/jslib/utils/uuidv4/) 帮助程序可以生成用于放置在请求属性中的 UUID。k6 HTTP 库并不假定请求应被格式化为 JSON，因此请使用 `JSON.stringify` 作为正文。

身份验证助手已经可用，并且预计几乎所有测试都需要它。

### Stages <a href="#stages" id="stages"></a>

[Stages](https://k6.io/docs/using-k6/k6-options/reference/#stages) 用于在脚本执行时配置变量负载：

```javascript
stages: [
  { duration: "30s", target: 10 },
  { duration: "1m", target: 20 },
  { duration: "2m", target: 25 },
  { duration: "30s", target: 0 },
];
```

上述程序在 30 秒内将负载提升到 10 VU，然后在一分钟内提升到 20 VU，再在两分钟内提升到 25 VU，最后在 30 秒内将负载降至空载。

根据目标，这可能会改变，通常所有测试都将使用相同的 Stages 以实现一致性。

### Thresholds <a href="#thresholds" id="thresholds"></a>

[Thresholds](https://k6.io/docs/using-k6/thresholds/) 也类似用于设定负载测试成功的预期值：

```javascript
thresholds: {
  http_req_failed: ["rate<0.01"],
  http_req_duration: ["p(95)<1500"]
  }
```

失败的请求计数可以设置为零，但对于短暂的异常，允许 1% 的失败率。超过 95% 的请求 (P95) 的持续时间将通过多次运行来测量，以作为基准线。

不仅脚本内的检查会建立指标，而且还可以参考核心指标和自定义指标（如果需要）作为 thresholds。同样，通常期望所有测试都将使用相同的 thresholds 作为基础。

## 结果​ <a href="#results" id="results"></a>

最好在提供交互式图形和图表的云中查看结果。结果包含：

* P95 响应时间。
* 请求总数和每秒请求数。
* 正在使用的 VU。
* HTTP 请求详细信息及其结果检查。
* Thresholds。

脚本创建并稳定后，就会为其设定基准线，并可用于随时间的推移进行比较。


# 单元测试


# 命名约定

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/testing/unit/naming-conventions)
{% endhint %}

{% hint style="info" %}
本节章节目前只涉及使用 Typescript 编写的客户端测试。本节无意用于当前形式的使用 C# 编写的服务器端测试，即使其基本原理可能仍然适用。
{% endhint %}

名称的主要目的是以一种易于理解的方式来描述测试，让人明白测试是做什么的，以及它打算覆盖哪些用例。一个好的测试名称列表能让开发人员快速检查套件是否涵盖了所有重要用例，并在未涵盖时轻松找出漏洞。

描述测试的方法有很多，但大多数方法的共同点是都包括以下内容：

* 被测单元
* 被测状态
* 预期行为

因此，我们选择使用以下格式（ `[]` 中内容表示可选）：

`<unit> [given <prerequisite>] <behavior> when <state>`

## 基本示例

#### 示例 1

* **单元**： `FormSelectionList.deselectItem`
* **状态**：调用了有效 ID
* **行为**：创建 `selectedItems` 和 `deselectedItems` 数组的新副本

#### 示例 2

* **单元**： `FormSelectionList.deselectItem`
* **状态**：调用了无效 ID
* **行为**：什么也不做

#### 实践

您可能已经注意到，我们的测试框架 `jest` 鼓励编写以 `it` 开头的测试。因此，我们选择了遵循这一模式的约定。下面是使用该约定编写的示例：

```
FormSelectionList
  deselectItem
    ✓ creates new copies of the selectedItems and deselectedItems arrays when called with a valid id
    ✓ does nothing when called with a invalid id
```

在代码中看上去类似这样：

```typescript
describe("FormSelectionList", () => {
  describe("deselectItem", () => {
    it("creates new copies of the selectedItems and deselectedItems arrays when called with a valid id", () => {...});
    it("does nothing when called with an invalid id", () => {...});
  })
})
```

## 测试中的状态排列和复杂状态

有时，我们的测试需要大量代码来设置状态，而这些代码中通常有一部分与理解测试本身无关，例如全局对象的复杂构造函数。

#### 重复的代码和函数工具 <a href="#duplicated-code-and-utility-functions" id="duplicated-code-and-utility-functions"></a>

大多数情况下，只需创建测试实用功能，就能去除重复代码和无关细节。以下面代码段中的 `createCipher` 函数为例：

```typescript
describe("VaultFilter", () => {
  describe("filterFunction", () => {

    it("returns true when cipher is deleted and function is filtering for trash", () => {
      const cipher = createCipher({ deletedDate: new Date() });
      const filterFunction = createFilterFunction({ status: "trash" });

      const result = filterFunction(cipher);

      expect(result).toBe(true);
    });

    it("returns false when cipher is deleted and function is filtering for favorites", () => {
      const cipher = createCipher({ deletedDate: new Date() });
      const filterFunction = createFilterFunction({ status: "favorites" });

      const result = filterFunction(cipher);

      expect(result).toBe(false);
    });

  })
})

function createCipher(options: Partial<CipherView> = {}) {
  const cipher = new CipherView();

  cipher.favorite = options.favorite ?? false;
  cipher.deletedDate = options.deletedDate;
  cipher.type = options.type;
  cipher.folderId = options.folderId;
  cipher.collectionIds = options.collectionIds;
  cipher.organizationId = options.organizationId;

  return cipher;
}

function createFilterFunction(...) {...}
```

正如您所看到的， `createCipher` 实用程序隐藏了大量代码，否则这些代码将在两个测试中重复出现。

请注意，该功能还允许我们隐藏与我们试图验证的行为无关的细节。测试清楚地表明， `deletedDate` 字段是唯一会对被测单元的行为产生影响的字段。

#### 共享状态和通用设置模块 <a href="#shared-state-and-common-setup-blocks" id="shared-state-and-common-setup-blocks"></a>

在某些情况下，多个测试的部分状态是相同的，因为它们共享一组共同的前提条件。在这种情况下，我们可以使用 `describe` 和 `beforeEach` 块将测试分组，并设置它们共享的状态的共同部分。您可能已经注意到，前面的代码段就是一个很好的例子，其中两个测试都需要删除密码。我们可以像这样将这些测试分组（注意 `given` 关键字）：

```typescript
describe("VaultFilter", () => {
  describe("filterFunction", () => {

    describe("given a deleted cipher", () => {
      let cipher;

      beforeEach(() => {
        cipher = createCipher({ deletedDate: new Date() });
      })

      it("returns true when filtering for trash", () => {
        const filterFunction = createFilterFunction({ status: "trash" });

        ...
      });

      it("returns false when filtering for favorites", () => {
        const filterFunction = createFilterFunction({ status: "favorites" });

        ...
      });
    });

  })
})

function createCipher(...) {...}
function createFilterFunction(...) {...}
```

结果是测试名称看起来像这样：

```
VaultFilter
  filterFunction
    given a deleted cipher
      ✓ returns true when filtering for trash
      ✓ returns false when filtering for favorites
```

## 陷阱 <a href="#pitfalls" id="pitfalls"></a>

### 验证一个行为 <a href="#verify-one-behavior" id="verify-one-behavior"></a>

如果您发现自己在编写冗长的名称时，使用了「*and*」一词将多个期望串联在一起，那么请考虑将测试拆分开来。理想情况下，测试应验证单一行为，以便开发人员更容易发现问题，审核人员也更容易理解修改测试的原因。

#### 示例 <a href="#example" id="example"></a>

* `adds item to selectedItems, removes from deselectedItems, and creates a form control when called with a valid id`

可以写成三个不同的测试：

* `adds item to selectedItems when called with a valid id`
* `removes item from deselectedItems when called with a valid id`
* `creates a form control when called with a valid id`

请记住，最终还是要由开发人员根据具体情况来决定什么算作「验证单一行为」。

### 包含被测状态 <a href="#include-state-under-test" id="include-state-under-test"></a>

一个容易陷入的常见模式是在编写测试名称时不考虑被测试状态。下面是一个虚构的排序函数的例子：

#### 无状态示例 <a href="#example-without-state" id="example-without-state"></a>

* `returns items in alphabetic order`
* `returns empty array`
* `throws error`

我们并不能立即看出我们实际测试了多少种行为，相反，我们是根据预期输出对测试进行分组的。要了解排序功能是如何工作的，以及我们是否有足够的覆盖率，我们必须深入到测试的源代码中。您可能会提出以下问题

* 如果我输入一个包含非字符串值的数组，会发生什么情况？会出错吗？
* 是什么原因导致函数返回一个空数组？
* 如果输入 `null` 呢？

如果我们把状态也加进来，就不得不分开测试，并正确回答上述问题：

#### 考虑状态时的示例 <a href="#example-when-taking-state-into-consideration" id="example-when-taking-state-into-consideration"></a>

* `returns empty array when input is empty`
* `returns empty array when input contains non-string values`
* `returns item when input only contains one item`
* `returns items in alphabetic order when input contains multiple items`
* `throws error when input is not an array`


# 测试结构

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/contributing/testing/unit/structure)
{% endhint %}

{% hint style="warning" %}
本章节仍在编写中。
{% endhint %}

常用的惯例是 AAA: Arrange Act Assert（排列行为断言）。这种类型的约定使阅读/理解测试变得更容易，同时也能促进更好的测试。


# 修改用户机密

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/getting-started/server/user-secrets)
{% endhint %}

## 手动编辑用户机密 <a href="#manually-editing-user-secrets" id="manually-editing-user-secrets"></a>

我们建议使用[服务器设置指南](/getting-started/server/guide)中描述的自动帮助程序脚本。然而，您也可以使用以下的教学手动编辑您的用户机密或对其进行故障排除。如果您手动编辑正在使用的机密，如果需要，请务必记住将您的更改复制到所有项目。

### 编辑用户机密 - Windows Visual Studio <a href="#editing-user-secrets-visual-studio-on-windows" id="editing-user-secrets-visual-studio-on-windows"></a>

右键点击解决方案资源管理器中的项目，然后点击 **Manage User Secrets**。

### 编辑用户机密 - macOS Visual Studio <a href="#editing-user-secrets-visual-studio-on-macos" id="editing-user-secrets-visual-studio-on-macos"></a>

打开终端并导航到项目的目录。

添加用户机密：

```bash
dotnet user-secrets set "<key>" "<value>"
```

查看当前设置的机密：

```bash
dotnet user-secrets list
```

默认情况下，用户机密文件位于：

```
~/.microsoft/usersecrets/<project name>/secrets.json
```

您可以直接编辑此文件，这比使用命令行工具要简单得多。

### 编辑用户机密 - Rider <a href="#editing-user-secrets-rider" id="editing-user-secrets-rider"></a>

* 导航到 **Preferences** -> **Plugins** 然后安装 .NET Core User Secrets
* 右键点击项目，然后点击 **Tools** -> **Open project user secrets**


# 架构

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/)
{% endhint %}

在本章节中，我们将介绍 Bitwarden 的高级架构及其结构，首先关注服务器和客户端如何相互交互，然后再深入了解服务器和客户端的细节。

由于基于 Web 的客户端大多表现相同，我们将主要介绍 Web Vault，但也包含了浏览器扩展、桌面应用程序和 CLI 等客户端的特定区域。移动应用程序有自己的代码库。

我们的架构文档主要是使用名为 IcePanel 的交互式工具完成。建议从我们应用程序结构的互动[向导](https://s.icepanel.io/jCGiag2SENoQIU/EACm)开始。

{% embed url="<https://s.icepanel.io/jCGiag2SENoQIU/EACm>" %}
Bitwarden 架构
{% endembed %}


# 架构决策记录 (ADR)

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/)
{% endhint %}

> 架构决策记录：Architectural Decision Records，简称 ADR。

[架构决策 (AD)](https://en.wikipedia.org/wiki/Architectural_decision) 是一种软件设计选择，用于解决在架构上具有重要意义的功能性或非功能性需求。例如，对于实例，这可能是技术选择（例如 Java 与 JavaScript）、IDE 的选择（例如 IntelliJ 与 Eclipse IDE）、库之间的选择（例如 [SLF4J](https://www.slf4j.org/) 与 [java.util.logging](https://docs.oracle.com/javase/8/docs/api/java/util/logging/package-summary.html)），或功能上的决策（例如，无限撤消与有限撤消）。

## @Bitwarden

在 Bitwarden 的工程团队中引入架构决策 (AD) 的目的是引导开发朝着可维护和可扩展的代码库方向发展。同时努力确保所有团队的一致性。

AD 还将作为提议和规划技术债务的基础。

### 状态定义 <a href="#status-definition" id="status-definition"></a>

* **进行中** - ADR 已获批准，我们正在整个项目中采用它。
* **标准** - ADR 已实施并假定为标准。
* **已放弃** - ADR 已被放弃，和/或被另一个 ADR 取代。

### 标签 <a href="#tags" id="tags"></a>

请确保每个 ADR 都包含一个标签，标记它们适用于哪些项目（*客户端*、*移动端*和/或*服务器*）。如果需要，请随意创建更多标签。

## 流程 <a href="#process" id="process"></a>

虽然流程最初主要是为发起者讨论架构决策而创建的，但对我们来说保持流程对任何人的建议开放是很重要的。为此，任何人都可以自由地打开 PR 来建议 AD。然后将讨论这些建议，以便在采纳之前在发起者之间达成普遍共识。

## ADR <a href="#adrs" id="adrs"></a>

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td>0001 - Angular Reactive Forms</td><td><a href="/architecture/adr/0001-angular-reactive-forms">0001 - Angular Reactive Forms</a></td></tr><tr><td>0002 - Public API for modules</td><td><a href="/architecture/adr/0002-public-api-for-modules">0002 - Public API for modules</a></td></tr><tr><td>0003 - Adopt Observable Data Services for Angular</td><td><a href="/architecture/adr/0003-adopt-observable-data-services-for-angular">0003 - Adopt Observable Data Services for Angular</a></td></tr><tr><td>0004 - Refactor State Service</td><td><a href="/architecture/adr/0004-refactor-state-service">0004 - Refactor State Service</a></td></tr><tr><td>0005 - Refactor Api Service</td><td><a href="/architecture/adr/0005-refactor-api-service">0005 - Refactor Api Service</a></td></tr><tr><td>0006 - Clients: Use Jest Mocks</td><td><a href="/architecture/adr/0006-clients-use-jest-mocks">0006 - Clients: Use Jest Mocks</a></td></tr><tr><td>0007 - Manifest V3 sync Observables</td><td><a href="/architecture/adr/0007-manifest-v3-sync-observables">0007 - Manifest V3 sync Observables</a></td></tr><tr><td>0008 - Server: Adopt CQRS</td><td><a href="/architecture/adr/0008-server-adopt-cqrs">0008 - Server: Adopt CQRS</a></td></tr><tr><td>0009 - Composition over inheritance</td><td><a href="/architecture/adr/0009-composition-over-inheritance">0009 - Composition over inheritance</a></td></tr><tr><td>0010 - Angular Modules</td><td><a href="/architecture/adr/0010-angular-modules">0010 - Angular Modules</a></td></tr><tr><td>0011 - Angular Clients folder structure</td><td><a href="/architecture/adr/0011-scalable-angular-clients-folder-structure">0011 - Scalable Angular Clients folder structure</a></td></tr><tr><td>0012 - Angular Filename convention</td><td><a href="/architecture/adr/0012-angular-filename-convention">0012 - Angular Filename convention</a></td></tr><tr><td>0013 - Avoid layered folder structure</td><td><a href="/architecture/adr/0013-avoid-layered-folder-structure-for-request-response-models">0013 - Avoid layered folder structure for request/response models</a></td></tr><tr><td>0014 - Adopt Typescript Strict flag</td><td><a href="/architecture/adr/0014-adopt-typescript-strict-flag">0014 - Adopt Typescript Strict flag</a></td></tr><tr><td>0015 - Short Lived Browser Services</td><td><a href="/architecture/adr/0015-short-lived-browser-services">0015 - Short Lived Browser Services</a></td></tr><tr><td>0016 - Move Decryption and Encryption to Views</td><td><a href="/architecture/adr/0016-move-decryption-and-encryption-to-views">0016 - Move Decryption and Encryption to Views</a></td></tr><tr><td>0017 - Use Swift to build watchOS app</td><td><a href="/architecture/adr/0017-use-swift-to-build-watchos-app">0017 - Use Swift to build watchOS app</a></td></tr><tr><td>0018 - Feature management</td><td><a href="/architecture/adr/0018-feature-management">0018 - Feature management</a></td></tr><tr><td>0019 - Adoption of Web Push</td><td><a href="/architecture/adr/0019-adoption-of-web-push">0019 - Adoption of Web Push</a></td></tr><tr><td>0020 - Observability with OpenTelemetry</td><td><a href="/architecture/adr/0020-observability-with-opentelemetry">0020 - Observability with OpenTelemetry</a></td></tr><tr><td>0021 - Logging to Standard Output</td><td><a href="/architecture/adr/0021-logging-to-standard-output">0021 - Logging to Standard Output</a></td></tr></tbody></table>


# 0001 - Angular Reactive Forms

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/reactive-forms)
{% endhint %}

| ID：  | ADR-0001   |
| ---- | ---------- |
| 状态：  | 进行中        |
| 发表于： | 2022-05-28 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

我们的 Angular 应用程序中的大多数表单都使用模板驱动的表单。最近我们注意到扩展和维护这些表单的问题。并开始混合使用模板驱动的表单和反应式表单。

维护两种处理表单的方法很复杂，完全转变为单一方法将确保开发人员和用户获得更一致的体验。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* **反应式表单** -- 提供对底层表单对象模型的直接、显式访问。与模板驱动的表单相比，它们更加健壮：它们更具可扩展性、可重用性和可测试性。如果表单是应用程序的关键部分，或者您已经使用反应式模式来构建应用程序，请使用反应式表单。
* **模板驱动的表单** -- 依靠模板中的指令来创建和操作底层对象模型。它们对于向应用程序添加简单的表单非常有用，例如电子邮件列表注册表单。它们可以直接添加到应用程序中，但它们的扩展性不如反应式表单。如果您有非常基本的表单需求，而且逻辑只需在模板中进行管理，那么模板驱动的表单可能非常适合您。

来源：<https://angular.io/guide/forms-overview#choosing-an-approach>

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

选择的方案：**反应式表单**，因为我们的需求超出了模板驱动表单的推荐范围。

### 积极的后果​ <a href="#positive-consequences" id="positive-consequences"></a>

* 您永远不需要考虑使用哪种表单方法。

### 消极的后果​ <a href="#negative-consequences" id="negative-consequences"></a>

* 仅使用反应式表单式意味着我们可能需要一些额外的模板。


# 0002 - Public API for modules

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/public-module-npm-packages)
{% endhint %}

| ID：  | ADR-0002   |
| ---- | ---------- |
| 状态：  | 进行中        |
| 发表于： | 2022-06-02 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

目前，我们在不同的包之间使用直接文件引用。这导致包中的所有内容都可供我们项目中的任何其他包使用。这就很难确定更改可能产生的潜在副作用，因为它们不是孤立于单个包的。传统上，只有包的特定子集才会作为公共模块公开，这提供了安全性，即更改将在包内部进行，并且最好由其单元测试覆盖。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* **直接引用** -- 我们可以决定继续原样。没有公共 API。
* **使用 index.ts 定义公共模块** -- 我们将 `index.ts` 添加到定义了「公共」接口的每一个文件夹中。然后其他包导入根索引文件，并禁止直接引用内部文件。

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

选择的方案：**使用 index.ts 定义公共模块**。

### 积极的后果​ <a href="#positive-consequences" id="positive-consequences"></a>

* 公共模块已定义。
* 导入可以保持清爽，因为并非每个文件都需要手动导入。
* 安全性 - 知道更改是与包隔离的。

### 消极的后果​ <a href="#negative-consequences" id="negative-consequences"></a>

* 每当添加新的导出 API 时，我们都必须更新 `index.ts` 文件。


# 0003 - Adopt Observable Data Services for Angular

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/observable-data-services)
{% endhint %}

| ID：  | ADR-0003   |
| ---- | ---------- |
| 状态：  | 进行中        |
| 发表于： | 2022-06-30 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

我们的许多组件和服务与不同域的状态紧密耦合。这导致了紧密耦合和难以修改不同区域。这导致使用 `MessagingService` 服务通过发送事件来同步状态更新。虽然事件溯源是一种完全有效的软件开发方式，但我们当前的事件是空的，这导致组件需要手动重新获取其状态。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* [可观察/反应数据服务](https://blog.angular-university.io/how-to-build-angular2-apps-using-rxjs-observable-data-services-pitfalls-to-avoid/)。
* [NGRX](https://ngrx.io/) - Angular 的反应状态（Redux 实现）。

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

选择的方案：**可观察数据服务**，因为：

* 使我们能够快速迭代到更具反应性的数据模型。
  * 反应式数据模型让我们摆脱事件消息。
  * 组件将始终显示最新状态。
* 不需要大量的前期投资。
* 响应式数据模型的工作将使我们能够在未来需要时采用 NGRX 等模式。

### 示例​ <a href="#example" id="example"></a>

#### 组织​ <a href="#organizations" id="organizations"></a>

`OrganizationService` 应拥有所有组织相关的数据的所有权。

```typescript
class OrganizationService {
  private _organizations: new BehaviorSubject<Organization[]>([]);
  organizations$: Observable<Organization[]> = this._organizations$.asObservable();

  async save(organizations: { [adr: string]: OrganizationData }) {
    await this._organizations$.next(await this.decryptOrgs(this._activeAccount, organizations));
  }
}

class Component implements OnDestroy {
  private destroy$: Subject<void> = new Subject<void>();

  ngInit() {
    this._organizationService.organizations$
      .pipe(takeUntil(this.destroy$))
      .subscribe((orgs) => {
        this.orgs = orgs;
      });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.unsubscribe();
  }
}
```

在此示例中，我们使用 `takeUntil` 模式，该模式可以与 eslint 规则结合使用，以确保每个组件自行清理。

## 方案的优点和缺点​ <a href="#pros-and-cons-of-the-options" id="pros-and-cons-of-the-options"></a>

### 可观察数据服务​ <a href="#observable-data-services" id="observable-data-services"></a>

* 优点：轻量级
* 优点：可以增量重构
* 优点：反应灵敏，更改时将通知
* 缺点：没有严格的标准，更多的是一套指导方针
* 缺点：状态被分成多个微小的「stores」

### NGRX​ <a href="#ngrx" id="ngrx"></a>

NGRX 是 Angular 最受欢迎的 Redux 实现。有关更多详细信息，请阅读 [redux 背后的动机](https://redux.js.org/understanding/thinking-in-redux/motivation)以及查看此 [NGRX 架构图](https://ngrx.io/guide/store)。

* 优点：Angular 是最受欢迎的 redux 库
* 优点：解耦组件
* 优点：单一状态简化了操作
* 缺点：需要对整个状态层进行重大重写
* 缺点：增加了复杂性，并且可能使数据流难以理解


# 0004 - Refactor State Service

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/refactor-state-service)
{% endhint %}

| ID：  | ADR-0004   |
| ---- | ---------- |
| 状态：  | 进行中        |
| 发表于： | 2022-06-30 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

此 ADR 建立在[采用适用于 Angular 的可观察数据服务](/architecture/adr/0003-adopt-observable-data-services-for-angular)的基础上。

Bitwarden 客户端目前拥有相当复杂的状态架构，其中所有状态均由单个服务处理。这导致一切都与 `StateService` 紧密耦合，本质上使其成为上帝对象。

此外，任何服务或组件都可以使用状态服务直接访问任何状态。这使得跟踪每种数据类型的状态生命周期变得困难，并给数据访问方式带来了不确定性。

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

我们应该将状态服务重构为通用存储容器。

* 优点：消除了状态服务的「good」功能
* 优点：状态由拥有它的服务维护
* 优点：不能任意访问数据
* 缺点：返回必须是唯一的任意键

### 示例​ <a href="#example" id="example"></a>

```typescript
interface StateService {
  getAccountData<T>: (account: Account, key: string, options?: StorageOptions) => Promise<T>;
  saveAccountData: (account: Account, key: string, options?: StorageOptions) => Promise<void>;
  deleteAccountData: (account: Account, key: string, options?: StorageOptions) => Promise<void>;

  deleteAllAccountData: (account: Account);

  getGlobalData<T>: (key: string, options?: StorageOptions) => Promise<T>;
  saveGlobalData: (key: string, options?: StorageOptions) => Promise<void>;
  deleteGlobalData: (key: string, options?: StorageOptions) => Promise<void>;
}
```

```typescript
// StorageKey is an internal constant, and should be prefixed with the domain.
//  DO NOT EXPORT IT.
const StorageKey = "organizations";

class OrganizationService {
  async save(organizations: { [adr: string]: OrganizationData }) {
    await this._stateService.saveAccountData(this._activeAccount, StorageKey, organizations);
    await this._organizations$.next(await this.decryptOrgs(this._activeAccount, organizations));
  }
}
```


# 0005 - Refactor Api Service

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/refactor-api-service)
{% endhint %}

| ID：  | ADR-0005   |
| ---- | ---------- |
| 状态：  | 进行中        |
| 发表于： | 2022-07-08 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

`ApiService` 目前负责处理所有 API 请求。这导致该类演变成了一个 Bloater，目前有 **2021** 行代码，**268** 个方法。此外，由于它知道与服务器相关的所有内容，因此还需要导入每个请求和响应，这需要将 `ApiService` 和请求/响应放在同一个 npm 包中。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* **提取类** -- 我们应该使用提取类重构来分解类，其中每个域上下文应该有自己的 API 服务。 `ApiService` 应被转换为通用服务，该服务不关心请求或响应是什么，只能在其他 API 服务中使用。
* **什么也不做** -- 保持原样。

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

选择的方案：**提取类**。

这些新类的命名应该表示为 `{Domain}ApiService`，例如文件夹域应该命名为 `FolderApiService`。

重构示例：

* [`folder-api.service.ts`](https://github.com/bitwarden/clients/pull/3011/files#diff-11b3488b9977f06625349680f81554505613715cfcc9890ebb356a74579c236a)：创建一个新的服务，将方法从 `ApiService` 移动到新服务。在此重构期间，我们还将服务器知识从 `FolderService` 中移出，因为它应该只负责维护其状态。
* 从 [`ApiService`](https://github.com/bitwarden/clients/pull/3011/files#diff-6c8f3163b688c01f589d1e9ee5b7998aea4a0aedde8333c3939fb6181c301bed) 中删除旧方法。


# 0006 - Clients: Use Jest Mocks

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/clients-use-jest-mocks)
{% endhint %}

| ID：  | ADR-0006   |
| ---- | ---------- |
| 状态：  | 完成         |
| 发表于： | 2022-07-18 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

我们目前使用 [Substitute](https://www.npmjs.com/package/@fluffy-spoon/substitute) 在测试中创建模拟。这包括匹配参数、模拟返回值和监视调用。不过，我们也使用 Jest，它带有自己的模拟、监视和匹配功能。这意味着我们有两个提供相同功能的库。

这是不受欢迎的，因为：

* 我们在两个竞争库之间存在重复的功能
* 目前尚不清楚团队应该使用哪个库
* 熟悉 Jest 的人自然会使用 Jest 进行模拟，这不是我们目前遵循的模式
* 与 Jest 相比，Substitute 的知名度和使用率较低，是另一个需要学习的库
* 它要求我们混合语法，特别是对于断言：我们使用 Jest 的 `expect` 来断言测试结果，但我们使用 Substitute 的 `Arg` 来匹配模拟调用中的参数

我们应该只选其一。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* **使用 Substitute 并禁止开发人员使用 Jest 模拟** -- 我们保留这两个库并继续同时使用它们。在这种情况下，我们应该为开发人员提供明确的指导和培训，让他们在所有模拟和参数匹配中使用 Substitute（而不是 Jest）。
* **弃用 Substitute，改用带有 jest-mock-extend 的 Jest** -- 我们弃用 Substitute，并更新所有现有测试以改用 Jest 模拟。我们使用 [jest-mock-extended](https://github.com/marchaos/jest-mock-extended/)，以方便的模拟语法并更好地与 Typescript 集成。此更改不应导致任何功能损失。
* **使用不同的模拟库和/或测试运行器** -- 我们完全更改为其他东西。这实际上并没有摆在桌面上，但也是一种选择。Jest 到目前为止运行良好，所以我不建议我们更换它。

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

选择的方案：**弃用 Substitute，改用带有 jest-mock-extend 的 Jest**。

我们还应该举办培训/学习会议，以鼓励并授权开发人员对其代码进行单元测试。

### 积极的后果​ <a href="#positive-consequences" id="positive-consequences"></a>

* 相比 Substitute，前端开发人员可能更熟悉 Jest。
* Jest 有更多的文档和资源（Medium 文章、StackOverflow 答案等）。
* 它是 Jest 的一个组成部分。
* 不需要混合不同的库或语法。

### 消极的后果​ <a href="#negative-consequences" id="negative-consequences"></a>

* 与 Substitute（NSubstitute 的 .NET 移植版本）相比，后端开发人员的学习曲线会更长。然而，这仍然不是一个特别陡峭的学习曲线，并且在前端环境中使用前端工具是有意义的。
* 需要对现有测试进行一些改动，但改动不大。


# 0007 - Manifest V3 sync Observables

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/manifest-v3-browser-memory-caching)
{% endhint %}

| ID：  | ADR-0007   |
| ---- | ---------- |
| 状态：  | 进行中        |
| 发表于： | 2022-07-12 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

Manifest v3 通过禁止长期活动的后台进程，为网络扩展带来了短暂的上下文。这意味着我们需要通过不同的弹出窗口/服务 worker/网络 worker 实例来存储和同步状态。这是通过 `LocalBackedMemoryStorageService` 完成的。

从 StateService 转向可观察量使这变得更加困难，因为可观察量从根本上与扩展实例相关联。

我们需要存储这些可观察量来维护状态。状态应该在初始化时加载，在下一步更新，并通过重新加载消息更新。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* **构造函数中的初始化和 UpdateObservables 中的更新** -- 这种方法简单明了，但无法保证我们始终更新存储并发出信息。此外，如果打开侧边栏并通过内容脚本保存密码，也无法实时同步数据。我们可以构建一个计时器来定期从内存中同步这些项目。
* **装饰器模式** -- 使用装饰器来包装服务构造函数。这个装饰器将：
  * 从 `LocalBackedMemoryStorage` 初始化
  * 订阅观察者以更新 `LocalBackedMemoryStorage`，并将事件推送给 worker 以从内存中更新可观察值。
  * 在更新事件上推送 `observable.next`
  * 在这里避免循环事件循环？（循环发生在消息和可观察对象上）
    * 消息可以通过 guid 来修复
    * TODO：消息问题 -> `observable.next` -> 订阅 -> 存储服务未知

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

选择的方案：**装饰器模式**。这将是更可重用、更简洁的解决方案。它需要更多的思考来确定如何避免循环事件，但一旦解决了，它也就解决了。

### 积极的后果​ <a href="#positive-consequences" id="positive-consequences"></a>

* 可重用、简洁的解决方案。
* 遵循可观察到的最佳实践。

### 消极的后果​ <a href="#negative-consequences" id="negative-consequences"></a>

* 一些未知的实施细节可能会导致开发过程中出现障碍。


# 0008 - Server: Adopt CQRS

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/server-CQRS-pattern)
{% endhint %}

| ID：  | ADR-0008   |
| ---- | ---------- |
| 状态：  | 进行中        |
| 发表于： | 2022-07-15 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

在 Bitwarden Server 中，我们目前使用 `<<Entity>>Service` 模式来作用于我们的实体。这些类最终成为了涉及实体的所有操作的垃圾场：导致[臃肿](https://refactoring.guru/refactoring/smells/bloaters)和[耦合](https://refactoring.guru/refactoring/smells/couplers)。有两个事实帮助我们确定了当前的设计：

* 我们使用实体模式来表示存储在数据库中的数据，并使用 Dapper 或实体框架自动绑定这些实体类。
* 我们使用基于构造函数的依赖注入来将依赖项传递给对象。

上述两个事实意味着，如果不接收所有必要的状态作为方法参数，我们的实体就无法运行，这与我们典型的 DI 模式背道而驰。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* **`<<Entity>>Services`** -- 上面讨论过了。
* **查询和命令** -- 从根本上来说，我们的问题是 `<<Entity>>Service` 名称完全封装了您可以对该实体执行的任何操作，并且排除了不同实体之间的任何代码重用。CQRS 模式根据对实体采取的操作创建类。这就自然而然地限制了类的范围，并在两个实体需要实现相同的命令行为时允许重复使用。<https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs>。
* **基于功能的小型服务** -- 这种设计会将 `<<Entity>>Service` 分解为 `<<Feature>>Service`，但最终会遇到同样的问题。随着功能的增加，该服务将变得臃肿，并与其他服务紧密耦合。

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

选择的方案：**查询和命令**。

对于现任者来说，命令似乎是更好的决定。我们获得了代码重用并限制了类的范围。此外，我们还拥有一条迭代路径，可以通过队列工作实现完整的 CQRS 管道。

查询基本上已经通过存储库和/或服务完成，但需要进行一些明显的重组。

## 过渡计划​ <a href="#transition-plan" id="transition-plan"></a>

随着时间的推移，我们将逐渐过渡到 CQRS 模式。如果开发人员正在进行使用或影响服务方法的更改，他们应该考虑是否可以将其提取到查询/命令中，并将其作为技术债务包含在他们的工作中。

当前的领域服务规模庞大且相互依赖，一次性将它们全部分解可能不太现实。重构「更深一层」并保留其他方法是可以接受的。这可能会导致新的查询/命令仍然在某种程度上与其他服务方法耦合。在过渡阶段，这种情况是可以接受的，但随着时间的推移，这些相互依赖关系应该被移除。


# 0009 - Composition over inheritance

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/angular-composition-over-inheritance)
{% endhint %}

| ID：  | ADR-0009   |
| ---- | ---------- |
| 状态：  | 进行中        |
| 发表于： | 2022-07-25 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

目前，我们的 Angular 应用程序严重依赖继承。虽然这在当时似乎是一个很自然的决定，因为它允许我们在不同领域之间快速共享代码。但这也导致了紧密耦合，因此难以理解变更会产生的影响。它还助长了大型页面级组件的出现。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* **什么也不做** -- 维持现状，这并不是一个真正的选择。
* **优先选择组合而不是继承** -- 将组件拆分成只做一件事的小组件。保持组件精简，并主要使用*服务*来共享功能。

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

选择的方案：**优先选择组合而不是继承**。

### 积极的后果​ <a href="#positive-consequences" id="positive-consequences"></a>

* 更轻巧的组件。
* 更好地理解变更所产生的影响，因为它现在仅被隔离到单个组件。

### 消极的后果​ <a href="#negative-consequences" id="negative-consequences"></a>

* 继承性更容易理解。


# 0010 - Angular Modules

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/angular-ngmodules)
{% endhint %}

| ID：  | ADR-0010   |
| ---- | ---------- |
| 状态：  | 进行中        |
| 发表于： | 2022-07-25 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

> NgModule 是一个内聚代码块的容器，专用于某个应用程序域、工作流程或密切相关的功能集。它们可以包含组件、服务提供商和其他代码文件，其范围由包含的 NgModule 定义。它们可以导入从其他 NgModule 导出的功能，并导出选定的功能以供其他 NgModule 使用。
>
> \-- [*https://angular.io/guide/architecture-modules*](https://angular.io/guide/architecture-modules)

在 ADR [0002 Define public module in NPM packages](https://contributing.bitwarden.com/architecture/adr/public-module-npm-packages)，我们决定开始使用桶形文件，并限制从其他“模块”导入。这解决了我们遇到的一些痛点，但由于所有组件都需要在 Angular 模块中定义，因此它们仍在桶形文件中导出。这就限制了桶形文件的实用性。

Angular 鼓励创建许多小型 NgModule，人们提倡每个功能一个模块，或者甚至每个组件定义一个模块。在 Angular v14 中，由于引入了[独立组件](https://angular.io/guide/standalone-components)，这变得更加容易。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* **什么都不做** -- 维持现状，在单一模块中定义大多数组件。这意味着每个组件本质上仍然是全局的。
* [**Angular 模块**](https://angular.io/guide/architecture-modules) -- 在我们的桶形文件旁边添加 NgModules。这样可以对内部组件进行适当的封装。
* [**独立组件**](https://angular.io/guide/standalone-components) -- 提供 NgModule 的优点，而无需大部分额外的样板。目前仍处于预览阶段，因此依赖它有一定风险。

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

选择的方案：**Angular 模块**。

### 积极的后果​ <a href="#positive-consequences" id="positive-consequences"></a>

* 内部组件不能在功能之外重复使用。
* 需要模块来支持延迟加载。
* 模块结构与独立组件类似，如果认为有必要，将来可以轻松迁移。

### 消极的后果​ <a href="#negative-consequences" id="negative-consequences"></a>

* Angular 对 NgModules 的错误处理非常糟糕，提供了难以调试的隐含错误。
* 额外的样板。

## 指南​ <a href="#guidelines" id="guidelines"></a>

* 旨在导出尽可能少的组件。在许多情况下，您根本不需要导出任何组件，而是可以将路由封装在该模块中，这样就可以在认为有用的情况下延迟加载它。
* 需要在所有模块之间共享的功能应放置在 `Shared` 功能中。
* 如果需要在个人密码库和组织密码库等模块之间共享附加功能，请考虑创建 `feature/shared` 模块。

### 实施 <a href="#implementation" id="implementation"></a>

功能模块的一个例子是**报告**。我们知道报告既可用于个人用户，也可用于组织。

```
reports
  shared
    report-card.component
    report-list.component

    reports-shared.module.ts
    index.ts
  reports
    breach-report.component
    ...
  reports.module.ts -> depends on reports-shared.module.ts
  reports.component.ts
  index.ts

organizations
  reports
    organization-reports.module.ts -> depends on reports-shared.module.ts
```


# 0011 - Scalable Angular Clients folder structure

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/angular-folder-structure)
{% endhint %}

| ID：  | ADR-0011   |
| ---- | ---------- |
| 状态：  | 进行中        |
| 发表于： | 2022-07-25 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

目前，我们的 Angular 客户端中的文件夹结构非常分散，具有多个相互竞争的文件夹结构。我们需要对单一文件夹结构进行标准化，这将使我们能够继续发展，而不会产生摩擦。本 ADR 以  [0010 Use Angular Modules](https://contributing.bitwarden.com/architecture/adr/angular-ngmodules) 为基础，提出了一种文件夹结构。

### 资源​ <a href="#resources" id="resources"></a>

这在很大程度上基于以下资源：

* [Angular - NgModules](https://angular.io/guide/ngmodules)
* [Angular - Application structure and NgModules](https://angular.io/guide/styleguide#application-structure-and-ngmodules)
* [Angular - Lazy-loading feature modules](https://angular.io/guide/lazy-loading-ngmodules)
* [Using Nx at Enterprises](https://nx.dev/guides/monorepo-nx-enterprise)

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* **Nx 文件夹结构** -- 看起来是一个经过深思熟虑的结构，但它本质上要求我们使用 nx，因为它严重依赖于 npm 包。我们还没有达到可以轻松采用这种工具的阶段。
* **受 Angular Docs 启发的轻量级结构** -- 从 Angular docs + Nx 中汲取灵感，提出了一种更轻量级的结构，仍然保留 `app` 目录中的功能。

### `SharedModule` & `CoreModule` <a href="#sharedmodule--coremodule" id="sharedmodule--coremodule"></a>

[Angular 文档](https://angular.io/guide/module-types#shared-ngmodules)规定应该有一个 `SharedModule`。它进一步指定共享模块不应提供提供程序。常见的做法是创建一个 `CoreModule` 来负责设置核心提供程序。

### 建议的文件夹结构​ <a href="#proposed-folder-structure" id="proposed-folder-structure"></a>

该文件夹结构基于我们现有的路由结构，因为常见的模式是使用模块进行嵌套路由以支持延迟加载。根文件夹主要基于我们拥有的根路由概念，各种公共路由被分类在 `accounts` 下。

```
web/src/app
  core
    services
    core.module.ts
    index.ts
  shared
    shared.module.ts
    index.ts

  accounts
  providers
  reports
  sends
  settings
  tools
  vault
    shared
      vault-shared.module.ts (this gets imported by Organization Vaults)
    vault.module.ts
    index.ts (exposes the following files:)
      vault-shared.module.ts
      vault.module.ts
  --
  app.component.html
  app.component.ts
  app.module
  oss-routing.module.ts
  oss.module.ts
  wildcard-routing.module.ts
```

该结构将分多个步骤实施：

1. 从 `src/app` 中提取不相关的文件。<https://github.com/bitwarden/clients/pull/3127>
2. 创建 `CoreModule`。<https://github.com/bitwarden/clients/pull/3149>
3. 创建 `SharedModule`。<https://github.com/bitwarden/clients/pull/3222>
4. 将所有现有的松散组件迁移到 `SharedModule`
   * 根中的任何剩余功能都应该提供一个模块

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

以上述示例为蓝本实现文件结构。


# 0012 - Angular Filename convention

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/angular-filename-convention)
{% endhint %}

| ID：  | ADR-0012   |
| ---- | ---------- |
| 状态：  | 进行中        |
| 发表于： | 2022-08-23 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

目前，我们使用混合文件名约定，其中一些文件遵循 Angular 样式指南，而其他文件则使用 camelCase（驼峰命名法）。这导致了一些混乱，不知道该遵循哪种约定，我们应该统一一种约定，以避免混乱。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* **Angular 编码风格指南** -- Angular 编码风格指南规定「使用点和破折号分隔文件名」。
* **camelCase** -- 长期以来，我们一直习惯使用 camelCase。

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

使用 [**Angular 编码风样式指南**](https://angular.io/guide/styleguide#naming)。更具体地说是[样式 02-02](https://angular.io/guide/styleguide#style-02-02) 和[样式 02-03](https://angular.io/guide/styleguide#style-02-03)。

这两个样式规则侧重于使用破折号来分隔描述性名称中的单词，以及使用点来分隔类型中的名称。 Angular 通常有以下类型：

* `service` - 抽象服务
* `component` - Angular 组件
* `pipe` - Angular 管道
* `module` - Angular 模块
* `directive` - Angular 指令

在 Bitwarden，我们还使用了更多其他类型：

* `.api` - API 模型
* `.data` - 数据模型（用于序列化域名模型）
* `.view` - 视图模型（已解密的域名模型）
* `.export` - 导出模型
* `.request` - API 请求
* `.response` - API 响应
* `.type` - 枚举
* `.service.abstraction` - 服务的抽象类，用于 DI，并非所有服务都需要抽象类

类名也应使用后缀作为其类名的一部分。例如，服务实现将被命名为 `FolderService`，请求模型将被命名为 `FolderRequest`。

如果服务无法完全实现，则会创建带有 `Abstraction` 后缀的抽象类。如果 Angular 和 Node 实现由于某种原因必须有所不同，通常会发生这种情况。传统上会使用接口，但在 JavaScript 中，TypeScript 接口不能用于连接依赖注入。

### 积极的后果​ <a href="#positive-consequences" id="positive-consequences"></a>

* 由于我们的大部分代码都是用 Angular 编写的，因此我们应该使用 Angular 编码风格指南。
* 不使用 camelCase 将避免与大小写敏感的文件系统发生问题。

### 消极的后果​ <a href="#negative-consequences" id="negative-consequences"></a>

* 我们需要更新大量文件才能保持一致。


# 0013 - Avoid layered folder structure for request/response models

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/avoid-layered-folder-structure)
{% endhint %}

| ID：  | ADR-0013   |
| ---- | ---------- |
| 状态：  | 进行中        |
| 发表于： | 2022-09-16 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

我们的 Angular 应用程序目前部分使用分层文件夹结构。这会导致域上下文被多个文件夹分割成多个层次。由于修改服务往往需要修改属于该服务的模型，这就造成了摩擦。

可以理解的是，我们的应用程序使用了大量模型，但是模型中最大且最孤立的部分是请求和响应。这使它们成为一个很好的起点。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* **保持原样** -- 我们可以保持原样，继续将模型放在 `libs/common/models` 中。
* **将模型放在其所有者旁边** -- 请求和响应由单个 API 服务拥有。因此，它们可以放置在靠近其服务的位置，从而使连接的变更彼此接近。

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

使用 **将模型放在其所有者旁边** 作为请求和响应模型。

一个经验法则是将抽象中使用的任何模型放在抽象目录中。而服务中使用的任何模型都放在服务目录中。抽象是服务公共接口的一部分，而服务是内部接口的一部分。

### 示例​ <a href="#example" id="example"></a>

```
libs/common/
  abstractions/folder/
    folder.service.abstraction.ts
    folder-api.service.abstraction.ts
    responses/
      folder.response.ts  (Exposed as public API)
  services/folder/
    folder.service.ts
    folder-api.service.ts
    requests/
      folder.request.ts  (Internal, only used within the implementation)
```


# 0014 - Adopt Typescript Strict flag

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/typescript-strict)
{% endhint %}

| ID：  | ADR-0014   |
| ---- | ---------- |
| 状态：  | 进行中        |
| 发表于： | 2022-09-02 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

由于代码库最初是使用 JavaScript 编写的，因此出于兼容性的原因，我们一直无法使用 TypeScript 严格标记。这导致了一些 bug 问题，以及降低了代码质量。

### TypeScript Strict​ <a href="#typescript-strict" id="typescript-strict"></a>

> `strict` 标记支持广泛的类型检查行为，从而更好地保证程序的正确性。打开此标记相当于启用了严格模式系列的所有选项。
>
> *<https://www.typescriptlang.org/tsconfig#strict>*

特别值得注意的是[严格的空检查](https://www.typescriptlang.org/tsconfig#strictNullChecks)。默认情况下，TypeScript 将忽略 `false`、`null` 和 `undefined`，允许它们用于任何类型。当启用 `strictNullChecks` 时，必须在类型定义中明确允许它们。

### Typescript 严格模式插件​ <a href="#typescript-strict-mode-plugin" id="typescript-strict-mode-plugin"></a>

[Typescript 严格模式插件](https://github.com/allegro/typescript-strict-plugin)是一个 TypeScript 插件，允许您逐个文件地逐步打开严格模式。这将使我们能够逐步更新代码库以支持严格模式，而无需进行大量的初始工程工作。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* **保持严格禁用** -- 我们将继续遇到空问题，并且代码质量将低于应有的水平。
* **立即启用严格标记** -- 立即打开严格标记并指派工程师解决由此引起的所有错误。这将需要很长时间，并导致大量冲突。
* **逐步启用严格标记** -- 使用 TypeScript 插件逐一迁移文件。

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

选择的方案：**逐步启用严格标记**。

### 积极的后果​ <a href="#positive-consequences" id="positive-consequences"></a>

* 我们可以立即开始使用严格标记。
* 降低初始开发人员的工作量。

### 消极的后果​ <a href="#negative-consequences" id="negative-consequences"></a>

* 迁移会更慢。
* 我们将继续保留部分存在空问题的代码。
* 如果文件是严格的但使用非严格依赖项，则可能会导致错误的安全性。
* 最佳实践较少。

### 待更新文件​ <a href="#files-to-be-updated" id="files-to-be-updated"></a>

* libs/common: 310
  * models: 174
  * services: 40
  * abstractions: 45
* libs/angular: 64
* libs/node: 11
* libs/electron: 10
* apps: 309
  * web: 163
  * browser: 80
  * desktop: 30
  * cli: 36

### 计划​ <a href="#plan" id="plan"></a>

逐步启用严格模式存在几个潜在的隐患。制定一个关于如何应对迁移的计划将受益匪浅。每个类别的文件都应该有一个正确的示例，说明如何迁移它们。在最初的迁移期间，应尽最大努力严格遵守，但我们不应该阻止人们添加不严格的新文件。我们应该防止人们在我们当前正在迁移的类别中添加新的非严格文件。

逐步启用严格模式有几个潜在的隐患。因此，我们需要制定一个迁移计划。每一类文件都应该有一个适当的示例，说明如何迁移它们。在迁移初期，我们应尽力做到严格合规，但不应阻止人们添加非严格的新文件。我们应该防止的是人们在当前正在迁移的类别中添加新的非严格文件。

#### 准备​ <a href="#preparation" id="preparation"></a>

启用插件并运行 `update-strict-comments`，这会将 `//@ts-strict-ignore` 注释添加到产生严格错误的所有文件中。

#### 过渡​ <a href="#transition" id="transition"></a>

1. 迁移 `libs/common/models`。
2. 迁移 `libs/common/services` 和 `libs/common/abstractions`。
3. 迁移剩余的 `libs/common`。
4. 迁移 `libs/angular`。
5. 迁移应用程序（并禁止任何人添加新的非严格文件）。

### 指南​ <a href="#guidelines" id="guidelines"></a>

以下是将文件迁移到严格模式的一些有用指南。

#### 避免 null，更喜欢 undefined <a href="#avoid-null-prefer-undefined" id="avoid-null-prefer-undefined"></a>

`strictNullChecks` 标志本质上要求我们在几乎所有类型中添加 `| null`。这很烦人，更好的方法是使用 `undefined` 代替。这允许我们使用 `?` 运算符将字段定义为可选。这实质上是在类型中添加 `| undefined`。

有关此讨论背后的一些其他背景信息，请参阅[使用 `strictNullChecks` 在 `null` 和 `undefined` 之间进行选择的指南 · 议题 #9653 · microsoft/TypeScript。](https://github.com/microsoft/TypeScript/issues/9653)

#### 使用查找参考/搜索​ <a href="#use-find-references-search" id="use-find-references-search"></a>

在迁移期间，仅允许在文件的子集上使用严格模式。这意味着将接口从 `null` 更改为 `undefined` 可能会导致某些地方仍在使用 `undefined`。请仔细检查 `null` 是否已删除以确保达到预期行为。

#### 避免将非可选字段设为可选​ <a href="#avoid-making-non-optional-fields-optional" id="avoid-making-non-optional-fields-optional"></a>

在迁移代码时，可能很想在所有字段中添加 `?`。但请仔细考虑该字段是否应该应为可选。例如，数组几乎不应该是 `undefined` ，更合理的默认值是 `[]`。

#### 使用可选链 <a href="#use-optional-chaining" id="use-optional-chaining"></a>

利用可选链来避免空检查。可选链允许您重新连接以下代码。

```typescript
// Avoid
if (obj == null || obj.field == null) {
  return false;
}
return true;

// Prefer
return obj?.field != null;
```

#### 使用 Nullish 合并运算符 (`??`) ​ <a href="#use-nullish-coalescing-operator" id="use-nullish-coalescing-operator"></a>

<https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator>

```typescript
const foo = null ?? "default string";
```


# 0015 - Short Lived Browser Services

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/short-lived-browser-services)
{% endhint %}

| ID：  | ADR-0015   |
| ---- | ---------- |
| 状态：  | 进行中        |
| 发表于： | 2022-12-09 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

[ADR-003](/architecture/adr/0003-adopt-observable-data-services-for-angular) 将 Observables 的使用引入到了我们的 TypeScript 代码库中。该 ADR 描述了使用 `ngDestroy` 触发取消订阅所有已订阅的 `Observables`。但是，只有当使用 Angular 路由器离开页面时才会调用 `ngDestroy`。在 SPA 中，这不是问题。如果没有使用路由器，则意味着您关闭了页面，SPA 已失效。在浏览器扩展中，我们有一个持久的背景，可以在此类关闭事件中存活下来。这意味着观察者队列中存在的订阅不再存在。

特别是在 Firefox 中，这是一个灾难性的失败。Firefox 将用 `DeadObject` 替换为属于不再存在的 DOM 对象的所有对象。当在 `Subject` 上调用 `next()` 时， `DeadObject` 会被视为观察者并抛出错误，从而阻止向后续观察者发出任何进一步的通知，并导致扩展中出现灾难性的中断。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* **还原 ADR 003 并移除 Observables** -- 我们从代码库中删除 Observables 并还原到之前的状态。
* **浏览器事件** -- 我们使用 `beforeunload`、`unload`、`visibilityChange` 和/或 `pageHide` 事件来触发 Angular 服务的销毁。
  * 这些触发器并不保证会被调用，并且可能不会在所有浏览器中被调用。
* **短期主题** -- 我们确保在组件中创建的订阅不会引用长期订阅。

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

选择的方案：**短期主题**。

这种方案最为灵活，既能保留 Observables 的优点，又能避免与页面可见性事件相关的不稳定性。

这些短期主题将通过创建可视化级服务来实现，这些服务的生命周期与它们所服务的组件相同。数据将在这些短期服务和后台的长期扩展之间同步。

### 积极的后果​ <a href="#positive-consequences" id="positive-consequences"></a>

* 对编写组件级代码没有影响。
* 无论如何都会将我们引向 Manifest V3 所需的方向。
* 绕过由于悬空订阅而导致的潜在内存泄漏。

### 消极的后果​ <a href="#negative-consequences" id="negative-consequences"></a>

* 由于需要根据可视化和后台上下文创建服务的可观察量，因此增加了浏览器扩展的内存占用。
* 需要在前台和后台上下文之间同步服务的可观察量。
* 需要对服务的可观察量进行更复杂的实现。

### 在上下文之间同步主题​ <a href="#synching-a-subject-between-contexts" id="synching-a-subject-between-contexts"></a>

上下文之间的主题同步是使用 `Observable` 订阅、浏览器消息传递 API 和浏览器存储 API 的组合来开发的。

存储或直接对象共享被用作公共数据存储。订阅用于写入公共存储和发出消息，这会触发另一端的后续读取。

### 实施浏览器服务​ <a href="#implementing-a-browser-service" id="implementing-a-browser-service"></a>

在本节中，我们将通过一个示例介绍如何实现浏览器服务。我们将使用 `FolderService` 作为示例。

`FolderService` 提供了两个 `Observables` ，由 `BehaviorSubject`、`_folders` 和 `_folderViews` 支持。

```typescript
export class FolderService {
  private _folders = new BehaviorSubject<Folder[]>([]);
  private _folderViews = new BehaviorSubject<FolderView[]>([]);

  readonly folders = this._folders.asObservable();
  readonly folderViews = this._folderViews.asObservable();
}
```

以下各章节讨论将服务的可观察量分为前台和后台上下文所需的更改。

#### **libs** 服务​ <a href="#the-libs-service" id="the-libs-service"></a>

libs 服务需要使支持 `Observable` 的 `Subject` 可供扩展它的浏览器服务使用。

```diff
export class FolderService {
-  private _folders = new BehaviorSubject<Folder[]>([]);
-  private _folderViews = new BehaviorSubject<FolderView[]>([]);
+  protected _folders = new BehaviorSubject<Folder[]>([]);
+  protected _folderViews = new BehaviorSubject<FolderView[]>([]);

  readonly folders = this._folders.asObservable();
  readonly folderViews = this._folderViews.asObservable();
}
```

#### 浏览器服务​ <a href="#the-browser-service" id="the-browser-service"></a>

浏览器服务必须简单地扩展库服务并将其自身和 `Subject` 包装在一些装饰器中。

```typescript
@browserSession
export class BrowserFolderService extends FolderService {
  @sessionSync({ initializer: Folder.fromJSON, initializeAs: "array" })
  protected _folders: BehaviorSubject<Folder[]>;
  @sessionSync({ initializer: FolderView.fromJSON, initializeAs: "array" })
  protected _folderViews: BehaviorSubject<FolderView[]>;
}
```

`@sessionSync` 装饰器负责注册要同步的属性，并提供有关如何在需要序列化数据时初始化数据的信息。`@browserSession` 装饰器读取注册的属性并在给定类的所有实例之间设置同步。

#### 依赖注入​ <a href="#dependency-injection" id="dependency-injection"></a>

一旦服务实现，就可以向 Angular 的依赖注入系统注册。请务必更新或添加任何必要的提供程序。

#### **main.background** <a href="#main.background" id="main.background"></a>

`@browserSession` 仅在相同的类之间同步，因此后台服务需要使用与前台服务相同的类。目前，后台服务在 `main.background.ts` 中初始化。

```diff
-import { FolderSerivce } from '@bitwarden/common/src/services/folder.service';
+import { BrowserFolderService } from '../services/browser-folder.service';

-this.folderService = new FolderService(
+this.folderService = new BrowserFolderService(
  this.cryptoService,
  this.i18nService,
  this.cipherService,
  this.stateService
);
```


# 0016 - Move Decryption and Encryption to Views

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/decryption-in-views)
{% endhint %}

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

Bitwarden 有几个不同的模型来表示数据，[数据模型](/architecture/clients/data-model)中详细描述了这些数据。本 ADR 中，我们将重点关注以下两种模型：

* `<Domain>` - 表示已加密数据状态的域模型。
* `<Domain>View` - 表示域模型的已解密状态的视图模型。

由于我们至少有两个不同的模型来表示同一域的已加密和已解密状态，这也意味着我们需要一种在两个模型之间进行转换的方法，即加密和解密数据。

### 目前是如何做的​ <a href="#how-its-currently-being-done" id="how-its-currently-being-done"></a>

当前完成此操作的方法是让 `<Domain>Service` 公开包含解密视图的 Observable，或者使用基于 Promise 的方法来解密它。`<Domain>Service` 通常还公开一个 `encrypt` 方法，该方法从 `View` 和 `Domain` 模型进行转换。

`Domain` 模型本身通常还有一个 `decrypt` 方法，用于执行实际的解密逻辑。它通过在 `EncString` 对象上调用 `decrypt` 来实现这一点，而对象又依赖于全局容器服务来检索 `CryptoService` 和 `EncryptService` 执行实际操作。

### 存在的问题​ <a href="#the-problems" id="the-problems"></a>

这种方法有几个问题：

* 域模型与视图模型紧密耦合。
* 加密和解密被分成两个不同的地方。解密直接发生在域模型上，而加密发生在服务中。从逻辑上讲，它们是紧密耦合的，并且应该彼此相邻。
* 我们依靠全局容器服务来检索 `CryptoService` 和 `EncryptService`。
* 我们当前的模型充当转型管道。`Request -> Data -> Domain -> View`。
* 将来如果有一种方法可以支持每个域的多个 `View` 模型，那就太好了。

### 为什么是现在？​ <a href="#why-now" id="why-now"></a>

目前，Secret Manager 在加密和解密的管理方式方面遇到了一些摩擦。它不遵循同步本地状态的典型模式，而是依赖于对服务器的直接请求来获取数据。然后需要对数据进行解密。

目前，此加密和解密逻辑由 `<Domain>Service` 处理，但这违反了单一责任原则。这也使得我们的服务难以跟踪，因为它现在需要了解请求、响应、加密和解密。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* **将解密转移至 `<Domain>Service`** -- 我们已经为不同的域提供了服务，这些服务目前用于处理加密，因此将逻辑转移到这里是合理的。
* **将逻辑移转移至 `<Domain>View` 本身** -- 将逻辑移至 `View` 模型本身，并结合通用服务来加密和解密视图。

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

选择的方案：**将逻辑转移至 `<Domain>View` 本身**。

### 积极的后果​ <a href="#positive-consequences" id="positive-consequences"></a>

* 域服务不再需要实现定制的加密和解密逻辑。这遵循单一责任原则。
* 域模型不再与视图紧密耦合。
* 目前，我们的每个域可以拥有多个视图。

### 消极的后果​ <a href="#negative-consequences" id="negative-consequences"></a>

* 由于加密和解密现在是在通用 `EncryptService` 上完成的，因此可以绕过预期的流程。一个例子是 `Cipher`，`CipherService` 有一个 `updateHistoryAndEncrypt` 方法，它在加密之前计算密码历史记录。

### 实施​ <a href="#implementation" id="implementation"></a>

[文件夹 PR](https://github.com/bitwarden/clients/pull/3732) 示例。

```typescript
class FolderDomain implements DecryptableDomain {
  id: string;
  name: EncString;
  revisionDate: Date;

  keyIdentifier(): string | null {
    return null;
  }
}

class FolderView implements Encryptable<Folder> {
  id: string = null;
  name: string = null;
  revisionDate: Date = null;

  keyIdentifier(): string | null {
    return null;
  }

  async encrypt(encryptService: EncryptService, key: SymmetricCryptoKey): Promise<Folder> {
    const folder = new Folder();
    folder.id = this.id;
    folder.revisionDate = this.revisionDate;

    folder.name = this.name != null ? await encryptService.encrypt(this.name, key) : null;

    return folder;
  }

  static async decrypt(encryptService: EncryptService, key: SymmetricCryptoKey, model: Folder) {
    const view = new FolderView();
    view.id = model.id;
    view.revisionDate = model.revisionDate;

    view.name = await model.name?.decryptWithEncryptService(encryptService, key);

    return view;
  }
}
```

它将像这样被使用：

```typescript
// Fetch from server
const response: FolderResponse = await this.folderApiService.getFolder(id);
const folderData: FolderData = new FolderData(response);
const folder: Folder = new Folder(folderData);

// Decrypt / Encrypt
const folderView: FolderView = this.encryptionService.decryptView(FolderView, folder, key);
folderView.name = "New folder name";
const encryptedFolder: Folder = this.encryptionService.encryptView(folderView, key);

// Update
const request: FolderRequest = new FolderUpdateRequest(encryptedFolder);
```


# 0017 - Use Swift to build watchOS app

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/watchOS-use-swift)
{% endhint %}

| ID：  | ADR-0017   |
| ---- | ---------- |
| 状态：  | 完成         |
| 发表于： | 2022-12-30 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

我们希望将 watchOS 应用程序与常规 iOS 应用程序捆绑在一起。watchOS 应用程序将作为一个辅助应用程序，最初提供一种从手表查看之前从 iPhone 同步的 TOTP 代码的方法。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* [使用 Xamarin 的 .Net](https://learn.microsoft.com/en-us/xamarin/ios/watchos/)
* [使用 ](https://developer.apple.com/documentation/watchkit/)[Swift 的 ](https://developer.apple.com/documentation/watchkit/)[WatchKit](https://developer.apple.com/documentation/watchkit/)
* [使用 ](https://developer.apple.com/xcode/swiftui/)[Swift 的 SwiftUI](https://developer.apple.com/xcode/swiftui/)

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

选择的方案：**使用 SwiftUI 的 Swift**。

### 积极的后果​ <a href="#positive-consequences" id="positive-consequences"></a>

* 完全支持 watchOS
* 我们可以正确构建和调试应用程序
* 通过声明式 UI 和预览功能进行快速开发，增强开发体验
* 使用 UI 上的组件组织代码
* 框架和 SDK 的更新可在 Apple 发布后立即获得
* 有更多的文档、示例和公共存储库可供查阅

### 消极的后果​ <a href="#negative-consequences" id="negative-consequences"></a>

* 团队需要学习新的技术堆栈
* 即使我们可以正确调试应用程序，我们也不能同时调试 iOS 和 watchOS 应用程序（调试 watchOS 应用程序时，iPhone 上安装了一个存根 iOS 应用程序，因此原始应用程序会被存根应用程序覆盖）
* 由于我们需要将 XCode 构建的 watchOS 应用程序捆绑到 Xamarin iOS 应用程序中并相应地更新 CI，因此设置更加困难

## 方案的优点和缺点​ <a href="#pros-and-cons-of-the-options" id="pros-and-cons-of-the-options"></a>

### 使用 Xamarin 的 .Net  <a href="#net-using-xamarin" id="net-using-xamarin"></a>

* ✅ 保持相同的技术堆栈
* ✅ 共享大量代码
* ✅ 更简单的学习曲线和审查
* ✅ 易于集成到常规 iOS 应用程序中
* ⛔ 影响 watchOS 开发体验的几个重要问题，尤其是无法正确调试
* ⛔ watchOS 平台不是 .Net 团队的优先考虑事项
* ⛔ 没有计划在 MAUI、.Net 7 和 .Net 8 上加入 watchOS 支持

### 使用 WatchKit​ 的 Swift <a href="#swift-using-watchkit" id="swift-using-watchkit"></a>

* ✅ 这是原生方法，意味着它始终是最新的
* ✅ 有大量的文档和示例/项目可供查看
* ✅ 调试工作符合预期
* ⛔ 陡峭的学习曲线（语言 + Watch 相关的内容）
* ⛔ 难以集成到常规 iOS 应用程序中

### 使用 SwiftUI 的 Swift ​ <a href="#swift-using-swiftui" id="swift-using-swiftui"></a>

* ✅ 这是原生方法，意味着它始终是最新的
* ✅ 有大量的文档和示例/项目可供查看
* ✅ 调试工作符合预期
* ✅ 使用 SwiftUI 框架快速开发
* ✅ 预览功能让开发体验更上一层楼，大大减少了工作量
* ⛔ 陡峭的学习曲线（语言 + Watch 相关的内容 + SwiftUI 框架）
* ⛔ 难以集成到常规 iOS 应用程序中
* ⛔ SwiftUI 还不够完善，因此需要注意一些特殊的导航和渲染问题


# 0018 - Feature management

{% hint style="info" %}
对应的[官方页面地址](https://contributing.bitwarden.com/architecture/adr/feature-management)
{% endhint %}

| ID：  | ADR-0018   |
| ---- | ---------- |
| 状态：  | 完成         |
| 发表于： | 2023-02-01 |

## 背景和问题陈述​ <a href="#context-and-problem-statement" id="context-and-problem-statement"></a>

新功能不断被添加到平台中，同时也面临着更频繁地部署、减少孤立开发的压力。质量保证团队希望保持产品的质量，运营团队也希望保持可靠性和性能。功能通常可以在一段时间内以小块并行的方式交付，并且需要通过限制向特定受众发布和支持实验来控制其对系统的影响。

## 考虑的方案​ <a href="#considered-options" id="considered-options"></a>

* **直接添加/更改功能** -- 通过 SDLC 和代码审查进行更改，并在获得批准后合并到主线发布分支中。在开发其他功能的同时，根据需要解决问题和进行热修复。
* **在内部实现功能管理系统** -- 在代码中利用框架，并在我们自己的存储中存储功能标志和其他组件，以专有方式流式传输它们的更新，如果有的话（例如，仅在应用程序启动时加载配置）。
* **采用具有本地回退功能的功能管理系统** -- 实施服务提供商提供的功能，以获得更强大的功能管理能力，如实时流和用户/上下文定位。为希望测试或采用尚未完全支持的功能的自托管安装支持本地配置。

## 决策结果​ <a href="#decision-outcome" id="decision-outcome"></a>

选择的方案：**采用具有本地回退功能的功能管理系统**。

### 积极的后果​ <a href="#positive-consequences" id="positive-consequences"></a>

* 针对标记及其变体的强大功能集。
* 防止更改和有针对性的影响，同时加快整体交付速度（假设托管功能处于「off」状态）。
* 功能的上下文相关应用。
* 记录和追踪谁可以体验或试验某项功能。

### 消极的后果​ <a href="#negative-consequences" id="negative-consequences"></a>

* 选择服务提供商的成本。

### 计划​ <a href="#plan" id="plan"></a>

[服务器](https://github.com/bitwarden/server)代码库将为提供功能管理的服务提供商采用 .NET SDK。只有服务器端 SDK 将用于管理访问和成本，并且功能状态将在适当的情况下通过 API 响应元素传达给调用客户端。新功能将在服务提供商的平台内设置，对它们的更改将流式传输到运行中的应用程序。对提供商的访问将由内部控制。

为方便客户端在其他配置中使用功能状态，API 将进行扩展以提供配置值的集合。其中一些值已经被持久维护，并将与功能键混合在一起。客户端将在启动、登录、本地配置更新以及同步事件发生时刷新配置。

将使用受支持的客户端建立与 API 通信的上下文。服务提供商可根据需要将上述上下文用于特定目标。将为用户、组织和服务账户建立上下文，实体的唯一 ID 将作为关键字，并根据需要提供其他详细信息。必要时，上下文属性可标记为私有，以避免溢出到服务提供商，如果需要使用 PII，服务提供商将被添加到[子处理器列表](https://help.ppgg.in/security/who-are-bitwardens-subprocessors)中，并进行相应的通信。

编译时配置将尽可能转换为使用功能管理服务提供商。SDK 对服务提供商的访问将按环境进行划分；某些功能可能永远不会在所有环境中使用。

新功能的状态将默认是「off」状态。非布尔值的变化将允许自定义。在默认状态配置下，可通过服务提供商对功能状态进行离线访问（这也意味着在安装之外无需连接）。本地文件可以为自托管安装的功能加载选择项，这些安装将默认为离线模式。

软件开发生命周期将得到加强，以明确所有功能开发都应使用标志保护。

将考虑支持在代码库中使用 [OpenFeature](https://docs.openfeature.dev/docs/reference/intro/) 兼容接口。




---

[Next Page](/llms-full.txt/1)

