fix: protect secrets values and deletion
This commit is contained in:
parent
aa7956dec4
commit
90ad8e8467
|
|
@ -60,6 +60,11 @@
|
||||||
'.secrets-table th{width:9rem;color:var(--vt-color-text-muted,#7f8aa3);font-weight:500;background:var(--vt-color-surface-muted,#111629)}',
|
'.secrets-table th{width:9rem;color:var(--vt-color-text-muted,#7f8aa3);font-weight:500;background:var(--vt-color-surface-muted,#111629)}',
|
||||||
'.secrets-table td{color:var(--vt-color-text-primary,#f4f7fb);overflow-wrap:anywhere}',
|
'.secrets-table td{color:var(--vt-color-text-primary,#f4f7fb);overflow-wrap:anywhere}',
|
||||||
'.secrets-table tr:last-child th,.secrets-table tr:last-child td{border-bottom:0}',
|
'.secrets-table tr:last-child th,.secrets-table tr:last-child td{border-bottom:0}',
|
||||||
|
'.secrets-modal-overlay{position:fixed;inset:0;z-index:10000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.6)}',
|
||||||
|
'.secrets-modal{width:24rem;max-width:90vw;display:grid;gap:.8rem;padding:1rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-lg,8px);background:var(--vt-color-surface,#15152c);box-shadow:var(--vt-elevation-menu,0 14px 32px rgba(0,0,0,.42))}',
|
||||||
|
'.secrets-modal-title{margin:0;font-size:.96rem}',
|
||||||
|
'.secrets-modal-message{margin:0;color:var(--vt-color-text-secondary,#b7c0d4);font-size:.84rem;line-height:1.45}',
|
||||||
|
'.secrets-modal-actions{display:flex;justify-content:flex-end;gap:.5rem}',
|
||||||
'@media(max-width:780px){.secrets-root{grid-template-columns:1fr}.secrets-panel{border-right:0;border-bottom:1px solid #252b36;max-height:45vh}.secrets-row,.secrets-filters{grid-template-columns:1fr}}'
|
'@media(max-width:780px){.secrets-root{grid-template-columns:1fr}.secrets-panel{border-right:0;border-bottom:1px solid #252b36;max-height:45vh}.secrets-row,.secrets-filters{grid-template-columns:1fr}}'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
|
@ -97,10 +102,10 @@
|
||||||
|| (node && (node.rootPath || node.name || node.id)));
|
|| (node && (node.rootPath || node.name || node.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function scopeLabel(record) {
|
function scopeLabel(record, translate) {
|
||||||
var scope = record && record.scope || {};
|
var scope = record && record.scope || {};
|
||||||
if (scope.kind === ScopeWorkspace) return cleanWorkspace(scope.workspaceRootPath) || 'Deal';
|
if (scope.kind === ScopeWorkspace) return cleanWorkspace(scope.workspaceRootPath) || translate('ui.deal', null, 'Deal');
|
||||||
return 'Global';
|
return translate('ui.global', null, 'Global');
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectedIDFromProps(props) {
|
function selectedIDFromProps(props) {
|
||||||
|
|
@ -111,16 +116,16 @@
|
||||||
return decodeURIComponent(path.replace(/^\/+/, ''));
|
return decodeURIComponent(path.replace(/^\/+/, ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupRecords(records) {
|
function groupRecords(records, translate) {
|
||||||
var groups = {};
|
var groups = {};
|
||||||
records.forEach(function (record) {
|
records.forEach(function (record) {
|
||||||
var label = scopeLabel(record);
|
var label = scopeLabel(record, translate);
|
||||||
groups[label] = groups[label] || [];
|
groups[label] = groups[label] || [];
|
||||||
groups[label].push(record);
|
groups[label].push(record);
|
||||||
});
|
});
|
||||||
return Object.keys(groups).sort(function (a, b) {
|
return Object.keys(groups).sort(function (a, b) {
|
||||||
if (a === 'Global') return -1;
|
if (a === translate('ui.global', null, 'Global')) return -1;
|
||||||
if (b === 'Global') return 1;
|
if (b === translate('ui.global', null, 'Global')) return 1;
|
||||||
return a.localeCompare(b);
|
return a.localeCompare(b);
|
||||||
}).map(function (label) {
|
}).map(function (label) {
|
||||||
groups[label].sort(function (a, b) {
|
groups[label].sort(function (a, b) {
|
||||||
|
|
@ -152,6 +157,8 @@
|
||||||
var searchQuery = '';
|
var searchQuery = '';
|
||||||
var selectedRecord = null;
|
var selectedRecord = null;
|
||||||
var selectedValue = '';
|
var selectedValue = '';
|
||||||
|
var isValueVisible = false;
|
||||||
|
var deleteModal = null;
|
||||||
var initialized = false;
|
var initialized = false;
|
||||||
var unlocked = false;
|
var unlocked = false;
|
||||||
var statusText = '';
|
var statusText = '';
|
||||||
|
|
@ -190,6 +197,7 @@
|
||||||
read: 'Could not open this secret. Please try again.',
|
read: 'Could not open this secret. Please try again.',
|
||||||
save: 'Could not save the secret. Please try again.',
|
save: 'Could not save the secret. Please try again.',
|
||||||
delete: 'Could not delete the secret. Please try again.',
|
delete: 'Could not delete the secret. Please try again.',
|
||||||
|
copyValue: 'Could not copy the secret value. Please try again.',
|
||||||
copyLink: 'Could not copy the secret link. 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.'
|
unlock: 'Could not unlock secrets. Check the master password and try again.'
|
||||||
}[action] || 'Could not complete this action. Please try again.');
|
}[action] || 'Could not complete this action. Please try again.');
|
||||||
|
|
@ -358,7 +366,7 @@
|
||||||
children.push(el('div', { className: 'secrets-empty' }, [tr('ui.empty', null, 'No secrets')]));
|
children.push(el('div', { className: 'secrets-empty' }, [tr('ui.empty', null, 'No secrets')]));
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
groupRecords(visibleRecords).forEach(function (group) {
|
groupRecords(visibleRecords, tr).forEach(function (group) {
|
||||||
children.push(el('div', { className: 'secrets-group' }, [group.label]));
|
children.push(el('div', { className: 'secrets-group' }, [group.label]));
|
||||||
children.push(el('div', { className: 'secrets-list' }, group.records.map(function (record) {
|
children.push(el('div', { className: 'secrets-list' }, group.records.map(function (record) {
|
||||||
var active = selectedRecord && selectedRecord.id === record.id;
|
var active = selectedRecord && selectedRecord.id === record.id;
|
||||||
|
|
@ -382,14 +390,16 @@
|
||||||
el('h2', {}, [tr('ui.select', null, 'Select a secret')]),
|
el('h2', {}, [tr('ui.select', null, 'Select a secret')]),
|
||||||
el('div', { className: statusError ? 'secrets-status error' : 'secrets-status' }, [statusText])
|
el('div', { className: statusError ? 'secrets-status error' : 'secrets-status' }, [statusText])
|
||||||
]);
|
]);
|
||||||
|
var valueIsAvailable = !!selectedValue;
|
||||||
|
var valueDisplay = valueIsAvailable && isValueVisible ? selectedValue : '••••••••••••';
|
||||||
return el('div', { className: 'secrets-card' }, [
|
return el('div', { className: 'secrets-card' }, [
|
||||||
el('h2', {}, [selectedRecord.title || selectedRecord.id]),
|
el('h2', {}, [selectedRecord.title || selectedRecord.id]),
|
||||||
el('table', { className: 'secrets-table' }, [
|
el('table', { className: 'secrets-table' }, [
|
||||||
el('tbody', {}, [
|
el('tbody', {}, [
|
||||||
fieldRow(tr('ui.group', null, 'Group'), scopeLabel(selectedRecord)),
|
fieldRow(tr('ui.group', null, 'Group'), scopeLabel(selectedRecord, tr)),
|
||||||
fieldRow('ID', selectedRecord.id),
|
fieldRow('ID', selectedRecord.id),
|
||||||
fieldRow(tr('ui.username', null, 'Username'), selectedRecord.username || ''),
|
fieldRow(tr('ui.username', null, 'Username'), selectedRecord.username || ''),
|
||||||
fieldRow(tr('ui.password', null, 'Password'), selectedValue ? selectedValue : '••••••••••••', selectedValue ? '' : 'secrets-hidden-value'),
|
fieldRow(tr('ui.password', null, 'Password'), valueDisplay, isValueVisible && valueIsAvailable ? 'secrets-secret-value' : 'secrets-hidden-value'),
|
||||||
fieldRow(tr('ui.updated', null, 'Updated'), selectedRecord.updatedAt || '')
|
fieldRow(tr('ui.updated', null, 'Updated'), selectedRecord.updatedAt || '')
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
|
@ -400,6 +410,24 @@
|
||||||
'data-secret-copy-link': selectedRecord.id,
|
'data-secret-copy-link': selectedRecord.id,
|
||||||
onClick: function () { copySecretLink(selectedRecord.id); }
|
onClick: function () { copySecretLink(selectedRecord.id); }
|
||||||
}, [tr('ui.copyLink', null, 'Copy secret link')]),
|
}, [tr('ui.copyLink', null, 'Copy secret link')]),
|
||||||
|
el('button', {
|
||||||
|
className: 'secrets-btn',
|
||||||
|
type: 'button',
|
||||||
|
disabled: !valueIsAvailable,
|
||||||
|
'data-secret-copy-value': selectedRecord.id,
|
||||||
|
onClick: function () { copySecretValue(selectedValue); }
|
||||||
|
}, [tr('ui.copyValue', null, 'Copy value')]),
|
||||||
|
el('button', {
|
||||||
|
className: 'secrets-btn',
|
||||||
|
type: 'button',
|
||||||
|
disabled: !valueIsAvailable,
|
||||||
|
'data-secret-toggle-value': selectedRecord.id,
|
||||||
|
onClick: function () {
|
||||||
|
if (!selectedValue) return;
|
||||||
|
isValueVisible = !isValueVisible;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}, [isValueVisible ? tr('ui.hideValue', null, 'Hide value') : tr('ui.showValue', null, 'Show value')]),
|
||||||
el('button', {
|
el('button', {
|
||||||
className: 'secrets-btn',
|
className: 'secrets-btn',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
|
|
@ -547,6 +575,7 @@
|
||||||
selectedID = id;
|
selectedID = id;
|
||||||
selectedRecord = records.find(function (record) { return record.id === id; }) || null;
|
selectedRecord = records.find(function (record) { return record.id === id; }) || null;
|
||||||
selectedValue = '';
|
selectedValue = '';
|
||||||
|
isValueVisible = false;
|
||||||
render();
|
render();
|
||||||
if (!id) return Promise.resolve();
|
if (!id) return Promise.resolve();
|
||||||
return api.secrets.read(id).then(function (record) {
|
return api.secrets.read(id).then(function (record) {
|
||||||
|
|
@ -576,17 +605,71 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSecret(id) {
|
function deleteSecret(id) {
|
||||||
if (!id || !window.confirm(tr('ui.deleteConfirm', null, 'Delete this secret?'))) return;
|
if (!id) return;
|
||||||
|
removeDeleteModal();
|
||||||
|
var title = selectedRecord && selectedRecord.id === id ? selectedRecord.title || selectedRecord.id : id;
|
||||||
|
var overlay = el('div', {
|
||||||
|
className: 'secrets-modal-overlay',
|
||||||
|
'data-secret-delete-modal': '',
|
||||||
|
role: 'presentation'
|
||||||
|
});
|
||||||
|
var cancel = el('button', {
|
||||||
|
className: 'secrets-btn',
|
||||||
|
type: 'button',
|
||||||
|
'data-secret-delete-cancel': '',
|
||||||
|
onClick: removeDeleteModal
|
||||||
|
}, [tr('ui.cancel', null, 'Cancel')]);
|
||||||
|
var confirm = el('button', {
|
||||||
|
className: 'secrets-btn danger',
|
||||||
|
type: 'button',
|
||||||
|
'data-secret-delete-confirm': '',
|
||||||
|
onClick: function () {
|
||||||
|
removeDeleteModal();
|
||||||
|
confirmDeleteSecret(id);
|
||||||
|
}
|
||||||
|
}, [tr('ui.delete', null, 'Delete')]);
|
||||||
|
overlay.appendChild(el('div', {
|
||||||
|
className: 'secrets-modal',
|
||||||
|
role: 'dialog',
|
||||||
|
'aria-modal': 'true',
|
||||||
|
'aria-label': tr('ui.deleteSecretTitle', null, 'Delete secret')
|
||||||
|
}, [
|
||||||
|
el('h2', { className: 'secrets-modal-title' }, [tr('ui.deleteSecretTitle', null, 'Delete secret')]),
|
||||||
|
el('p', { className: 'secrets-modal-message' }, [tr('ui.deleteConfirm', { title: title }, 'Delete "' + title + '"?')]),
|
||||||
|
el('div', { className: 'secrets-modal-actions' }, [cancel, confirm])
|
||||||
|
]));
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
deleteModal = overlay;
|
||||||
|
if (cancel.focus) cancel.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDeleteModal() {
|
||||||
|
if (deleteModal && typeof deleteModal.remove === 'function') deleteModal.remove();
|
||||||
|
else if (deleteModal && deleteModal.parentNode) deleteModal.parentNode.removeChild(deleteModal);
|
||||||
|
deleteModal = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDeleteSecret(id) {
|
||||||
api.secrets.delete(id).then(function () {
|
api.secrets.delete(id).then(function () {
|
||||||
selectedID = '';
|
selectedID = '';
|
||||||
selectedRecord = null;
|
selectedRecord = null;
|
||||||
selectedValue = '';
|
selectedValue = '';
|
||||||
|
isValueVisible = false;
|
||||||
return loadRecords();
|
return loadRecords();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setUserFacingError('delete', err);
|
setUserFacingError('delete', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copySecretValue(value) {
|
||||||
|
if (!value) return;
|
||||||
|
writeClipboard(api, value).then(function () {
|
||||||
|
setStatus(tr('ui.valueCopied', null, 'Secret value copied'), false);
|
||||||
|
}).catch(function (err) {
|
||||||
|
setUserFacingError('copyValue', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function copySecretLink(id) {
|
function copySecretLink(id) {
|
||||||
api.secrets.copyLink(id).then(function (link) {
|
api.secrets.copyLink(id).then(function (link) {
|
||||||
return writeClipboard(api, link).then(function () {
|
return writeClipboard(api, link).then(function () {
|
||||||
|
|
@ -614,6 +697,7 @@
|
||||||
|
|
||||||
containerEl.__secretsCleanup = function () {
|
containerEl.__secretsCleanup = function () {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
removeDeleteModal();
|
||||||
if (typeof localeUnsubscribe === 'function') localeUnsubscribe();
|
if (typeof localeUnsubscribe === 'function') localeUnsubscribe();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"manifest.name": "Secrets",
|
"manifest.name": "Secrets",
|
||||||
"manifest.description": "Encrypted global and workspace-scoped secret manager.",
|
"manifest.description": "Encrypted global and Deal-scoped secret manager.",
|
||||||
"contributions.views.verstak.secrets.view.title": "Secrets",
|
"contributions.views.verstak.secrets.view.title": "Secrets",
|
||||||
"contributions.sidebarItems.verstak.secrets.sidebar.title": "Secrets",
|
"contributions.sidebarItems.verstak.secrets.sidebar.title": "Secrets",
|
||||||
"contributions.openProviders.verstak.secrets.secret.title": "Secrets",
|
"contributions.openProviders.verstak.secrets.secret.title": "Secrets",
|
||||||
|
|
@ -28,11 +28,14 @@
|
||||||
"ui.copyLink": "Copy secret link",
|
"ui.copyLink": "Copy secret link",
|
||||||
"ui.edit": "Edit",
|
"ui.edit": "Edit",
|
||||||
"ui.delete": "Delete",
|
"ui.delete": "Delete",
|
||||||
|
"ui.copyValue": "Copy value",
|
||||||
|
"ui.showValue": "Show value",
|
||||||
|
"ui.hideValue": "Hide value",
|
||||||
"ui.fieldTitle": "Title",
|
"ui.fieldTitle": "Title",
|
||||||
"ui.optionalUsername": "optional username",
|
"ui.optionalUsername": "optional username",
|
||||||
"ui.secretValue": "Secret value",
|
"ui.secretValue": "Secret value",
|
||||||
"ui.global": "Global",
|
"ui.global": "Global",
|
||||||
"ui.workspace": "Workspace",
|
"ui.workspace": "Deal",
|
||||||
"ui.editSecret": "Edit secret",
|
"ui.editSecret": "Edit secret",
|
||||||
"ui.newSecret": "New secret",
|
"ui.newSecret": "New secret",
|
||||||
"ui.scope": "Scope",
|
"ui.scope": "Scope",
|
||||||
|
|
@ -46,11 +49,14 @@
|
||||||
"ui.readError": "Could not open this secret. Please try again.",
|
"ui.readError": "Could not open this secret. Please try again.",
|
||||||
"ui.saveError": "Could not save the 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.deleteError": "Could not delete the secret. Please try again.",
|
||||||
|
"ui.copyValueError": "Could not copy the secret value. Please try again.",
|
||||||
"ui.copyLinkError": "Could not copy the secret link. 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.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",
|
||||||
"ui.deleteConfirm": "Delete this secret?",
|
"ui.deleteSecretTitle": "Delete secret",
|
||||||
"ui.linkCopied": "Secret link copied"
|
"ui.deleteConfirm": "Delete \"{title}\"? This cannot be undone.",
|
||||||
|
"ui.linkCopied": "Secret link copied",
|
||||||
|
"ui.valueCopied": "Secret value copied"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"manifest.name": "Секреты",
|
"manifest.name": "Секреты",
|
||||||
"manifest.description": "Зашифрованное хранилище общих секретов и секретов рабочих пространств.",
|
"manifest.description": "Зашифрованное хранилище общих секретов и секретов Дел.",
|
||||||
"contributions.views.verstak.secrets.view.title": "Секреты",
|
"contributions.views.verstak.secrets.view.title": "Секреты",
|
||||||
"contributions.sidebarItems.verstak.secrets.sidebar.title": "Секреты",
|
"contributions.sidebarItems.verstak.secrets.sidebar.title": "Секреты",
|
||||||
"contributions.openProviders.verstak.secrets.secret.title": "Секреты",
|
"contributions.openProviders.verstak.secrets.secret.title": "Секреты",
|
||||||
|
|
@ -28,11 +28,14 @@
|
||||||
"ui.copyLink": "Копировать ссылку на секрет",
|
"ui.copyLink": "Копировать ссылку на секрет",
|
||||||
"ui.edit": "Изменить",
|
"ui.edit": "Изменить",
|
||||||
"ui.delete": "Удалить",
|
"ui.delete": "Удалить",
|
||||||
|
"ui.copyValue": "Копировать значение",
|
||||||
|
"ui.showValue": "Показать значение",
|
||||||
|
"ui.hideValue": "Скрыть значение",
|
||||||
"ui.fieldTitle": "Название",
|
"ui.fieldTitle": "Название",
|
||||||
"ui.optionalUsername": "необязательное имя пользователя",
|
"ui.optionalUsername": "необязательное имя пользователя",
|
||||||
"ui.secretValue": "Значение секрета",
|
"ui.secretValue": "Значение секрета",
|
||||||
"ui.global": "Общий",
|
"ui.global": "Общий",
|
||||||
"ui.workspace": "Рабочее пространство",
|
"ui.workspace": "Дело",
|
||||||
"ui.editSecret": "Изменить секрет",
|
"ui.editSecret": "Изменить секрет",
|
||||||
"ui.newSecret": "Новый секрет",
|
"ui.newSecret": "Новый секрет",
|
||||||
"ui.scope": "Область",
|
"ui.scope": "Область",
|
||||||
|
|
@ -46,11 +49,14 @@
|
||||||
"ui.readError": "Не удалось открыть секрет. Повторите попытку.",
|
"ui.readError": "Не удалось открыть секрет. Повторите попытку.",
|
||||||
"ui.saveError": "Не удалось сохранить секрет. Повторите попытку.",
|
"ui.saveError": "Не удалось сохранить секрет. Повторите попытку.",
|
||||||
"ui.deleteError": "Не удалось удалить секрет. Повторите попытку.",
|
"ui.deleteError": "Не удалось удалить секрет. Повторите попытку.",
|
||||||
|
"ui.copyValueError": "Не удалось скопировать значение секрета. Повторите попытку.",
|
||||||
"ui.copyLinkError": "Не удалось скопировать ссылку на секрет. Повторите попытку.",
|
"ui.copyLinkError": "Не удалось скопировать ссылку на секрет. Повторите попытку.",
|
||||||
"ui.unlockError": "Не удалось разблокировать секреты. Проверьте мастер-пароль и повторите попытку.",
|
"ui.unlockError": "Не удалось разблокировать секреты. Проверьте мастер-пароль и повторите попытку.",
|
||||||
"ui.value": "Значение",
|
"ui.value": "Значение",
|
||||||
"ui.save": "Сохранить",
|
"ui.save": "Сохранить",
|
||||||
"ui.cancel": "Отмена",
|
"ui.cancel": "Отмена",
|
||||||
"ui.deleteConfirm": "Удалить этот секрет?",
|
"ui.deleteSecretTitle": "Удалить секрет",
|
||||||
"ui.linkCopied": "Ссылка на секрет скопирована"
|
"ui.deleteConfirm": "Удалить «{title}»? Это действие нельзя отменить.",
|
||||||
|
"ui.linkCopied": "Ссылка на секрет скопирована",
|
||||||
|
"ui.valueCopied": "Значение секрета скопировано"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"name": "Secrets",
|
"name": "Secrets",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Encrypted global and workspace-scoped secret manager.",
|
"description": "Encrypted global and Deal-scoped secret manager.",
|
||||||
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"icon": "key-round",
|
"icon": "key-round",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,16 @@ class FakeNode {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeChild(node) {
|
||||||
|
this.children = this.children.filter((child) => child !== node);
|
||||||
|
node.parentNode = null;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
if (this.parentNode) this.parentNode.removeChild(this);
|
||||||
|
}
|
||||||
|
|
||||||
setAttribute(name, value) {
|
setAttribute(name, value) {
|
||||||
this.attributes[name] = String(value);
|
this.attributes[name] = String(value);
|
||||||
}
|
}
|
||||||
|
|
@ -235,7 +245,17 @@ async function flush() {
|
||||||
if (!container.textContent.includes('Group')) throw new Error('secret field table missing Group row');
|
if (!container.textContent.includes('Group')) throw new Error('secret field table missing Group row');
|
||||||
if (!container.textContent.includes('Username')) throw new Error('secret field table missing Username row');
|
if (!container.textContent.includes('Username')) throw new Error('secret field table missing Username row');
|
||||||
if (!container.textContent.includes('Password')) throw new Error('secret field table missing Password row');
|
if (!container.textContent.includes('Password')) throw new Error('secret field table missing Password row');
|
||||||
if (!container.textContent.includes('secret-value')) throw new Error('secret value was not shown in the field table');
|
if (container.textContent.includes('secret-value')) throw new Error('secret value must stay hidden until the user explicitly reveals it');
|
||||||
|
const copyValueButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-copy-value') === 'client-a.db');
|
||||||
|
if (!copyValueButton) throw new Error('copy value button missing');
|
||||||
|
copyValueButton.click();
|
||||||
|
await flush();
|
||||||
|
if (!copied.includes('secret-value')) throw new Error('secret value was not copied');
|
||||||
|
const showValueButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-toggle-value') === 'client-a.db');
|
||||||
|
if (!showValueButton) throw new Error('show value button missing');
|
||||||
|
showValueButton.click();
|
||||||
|
await flush();
|
||||||
|
if (!container.textContent.includes('secret-value')) throw new Error('secret value was not revealed after explicit request');
|
||||||
|
|
||||||
const copyButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-copy-link') === 'client-a.db');
|
const copyButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-copy-link') === 'client-a.db');
|
||||||
if (!copyButton) throw new Error('copy link button missing');
|
if (!copyButton) throw new Error('copy link button missing');
|
||||||
|
|
@ -261,6 +281,20 @@ async function flush() {
|
||||||
if (!deleteButton) throw new Error('delete button missing');
|
if (!deleteButton) throw new Error('delete button missing');
|
||||||
deleteButton.click();
|
deleteButton.click();
|
||||||
await flush();
|
await flush();
|
||||||
|
if (deleted.includes('client-a.db')) throw new Error('secret delete must wait for confirmation');
|
||||||
|
const deleteConfirmModal = walk(document.body, (node) => node.getAttribute && node.getAttribute('data-secret-delete-modal') === '');
|
||||||
|
if (!deleteConfirmModal) throw new Error('secret delete confirmation modal missing');
|
||||||
|
const deleteCancelButton = walk(deleteConfirmModal, (node) => node.getAttribute && node.getAttribute('data-secret-delete-cancel') === '');
|
||||||
|
if (!deleteCancelButton) throw new Error('secret delete cancel button missing');
|
||||||
|
deleteCancelButton.click();
|
||||||
|
await flush();
|
||||||
|
if (deleted.includes('client-a.db')) throw new Error('secret delete ran after cancelling the confirmation');
|
||||||
|
deleteButton.click();
|
||||||
|
await flush();
|
||||||
|
const deleteConfirmButton = walk(document.body, (node) => node.getAttribute && node.getAttribute('data-secret-delete-confirm') === '');
|
||||||
|
if (!deleteConfirmButton) throw new Error('secret delete confirm button missing');
|
||||||
|
deleteConfirmButton.click();
|
||||||
|
await flush();
|
||||||
if (!deleted.includes('client-a.db')) throw new Error('secret delete was not called');
|
if (!deleted.includes('client-a.db')) throw new Error('secret delete was not called');
|
||||||
|
|
||||||
records.push({ id: 'client-a.global-view', title: 'Client A Global View', username: 'app', scope: { kind: 'workspace', workspaceRootPath: 'ClientA' }, updatedAt: '2026-06-29T00:00:00Z' });
|
records.push({ id: 'client-a.global-view', title: 'Client A Global View', username: 'app', scope: { kind: 'workspace', workspaceRootPath: 'ClientA' }, updatedAt: '2026-06-29T00:00:00Z' });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue