feat: simplify agent permissions and require current ZIP backups

This commit is contained in:
陈煜 committed 2026-10-04 01:46:47 +08:00
1 parent efc2c7c734
commit 91c489513a
51 files changed
+1680 -1270

No files matched your search

+50 -99
View File
@@ -7,14 +7,24 @@ import { packBackup, readBackupZip } from '../src/zip';
const empty = () =>
validateBackup({
format: 'worthpath',
version: 2,
version: 9,
exportedAt: new Date().toISOString(),
baseCurrency: 'CNY',
preferences: { hiddenMenus: ['asset'], showNotes: false, idleMinutes: 9 },
preferences: {
hiddenMenus: ['asset'],
showNotes: false,
idleMinutes: 9,
accountGroupOrder: [],
sessionHours: 168,
requireHiddenPassword: true,
overviewCards: ['net'],
includeIndependentAssets: true,
},
currencies: ['CNY'],
icons: [],
transfers: [],
schedules: [],
metalPrices: [],
positions: [],
rates: [],
links: [],
@@ -48,7 +58,10 @@ test('ZIP contains separate JSON files and restores settings without authenticat
'settings.json',
'transfers.json',
]);
assert.doesNotMatch(JSON.stringify(contents), /password|token|session|userId/i);
assert.doesNotMatch(
Object.values(contents).join('\n'),
/"(?:passwordHash|password|token|sessionId|userId|accessDigest|refreshDigest)"/i,
);
assert.deepEqual(validateBackup(await readBackupZip(await archive(contents))), b);
});
test('ZIP rejects incomplete files, tampering, unknown entries and invalid entry names', async () => {
@@ -69,6 +82,15 @@ test('backup accepts over 1000 positions, 10000 revisions per position and 20000
const position = (count: number) => ({
id: randomUUID(),
name: 'count acceptance',
groupName: '',
iconId: null,
included: true,
importedFromId: null,
metalType: null,
metalGrams: null,
metalCostPerGram: null,
metalPurity: '1',
autoValuation: false,
kind: 'asset',
side: 'asset',
category: 'other',
@@ -100,102 +122,31 @@ test('backup accepts over 1000 positions, 10000 revisions per position and 20000
);
});
test('legacy v3 ZIP remains readable without icons', async () => {
const contents = packBackup(empty());
delete contents['icons.json'];
delete contents['transfers.json'];
delete contents['schedules.json'];
const manifest = JSON.parse(contents['manifest.json']);
manifest.version = 3;
manifest.files = manifest.files.filter(
(f: { name: string }) => !['icons.json', 'transfers.json', 'schedules.json'].includes(f.name),
);
contents['manifest.json'] = JSON.stringify(manifest);
const restored = validateBackup(await readBackupZip(await archive(contents)));
assert.equal(restored.icons, undefined);
assert.deepEqual(restored.positions, []);
test('all historical ZIP versions and JSON formats are rejected', async () => {
for (const version of [3, 4, 5, 6, 7, 8]) {
const contents = packBackup(empty());
const manifest = JSON.parse(contents['manifest.json']);
manifest.version = version;
contents['manifest.json'] = JSON.stringify(manifest);
await assert.rejects(async () => readBackupZip(await archive(contents)));
}
for (const version of [1, 2, 3]) assert.throws(() => validateBackup({ ...empty(), version }));
});
test('legacy v4 ZIP remains readable without transfers', async () => {
const contents = packBackup(empty());
delete contents['transfers.json'];
delete contents['schedules.json'];
const manifest = JSON.parse(contents['manifest.json']);
manifest.version = 4;
manifest.files = manifest.files.filter(
(f: { name: string }) => !['transfers.json', 'schedules.json'].includes(f.name),
);
contents['manifest.json'] = JSON.stringify(manifest);
const restored = validateBackup(await readBackupZip(await archive(contents)));
assert.deepEqual(restored.icons, []);
assert.equal(restored.transfers, undefined);
assert.equal(restored.preferences?.showNotes, false);
assert.deepEqual(restored.preferences?.hiddenMenus, ['asset']);
});
test('ZIP v7 restores groups and schedules while v6 remains readable', async () => {
const id = randomUUID(),
stamp = new Date().toISOString();
const b = validateBackup({
...empty(),
positions: [
{
id,
name: '账户',
groupName: '日常',
kind: 'account',
side: 'asset',
category: 'bank',
currency: 'CNY',
notes: '',
archived: false,
hidden: false,
createdAt: stamp,
updatedAt: stamp,
revisions: [
{
id: randomUUID(),
amount: '-10',
date: '2026-09-01T10:00',
notes: '',
reason: 'balance',
createdAt: stamp,
updatedAt: stamp,
},
],
},
],
schedules: [
{
id: randomUUID(),
name: '租金',
operation: 'expense',
sourceId: id,
targetId: null,
amount: '100',
received: '0',
nextAt: '2026-11-01T10:00',
intervalDays: 30,
enabled: true,
notes: '',
},
],
});
const contents = packBackup(b);
assert.deepEqual(validateBackup(await readBackupZip(await archive(contents))), b);
delete contents['schedules.json'];
const manifest = JSON.parse(contents['manifest.json']);
manifest.version = 6;
manifest.files = manifest.files.filter((f: { name: string }) => f.name !== 'schedules.json');
contents['manifest.json'] = JSON.stringify(manifest);
assert.equal(validateBackup(await readBackupZip(await archive(contents))).schedules, undefined);
});
test('ZIP settings retain group order while missing legacy order remains optional', async () => {
test('current backups require complete settings and metadata, with no legacy defaults', async () => {
const b = empty();
b.preferences!.accountGroupOrder = ['理财', '', '未分组', '日常'];
const restored = validateBackup(await readBackupZip(await archive(packBackup(b))));
assert.deepEqual(restored.preferences!.accountGroupOrder, b.preferences!.accountGroupOrder);
const legacy = validateBackup(await readBackupZip(await archive(packBackup(empty()))));
assert.equal(legacy.preferences!.accountGroupOrder, undefined);
b.preferences.accountGroupOrder = ['日常', ''];
assert.deepEqual(validateBackup(await readBackupZip(await archive(packBackup(b)))), b);
for (const key of Object.keys(b.preferences)) {
const incomplete = structuredClone(b);
delete (incomplete.preferences as any)[key];
assert.throws(() => validateBackup(incomplete));
}
for (const key of ['icons', 'transfers', 'schedules', 'metalPrices']) {
const incomplete = structuredClone(b);
delete (incomplete as any)[key];
assert.throws(() => validateBackup(incomplete));
}
assert.throws(() =>
validateBackup({ ...b, preferences: { ...b.preferences, showSidebar: false } }),
);
});