mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
feat: add contributing guidelines and pull request template; update schema comments and documentation
This commit is contained in:
@@ -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('.');
|
||||
|
||||
@@ -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<void>;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<string, unknown>, keys: string[]): unknown {
|
||||
for (const key of keys) {
|
||||
if (Object.prototype.hasOwnProperty.call(payload, key)) return payload[key];
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<string, string | number | null>;
|
||||
|
||||
const BACKUP_FORMAT_VERSION = 1;
|
||||
|
||||
@@ -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<string, string | number | null>;
|
||||
type BackupTableName =
|
||||
| 'config'
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<GlobalEquivalentDomain> & {
|
||||
Type?: unknown;
|
||||
Domains?: unknown;
|
||||
|
||||
@@ -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<T>(raw: string | null | undefined, fallback: T[]): T[] {
|
||||
if (!raw) return fallback;
|
||||
try {
|
||||
|
||||
@@ -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, ' +
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user