fix: localize Secrets operation errors

This commit is contained in:
mirivlad 2026-07-14 20:56:25 +08:00
parent bee365ad22
commit 1f03226378
4 changed files with 86 additions and 9 deletions

View File

@ -167,6 +167,38 @@
render(); render();
} }
function userFacingError(action, err) {
var raw = (err && err.message) ? err.message : String(err || '');
if (typeof console !== 'undefined' && typeof console.error === 'function') {
console.error('[verstak.secrets] ' + action + ' failed', err);
}
if (action === 'unlock') {
var minimumLength = raw.match(/master password must be at least (\d+) characters/i);
if (minimumLength) {
return tr('ui.masterPasswordMinLength', { count: minimumLength[1] }, 'Master password must be at least ' + minimumLength[1] + ' characters.');
}
if (/master password is empty/i.test(raw)) {
return tr('ui.masterPasswordRequired', null, 'Enter a master password.');
}
if (/invalid master password/i.test(raw)) {
return tr('ui.masterPasswordInvalid', null, 'The master password is incorrect.');
}
}
return tr('ui.' + action + 'Error', null, {
status: 'Could not check the secret storage. Please try again.',
load: 'Could not load secrets. Please try again.',
read: 'Could not open this secret. Please try again.',
save: 'Could not save the secret. Please try again.',
delete: 'Could not delete the secret. Please try again.',
copyLink: 'Could not copy the secret link. Please try again.',
unlock: 'Could not unlock secrets. Check the master password and try again.'
}[action] || 'Could not complete this action. Please try again.');
}
function setUserFacingError(action, err) {
setStatus(userFacingError(action, err), true);
}
function recordWorkspaceRoot(record) { function recordWorkspaceRoot(record) {
var scope = record && record.scope || {}; var scope = record && record.scope || {};
return scope.kind === ScopeWorkspace ? cleanWorkspace(scope.workspaceRootPath) : ''; return scope.kind === ScopeWorkspace ? cleanWorkspace(scope.workspaceRootPath) : '';
@ -249,7 +281,7 @@
return loadRecords(); return loadRecords();
}).catch(function (err) { }).catch(function (err) {
unlockBtn.disabled = false; unlockBtn.disabled = false;
setStatus((err && err.message) ? err.message : String(err), true); setUserFacingError('unlock', err);
}); });
} }
}, [tr('ui.unlock', null, 'Unlock')]); }, [tr('ui.unlock', null, 'Unlock')]);
@ -458,7 +490,7 @@
selectedValue = ''; selectedValue = '';
return loadRecords(); return loadRecords();
}).catch(function (err) { }).catch(function (err) {
setStatus((err && err.message) ? err.message : String(err), true); setUserFacingError('save', err);
}); });
} }
}, [tr('ui.save', null, 'Save')]), }, [tr('ui.save', null, 'Save')]),
@ -505,6 +537,8 @@
selectedValue = ''; selectedValue = '';
mode = 'selected'; mode = 'selected';
render(); render();
}).catch(function (err) {
setUserFacingError('load', err);
}); });
} }
@ -522,7 +556,7 @@
render(); render();
}).catch(function (err) { }).catch(function (err) {
if (disposed) return; if (disposed) return;
setStatus((err && err.message) ? err.message : String(err), true); setUserFacingError('read', err);
}); });
} }
@ -549,7 +583,7 @@
selectedValue = ''; selectedValue = '';
return loadRecords(); return loadRecords();
}).catch(function (err) { }).catch(function (err) {
setStatus((err && err.message) ? err.message : String(err), true); setUserFacingError('delete', err);
}); });
} }
@ -559,7 +593,7 @@
setStatus(tr('ui.linkCopied', null, 'Secret link copied'), false); setStatus(tr('ui.linkCopied', null, 'Secret link copied'), false);
}); });
}).catch(function (err) { }).catch(function (err) {
setStatus((err && err.message) ? err.message : String(err), true); setUserFacingError('copyLink', err);
}); });
} }
@ -569,7 +603,7 @@
if (unlocked) return loadWorkspaceOptions().then(loadRecords); if (unlocked) return loadWorkspaceOptions().then(loadRecords);
render(); render();
}).catch(function (err) { }).catch(function (err) {
statusText = (err && err.message) ? err.message : String(err); statusText = userFacingError('status', err);
statusError = true; statusError = true;
renderLocked(); renderLocked();
}); });

