Encrypted upstream credentials for bearer tokens, basic auth, and API keys used by auth profiles.
+
+
+
+
+
+
+
+
+
+ Secret plaintext is write-only.
+ Crank encrypts secret values with the workspace master key. After create or rotate, the API returns only metadata, so this page focuses on lifecycle and usage references.
+
+
+
+
+
+
+
Workspace secrets
+
+
+
+
+
+
+
+
+
+
+
+
Name
+
Kind
+
Version
+
Last used
+
Used by
+
Status
+
+
+
+
+
+
+
+
+
+
+
+
Auth profile references
+
Profiles resolve secrets at runtime before upstream execution.
+
+
+
+
+
+
+
+
+
+
+
+ Create secret
+
+
+
+
+
+
+
Use a stable operator-facing label. Plaintext values are never returned after storage.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Useful for generic secret payloads. The value must be valid JSON.
+
+
+
+
Creation stores encrypted plaintext and returns metadata only.
+
+
+
+
+
+
+
+
+
diff --git a/apps/ui/html/settings.html b/apps/ui/html/settings.html
index b327b30..3100b6a 100644
--- a/apps/ui/html/settings.html
+++ b/apps/ui/html/settings.html
@@ -49,6 +49,7 @@
OperationsAgentsAPI Keys
+ SecretsLogsUsage
@@ -87,6 +88,7 @@
OperationsAgentsAPI Keys
+ SecretsLogsUsage
diff --git a/apps/ui/html/stream-sessions.html b/apps/ui/html/stream-sessions.html
index 223fb44..3890dbb 100644
--- a/apps/ui/html/stream-sessions.html
+++ b/apps/ui/html/stream-sessions.html
@@ -44,6 +44,7 @@
OperationsAgentsAPI Keys
+ SecretsLogsUsage
@@ -68,6 +69,7 @@
OperationsAgentsAPI Keys
+ SecretsLogsUsage
diff --git a/apps/ui/html/usage.html b/apps/ui/html/usage.html
index 9db8a9c..888f3ea 100644
--- a/apps/ui/html/usage.html
+++ b/apps/ui/html/usage.html
@@ -49,6 +49,7 @@
OperationsAgentsAPI Keys
+ SecretsLogsUsage
@@ -87,6 +88,7 @@
OperationsAgentsAPI Keys
+ SecretsLogsUsage
diff --git a/apps/ui/html/wizard/index.html b/apps/ui/html/wizard/index.html
index 38e864c..416a8d2 100644
--- a/apps/ui/html/wizard/index.html
+++ b/apps/ui/html/wizard/index.html
@@ -50,6 +50,7 @@
OperationsAgentsAPI Keys
+ SecretsLogsUsage
@@ -93,6 +94,7 @@
OperationsAgentsAPI Keys
+ SecretsLogsUsage
diff --git a/apps/ui/index.html b/apps/ui/index.html
index fd7b99f..fcb97c8 100644
--- a/apps/ui/index.html
+++ b/apps/ui/index.html
@@ -49,6 +49,7 @@
OperationsAgentsAPI Keys
+ SecretsLogsUsage
@@ -98,6 +99,7 @@
OperationsAgentsAPI Keys
+ SecretsLogsUsage
diff --git a/apps/ui/js/api.js b/apps/ui/js/api.js
index c5907f9..760439c 100644
--- a/apps/ui/js/api.js
+++ b/apps/ui/js/api.js
@@ -323,6 +323,24 @@
deletePlatformApiKey: function(workspaceId, keyId) {
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/platform-api-keys/' + encodeURIComponent(keyId));
},
+ listSecrets: function(workspaceId) {
+ return get('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets');
+ },
+ createSecret: function(workspaceId, payload) {
+ return post('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets', payload);
+ },
+ getSecret: function(workspaceId, secretId) {
+ return get('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets/' + encodeURIComponent(secretId));
+ },
+ rotateSecret: function(workspaceId, secretId, payload) {
+ return post('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets/' + encodeURIComponent(secretId) + '/rotate', payload);
+ },
+ deleteSecret: function(workspaceId, secretId) {
+ return del('/workspaces/' + encodeURIComponent(workspaceId) + '/secrets/' + encodeURIComponent(secretId));
+ },
+ listAuthProfiles: function(workspaceId) {
+ return get('/workspaces/' + encodeURIComponent(workspaceId) + '/auth-profiles');
+ },
listLogs: function(workspaceId, params) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/logs' + query(params));
},
diff --git a/apps/ui/js/config.js b/apps/ui/js/config.js
index 1467fff..98b80bc 100644
--- a/apps/ui/js/config.js
+++ b/apps/ui/js/config.js
@@ -6,6 +6,7 @@
login: '/login',
agents: '/agents',
apiKeys: '/api-keys',
+ secrets: '/secrets',
logs: '/logs',
usage: '/usage',
streamSessions: '/stream-sessions',
diff --git a/apps/ui/js/i18n.js b/apps/ui/js/i18n.js
index 613d177..79b1452 100644
--- a/apps/ui/js/i18n.js
+++ b/apps/ui/js/i18n.js
@@ -9,6 +9,7 @@ var TRANSLATIONS = {
'nav.operations': 'Operations',
'nav.agents': 'Agents',
'nav.apikeys': 'API Keys',
+ 'nav.secrets': 'Secrets',
'nav.logs': 'Logs',
'nav.usage': 'Usage',
'nav.settings': 'Settings',
@@ -157,6 +158,97 @@ var TRANSLATIONS = {
'apikeys.action.delete': 'Delete',
'apikeys.creating': 'Creating…',
+ // Secrets page
+ 'secrets.title': 'Secrets',
+ 'secrets.subtitle': 'Encrypted upstream credentials for bearer tokens, basic auth, and API keys used by auth profiles.',
+ 'secrets.new': 'Create secret',
+ 'secrets.callout.title': 'Secret plaintext is write-only.',
+ 'secrets.callout.body': 'Crank encrypts secret values with the workspace master key. After create or rotate, the API returns only metadata, so this page focuses on lifecycle and usage references.',
+ 'secrets.active.title': 'Workspace secrets',
+ 'secrets.active.subtitle': '{active} active · {total} total',
+ 'secrets.search': 'Filter secrets…',
+ 'secrets.th.name': 'NAME',
+ 'secrets.th.kind': 'KIND',
+ 'secrets.th.version': 'VERSION',
+ 'secrets.th.last_used': 'LAST USED',
+ 'secrets.th.used_by': 'USED BY',
+ 'secrets.th.status': 'STATUS',
+ 'secrets.kind.token': 'Token',
+ 'secrets.kind.username_password': 'Username/password',
+ 'secrets.kind.header': 'Header value',
+ 'secrets.kind.generic': 'Generic JSON',
+ 'secrets.status.active': 'Active',
+ 'secrets.status.disabled': 'Disabled',
+ 'secrets.last_used.never': 'Never',
+ 'secrets.used_by_count': '{count} profiles',
+ 'secrets.loading': 'Loading…',
+ 'secrets.empty.none': 'No secrets yet',
+ 'secrets.empty.search': 'No secrets match your search',
+ 'secrets.error.workspace': 'Workspace is not selected',
+ 'secrets.error.load': 'Failed to load secrets',
+ 'secrets.action.rotate': 'Rotate',
+ 'secrets.action.delete': 'Delete',
+ 'secrets.confirm.delete': 'Delete secret {name}? This cannot be undone while the secret is still referenced by an auth profile.',
+ 'secrets.toast.create_title': 'Secret created',
+ 'secrets.toast.create_message': 'The secret was stored encrypted. Plaintext cannot be viewed again through the API.',
+ 'secrets.toast.create_error_title': 'Secret creation failed',
+ 'secrets.toast.create_error_message': 'Failed to create secret',
+ 'secrets.toast.rotate_title': 'Secret rotated',
+ 'secrets.toast.rotate_message': 'A new secret version is now active.',
+ 'secrets.toast.rotate_error_title': 'Secret rotation failed',
+ 'secrets.toast.rotate_error_message': 'Failed to rotate secret',
+ 'secrets.toast.delete_title': 'Secret deleted',
+ 'secrets.toast.delete_message': '{name} was deleted.',
+ 'secrets.toast.delete_error_title': 'Secret deletion failed',
+ 'secrets.toast.delete_error_message': 'Failed to delete secret',
+ 'secrets.modal.create_title': 'Create secret',
+ 'secrets.modal.rotate_title': 'Rotate secret',
+ 'secrets.modal.name': 'Secret name',
+ 'secrets.modal.name_hint': 'Use a stable operator-facing label. Plaintext values are never returned after storage.',
+ 'secrets.modal.kind': 'Secret kind',
+ 'secrets.modal.value': 'Secret value',
+ 'secrets.modal.header_value': 'Header value',
+ 'secrets.modal.username': 'Username',
+ 'secrets.modal.password': 'Password',
+ 'secrets.modal.json_payload': 'JSON payload',
+ 'secrets.modal.json_hint': 'Useful for generic secret payloads. The value must be valid JSON.',
+ 'secrets.modal.create_note': 'Creation stores encrypted plaintext and returns metadata only.',
+ 'secrets.modal.rotate_note': 'Rotation creates a new encrypted version and keeps the same secret id.',
+ 'secrets.modal.cancel': 'Cancel',
+ 'secrets.modal.create_action': 'Create secret',
+ 'secrets.modal.rotate_action': 'Rotate secret',
+ 'secrets.modal.creating': 'Creating…',
+ 'secrets.modal.rotating': 'Rotating…',
+ 'secrets.hint.token': 'Best for bearer tokens and simple API key values.',
+ 'secrets.hint.username_password': 'Stores username and password as one encrypted payload.',
+ 'secrets.hint.header': 'Stores a raw header value. Header name is configured in the auth profile.',
+ 'secrets.hint.generic': 'Stores arbitrary JSON for advanced future integrations.',
+ 'secrets.validation.name_required': 'Secret name is required.',
+ 'secrets.validation.value_required': 'Secret value is required.',
+ 'secrets.validation.username_password_required': 'Username and password are both required.',
+ 'secrets.validation.json_invalid': 'Generic secret payload must be valid JSON.',
+ 'secrets.profiles.title': 'Auth profile references',
+ 'secrets.profiles.subtitle': 'Profiles resolve secrets at runtime before upstream execution.',
+ 'secrets.profiles.subtitle_count': '{count} auth profiles',
+ 'secrets.profiles.error_title': 'Unable to load auth profile references',
+ 'secrets.profiles.loading_title': 'Loading auth profile references…',
+ 'secrets.profiles.loading_body': 'Fetching auth profiles for the current workspace.',
+ 'secrets.profiles.empty_title': 'No auth profiles yet',
+ 'secrets.profiles.empty_body': 'Create auth profiles in the next slice to bind secrets to upstream authentication.',
+ 'secrets.profiles.references': 'Referenced secrets',
+ 'secrets.profiles.reference_count': '{count} refs',
+ 'secrets.profiles.meta.created': 'Created',
+ 'secrets.profiles.meta.updated': 'Updated',
+ 'secrets.profiles.summary_bearer': 'Bearer auth via {header} using {secret}.',
+ 'secrets.profiles.summary_basic': 'Basic auth using {username} and {password}.',
+ 'secrets.profiles.summary_api_key_header': 'API key header {header} using {secret}.',
+ 'secrets.profiles.summary_api_key_query': 'API key query param {param} using {secret}.',
+ 'secrets.profiles.summary_unknown': 'Unknown auth profile configuration.',
+ 'secrets.auth_kind.bearer': 'Bearer auth',
+ 'secrets.auth_kind.basic': 'Basic auth',
+ 'secrets.auth_kind.api_key_header': 'API key header',
+ 'secrets.auth_kind.api_key_query': 'API key query',
+
// Logs page
'logs.title': 'Logs',
'logs.subtitle': 'Real-time invocation log for all operations in this workspace.',
@@ -797,6 +889,7 @@ var TRANSLATIONS = {
'nav.operations': 'Операции',
'nav.agents': 'Агенты',
'nav.apikeys': 'API Ключи',
+ 'nav.secrets': 'Секреты',
'nav.logs': 'Логи',
'nav.usage': 'Использование',
'nav.settings': 'Настройки',
@@ -945,6 +1038,97 @@ var TRANSLATIONS = {
'apikeys.action.delete': 'Удалить',
'apikeys.creating': 'Создание…',
+ // Secrets page
+ 'secrets.title': 'Секреты',
+ 'secrets.subtitle': 'Зашифрованные upstream-учетные данные для bearer token, basic auth и API key, которые используют auth profiles.',
+ 'secrets.new': 'Создать секрет',
+ 'secrets.callout.title': 'Plaintext секрета доступен только на запись.',
+ 'secrets.callout.body': 'Crank шифрует secret values мастер-ключом воркспейса. После создания или ротации API возвращает только metadata, поэтому эта страница сфокусирована на lifecycle и usage references.',
+ 'secrets.active.title': 'Секреты воркспейса',
+ 'secrets.active.subtitle': '{active} активных · {total} всего',
+ 'secrets.search': 'Фильтр секретов…',
+ 'secrets.th.name': 'НАЗВАНИЕ',
+ 'secrets.th.kind': 'ТИП',
+ 'secrets.th.version': 'ВЕРСИЯ',
+ 'secrets.th.last_used': 'ПОСЛЕДНЕЕ ИСПОЛЬЗОВАНИЕ',
+ 'secrets.th.used_by': 'ИСПОЛЬЗУЕТСЯ В',
+ 'secrets.th.status': 'СТАТУС',
+ 'secrets.kind.token': 'Токен',
+ 'secrets.kind.username_password': 'Логин/пароль',
+ 'secrets.kind.header': 'Значение заголовка',
+ 'secrets.kind.generic': 'Общий JSON',
+ 'secrets.status.active': 'Активен',
+ 'secrets.status.disabled': 'Отключен',
+ 'secrets.last_used.never': 'Никогда',
+ 'secrets.used_by_count': '{count} профилей',
+ 'secrets.loading': 'Загрузка…',
+ 'secrets.empty.none': 'Секретов пока нет',
+ 'secrets.empty.search': 'Нет секретов по текущему поиску',
+ 'secrets.error.workspace': 'Воркспейс не выбран',
+ 'secrets.error.load': 'Не удалось загрузить секреты',
+ 'secrets.action.rotate': 'Ротировать',
+ 'secrets.action.delete': 'Удалить',
+ 'secrets.confirm.delete': 'Удалить секрет {name}? Это нельзя сделать, пока на секрет ссылается auth profile.',
+ 'secrets.toast.create_title': 'Секрет создан',
+ 'secrets.toast.create_message': 'Секрет сохранен в зашифрованном виде. Повторно посмотреть plaintext через API нельзя.',
+ 'secrets.toast.create_error_title': 'Не удалось создать секрет',
+ 'secrets.toast.create_error_message': 'Не удалось создать секрет',
+ 'secrets.toast.rotate_title': 'Секрет ротирован',
+ 'secrets.toast.rotate_message': 'Новая версия секрета теперь активна.',
+ 'secrets.toast.rotate_error_title': 'Не удалось ротировать секрет',
+ 'secrets.toast.rotate_error_message': 'Не удалось ротировать секрет',
+ 'secrets.toast.delete_title': 'Секрет удален',
+ 'secrets.toast.delete_message': 'Секрет {name} удален.',
+ 'secrets.toast.delete_error_title': 'Не удалось удалить секрет',
+ 'secrets.toast.delete_error_message': 'Не удалось удалить секрет',
+ 'secrets.modal.create_title': 'Создать секрет',
+ 'secrets.modal.rotate_title': 'Ротировать секрет',
+ 'secrets.modal.name': 'Имя секрета',
+ 'secrets.modal.name_hint': 'Используйте стабильную операторскую метку. Plaintext после сохранения не возвращается.',
+ 'secrets.modal.kind': 'Тип секрета',
+ 'secrets.modal.value': 'Значение секрета',
+ 'secrets.modal.header_value': 'Значение заголовка',
+ 'secrets.modal.username': 'Логин',
+ 'secrets.modal.password': 'Пароль',
+ 'secrets.modal.json_payload': 'JSON payload',
+ 'secrets.modal.json_hint': 'Подходит для общих secret payloads. Значение должно быть валидным JSON.',
+ 'secrets.modal.create_note': 'Создание сохраняет зашифрованный plaintext и возвращает только metadata.',
+ 'secrets.modal.rotate_note': 'Ротация создает новую зашифрованную версию и сохраняет тот же secret id.',
+ 'secrets.modal.cancel': 'Отмена',
+ 'secrets.modal.create_action': 'Создать секрет',
+ 'secrets.modal.rotate_action': 'Ротировать секрет',
+ 'secrets.modal.creating': 'Создание…',
+ 'secrets.modal.rotating': 'Ротация…',
+ 'secrets.hint.token': 'Лучший вариант для bearer tokens и простых API key values.',
+ 'secrets.hint.username_password': 'Хранит логин и пароль как один зашифрованный payload.',
+ 'secrets.hint.header': 'Хранит сырое значение заголовка. Имя заголовка задается в auth profile.',
+ 'secrets.hint.generic': 'Хранит произвольный JSON для advanced future integrations.',
+ 'secrets.validation.name_required': 'Нужно указать имя секрета.',
+ 'secrets.validation.value_required': 'Нужно указать значение секрета.',
+ 'secrets.validation.username_password_required': 'Нужно указать и логин, и пароль.',
+ 'secrets.validation.json_invalid': 'Generic secret payload должен быть валидным JSON.',
+ 'secrets.profiles.title': 'Ссылки auth profiles',
+ 'secrets.profiles.subtitle': 'Профили резолвят секреты в runtime перед upstream execution.',
+ 'secrets.profiles.subtitle_count': '{count} auth profiles',
+ 'secrets.profiles.error_title': 'Не удалось загрузить ссылки auth profiles',
+ 'secrets.profiles.loading_title': 'Загрузка ссылок auth profiles…',
+ 'secrets.profiles.loading_body': 'Получаем auth profiles для текущего воркспейса.',
+ 'secrets.profiles.empty_title': 'Auth profiles пока нет',
+ 'secrets.profiles.empty_body': 'В следующем срезе появится UI для привязки секретов к upstream authentication через auth selector.',
+ 'secrets.profiles.references': 'Связанные секреты',
+ 'secrets.profiles.reference_count': '{count} ссылок',
+ 'secrets.profiles.meta.created': 'Создан',
+ 'secrets.profiles.meta.updated': 'Обновлен',
+ 'secrets.profiles.summary_bearer': 'Bearer auth через {header} с использованием {secret}.',
+ 'secrets.profiles.summary_basic': 'Basic auth с использованием {username} и {password}.',
+ 'secrets.profiles.summary_api_key_header': 'API key header {header} с использованием {secret}.',
+ 'secrets.profiles.summary_api_key_query': 'API key query param {param} с использованием {secret}.',
+ 'secrets.profiles.summary_unknown': 'Неизвестная конфигурация auth profile.',
+ 'secrets.auth_kind.bearer': 'Bearer auth',
+ 'secrets.auth_kind.basic': 'Basic auth',
+ 'secrets.auth_kind.api_key_header': 'API key header',
+ 'secrets.auth_kind.api_key_query': 'API key query',
+
// Logs page
'logs.title': 'Логи',
'logs.subtitle': 'Живой журнал вызовов по всем операциям этого воркспейса.',
diff --git a/apps/ui/js/secrets.js b/apps/ui/js/secrets.js
new file mode 100644
index 0000000..8b6de32
--- /dev/null
+++ b/apps/ui/js/secrets.js
@@ -0,0 +1,481 @@
+document.addEventListener('DOMContentLoaded', function () {
+ var state = {
+ workspaceId: null,
+ secrets: [],
+ profiles: [],
+ search: '',
+ loading: false,
+ error: '',
+ modalMode: 'create',
+ modalSecretId: null,
+ };
+
+ var modal = document.getElementById('secret-modal');
+ var tbody = document.getElementById('secrets-tbody');
+ var profilesList = document.getElementById('auth-profiles-list');
+ var summary = document.getElementById('secrets-summary');
+ var profilesSummary = document.getElementById('secret-profiles-summary');
+ var searchInput = document.getElementById('secret-search');
+ var modalTitle = document.getElementById('secret-modal-title');
+ var modalName = document.getElementById('secret-name');
+ var modalKind = document.getElementById('secret-kind');
+ var modalNote = document.getElementById('secret-modal-note');
+ var modalSubmit = document.getElementById('secret-modal-submit-btn');
+ var kindHint = document.getElementById('secret-kind-hint');
+ var stringLabel = document.getElementById('secret-string-label');
+ var stringField = document.getElementById('secret-string-value');
+ var usernameField = document.getElementById('secret-username');
+ var passwordField = document.getElementById('secret-password');
+ var jsonField = document.getElementById('secret-json-value');
+
+ function tKey(key) {
+ return typeof t === 'function' ? t(key) : key;
+ }
+
+ function tfKey(key, vars) {
+ return typeof tf === 'function' ? tf(key, vars) : tKey(key);
+ }
+
+ function currentWorkspaceId() {
+ var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
+ return workspace ? workspace.id : null;
+ }
+
+ function currentWorkspace() {
+ return window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
+ }
+
+ function secretKindLabel(kind) {
+ return tKey('secrets.kind.' + String(kind || '').toLowerCase());
+ }
+
+ function authKindLabel(kind) {
+ return tKey('secrets.auth_kind.' + String(kind || '').toLowerCase());
+ }
+
+ function formatDate(value) {
+ if (!value) return '—';
+ return new Date(value).toLocaleDateString();
+ }
+
+ function secretIdsForProfile(profile) {
+ var config = profile && profile.config ? profile.config : {};
+ if (config.bearer) return [config.bearer.secret_id];
+ if (config.basic) return [config.basic.username_secret_id, config.basic.password_secret_id];
+ if (config.api_key_header) return [config.api_key_header.secret_id];
+ if (config.api_key_query) return [config.api_key_query.secret_id];
+ return [];
+ }
+
+ function profileSummary(profile, secretsById) {
+ var config = profile && profile.config ? profile.config : {};
+ if (config.bearer) {
+ return tfKey('secrets.profiles.summary_bearer', {
+ header: config.bearer.header_name,
+ secret: secretName(secretsById, config.bearer.secret_id),
+ });
+ }
+ if (config.basic) {
+ return tfKey('secrets.profiles.summary_basic', {
+ username: secretName(secretsById, config.basic.username_secret_id),
+ password: secretName(secretsById, config.basic.password_secret_id),
+ });
+ }
+ if (config.api_key_header) {
+ return tfKey('secrets.profiles.summary_api_key_header', {
+ header: config.api_key_header.header_name,
+ secret: secretName(secretsById, config.api_key_header.secret_id),
+ });
+ }
+ if (config.api_key_query) {
+ return tfKey('secrets.profiles.summary_api_key_query', {
+ param: config.api_key_query.param_name,
+ secret: secretName(secretsById, config.api_key_query.secret_id),
+ });
+ }
+ return tKey('secrets.profiles.summary_unknown');
+ }
+
+ function secretName(secretsById, secretId) {
+ var secret = secretsById[secretId];
+ return secret ? secret.name : secretId;
+ }
+
+ function usageBySecret() {
+ var usage = {};
+ state.profiles.forEach(function (profile) {
+ secretIdsForProfile(profile).forEach(function (secretId) {
+ if (!secretId) return;
+ if (!usage[secretId]) usage[secretId] = [];
+ usage[secretId].push(profile);
+ });
+ });
+ return usage;
+ }
+
+ function resetModalFields() {
+ modalName.value = '';
+ modalKind.value = 'token';
+ stringField.value = '';
+ usernameField.value = '';
+ passwordField.value = '';
+ jsonField.value = '{\n "value": ""\n}';
+ }
+
+ function openModal(mode, secret) {
+ state.modalMode = mode;
+ state.modalSecretId = secret ? secret.id : null;
+ resetModalFields();
+
+ if (mode === 'rotate' && secret) {
+ modalTitle.textContent = tKey('secrets.modal.rotate_title');
+ modalName.value = secret.name;
+ modalName.disabled = true;
+ modalKind.value = secret.kind;
+ modalKind.disabled = true;
+ modalSubmit.textContent = tKey('secrets.modal.rotate_action');
+ modalNote.textContent = tKey('secrets.modal.rotate_note');
+ } else {
+ modalTitle.textContent = tKey('secrets.modal.create_title');
+ modalName.disabled = false;
+ modalKind.disabled = false;
+ modalSubmit.textContent = tKey('secrets.modal.create_action');
+ modalNote.textContent = tKey('secrets.modal.create_note');
+ }
+
+ updateKindFields();
+ modal.classList.add('open');
+ setTimeout(function () {
+ if (mode === 'rotate') {
+ stringField.focus();
+ } else {
+ modalName.focus();
+ }
+ }, 30);
+ }
+
+ function closeModal() {
+ modal.classList.remove('open');
+ state.modalMode = 'create';
+ state.modalSecretId = null;
+ }
+
+ function updateKindFields() {
+ var kind = modalKind.value;
+ document.querySelectorAll('[data-kind-field="string"]').forEach(function (element) {
+ element.style.display = kind === 'token' || kind === 'header' ? '' : 'none';
+ });
+ document.querySelectorAll('[data-kind-field="basic"]').forEach(function (element) {
+ element.style.display = kind === 'username_password' ? '' : 'none';
+ });
+ document.querySelectorAll('[data-kind-field="json"]').forEach(function (element) {
+ element.style.display = kind === 'generic' ? '' : 'none';
+ });
+
+ if (kind === 'token') {
+ stringLabel.textContent = tKey('secrets.modal.value');
+ } else if (kind === 'header') {
+ stringLabel.textContent = tKey('secrets.modal.header_value');
+ } else {
+ stringLabel.textContent = tKey('secrets.modal.value');
+ }
+ kindHint.textContent = tKey('secrets.hint.' + kind);
+ }
+
+ function buildSecretValue() {
+ var kind = modalKind.value;
+ if (kind === 'token' || kind === 'header') {
+ var value = stringField.value.trim();
+ if (!value) {
+ throw new Error(tKey('secrets.validation.value_required'));
+ }
+ return value;
+ }
+ if (kind === 'username_password') {
+ var username = usernameField.value.trim();
+ var password = passwordField.value;
+ if (!username || !password) {
+ throw new Error(tKey('secrets.validation.username_password_required'));
+ }
+ return {
+ username: username,
+ password: password,
+ };
+ }
+ try {
+ return JSON.parse(jsonField.value);
+ } catch (_error) {
+ throw new Error(tKey('secrets.validation.json_invalid'));
+ }
+ }
+
+ function renderSecrets() {
+ var usage = usageBySecret();
+ var query = state.search.toLowerCase();
+ var rows = state.secrets.filter(function (secret) {
+ return !query || secret.name.toLowerCase().includes(query) || String(secret.kind || '').toLowerCase().includes(query);
+ });
+
+ var active = state.secrets.filter(function (secret) { return secret.status === 'active'; }).length;
+ summary.textContent = tfKey('secrets.active.subtitle', {
+ active: active,
+ total: state.secrets.length,
+ });
+
+ tbody.innerHTML = '';
+
+ if (state.loading && state.secrets.length === 0) {
+ var loadingRow = document.createElement('tr');
+ var loadingCell = document.createElement('td');
+ loadingCell.colSpan = 7;
+ loadingCell.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);';
+ loadingCell.textContent = tKey('secrets.loading');
+ loadingRow.appendChild(loadingCell);
+ tbody.appendChild(loadingRow);
+ return;
+ }
+
+ if (state.error) {
+ var errorRow = document.createElement('tr');
+ var errorCell = document.createElement('td');
+ errorCell.colSpan = 7;
+ errorCell.style.cssText = 'text-align:center;padding:36px;color:var(--danger,#f85149);';
+ errorCell.textContent = state.error;
+ errorRow.appendChild(errorCell);
+ tbody.appendChild(errorRow);
+ return;
+ }
+
+ if (!rows.length) {
+ var emptyRow = document.createElement('tr');
+ var emptyCell = document.createElement('td');
+ emptyCell.colSpan = 7;
+ emptyCell.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);';
+ emptyCell.textContent = state.secrets.length ? tKey('secrets.empty.search') : tKey('secrets.empty.none');
+ emptyRow.appendChild(emptyCell);
+ tbody.appendChild(emptyRow);
+ return;
+ }
+
+ rows.forEach(function (secret) {
+ var tr = document.createElement('tr');
+ var references = usage[secret.id] || [];
+ tr.innerHTML = [
+ '