From 669d7ef2423423e50d76b4d42b3b825a6f279f95 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Thu, 7 May 2026 19:23:22 +0800 Subject: [PATCH] feat: add function to export portable backup settings envelope --- src/services/backup-archive.ts | 25 +++++++++++++++++++++++-- src/services/backup-settings-crypto.ts | 14 ++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/services/backup-archive.ts b/src/services/backup-archive.ts index c39b4e1..8db9bcb 100644 --- a/src/services/backup-archive.ts +++ b/src/services/backup-archive.ts @@ -1,6 +1,8 @@ import { zipSync, unzipSync } from 'fflate'; import type { Env } from '../types'; import { APP_VERSION } from '../../shared/app-version'; +import { BACKUP_SETTINGS_CONFIG_KEY } from './backup-config'; +import { exportPortableBackupSettingsEnvelope } from './backup-settings-crypto'; import { getAttachmentObjectKey, getBlobStorageKind, @@ -9,6 +11,7 @@ import { type SqlRow = Record; const BACKUP_FORMAT_VERSION = 1; +const BACKUP_RUNNER_LOCK_CONFIG_KEY = 'backup.runner.lock.v1'; const BACKUP_FILE_HASH_PREFIX_LENGTH = 5; // Worker-side backup export must stay well below Cloudflare CPU limits. // Prefer store-only ZIP entries over heavier compression to keep exports reliable. @@ -89,6 +92,23 @@ async function queryRows(db: D1Database, sql: string, ...values: unknown[]): Pro return (result.results || []).map((row) => ({ ...row })); } +function sanitizeConfigRowsForExport(rows: SqlRow[]): SqlRow[] { + const sanitized: SqlRow[] = []; + for (const row of rows) { + const key = String(row.key || '').trim(); + if (!key || key === BACKUP_RUNNER_LOCK_CONFIG_KEY) continue; + + if (key === BACKUP_SETTINGS_CONFIG_KEY) { + const portableOnly = exportPortableBackupSettingsEnvelope(typeof row.value === 'string' ? row.value : null); + if (portableOnly) sanitized.push({ ...row, value: portableOnly }); + continue; + } + + sanitized.push({ ...row }); + } + return sanitized; +} + async function sha256Hex(bytes: Uint8Array): Promise { const digest = await crypto.subtle.digest('SHA-256', bytes); return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, '0')).join(''); @@ -353,6 +373,7 @@ export async function buildBackupArchive( queryRows(env.DB, 'SELECT id, user_id, type, folder_id, name, notes, favorite, data, reprompt, key, created_at, updated_at, archived_at, deleted_at FROM ciphers ORDER BY created_at ASC'), queryRows(env.DB, 'SELECT id, cipher_id, file_name, size, size_name, key FROM attachments ORDER BY cipher_id ASC, id ASC'), ]); + const exportedConfigRows = sanitizeConfigRowsForExport(configRows); const exportedAttachmentRows = includeAttachments ? attachmentRows : []; const attachmentBlobs: BackupManifestAttachmentBlob[] = exportedAttachmentRows.map((row) => { const cipherId = String(row.cipher_id || '').trim(); @@ -371,7 +392,7 @@ export async function buildBackupArchive( appVersion: APP_VERSION, storageKind: getBlobStorageKind(env), tableCounts: { - config: configRows.length, + config: exportedConfigRows.length, users: userRows.length, user_revisions: revisionRows.length, folders: folderRows.length, @@ -392,7 +413,7 @@ export async function buildBackupArchive( const files: Record = { 'manifest.json': encoder.encode(JSON.stringify(manifestBase, null, BACKUP_JSON_INDENT)), 'db.json': encoder.encode(JSON.stringify({ - config: configRows, + config: exportedConfigRows, users: userRows, user_revisions: revisionRows, folders: folderRows, diff --git a/src/services/backup-settings-crypto.ts b/src/services/backup-settings-crypto.ts index cd2e722..02cef35 100644 --- a/src/services/backup-settings-crypto.ts +++ b/src/services/backup-settings-crypto.ts @@ -155,6 +155,20 @@ export function parseBackupSettingsEnvelope(raw: string | null): BackupSettingsE } } +export function exportPortableBackupSettingsEnvelope(raw: string | null): string | null { + const envelope = parseBackupSettingsEnvelope(raw); + if (!envelope) return null; + return JSON.stringify({ + version: 2, + portableOnly: true, + runtime: { + iv: '', + ciphertext: '', + }, + portable: envelope.portable, + }); +} + export async function encryptBackupSettingsEnvelope( plaintext: string, env: Env,