View File

@ -9,6 +9,9 @@
"ui.masterPassword": "Master password", "ui.masterPassword": "Master password",
"ui.repeatPassword": "Repeat master password", "ui.repeatPassword": "Repeat master password",
"ui.passwordMismatch": "Master passwords do not match", "ui.passwordMismatch": "Master passwords do not match",
"ui.masterPasswordMinLength": "Master password must be at least {count} characters.",
"ui.masterPasswordRequired": "Enter a master password.",
"ui.masterPasswordInvalid": "The master password is incorrect.",
"ui.unlock": "Unlock", "ui.unlock": "Unlock",
"ui.createMaster": "Create master password", "ui.createMaster": "Create master password",
"ui.password": "Password", "ui.password": "Password",
@ -38,6 +41,13 @@
"ui.search": "Search secrets", "ui.search": "Search secrets",
"ui.chooseWorkspace": "Choose a Deal", "ui.chooseWorkspace": "Choose a Deal",
"ui.workspaceRequired": "Choose a Deal for this secret.", "ui.workspaceRequired": "Choose a Deal for this secret.",
"ui.statusError": "Could not check the secret storage. Please try again.",
"ui.loadError": "Could not load secrets. Please try again.",
"ui.readError": "Could not open this secret. Please try again.",
"ui.saveError": "Could not save the secret. Please try again.",
"ui.deleteError": "Could not delete the secret. Please try again.",
"ui.copyLinkError": "Could not copy the secret link. Please try again.",
"ui.unlockError": "Could not unlock secrets. Check the master password and try again.",
"ui.value": "Value", "ui.value": "Value",
"ui.save": "Save", "ui.save": "Save",
"ui.cancel": "Cancel", "ui.cancel": "Cancel",

View File

@ -9,6 +9,9 @@
"ui.masterPassword": "Мастер-пароль", "ui.masterPassword": "Мастер-пароль",
"ui.repeatPassword": "Повторите мастер-пароль", "ui.repeatPassword": "Повторите мастер-пароль",
"ui.passwordMismatch": "Мастер-пароли не совпадают", "ui.passwordMismatch": "Мастер-пароли не совпадают",
"ui.masterPasswordMinLength": "Мастер-пароль должен содержать не менее {count} символов.",
"ui.masterPasswordRequired": "Введите мастер-пароль.",
"ui.masterPasswordInvalid": "Мастер-пароль указан неверно.",
"ui.unlock": "Разблокировать", "ui.unlock": "Разблокировать",
"ui.createMaster": "Создать мастер-пароль", "ui.createMaster": "Создать мастер-пароль",
"ui.password": "Пароль", "ui.password": "Пароль",
@ -38,6 +41,13 @@
"ui.search": "Поиск секретов", "ui.search": "Поиск секретов",
"ui.chooseWorkspace": "Выберите дело", "ui.chooseWorkspace": "Выберите дело",
"ui.workspaceRequired": "Выберите дело для этого секрета.", "ui.workspaceRequired": "Выберите дело для этого секрета.",
"ui.statusError": "Не удалось проверить хранилище секретов. Повторите попытку.",
"ui.loadError": "Не удалось загрузить секреты. Повторите попытку.",
"ui.readError": "Не удалось открыть секрет. Повторите попытку.",
"ui.saveError": "Не удалось сохранить секрет. Повторите попытку.",
"ui.deleteError": "Не удалось удалить секрет. Повторите попытку.",
"ui.copyLinkError": "Не удалось скопировать ссылку на секрет. Повторите попытку.",
"ui.unlockError": "Не удалось разблокировать секреты. Проверьте мастер-пароль и повторите попытку.",
"ui.value": "Значение", "ui.value": "Значение",
"ui.save": "Сохранить", "ui.save": "Сохранить",
"ui.cancel": "Отмена", "ui.cancel": "Отмена",

