From 37ae493fa73db6118d707c9c7e7d1a411249026d Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Thu, 7 May 2026 20:29:39 +0800 Subject: [PATCH] feat: add contributing guidelines and pull request template; update schema comments and documentation --- .github/PULL_REQUEST_TEMPLATE.md | 31 +++++ .github/scripts/security.cjs | 4 +- CONTRIBUTING.md | 133 ++++++++++++++++++++++ README.md | 3 + README_EN.md | 3 + migrations/0001_init.sql | 8 +- scripts/i18n-utils.cjs | 3 + scripts/i18n-validate.cjs | 4 + shared/backup-schema.ts | 8 ++ src/handlers/accounts.ts | 5 + src/handlers/backup.ts | 4 + src/handlers/ciphers.ts | 5 + src/handlers/domains.ts | 5 + src/handlers/sync.ts | 5 + src/services/backup-archive.ts | 11 ++ src/services/backup-import.ts | 10 ++ src/services/backup-settings-crypto.ts | 10 ++ src/services/domain-rules.ts | 8 ++ src/services/storage-domain-rules-repo.ts | 6 + src/services/storage-schema.ts | 12 +- src/services/storage.ts | 4 + webapp/src/lib/i18n.ts | 7 ++ 22 files changed, 284 insertions(+), 5 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CONTRIBUTING.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e91f21c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ +## Summary + + + +## 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 + + diff --git a/.github/scripts/security.cjs b/.github/scripts/security.cjs index 3c71c3b..bead320 100644 --- a/.github/scripts/security.cjs +++ b/.github/scripts/security.cjs @@ -67,7 +67,7 @@ class SecurityReport { guideStep1: '1. **开发人员**:使用上方表格中的 **位置** 列找到确切的文件和行号。', guideStep2: '2. **纠正**:遵循为每个规则提供的文档链接以提交修复。', guideStep3: '3. **可追溯性**:完整的原始 `.sarif` 数据已附加到此分支。下载并将其导入您的 IDE(例如 VS Code SARIF 查看器)进行本地分析。', - footer: '💡 *由 Antigravity AI 安全引擎生成。透明度是我们的承诺。*', + footer: '💡 *由 NodeWarden 安全工作流生成。透明度是我们的承诺。*', auditedIcon: '✅ **已审计**', noFiles: '未检索到文件。', 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.', 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.', - footer: '💡 *Generated by Antigravity AI Security Engine. Transparency is our commitment.*', + footer: '💡 *Generated by the NodeWarden security workflow. Transparency is our commitment.*', auditedIcon: '✅ **Audited**', noFiles: 'No files found.', trivyTitle: '🛡️ Container Config Security (Trivy)', diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a85c5a1 --- /dev/null +++ b/CONTRIBUTING.md @@ -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 +``` diff --git a/README.md b/README.md index f99a5f1..1015897 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ English: README_EN.md +贡献指南:CONTRIBUTING.md | +贡献与开发说明:CONTRIBUTING.md + > **免责声明** > 本项目仅供学习与交流使用,请定期备份你的密码库。 > 本项目与 Bitwarden 官方无关,请不要向 Bitwarden 官方反馈 NodeWarden 的问题。 diff --git a/README_EN.md b/README_EN.md index 49b515c..73eb01d 100644 --- a/README_EN.md +++ b/README_EN.md @@ -25,6 +25,9 @@ 中文说明:README.md +Contributing guide: CONTRIBUTING.md | +Contributing and development notes: CONTRIBUTING.md + > **Disclaimer** > > This project is for learning and discussion purposes only. Please back up your vault regularly. diff --git a/migrations/0001_init.sql b/migrations/0001_init.sql index 0197694..1e25231 100644 --- a/migrations/0001_init.sql +++ b/migrations/0001_init.sql @@ -1,8 +1,14 @@ PRAGMA foreign_keys = ON; -- 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. +-- +-- 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 ( key TEXT PRIMARY KEY, diff --git a/scripts/i18n-utils.cjs b/scripts/i18n-utils.cjs index 93817df..028b377 100644 --- a/scripts/i18n-utils.cjs +++ b/scripts/i18n-utils.cjs @@ -2,6 +2,9 @@ const fs = require('fs'); const path = require('path'); 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 localeFiles = [ diff --git a/scripts/i18n-validate.cjs b/scripts/i18n-validate.cjs index 22e8866..914ed7e 100644 --- a/scripts/i18n-validate.cjs +++ b/scripts/i18n-validate.cjs @@ -1,5 +1,9 @@ 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( localeFiles.map(([locale, fileName, variableName]) => [locale, readLocale(fileName, variableName)]) ); diff --git a/shared/backup-schema.ts b/shared/backup-schema.ts index 06d1dfc..3ccb519 100644 --- a/shared/backup-schema.ts +++ b/shared/backup-schema.ts @@ -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_RETENTION_COUNT = 30; export const BACKUP_DEFAULT_S3_REGION = 'auto'; diff --git a/src/handlers/accounts.ts b/src/handlers/accounts.ts index 03aee60..5d9f8d9 100644 --- a/src/handlers/accounts.ts +++ b/src/handlers/accounts.ts @@ -9,6 +9,11 @@ import { isTotpEnabled, verifyTotpToken } from '../utils/totp'; import { createRecoveryCode, recoveryCodeEquals } from '../utils/recovery-code'; 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 { if (!value) return false; const firstDot = value.indexOf('.'); diff --git a/src/handlers/backup.ts b/src/handlers/backup.ts index bb75a61..a221120 100644 --- a/src/handlers/backup.ts +++ b/src/handlers/backup.ts @@ -85,6 +85,10 @@ const BACKUP_RUNNER_LOCK_KEY = 'backup.runner.lock.v1'; const BACKUP_RUNNER_LEASE_MS = 10 * 60 * 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 { token: string; touch: () => Promise; diff --git a/src/handlers/ciphers.ts b/src/handlers/ciphers.ts index 2802634..8c2afa3 100644 --- a/src/handlers/ciphers.ts +++ b/src/handlers/ciphers.ts @@ -18,6 +18,11 @@ import { deleteAllAttachmentsForCipher, deleteAllAttachmentsForCiphers } from '. import { parsePagination, encodeContinuationToken } from '../utils/pagination'; 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 { if (value == null) return null; const normalized = String(value).trim(); diff --git a/src/handlers/domains.ts b/src/handlers/domains.ts index 78a92d1..0e7c173 100644 --- a/src/handlers/domains.ts +++ b/src/handlers/domains.ts @@ -9,6 +9,11 @@ import { } from '../services/domain-rules'; 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, keys: string[]): unknown { for (const key of keys) { if (Object.prototype.hasOwnProperty.call(payload, key)) return payload[key]; diff --git a/src/handlers/sync.ts b/src/handlers/sync.ts index 5dc6f9a..b5b98c8 100644 --- a/src/handlers/sync.ts +++ b/src/handlers/sync.ts @@ -11,6 +11,11 @@ import { } from '../utils/user-decryption'; 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 { const url = new URL(request.url); const cacheUrl = new URL( diff --git a/src/services/backup-archive.ts b/src/services/backup-archive.ts index 0091584..dc48959 100644 --- a/src/services/backup-archive.ts +++ b/src/services/backup-archive.ts @@ -8,6 +8,17 @@ import { getBlobStorageKind, } 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; const BACKUP_FORMAT_VERSION = 1; diff --git a/src/services/backup-import.ts b/src/services/backup-import.ts index b00e696..22bd09a 100644 --- a/src/services/backup-import.ts +++ b/src/services/backup-import.ts @@ -8,6 +8,16 @@ import { validateBackupPayloadContents, } 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; type BackupTableName = | 'config' diff --git a/src/services/backup-settings-crypto.ts b/src/services/backup-settings-crypto.ts index 02cef35..2d11e08 100644 --- a/src/services/backup-settings-crypto.ts +++ b/src/services/backup-settings-crypto.ts @@ -1,5 +1,15 @@ 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_INFO = 'runtime'; const PORTABLE_ALGORITHM = 'RSA-OAEP'; diff --git a/src/services/domain-rules.ts b/src/services/domain-rules.ts index 45934a6..a2e634f 100644 --- a/src/services/domain-rules.ts +++ b/src/services/domain-rules.ts @@ -3,6 +3,14 @@ import customGlobalDomainsRaw from '../static/global_domains.custom.json'; import type { CustomEquivalentDomain, DomainRulesResponse, GlobalEquivalentDomain } from '../types'; 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 & { Type?: unknown; Domains?: unknown; diff --git a/src/services/storage-domain-rules-repo.ts b/src/services/storage-domain-rules-repo.ts index 50aa674..be1747a 100644 --- a/src/services/storage-domain-rules-repo.ts +++ b/src/services/storage-domain-rules-repo.ts @@ -1,6 +1,12 @@ import type { UserDomainSettings } from '../types'; 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(raw: string | null | undefined, fallback: T[]): T[] { if (!raw) return fallback; try { diff --git a/src/services/storage-schema.ts b/src/services/storage-schema.ts index 4fdafd1..da996bb 100644 --- a/src/services/storage-schema.ts +++ b/src/services/storage-schema.ts @@ -1,6 +1,14 @@ // IMPORTANT: -// Keep this schema list in sync with migrations/0001_init.sql. -// Any new table/column/index must be added to both places together. +// This is the runtime D1 schema bootstrap. Keep it in sync with +// 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[] = [ '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, ' + diff --git a/src/services/storage.ts b/src/services/storage.ts index fef6a65..0a8cbde 100644 --- a/src/services/storage.ts +++ b/src/services/storage.ts @@ -112,6 +112,10 @@ import { const TWO_FACTOR_REMEMBER_TTL_MS = 30 * 24 * 60 * 60 * 1000; 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'; // D1-backed storage. diff --git a/webapp/src/lib/i18n.ts b/webapp/src/lib/i18n.ts index 49acf26..ce79041 100644 --- a/webapp/src/lib/i18n.ts +++ b/webapp/src/lib/i18n.ts @@ -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 = | 'en' | 'zh-CN'