Harden core snapshot sync and workspace lifecycle

This commit is contained in:
2026-07-17 04:10:59 +08:00
parent ba0ba5f8c4
commit e3d8078ad5
22 changed files with 2891 additions and 132 deletions
+1 -1
View File
@@ -99,7 +99,7 @@ test.describe('Command Palette', () => {
test('runs sync workflow commands', async ({ page }) => {
await page.evaluate(async () => {
const err = await window.go.api.App.PluginSyncConfigure('verstak.sync', 'https://sync.example.test', 'alice', 'secret');
const err = await window.go.api.App.PluginSyncConfigure('verstak.sync', 'https://sync.example.test', 'alice', 'secret', '');
if (err) throw new Error(err);
});
+3 -1
View File
@@ -102,7 +102,7 @@ test.describe('D: Plugin API bridge', () => {
const api = window.createPluginAPI('verstak.sync');
const initial = await api.sync.status();
await api.sync.testConnection('https://sync.example.test', 'alice', 'secret');
await api.sync.configure('https://sync.example.test', 'alice', 'secret');
await api.sync.configure('https://sync.example.test', 'alice', 'secret', 'existing-remote-vault');
await api.sync.setInterval(15);
const configured = await api.sync.status();
const syncNow = await api.sync.now();
@@ -117,6 +117,8 @@ test.describe('D: Plugin API bridge', () => {
expect(result.initial.statusLabel).toBe('disabled');
expect(result.configured.configured).toBe(true);
expect(result.configured.serverUrl).toBe('https://sync.example.test');
expect(result.configured.vaultId).toBe('existing-remote-vault');
expect(result.configured.lastWarning).toBe('');
expect(result.configured.syncInterval).toBe(15);
expect(result.syncNow).toEqual({ pushed: 0, pulled: 0, serverSequence: 0 });
expect(result.reset.configured).toBe(false);
@@ -468,10 +468,10 @@ export function createPluginAPI(pluginId) {
return App.PluginSyncStatus(pluginId);
});
},
configure: function(serverURL, username, password) {
configure: function(serverURL, username, password, vaultId) {
assertActive('sync.configure');
return callBackendErrorString(pluginId, 'sync.configure', function() {
return App.PluginSyncConfigure(pluginId, serverURL || '', username || '', password || '');
return App.PluginSyncConfigure(pluginId, serverURL || '', username || '', password || '', vaultId || '');
});
},
disconnect: function() {
+8 -2
View File
@@ -626,6 +626,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
return {
configured: false,
serverUrl: '',
vaultId: '',
deviceId: 'mock-device',
deviceName: '',
connected: false,
@@ -635,6 +636,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
lastSyncAt: '',
syncInterval: 0,
lastError: '',
lastWarning: '',
statusLabel: 'disabled',
serverSequence: 0
};
@@ -649,7 +651,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
if (p.charAt(0) === '/' || /^[A-Za-z]:/.test(p)) return { error: 'invalid-path: absolute path rejected' };
var parts = p.split('/').filter(Boolean);
if (parts.indexOf('..') !== -1) return { error: 'invalid-path: path-traversal' };
if (parts[0] && parts[0].toLowerCase() === '.verstak') return { error: 'reserved-path: .verstak is internal' };
if (parts.some(function(part) { return part.toLowerCase() === '.verstak'; })) return { error: 'reserved-path: .verstak is internal' };
return { path: parts.join('/') };
}
@@ -751,6 +753,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
return {
configured: syncState.configured,
serverUrl: syncState.serverUrl,
vaultId: syncState.vaultId,
deviceId: syncState.deviceId,
deviceName: syncState.deviceName,
connected: syncState.connected,
@@ -760,6 +763,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
lastSyncAt: syncState.lastSyncAt,
syncInterval: syncState.syncInterval,
lastError: syncState.lastError,
lastWarning: syncState.lastWarning,
statusLabel: syncState.statusLabel
};
}
@@ -3477,11 +3481,12 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
if (err) return Promise.resolve([{}, err]);
return Promise.resolve([syncStatusDTO(), '']);
},
PluginSyncConfigure: function (pluginId, serverUrl) {
PluginSyncConfigure: function (pluginId, serverUrl, username, password, vaultId) {
var err = requirePluginSyncPermission(pluginId, true);
if (err) return Promise.resolve(err);
syncState.configured = true;
syncState.serverUrl = serverUrl || '';
syncState.vaultId = vaultId || 'test-vault-001';
syncState.deviceId = 'mock-device';
syncState.deviceName = 'mock-device';
syncState.connected = true;
@@ -3491,6 +3496,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
syncState.statusLabel = 'connected';
pluginSettings[pluginId] = Object.assign({}, pluginSettings[pluginId] || {}, {
serverUrl: syncState.serverUrl,
vaultId: syncState.vaultId,
syncStatus: syncState.statusLabel
});
return Promise.resolve('');
+1 -1
View File
@@ -114,7 +114,7 @@ export function PluginSecretsUnlock(arg1:string,arg2:string):Promise<string>;
export function PluginSecretsWrite(arg1:string,arg2:Record<string, any>):Promise<Record<string, any>|string>;
export function PluginSyncConfigure(arg1:string,arg2:string,arg3:string,arg4:string):Promise<string>;
export function PluginSyncConfigure(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<string>;
export function PluginSyncDisconnect(arg1:string):Promise<string>;
+2 -2
View File
@@ -210,8 +210,8 @@ export function PluginSecretsWrite(arg1, arg2) {
return window['go']['api']['App']['PluginSecretsWrite'](arg1, arg2);
}
export function PluginSyncConfigure(arg1, arg2, arg3, arg4) {
return window['go']['api']['App']['PluginSyncConfigure'](arg1, arg2, arg3, arg4);
export function PluginSyncConfigure(arg1, arg2, arg3, arg4, arg5) {
return window['go']['api']['App']['PluginSyncConfigure'](arg1, arg2, arg3, arg4, arg5);
}
export function PluginSyncDisconnect(arg1) {
+4
View File
@@ -310,6 +310,7 @@ export namespace api {
export class SyncStatusDTO {
configured: boolean;
serverUrl: string;
vaultId: string;
deviceId: string;
deviceName: string;
connected: boolean;
@@ -319,6 +320,7 @@ export namespace api {
lastSyncAt: string;
syncInterval: number;
lastError: string;
lastWarning: string;
statusLabel: string;
static createFrom(source: any = {}) {
@@ -329,6 +331,7 @@ export namespace api {
if ('string' === typeof source) source = JSON.parse(source);
this.configured = source["configured"];
this.serverUrl = source["serverUrl"];
this.vaultId = source["vaultId"];
this.deviceId = source["deviceId"];
this.deviceName = source["deviceName"];
this.connected = source["connected"];
@@ -338,6 +341,7 @@ export namespace api {
this.lastSyncAt = source["lastSyncAt"];
this.syncInterval = source["syncInterval"];
this.lastError = source["lastError"];
this.lastWarning = source["lastWarning"];
this.statusLabel = source["statusLabel"];
}
}