View File

@ -96,10 +96,15 @@ function makeDocument() {
}; };
} }
function loadComponent(document) { function loadComponent(document, errorLog) {
const registry = {}; const registry = {};
vm.runInNewContext(source, { vm.runInNewContext(source, {
console, console: {
...console,
error(...args) {
errorLog.push(args.map((value) => String(value)).join(' '));
},
},
document, document,
window: { window: {
VerstakPluginRegister(pluginId, bundle) { VerstakPluginRegister(pluginId, bundle) {
@ -138,13 +143,15 @@ async function flush() {
if (!(manifest.contributes.settingsPanels || []).some((item) => item.component === 'SecretsView')) throw new Error('secrets settings panel missing'); if (!(manifest.contributes.settingsPanels || []).some((item) => item.component === 'SecretsView')) throw new Error('secrets settings panel missing');
const document = makeDocument(); const document = makeDocument();
const component = loadComponent(document); const errorLog = [];
const component = loadComponent(document, errorLog);
const records = [ const records = [
{ id: 'global.server', title: 'Global Server', username: 'root', scope: { kind: 'global' }, updatedAt: '2026-06-29T00:00:00Z' }, { id: 'global.server', title: 'Global Server', username: 'root', scope: { kind: 'global' }, updatedAt: '2026-06-29T00:00:00Z' },
{ id: 'client-a.db', title: 'Client A DB', username: 'app', scope: { kind: 'workspace', workspaceRootPath: 'ClientA' }, updatedAt: '2026-06-29T00:00:00Z' }, { id: 'client-a.db', title: 'Client A DB', username: 'app', scope: { kind: 'workspace', workspaceRootPath: 'ClientA' }, updatedAt: '2026-06-29T00:00:00Z' },
]; ];
let initialized = false; let initialized = false;
let unlocked = false; let unlocked = false;
let unlockError = '';
const readCalls = []; const readCalls = [];
const copied = []; const copied = [];
const deleted = []; const deleted = [];
@ -152,6 +159,7 @@ async function flush() {
secrets: { secrets: {
status: async () => ({ initialized, unlocked }), status: async () => ({ initialized, unlocked }),
unlock: async (password) => { unlock: async (password) => {
if (unlockError) throw new Error(unlockError);
if (password !== 'master-password') throw new Error('bad password'); if (password !== 'master-password') throw new Error('bad password');
initialized = true; initialized = true;
unlocked = true; unlocked = true;
@ -197,6 +205,21 @@ async function flush() {
const confirmInput = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-master-password-confirm') === ''); const confirmInput = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-master-password-confirm') === '');
const unlockButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-unlock') === ''); const unlockButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-unlock') === '');
if (!passwordInput || !confirmInput || !unlockButton) throw new Error('setup controls missing'); if (!passwordInput || !confirmInput || !unlockButton) throw new Error('setup controls missing');
unlockError = '[plugin:verstak.secrets] secrets.unlock failed: master password must be at least 8 characters';
passwordInput.value = 'short';
confirmInput.value = 'short';
unlockButton.click();
await flush();
if (!container.textContent.includes('Master password must be at least 8 characters')) {
throw new Error('weak master password error was not explained clearly');
}
if (container.textContent.includes('[plugin:') || container.textContent.includes('secrets.unlock')) {
throw new Error('technical unlock details leaked into the Secrets UI');
}
if (!errorLog.some((entry) => entry.includes('[plugin:verstak.secrets] secrets.unlock failed'))) {
throw new Error('technical unlock details were not retained in the console log');
}
unlockError = '';
passwordInput.value = 'master-password'; passwordInput.value = 'master-password';
confirmInput.value = 'master-password'; confirmInput.value = 'master-password';
unlockButton.click(); unlockButton.click();