feat: add contributing guidelines and pull request template; update schema comments and documentation

This commit is contained in:
shuaiplus
2026-05-07 20:29:39 +08:00
parent 33f7c5d88a
commit 37ae493fa7
22 changed files with 284 additions and 5 deletions
+31
View File
@@ -0,0 +1,31 @@
## Summary
<!-- What changed and why? -->
## Change Type
- [ ] Bug fix
- [ ] Feature
- [ ] Compatibility update
- [ ] Documentation
- [ ] Refactor
## Cross-File Checklist
- [ ] I read `CONTRIBUTING.md`.
- [ ] Schema changes, if any, updated both runtime schema and `migrations/0001_init.sql`.
- [ ] Persistent data changes, if any, updated backup export/import or documented why backup is not needed.
- [ ] User-facing text changes, if any, updated all locale files.
- [ ] Bitwarden client compatibility was considered for sync/API shape changes.
- [ ] No secrets, tokens, private deployment values, or real vault data are included.
## Checks
- [ ] `npx tsc -p tsconfig.json --noEmit`
- [ ] `npx tsc -p webapp/tsconfig.json --noEmit`
- [ ] `npm run i18n:validate`
- [ ] `npm run build`
## Notes
<!-- Anything reviewers should pay special attention to? -->
+2 -2
View File
@@ -67,7 +67,7 @@ class SecurityReport {
guideStep1: '1. **开发人员**:使用上方表格中的 **位置** 列找到确切的文件和行号。', guideStep1: '1. **开发人员**:使用上方表格中的 **位置** 列找到确切的文件和行号。',
guideStep2: '2. **纠正**:遵循为每个规则提供的文档链接以提交修复。', guideStep2: '2. **纠正**:遵循为每个规则提供的文档链接以提交修复。',
guideStep3: '3. **可追溯性**:完整的原始 `.sarif` 数据已附加到此分支。下载并将其导入您的 IDE(例如 VS Code SARIF 查看器)进行本地分析。', guideStep3: '3. **可追溯性**:完整的原始 `.sarif` 数据已附加到此分支。下载并将其导入您的 IDE(例如 VS Code SARIF 查看器)进行本地分析。',
footer: '💡 *由 Antigravity AI 安全引擎生成。透明度是我们的承诺。*', footer: '💡 *由 NodeWarden 安全工作流生成。透明度是我们的承诺。*',
auditedIcon: '✅ **已审计**', auditedIcon: '✅ **已审计**',
noFiles: '未检索到文件。', noFiles: '未检索到文件。',
trivyTitle: '🛡️ 容器配置安全 (Trivy)', trivyTitle: '🛡️ 容器配置安全 (Trivy)',
@@ -119,7 +119,7 @@ class SecurityReport {
guideStep1: '1. **Developers**: Use the **Location** column in the tables above to find the exact file and line number.', guideStep1: '1. **Developers**: Use the **Location** column in the tables above to find the exact file and line number.',
guideStep2: '2. **Remediate**: Follow the documentation links provided for each rule to submit a fix.', guideStep2: '2. **Remediate**: Follow the documentation links provided for each rule to submit a fix.',
guideStep3: '3. **Traceability**: Full raw `.sarif` data is attached to this branch. Download and import it into your IDE (e.g., VS Code SARIF Viewer) for local analysis.', guideStep3: '3. **Traceability**: Full raw `.sarif` data is attached to this branch. Download and import it into your IDE (e.g., VS Code SARIF Viewer) for local analysis.',
footer: '💡 *Generated by Antigravity AI Security Engine. Transparency is our commitment.*', footer: '💡 *Generated by the NodeWarden security workflow. Transparency is our commitment.*',
auditedIcon: '✅ **Audited**', auditedIcon: '✅ **Audited**',
noFiles: 'No files found.', noFiles: 'No files found.',
trivyTitle: '🛡️ Container Config Security (Trivy)', trivyTitle: '🛡️ Container Config Security (Trivy)',
+133
View File
@@ -0,0 +1,133 @@
# Contributing to NodeWarden
Thanks for taking the time to improve NodeWarden.
NodeWarden is a Bitwarden-compatible server with a custom web vault, Cloudflare
Workers/D1 storage, attachment storage, imports/exports, and scheduled backups.
Small changes can affect official clients, backups, migrations, or locale files,
so please keep changes focused and check the related parts of the project.
## Before Opening an Issue
For bug reports, include enough detail for someone else to reproduce the problem:
- The client or browser you used.
- The page, API route, or action that failed.
- Screenshots, logs, or the exact error message.
- Whether the problem happened after sync, import, export, restore, upgrade, or
a fresh deployment.
Please do not report NodeWarden-specific problems to the official Bitwarden
team. This project is independent from Bitwarden.
## Pull Request Guidelines
Keep pull requests small enough to review. A good PR should explain:
- What changed and why.
- What user-facing behavior changed.
- Which related areas were checked.
- Which commands were run before submitting.
Avoid mixing unrelated refactors with feature or bug-fix work. If a cleanup is
needed before the real fix, mention that clearly in the PR.
## Areas That Need Extra Care
Some parts of the codebase are deliberately connected. When changing one of
these areas, check the related files before calling the work complete.
### Database Changes
Runtime schema lives in `src/services/storage-schema.ts`. The initial D1 schema
lives in `migrations/0001_init.sql`.
If you add or change a table, column, or index:
- Update both schema files.
- Bump `STORAGE_SCHEMA_VERSION` in `src/services/storage.ts`.
- Decide whether the data should be included in instance backup.
### Backup And Restore
Backup export and restore are whitelist-based. This protects old backups from
breaking when fields are removed and prevents transient or secret runtime data
from being exported by accident.
When adding persistent data, check:
- `src/services/backup-archive.ts`
- `src/services/backup-import.ts`
- `webapp/src/lib/api/backup.ts`
Do not export runtime lock rows such as `backup.runner.lock.v1`. Do not import
retired sensitive fields such as `users.api_key`.
### Secrets And Provider Settings
Provider credentials must not be stored or exported as plain config JSON. Follow
the encrypted settings pattern in `src/services/backup-settings-crypto.ts`, or
document a replacement design before changing it.
### Bitwarden Client Compatibility
Official Bitwarden clients may send or expect fields that are not used directly
by the web vault. Cipher and sync changes should preserve unknown client fields
unless they are known-invalid or server-owned.
Check these files when changing vault item shape or sync behavior:
- `src/handlers/ciphers.ts`
- `src/handlers/sync.ts`
- `src/services/storage-cipher-repo.ts`
### Domain Rules
Equivalent-domain settings store both client/UI rule state and derived active
groups. Do not remove `equivalent_domains`, `custom_equivalent_domains`, or
`excluded_global_equivalent_domains` as duplicates without a migration and
compatibility plan.
### Accounts And Passwords
`users.master_password_hash` is for server-side login verification. It is not the
vault decryption key. Password changes, key material, `securityStamp`, and
refresh-token revocation must stay aligned.
Password hints are reminders, not recovery secrets. They must never contain the
master password, recovery codes, API keys, or anything that directly unlocks the
vault.
### i18n
Locale files are complete standalone bundles. When adding or changing user-facing
text, keep every locale in sync and run the validation script.
For new locales, update:
- `webapp/src/lib/i18n.ts`
- `webapp/src/lib/i18n/locales/*`
- `scripts/i18n-utils.cjs`
## Recommended Checks
For most backend or shared changes:
```sh
npx tsc -p tsconfig.json --noEmit
npm run build
```
For webapp text or locale changes:
```sh
npm run i18n:validate
npx tsc -p webapp/tsconfig.json --noEmit
npm run build
```
For documentation-only changes:
```sh
git diff --check
```
+3
View File
@@ -25,6 +25,9 @@
English: <a href="./README_EN.md"><code>README_EN.md</code></a> English: <a href="./README_EN.md"><code>README_EN.md</code></a>
贡献指南:<a href="./CONTRIBUTING.md"><code>CONTRIBUTING.md</code></a> |
贡献与开发说明:<a href="./CONTRIBUTING.md"><code>CONTRIBUTING.md</code></a>
> **免责声明** > **免责声明**
> 本项目仅供学习与交流使用,请定期备份你的密码库。 > 本项目仅供学习与交流使用,请定期备份你的密码库。
> 本项目与 Bitwarden 官方无关,请不要向 Bitwarden 官方反馈 NodeWarden 的问题。 > 本项目与 Bitwarden 官方无关,请不要向 Bitwarden 官方反馈 NodeWarden 的问题。
+3
View File
@@ -25,6 +25,9 @@
中文说明:<a href="./README.md"><code>README.md</code></a> 中文说明:<a href="./README.md"><code>README.md</code></a>
Contributing guide: <a href="./CONTRIBUTING.md"><code>CONTRIBUTING.md</code></a> |
Contributing and development notes: <a href="./CONTRIBUTING.md"><code>CONTRIBUTING.md</code></a>
> **Disclaimer** > **Disclaimer**
> >
> This project is for learning and discussion purposes only. Please back up your vault regularly. > This project is for learning and discussion purposes only. Please back up your vault regularly.
+7 -1
View File
@@ -1,8 +1,14 @@
PRAGMA foreign_keys = ON; PRAGMA foreign_keys = ON;
-- IMPORTANT: -- IMPORTANT:
-- Keep this file in sync with src/services/storage-schema.ts (SCHEMA_STATEMENTS). -- This is the initial D1 schema. Keep it in sync with
-- src/services/storage-schema.ts (SCHEMA_STATEMENTS).
-- Any new table/column/index must be added to both places together. -- Any new table/column/index must be added to both places together.
--
-- WHEN CHANGING THIS:
-- - Also bump STORAGE_SCHEMA_VERSION in src/services/storage.ts.
-- - If the new table stores persistent data, update backup export/import.
-- - Keep src/services/storage-schema.ts idempotent for existing installs.
CREATE TABLE IF NOT EXISTS config ( CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
+3
View File
@@ -2,6 +2,9 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const vm = require('vm'); const vm = require('vm');
// CONTRACT:
// This list is the script-side locale source of truth. Keep it in sync with
// webapp/src/lib/i18n.ts whenever adding/removing a locale.
const localeDir = path.join(__dirname, '..', 'webapp', 'src', 'lib', 'i18n', 'locales'); const localeDir = path.join(__dirname, '..', 'webapp', 'src', 'lib', 'i18n', 'locales');
const localeFiles = [ const localeFiles = [
+4
View File
@@ -1,5 +1,9 @@
const { localeFiles, readLocale } = require('./i18n-utils.cjs'); const { localeFiles, readLocale } = require('./i18n-utils.cjs');
// CONTRACT:
// This is the authoritative locale consistency gate. It checks key parity,
// placeholder parity, and accidental mostly-English locale files. Run after any
// user-facing text or locale-file change.
const locales = Object.fromEntries( const locales = Object.fromEntries(
localeFiles.map(([locale, fileName, variableName]) => [locale, readLocale(fileName, variableName)]) localeFiles.map(([locale, fileName, variableName]) => [locale, readLocale(fileName, variableName)])
); );
+8
View File
@@ -1,3 +1,11 @@
// Shared backup settings types used by both Worker and webapp code.
//
// CONTRACT:
// Keep this file serializable and provider-neutral. Runtime state is operational
// metadata; destination fields can contain provider credentials and must be
// encrypted by src/services/backup-settings-crypto.ts before storage/export.
// User-facing provider names should use canonical values here. Legacy aliases
// belong in backend normalization, not in this shared type.
export const BACKUP_DEFAULT_TIMEZONE = 'UTC'; export const BACKUP_DEFAULT_TIMEZONE = 'UTC';
export const BACKUP_DEFAULT_RETENTION_COUNT = 30; export const BACKUP_DEFAULT_RETENTION_COUNT = 30;
export const BACKUP_DEFAULT_S3_REGION = 'auto'; export const BACKUP_DEFAULT_S3_REGION = 'auto';
+5
View File
@@ -9,6 +9,11 @@ import { isTotpEnabled, verifyTotpToken } from '../utils/totp';
import { createRecoveryCode, recoveryCodeEquals } from '../utils/recovery-code'; import { createRecoveryCode, recoveryCodeEquals } from '../utils/recovery-code';
import { buildAccountKeys } from '../utils/user-decryption'; import { buildAccountKeys } from '../utils/user-decryption';
// CONTRACT:
// users.master_password_hash is server-side login verification only. It does
// not decrypt vault data. Password changes must keep encrypted user key material,
// securityStamp, refresh-token invalidation, and client compatibility together.
// Password hints are non-secret reminders; never treat them as recovery secrets.
function looksLikeEncString(value: string): boolean { function looksLikeEncString(value: string): boolean {
if (!value) return false; if (!value) return false;
const firstDot = value.indexOf('.'); const firstDot = value.indexOf('.');
+4
View File
@@ -85,6 +85,10 @@ const BACKUP_RUNNER_LOCK_KEY = 'backup.runner.lock.v1';
const BACKUP_RUNNER_LEASE_MS = 10 * 60 * 1000; const BACKUP_RUNNER_LEASE_MS = 10 * 60 * 1000;
const BACKUP_RUNNER_HEARTBEAT_MS = 30 * 1000; const BACKUP_RUNNER_HEARTBEAT_MS = 30 * 1000;
// CONTRACT:
// The runner lock is a config-row lease, not a queue. It only prevents two
// backup/restore jobs from overlapping. Manual runs return conflict when the
// lease is held; scheduled runs skip quietly. Never export this row in backups.
interface BackupRunnerLease { interface BackupRunnerLease {
token: string; token: string;
touch: () => Promise<void>; touch: () => Promise<void>;
+5
View File
@@ -18,6 +18,11 @@ import { deleteAllAttachmentsForCipher, deleteAllAttachmentsForCiphers } from '.
import { parsePagination, encodeContinuationToken } from '../utils/pagination'; import { parsePagination, encodeContinuationToken } from '../utils/pagination';
import { readActingDeviceIdentifier } from '../utils/device'; import { readActingDeviceIdentifier } from '../utils/device';
// CONTRACT:
// Cipher JSON is the highest-risk Bitwarden compatibility surface. Preserve
// unknown/future client fields by default, then override only server-owned
// fields. Any change to cipher response shape must be checked against /api/sync,
// attachments, import/export, and current official clients.
function normalizeOptionalId(value: unknown): string | null { function normalizeOptionalId(value: unknown): string | null {
if (value == null) return null; if (value == null) return null;
const normalized = String(value).trim(); const normalized = String(value).trim();
+5
View File
@@ -9,6 +9,11 @@ import {
} from '../services/domain-rules'; } from '../services/domain-rules';
import { errorResponse, jsonResponse } from '../utils/response'; import { errorResponse, jsonResponse } from '../utils/response';
// CONTRACT:
// This route accepts both camelCase and PascalCase Bitwarden-compatible payloads.
// It stores custom rules, then derives equivalentDomains from the non-excluded
// custom rules. Keep this behavior aligned with backup import/export and
// src/services/storage-domain-rules-repo.ts.
function firstPresent(payload: Record<string, unknown>, keys: string[]): unknown { function firstPresent(payload: Record<string, unknown>, keys: string[]): unknown {
for (const key of keys) { for (const key of keys) {
if (Object.prototype.hasOwnProperty.call(payload, key)) return payload[key]; if (Object.prototype.hasOwnProperty.call(payload, key)) return payload[key];
+5
View File
@@ -11,6 +11,11 @@ import {
} from '../utils/user-decryption'; } from '../utils/user-decryption';
import { buildDomainsResponse } from '../services/domain-rules'; import { buildDomainsResponse } from '../services/domain-rules';
// CONTRACT:
// /api/sync reuses cipherToResponse() as the single cipher response shaper.
// Filtering invalid cipher responses here protects clients from stored rows that
// would otherwise make official apps fail after an HTTP 200 sync.
// Keep this aligned with src/handlers/ciphers.ts when adding new vault fields.
function buildSyncCacheRequest(request: Request, userId: string, revisionDate: string, excludeDomains: boolean, excludeSends: boolean): Request { function buildSyncCacheRequest(request: Request, userId: string, revisionDate: string, excludeDomains: boolean, excludeSends: boolean): Request {
const url = new URL(request.url); const url = new URL(request.url);
const cacheUrl = new URL( const cacheUrl = new URL(
+11
View File
@@ -8,6 +8,17 @@ import {
getBlobStorageKind, getBlobStorageKind,
} from './blob-store'; } from './blob-store';
// CONTRACT:
// This file defines the exported instance-backup archive shape. Keep it in lock
// step with src/services/backup-import.ts and webapp/src/lib/api/backup.ts.
//
// WHEN CHANGING THIS:
// - Add persistent tables to BackupPayload, export SQL, manifest tableCounts,
// and validateBackupPayloadContents().
// - Keep secrets and transient runtime rows sanitized before writing db.json.
// - users.api_key is intentionally not exported.
// - backup.settings.v1 is exported as portable-only; the current server runtime
// envelope must not leave the instance.
type SqlRow = Record<string, string | number | null>; type SqlRow = Record<string, string | number | null>;
const BACKUP_FORMAT_VERSION = 1; const BACKUP_FORMAT_VERSION = 1;
+10
View File
@@ -8,6 +8,16 @@ import {
validateBackupPayloadContents, validateBackupPayloadContents,
} from './backup-archive'; } from './backup-archive';
// CONTRACT:
// Restore is intentionally whitelist-based. Old backups may contain retired
// fields, but only the columns listed here are imported. Keep this file in sync
// with src/services/backup-archive.ts whenever backup contents change.
//
// WHEN CHANGING THIS:
// - Update BackupTableName, BACKUP_TABLES, reset statements, prepared payloads,
// shadow-table count validation, insert column lists, and frontend import
// count types together.
// - Do not import users.api_key, even if an older backup contains it.
type SqlRow = Record<string, string | number | null>; type SqlRow = Record<string, string | number | null>;
type BackupTableName = type BackupTableName =
| 'config' | 'config'
+10
View File
@@ -1,5 +1,15 @@
import type { Env, User } from '../types'; import type { Env, User } from '../types';
// CONTRACT:
// Backup settings contain provider credentials. They are stored as a v2 envelope:
// - runtime: AES-GCM encrypted with a key derived from JWT_SECRET for the current
// server's scheduled backup runner.
// - portable: AES-GCM encrypted with a random DEK; that DEK is RSA-wrapped for
// active admin public keys so settings can be repaired after restore/migration.
//
// New admin-entered provider secrets, such as mail API keys, should use this
// pattern or a deliberately documented replacement. Do not store provider
// secrets as plain config JSON.
const RUNTIME_SALT = 'nodewarden.backup-settings.runtime.v2'; const RUNTIME_SALT = 'nodewarden.backup-settings.runtime.v2';
const RUNTIME_INFO = 'runtime'; const RUNTIME_INFO = 'runtime';
const PORTABLE_ALGORITHM = 'RSA-OAEP'; const PORTABLE_ALGORITHM = 'RSA-OAEP';
+8
View File
@@ -3,6 +3,14 @@ import customGlobalDomainsRaw from '../static/global_domains.custom.json';
import type { CustomEquivalentDomain, DomainRulesResponse, GlobalEquivalentDomain } from '../types'; import type { CustomEquivalentDomain, DomainRulesResponse, GlobalEquivalentDomain } from '../types';
import { normalizeEquivalentDomain } from '../../shared/domain-normalize'; import { normalizeEquivalentDomain } from '../../shared/domain-normalize';
// CONTRACT:
// Equivalent domains are a Bitwarden compatibility surface. The DB stores both
// the full custom rule list and the derived active equivalent-domain groups:
// - custom_equivalent_domains: UI/client rules with id + excluded state.
// - equivalent_domains: active groups derived from non-excluded custom rules.
// - excluded_global_equivalent_domains: disabled global rule type ids.
// Do not treat equivalent_domains and custom_equivalent_domains as accidental
// duplicates without a migration and compatibility plan.
type RawGlobalDomain = Partial<GlobalEquivalentDomain> & { type RawGlobalDomain = Partial<GlobalEquivalentDomain> & {
Type?: unknown; Type?: unknown;
Domains?: unknown; Domains?: unknown;
@@ -1,6 +1,12 @@
import type { UserDomainSettings } from '../types'; import type { UserDomainSettings } from '../types';
import { normalizeCustomEquivalentDomains, normalizeEquivalentDomains } from './domain-rules'; import { normalizeCustomEquivalentDomains, normalizeEquivalentDomains } from './domain-rules';
// Storage adapter for the domain_settings table.
//
// CONTRACT:
// equivalent_domains is kept as the active derived groups for compatibility and
// fallback reads. custom_equivalent_domains is the full rule list that preserves
// UI/client state. Save both together through saveUserDomainSettings().
function parseJsonArray<T>(raw: string | null | undefined, fallback: T[]): T[] { function parseJsonArray<T>(raw: string | null | undefined, fallback: T[]): T[] {
if (!raw) return fallback; if (!raw) return fallback;
try { try {
+10 -2
View File
@@ -1,6 +1,14 @@
// IMPORTANT: // IMPORTANT:
// Keep this schema list in sync with migrations/0001_init.sql. // This is the runtime D1 schema bootstrap. Keep it in sync with
// Any new table/column/index must be added to both places together. // migrations/0001_init.sql. Any new table/column/index must be added to both
// places together.
//
// WHEN CHANGING THIS:
// - Bump STORAGE_SCHEMA_VERSION in src/services/storage.ts so existing installs
// rerun these idempotent statements.
// - If the new table stores persistent data, update the backup export/import
// contract in src/services/backup-archive.ts and backup-import.ts.
// - Keep statements idempotent; D1 may execute them again on later requests.
const SCHEMA_STATEMENTS: readonly string[] = [ const SCHEMA_STATEMENTS: readonly string[] = [
'CREATE TABLE IF NOT EXISTS users (' + 'CREATE TABLE IF NOT EXISTS users (' +
'id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT, master_password_hint TEXT, master_password_hash TEXT NOT NULL, ' + 'id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT, master_password_hint TEXT, master_password_hash TEXT NOT NULL, ' +
+4
View File
@@ -112,6 +112,10 @@ import {
const TWO_FACTOR_REMEMBER_TTL_MS = 30 * 24 * 60 * 60 * 1000; const TWO_FACTOR_REMEMBER_TTL_MS = 30 * 24 * 60 * 60 * 1000;
const STORAGE_SCHEMA_VERSION_KEY = 'schema.version'; const STORAGE_SCHEMA_VERSION_KEY = 'schema.version';
// IMPORTANT:
// Bump this whenever src/services/storage-schema.ts or migrations/0001_init.sql
// changes. Existing D1 installs only rerun ensureStorageSchema() when this value
// differs from config.schema.version.
const STORAGE_SCHEMA_VERSION = '2026-05-05-domain-rules-v2'; const STORAGE_SCHEMA_VERSION = '2026-05-05-domain-rules-v2';
// D1-backed storage. // D1-backed storage.
+7
View File
@@ -1,3 +1,10 @@
// CONTRACT:
// Locale bundles are standalone and loaded on demand. Adding a locale requires
// updating Locale, AVAILABLE_LOCALES, browser-language detection, localeLoaders,
// scripts/i18n-utils.cjs, and the locale file itself.
//
// Do not call t() at module scope for exported arrays/constants; async init can
// otherwise leave raw txt_* keys in the rendered UI.
export type Locale = export type Locale =
| 'en' | 'en'
| 'zh-CN' | 'zh-CN'