These headers are sent on every request to this upstream. Use secrets for credentials.
+
Optional non-secret headers sent on every request to this upstream. Use auth profiles for credentials.
diff --git a/apps/ui/html/wizard/step5.html b/apps/ui/html/wizard/step5.html
index 96168f9..c7fba9e 100644
--- a/apps/ui/html/wizard/step5.html
+++ b/apps/ui/html/wizard/step5.html
@@ -5,7 +5,7 @@
Step 5 of 5
Mapping and execution
-
Define how MCP tool arguments map to upstream request fields, and how the upstream response maps back to MCP output. Then set execution parameters — auth is configured per-upstream in step 3.
+
Define how MCP tool arguments map to upstream request fields, and how the upstream response maps back to MCP output. Then set execution parameters — auth is configured per-upstream in step 2.
diff --git a/apps/ui/js/i18n.js b/apps/ui/js/i18n.js
index 79b1452..08795de 100644
--- a/apps/ui/js/i18n.js
+++ b/apps/ui/js/i18n.js
@@ -558,14 +558,44 @@ var TRANSLATIONS = {
'wizard.step2.change': 'Change',
'wizard.step2.auth_none': 'No auth',
'wizard.step2.auth_bearer': 'Bearer token',
+ 'wizard.step2.auth_basic': 'Basic auth',
'wizard.step2.auth_apikey': 'API key',
'wizard.step2.register_new': 'Register new upstream',
'wizard.step2.name': 'Name',
'wizard.step2.base_url': 'Base URL',
'wizard.step2.unique_hint': 'Unique identifier for this upstream.',
- 'wizard.step2.base_url_hint': 'Root URL — no trailing slash. Supports ${secrets.*}.',
- 'wizard.step2.auth_headers': 'Auth headers',
- 'wizard.step2.auth_headers_hint': 'These headers are sent on every request to this upstream. Use secrets for credentials.',
+ 'wizard.step2.base_url_hint': 'Root URL without a trailing slash. Auth is configured separately below.',
+ 'wizard.step2.auth_selector': 'Upstream auth',
+ 'wizard.step2.auth_selector_hint': 'Use secrets-backed auth profiles instead of embedding credential headers in the upstream definition.',
+ 'wizard.step2.auth_mode.none': 'No auth',
+ 'wizard.step2.auth_mode.existing': 'Use existing auth profile',
+ 'wizard.step2.auth_mode.create': 'Create auth profile now',
+ 'wizard.step2.auth_profile': 'Auth profile',
+ 'wizard.step2.auth_profile_hint': 'Selected profile will be resolved at runtime and attached through `execution_config.auth_profile_ref`.',
+ 'wizard.step2.auth_profile_empty': 'No auth profiles yet. Create one below or on the Secrets page.',
+ 'wizard.step2.create_profile_title': 'Create auth profile',
+ 'wizard.step2.create_profile_subtitle': 'Create a reusable profile backed by encrypted workspace secrets.',
+ 'wizard.step2.profile_name': 'Profile name',
+ 'wizard.step2.profile_kind': 'Auth kind',
+ 'wizard.step2.auth_kind.bearer': 'Bearer token',
+ 'wizard.step2.auth_kind.basic': 'Basic auth',
+ 'wizard.step2.auth_kind.api_key_header': 'API key header',
+ 'wizard.step2.auth_kind.api_key_query': 'API key query',
+ 'wizard.step2.header_name': 'Header name',
+ 'wizard.step2.query_param': 'Query param',
+ 'wizard.step2.username_secret': 'Username secret',
+ 'wizard.step2.password_secret': 'Password secret',
+ 'wizard.step2.secret_value': 'Secret',
+ 'wizard.step2.quick_secret': 'Quick create secret',
+ 'wizard.step2.manage_secrets': 'Open secrets page',
+ 'wizard.step2.static_headers': 'Static headers (optional)',
+ 'wizard.step2.static_headers_hint': 'Optional non-secret headers sent on every request to this upstream. Use auth profiles for credentials.',
+ 'wizard.step2.quick_secret_title': 'Quick create secret',
+ 'wizard.step2.quick_secret_name': 'Secret name',
+ 'wizard.step2.quick_secret_kind': 'Secret kind',
+ 'wizard.step2.quick_secret_value': 'Secret value',
+ 'wizard.step2.quick_secret_hint': 'The plaintext is encrypted immediately and will not be returned again.',
+ 'wizard.step2.quick_secret_submit': 'Create secret',
'wizard.step2.save_upstream': 'Save upstream',
'wizard.step2.endpoint_title': 'Endpoint',
'wizard.step2.endpoint_subtitle': 'Path and HTTP method for this specific operation',
@@ -657,7 +687,7 @@ var TRANSLATIONS = {
'wizard.step4.protocol_agnostic_title': 'Schemas are protocol-agnostic',
'wizard.step4.protocol_agnostic_body': 'These schemas describe the MCP contract, not the upstream wire format. Field mapping in Step 5 translates between the two. You can upload a sample JSON response to auto-generate the output schema.',
'wizard.step5.title': 'Mapping and execution',
- 'wizard.step5.subtitle': 'Define how MCP tool arguments map to upstream request fields, and how the upstream response maps back to MCP output. Then set execution parameters — auth is configured per-upstream in step 3.',
+ 'wizard.step5.subtitle': 'Define how MCP tool arguments map to upstream request fields, and how the upstream response maps back to MCP output. Then set execution parameters — auth is configured per-upstream in step 2.',
'wizard.step5.input_request': 'Input → Request',
'wizard.step5.output_response': 'Response → Output',
'wizard.step5.execution': 'Execution',
@@ -706,6 +736,21 @@ var TRANSLATIONS = {
'wizard.error.tool_name': 'Tool name is required',
'wizard.error.parser_unavailable': 'YAML parser is not available',
'wizard.error.select_upstream': 'Select or register an upstream first',
+ 'wizard.error.auth_profile_required': 'Select an auth profile or switch upstream auth to No auth.',
+ 'wizard.error.auth_profile_name': 'Auth profile name is required.',
+ 'wizard.error.secret_required': 'Select the secret required by this auth profile.',
+ 'wizard.error.basic_secrets_required': 'Select both username and password secrets.',
+ 'wizard.error.header_name_required': 'Header name is required for this auth profile.',
+ 'wizard.error.query_param_required': 'Query parameter name is required for this auth profile.',
+ 'wizard.error.secret_name_required': 'Secret name is required.',
+ 'wizard.error.secret_value_required': 'Secret value is required.',
+ 'wizard.error.save_upstream_first': 'Save the upstream after creating a new auth profile before continuing.',
+ 'wizard.toast.auth_profile_created_title': 'Auth profile created',
+ 'wizard.toast.auth_profile_created_body': 'New upstream auth profile is ready and selected.',
+ 'wizard.toast.auth_profile_error_title': 'Auth profile creation failed',
+ 'wizard.toast.quick_secret_created_title': 'Secret created',
+ 'wizard.toast.quick_secret_created_body': 'The new secret is now available in the auth selector.',
+ 'wizard.toast.quick_secret_error_title': 'Secret creation failed',
'wizard.error.save': 'Failed to save operation',
'wizard.error.save_title': 'Save failed',
'wizard.save.created': 'Draft created',
@@ -1443,9 +1488,38 @@ var TRANSLATIONS = {
'wizard.step2.name': 'Имя',
'wizard.step2.base_url': 'Base URL',
'wizard.step2.unique_hint': 'Уникальный идентификатор этого upstream-а.',
- 'wizard.step2.base_url_hint': 'Корневой URL без завершающего слеша. Поддерживает ${secrets.*}.',
- 'wizard.step2.auth_headers': 'Auth headers',
- 'wizard.step2.auth_headers_hint': 'Эти заголовки отправляются с каждым запросом в этот upstream. Используйте secrets для учетных данных.',
+ 'wizard.step2.base_url_hint': 'Корневой URL без завершающего слеша. Auth настраивается отдельно ниже.',
+ 'wizard.step2.auth_selector': 'Upstream auth',
+ 'wizard.step2.auth_selector_hint': 'Используйте secrets-backed auth profiles вместо встраивания credential headers в определение upstream-а.',
+ 'wizard.step2.auth_mode.none': 'Без авторизации',
+ 'wizard.step2.auth_mode.existing': 'Использовать существующий auth profile',
+ 'wizard.step2.auth_mode.create': 'Создать auth profile сейчас',
+ 'wizard.step2.auth_profile': 'Auth profile',
+ 'wizard.step2.auth_profile_hint': 'Выбранный профиль будет резолвиться в runtime и подключаться через `execution_config.auth_profile_ref`.',
+ 'wizard.step2.auth_profile_empty': 'Auth profiles пока нет. Создайте его ниже или на странице Secrets.',
+ 'wizard.step2.create_profile_title': 'Создать auth profile',
+ 'wizard.step2.create_profile_subtitle': 'Создайте переиспользуемый профиль на базе зашифрованных workspace secrets.',
+ 'wizard.step2.profile_name': 'Имя профиля',
+ 'wizard.step2.profile_kind': 'Тип auth',
+ 'wizard.step2.auth_kind.bearer': 'Bearer token',
+ 'wizard.step2.auth_kind.basic': 'Basic auth',
+ 'wizard.step2.auth_kind.api_key_header': 'API key header',
+ 'wizard.step2.auth_kind.api_key_query': 'API key query',
+ 'wizard.step2.header_name': 'Имя заголовка',
+ 'wizard.step2.query_param': 'Query param',
+ 'wizard.step2.username_secret': 'Секрет логина',
+ 'wizard.step2.password_secret': 'Секрет пароля',
+ 'wizard.step2.secret_value': 'Секрет',
+ 'wizard.step2.quick_secret': 'Быстро создать секрет',
+ 'wizard.step2.manage_secrets': 'Открыть страницу Secrets',
+ 'wizard.step2.static_headers': 'Статические заголовки (опционально)',
+ 'wizard.step2.static_headers_hint': 'Необязательные не-секретные заголовки для каждого запроса к этому upstream-у. Для credential flows используйте auth profiles.',
+ 'wizard.step2.quick_secret_title': 'Быстро создать секрет',
+ 'wizard.step2.quick_secret_name': 'Имя секрета',
+ 'wizard.step2.quick_secret_kind': 'Тип секрета',
+ 'wizard.step2.quick_secret_value': 'Значение секрета',
+ 'wizard.step2.quick_secret_hint': 'Plaintext сразу шифруется и больше не возвращается.',
+ 'wizard.step2.quick_secret_submit': 'Создать секрет',
'wizard.step2.save_upstream': 'Сохранить upstream',
'wizard.step2.endpoint_title': 'Endpoint',
'wizard.step2.endpoint_subtitle': 'Путь и HTTP-метод для конкретной операции',
@@ -1537,7 +1611,7 @@ var TRANSLATIONS = {
'wizard.step4.protocol_agnostic_title': 'Схемы не зависят от протокола',
'wizard.step4.protocol_agnostic_body': 'Эти схемы описывают MCP-контракт, а не wire-format upstream-а. Маппинг на шаге 5 переводит одно в другое. Можно загрузить sample JSON-ответа, чтобы автоматически сгенерировать output schema.',
'wizard.step5.title': 'Mapping и исполнение',
- 'wizard.step5.subtitle': 'Определите, как аргументы MCP-инструмента мапятся в поля upstream-запроса и как ответ upstream-а мапится обратно в MCP output. Затем задайте execution-параметры — auth настраивается на уровне upstream на шаге 3.',
+ 'wizard.step5.subtitle': 'Определите, как аргументы MCP-инструмента мапятся в поля upstream-запроса и как ответ upstream-а мапится обратно в MCP output. Затем задайте execution-параметры — auth настраивается на уровне upstream на шаге 2.',
'wizard.step5.input_request': 'Input → Request',
'wizard.step5.output_response': 'Response → Output',
'wizard.step5.execution': 'Исполнение',
@@ -1586,6 +1660,21 @@ var TRANSLATIONS = {
'wizard.error.tool_name': 'Имя инструмента обязательно',
'wizard.error.parser_unavailable': 'YAML parser недоступен',
'wizard.error.select_upstream': 'Сначала выберите или зарегистрируйте upstream',
+ 'wizard.error.auth_profile_required': 'Выберите auth profile или переключите upstream auth в режим «Без авторизации».',
+ 'wizard.error.auth_profile_name': 'Нужно указать имя auth profile.',
+ 'wizard.error.secret_required': 'Выберите секрет, необходимый для этого auth profile.',
+ 'wizard.error.basic_secrets_required': 'Выберите секреты и для логина, и для пароля.',
+ 'wizard.error.header_name_required': 'Для этого auth profile нужно указать имя заголовка.',
+ 'wizard.error.query_param_required': 'Для этого auth profile нужно указать имя query-параметра.',
+ 'wizard.error.secret_name_required': 'Нужно указать имя секрета.',
+ 'wizard.error.secret_value_required': 'Нужно указать значение секрета.',
+ 'wizard.error.save_upstream_first': 'Сначала сохраните upstream после создания нового auth profile.',
+ 'wizard.toast.auth_profile_created_title': 'Auth profile создан',
+ 'wizard.toast.auth_profile_created_body': 'Новый upstream auth profile готов и уже выбран.',
+ 'wizard.toast.auth_profile_error_title': 'Не удалось создать auth profile',
+ 'wizard.toast.quick_secret_created_title': 'Секрет создан',
+ 'wizard.toast.quick_secret_created_body': 'Новый секрет уже доступен в auth selector.',
+ 'wizard.toast.quick_secret_error_title': 'Не удалось создать секрет',
'wizard.error.save': 'Не удалось сохранить операцию',
'wizard.error.save_title': 'Не удалось сохранить',
'wizard.save.created': 'Черновик создан',
diff --git a/apps/ui/js/wizard.js b/apps/ui/js/wizard.js
index d038afd..4b5e5f0 100644
--- a/apps/ui/js/wizard.js
+++ b/apps/ui/js/wizard.js
@@ -276,9 +276,44 @@ document.addEventListener('DOMContentLoaded', async function() {
await loadProtocolCapabilities();
await loadWizardPanels([1, 2, 3, 4, 5]);
+ await loadWizardAuthResources();
bindProtocolCards();
bindWizardLiveActions();
bindStreamingConfigControls();
+ updateUpstreamAuthUi();
+
+ var quickSecretModal = document.getElementById('quick-secret-modal');
+ var quickSecretClose = document.getElementById('quick-secret-close-btn');
+ var quickSecretCancel = document.getElementById('quick-secret-cancel-btn');
+ var quickSecretSubmit = document.getElementById('quick-secret-submit-btn');
+ if (quickSecretClose) quickSecretClose.addEventListener('click', closeQuickSecretModal);
+ if (quickSecretCancel) quickSecretCancel.addEventListener('click', closeQuickSecretModal);
+ if (quickSecretModal) {
+ quickSecretModal.addEventListener('click', function(event) {
+ if (event.target === quickSecretModal) closeQuickSecretModal();
+ });
+ }
+ if (quickSecretSubmit) {
+ quickSecretSubmit.addEventListener('click', async function() {
+ var button = quickSecretSubmit;
+ var original = button.textContent;
+ button.disabled = true;
+ button.textContent = tKey('wizard.step2.quick_secret_submit');
+ try {
+ await submitQuickSecret();
+ } catch (error) {
+ if (window.CrankUi) {
+ window.CrankUi.error(
+ error.message || tKey('wizard.toast.quick_secret_error_title'),
+ tKey('wizard.toast.quick_secret_error_title')
+ );
+ }
+ } finally {
+ button.disabled = false;
+ button.textContent = original;
+ }
+ });
+ }
var params = new URLSearchParams(window.location.search);
if (params.get('mode') === 'edit' && params.get('operationId')) {
@@ -338,46 +373,170 @@ function selectGqlType(btn) {
if (defaults[type]) editor.value = defaults[type];
}
-/* ── Upstream selector (Step 3) ── */
+/* ── Upstream selector (Step 2) ── */
var upstreams = [
- { id: 'acme-api', name: 'acme-api', url: 'https://api.acme.com', authHeaders: '{\n \"Authorization\": \"Bearer ${secrets.ACME_API_TOKEN}\"\n}' },
- { id: 'stripe', name: 'stripe-payments', url: 'https://api.stripe.com', authHeaders: '{\n \"Authorization\": \"Bearer ${secrets.STRIPE_API_KEY}\"\n}' },
- { id: 'internal-crm', name: 'internal-crm', url: 'https://crm.internal.acme.io', authHeaders: '{\n}' },
- { id: 'sendgrid', name: 'sendgrid-mail', url: 'https://api.sendgrid.com', authHeaders: '{\n \"Authorization\": \"Bearer ${secrets.SENDGRID_API_KEY}\"\n}' },
- { id: 'twilio', name: 'twilio-sms', url: 'https://api.twilio.com', authHeaders: '{\n \"Authorization\": \"Bearer ${secrets.TWILIO_API_KEY}\"\n}' },
+ { id: 'open-meteo', name: 'Open Meteo', url: 'https://api.open-meteo.com', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null },
+ { id: 'countries-graphql', name: 'Countries GraphQL', url: 'https://countries.trevorblades.com', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null },
+ { id: 'grpcb', name: 'grpcb.in', url: 'https://grpcb.in:443', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null }
];
+var wizardSecrets = [];
+var wizardAuthProfiles = [];
var selectedUpstreamId = null;
var dropdownOpen = false;
var editingUpstreamId = null;
-function inferUpstreamAuth(headersText) {
- var parsed = parseStructuredText(headersText || '{}');
- var keys = parsed && typeof parsed === 'object' ? Object.keys(parsed) : [];
- if (!keys.length) {
+function authProfileById(authProfileId) {
+ return wizardAuthProfiles.find(function(item) { return item.id === authProfileId; }) || null;
+}
+
+function syncUpstreamAuthMetadata(upstream) {
+ if (!upstream) return upstream;
+ if (!upstream.authProfileId) {
+ upstream.authProfileName = '';
+ upstream.authKind = null;
+ return upstream;
+ }
+
+ var profile = authProfileById(upstream.authProfileId);
+ if (profile) {
+ upstream.authProfileName = profile.name;
+ upstream.authKind = profile.kind;
+ }
+ return upstream;
+}
+
+function inferUpstreamAuth(upstream) {
+ syncUpstreamAuthMetadata(upstream);
+ if (!upstream || !upstream.authProfileId || !upstream.authKind) {
return { kind: 'none', label: tKey('wizard.step2.auth_none') };
}
-
- var hasAuthorization = keys.some(function(key) {
- return String(key).toLowerCase() === 'authorization';
- });
-
- if (hasAuthorization) {
+ if (upstream.authKind === 'bearer') {
return { kind: 'bearer', label: tKey('wizard.step2.auth_bearer') };
}
-
+ if (upstream.authKind === 'basic') {
+ return { kind: 'basic', label: tKey('wizard.step2.auth_basic') };
+ }
return { kind: 'apikey', label: tKey('wizard.step2.auth_apikey') };
}
function upstreamMeta(upstream) {
- var auth = inferUpstreamAuth(upstream && upstream.authHeaders);
+ var auth = inferUpstreamAuth(upstream);
return {
kind: auth.kind,
label: auth.label,
};
}
+async function loadWizardAuthResources() {
+ if (!wizardWorkspaceId || !window.CrankApi) {
+ wizardSecrets = [];
+ wizardAuthProfiles = [];
+ renderAuthProfileOptions();
+ return;
+ }
+
+ try {
+ var results = await Promise.all([
+ window.CrankApi.listSecrets(wizardWorkspaceId),
+ window.CrankApi.listAuthProfiles(wizardWorkspaceId),
+ ]);
+ wizardSecrets = (results[0] && results[0].items) || [];
+ wizardAuthProfiles = (results[1] && results[1].items) || [];
+ } catch (_error) {
+ wizardSecrets = [];
+ wizardAuthProfiles = [];
+ }
+
+ upstreams.forEach(syncUpstreamAuthMetadata);
+ renderAuthProfileOptions();
+}
+
+function renderAuthProfileOptions() {
+ var profileSelect = document.getElementById('new-upstream-auth-profile');
+ var secretSelect = document.getElementById('new-auth-profile-secret-id');
+ var usernameSelect = document.getElementById('new-auth-profile-username-secret');
+ var passwordSelect = document.getElementById('new-auth-profile-password-secret');
+ var profileHint = document.getElementById('new-upstream-auth-profile-hint');
+
+ if (profileSelect) {
+ var previousProfile = profileSelect.value;
+ profileSelect.innerHTML = '';
+ if (!wizardAuthProfiles.length) {
+ var emptyProfile = document.createElement('option');
+ emptyProfile.value = '';
+ emptyProfile.textContent = tKey('wizard.step2.auth_profile_empty');
+ profileSelect.appendChild(emptyProfile);
+ if (profileHint) profileHint.textContent = tKey('wizard.step2.auth_profile_empty');
+ } else {
+ var placeholder = document.createElement('option');
+ placeholder.value = '';
+ placeholder.textContent = tKey('wizard.step2.auth_profile');
+ profileSelect.appendChild(placeholder);
+ wizardAuthProfiles.forEach(function(profile) {
+ var option = document.createElement('option');
+ option.value = profile.id;
+ option.textContent = profile.name + ' · ' + profile.kind;
+ profileSelect.appendChild(option);
+ });
+ if (previousProfile) {
+ profileSelect.value = previousProfile;
+ }
+ if (profileHint) profileHint.textContent = tKey('wizard.step2.auth_profile_hint');
+ }
+ }
+
+ [secretSelect, usernameSelect, passwordSelect].forEach(function(select) {
+ if (!select) return;
+ var previous = select.value;
+ select.innerHTML = '';
+ var placeholder = document.createElement('option');
+ placeholder.value = '';
+ placeholder.textContent = tKey('wizard.step2.secret_value');
+ select.appendChild(placeholder);
+ wizardSecrets.forEach(function(secret) {
+ var option = document.createElement('option');
+ option.value = secret.id;
+ option.textContent = secret.name + ' · ' + secret.kind;
+ select.appendChild(option);
+ });
+ if (previous) {
+ select.value = previous;
+ }
+ });
+}
+
+function updateAuthProfileCreateUi() {
+ var kind = textValue('new-auth-profile-kind') || 'bearer';
+ var headerGroup = document.getElementById('auth-profile-header-name-group');
+ var queryGroup = document.getElementById('auth-profile-query-param-group');
+ var basicRow = document.getElementById('auth-profile-basic-secret-row');
+ var singleSecretGroup = document.getElementById('auth-profile-single-secret-group');
+
+ if (headerGroup) {
+ headerGroup.style.display = (kind === 'bearer' || kind === 'api_key_header') ? '' : 'none';
+ }
+ if (queryGroup) {
+ queryGroup.style.display = kind === 'api_key_query' ? '' : 'none';
+ }
+ if (basicRow) {
+ basicRow.style.display = kind === 'basic' ? '' : 'none';
+ }
+ if (singleSecretGroup) {
+ singleSecretGroup.style.display = kind === 'basic' ? 'none' : '';
+ }
+}
+
+function updateUpstreamAuthUi() {
+ var mode = textValue('new-upstream-auth-mode') || 'none';
+ var existingGroup = document.getElementById('upstream-auth-existing-group');
+ var createGroup = document.getElementById('upstream-auth-create-group');
+ if (existingGroup) existingGroup.style.display = mode === 'existing' ? '' : 'none';
+ if (createGroup) createGroup.style.display = mode === 'create' ? '' : 'none';
+ updateAuthProfileCreateUi();
+}
+
function renderDropdownList(filter) {
var list = document.getElementById('upstream-dropdown-list');
if (!list) return;
@@ -522,7 +681,17 @@ function startNewUpstream() {
form.style.display = '';
setValue('new-upstream-name', '');
setValue('new-upstream-url', '');
- setValue('new-upstream-auth-headers', '{\n}');
+ setValue('new-upstream-static-headers', '{\n}');
+ setValue('new-upstream-auth-mode', 'none');
+ setValue('new-upstream-auth-profile', '');
+ setValue('new-auth-profile-name', '');
+ setValue('new-auth-profile-kind', 'bearer');
+ setValue('new-auth-profile-header-name', 'Authorization');
+ setValue('new-auth-profile-query-param', 'api_key');
+ setValue('new-auth-profile-secret-id', '');
+ setValue('new-auth-profile-username-secret', '');
+ setValue('new-auth-profile-password-secret', '');
+ updateUpstreamAuthUi();
var first = form.querySelector('input');
if (first) setTimeout(function() { first.focus(); }, 50);
}
@@ -545,46 +714,151 @@ function beginEditSelectedUpstream(e) {
form.style.display = '';
setValue('new-upstream-name', upstream.name);
setValue('new-upstream-url', upstream.url);
- setValue('new-upstream-auth-headers', upstream.authHeaders || '{\n}');
+ setValue('new-upstream-static-headers', upstream.staticHeaders || '{\n}');
+ setValue('new-upstream-auth-profile', upstream.authProfileId || '');
+ setValue('new-upstream-auth-mode', upstream.authProfileId ? 'existing' : 'none');
+ updateUpstreamAuthUi();
}
}
-function saveNewUpstream(e) {
- e.stopPropagation();
- var nameEl = document.getElementById('new-upstream-name');
- var urlEl = document.getElementById('new-upstream-url');
- var name = nameEl ? nameEl.value.trim() : '';
- var url = urlEl ? urlEl.value.trim() : '';
- if (!name) { if (nameEl) nameEl.focus(); return; }
- if (!url) { if (urlEl) urlEl.focus(); return; }
-
- var authHeadersEl = document.getElementById('new-upstream-auth-headers');
- var authHeaders = authHeadersEl ? authHeadersEl.value : '{\n}';
- var nextRecord = {
- id: editingUpstreamId || ('custom-' + Date.now()),
- name: name,
- url: url,
- authHeaders: authHeaders
- };
-
- if (editingUpstreamId) {
- upstreams = upstreams.map(function(item) {
- return item.id === editingUpstreamId ? nextRecord : item;
- });
- } else {
- upstreams.push(nextRecord);
+function buildAuthProfileConfig(kind) {
+ if (kind === 'bearer') {
+ var bearerHeader = textValue('new-auth-profile-header-name') || 'Authorization';
+ var bearerSecretId = textValue('new-auth-profile-secret-id');
+ if (!bearerHeader) throw new Error(tKey('wizard.error.header_name_required'));
+ if (!bearerSecretId) throw new Error(tKey('wizard.error.secret_required'));
+ return {
+ bearer: {
+ header_name: bearerHeader,
+ secret_id: bearerSecretId,
+ }
+ };
}
- // Reset form
- if (nameEl) nameEl.value = '';
- if (urlEl) urlEl.value = '';
- if (authHeadersEl) authHeadersEl.value = '{\n}';
- var form = document.getElementById('upstream-new-form');
- if (form) form.style.display = 'none';
- editingUpstreamId = null;
+ if (kind === 'basic') {
+ var usernameSecretId = textValue('new-auth-profile-username-secret');
+ var passwordSecretId = textValue('new-auth-profile-password-secret');
+ if (!usernameSecretId || !passwordSecretId) throw new Error(tKey('wizard.error.basic_secrets_required'));
+ return {
+ basic: {
+ username_secret_id: usernameSecretId,
+ password_secret_id: passwordSecretId,
+ }
+ };
+ }
- // Select the new upstream
- pickUpstream(nextRecord.id);
+ if (kind === 'api_key_header') {
+ var headerName = textValue('new-auth-profile-header-name');
+ var headerSecretId = textValue('new-auth-profile-secret-id');
+ if (!headerName) throw new Error(tKey('wizard.error.header_name_required'));
+ if (!headerSecretId) throw new Error(tKey('wizard.error.secret_required'));
+ return {
+ api_key_header: {
+ header_name: headerName,
+ secret_id: headerSecretId,
+ }
+ };
+ }
+
+ var queryParam = textValue('new-auth-profile-query-param');
+ var querySecretId = textValue('new-auth-profile-secret-id');
+ if (!queryParam) throw new Error(tKey('wizard.error.query_param_required'));
+ if (!querySecretId) throw new Error(tKey('wizard.error.secret_required'));
+ return {
+ api_key_query: {
+ param_name: queryParam,
+ secret_id: querySecretId,
+ }
+ };
+}
+
+async function createAuthProfileFromForm() {
+ var name = textValue('new-auth-profile-name');
+ var kind = textValue('new-auth-profile-kind') || 'bearer';
+ if (!name) throw new Error(tKey('wizard.error.auth_profile_name'));
+
+ var profile = await window.CrankApi.createAuthProfile(wizardWorkspaceId, {
+ name: name,
+ kind: kind,
+ config: buildAuthProfileConfig(kind),
+ });
+ wizardAuthProfiles.push(profile);
+ renderAuthProfileOptions();
+ if (window.CrankUi) {
+ window.CrankUi.success(
+ tKey('wizard.toast.auth_profile_created_body'),
+ tKey('wizard.toast.auth_profile_created_title')
+ );
+ }
+ return profile;
+}
+
+async function saveNewUpstream(e) {
+ e.stopPropagation();
+ try {
+ var nameEl = document.getElementById('new-upstream-name');
+ var urlEl = document.getElementById('new-upstream-url');
+ var name = nameEl ? nameEl.value.trim() : '';
+ var url = urlEl ? urlEl.value.trim() : '';
+ if (!name) { if (nameEl) nameEl.focus(); return; }
+ if (!url) { if (urlEl) urlEl.focus(); return; }
+
+ var staticHeadersEl = document.getElementById('new-upstream-static-headers');
+ var staticHeaders = staticHeadersEl ? staticHeadersEl.value : '{\n}';
+ parseHeaderMap(staticHeaders);
+
+ var authMode = textValue('new-upstream-auth-mode') || 'none';
+ var authProfileId = null;
+ var authProfileName = '';
+ var authKind = null;
+
+ if (authMode === 'existing') {
+ authProfileId = textValue('new-upstream-auth-profile');
+ if (!authProfileId) throw new Error(tKey('wizard.error.auth_profile_required'));
+ var existingProfile = authProfileById(authProfileId);
+ authProfileName = existingProfile ? existingProfile.name : '';
+ authKind = existingProfile ? existingProfile.kind : null;
+ } else if (authMode === 'create') {
+ var createdProfile = await createAuthProfileFromForm();
+ authProfileId = createdProfile.id;
+ authProfileName = createdProfile.name;
+ authKind = createdProfile.kind;
+ setValue('new-upstream-auth-profile', createdProfile.id);
+ setValue('new-upstream-auth-mode', 'existing');
+ }
+ var nextRecord = {
+ id: editingUpstreamId || ('custom-' + Date.now()),
+ name: name,
+ url: url,
+ staticHeaders: staticHeaders,
+ authProfileId: authProfileId,
+ authProfileName: authProfileName,
+ authKind: authKind,
+ };
+
+ if (editingUpstreamId) {
+ upstreams = upstreams.map(function(item) {
+ return item.id === editingUpstreamId ? nextRecord : item;
+ });
+ } else {
+ upstreams.push(nextRecord);
+ }
+
+ if (nameEl) nameEl.value = '';
+ if (urlEl) urlEl.value = '';
+ if (staticHeadersEl) staticHeadersEl.value = '{\n}';
+ var form = document.getElementById('upstream-new-form');
+ if (form) form.style.display = 'none';
+ editingUpstreamId = null;
+ pickUpstream(nextRecord.id);
+ } catch (error) {
+ if (window.CrankUi) {
+ window.CrankUi.error(
+ error.message || tKey('wizard.toast.auth_profile_error_title'),
+ tKey('wizard.toast.auth_profile_error_title')
+ );
+ }
+ }
}
function cancelNewUpstream(e) {
@@ -596,6 +870,58 @@ function cancelNewUpstream(e) {
editingUpstreamId = null;
}
+function openQuickSecretModal(e) {
+ if (e) e.preventDefault();
+ var modal = document.getElementById('quick-secret-modal');
+ if (!modal) return;
+ setValue('quick-secret-name', '');
+ setValue('quick-secret-value', '');
+ setValue('quick-secret-kind', 'token');
+ modal.classList.add('open');
+ setTimeout(function() {
+ var input = document.getElementById('quick-secret-name');
+ if (input) input.focus();
+ }, 30);
+}
+
+function closeQuickSecretModal() {
+ var modal = document.getElementById('quick-secret-modal');
+ if (modal) modal.classList.remove('open');
+}
+
+async function submitQuickSecret() {
+ var name = textValue('quick-secret-name');
+ var value = textValue('quick-secret-value');
+ var kind = textValue('quick-secret-kind') || 'token';
+ if (!name) throw new Error(tKey('wizard.error.secret_name_required'));
+ if (!value) throw new Error(tKey('wizard.error.secret_value_required'));
+
+ var secret = await window.CrankApi.createSecret(wizardWorkspaceId, {
+ name: name,
+ kind: kind,
+ value: value,
+ });
+ wizardSecrets.push(secret);
+ renderAuthProfileOptions();
+ var primarySelect = document.getElementById('new-auth-profile-secret-id');
+ var usernameSelect = document.getElementById('new-auth-profile-username-secret');
+ var passwordSelect = document.getElementById('new-auth-profile-password-secret');
+ if (primarySelect && primarySelect.offsetParent !== null) {
+ primarySelect.value = secret.id;
+ } else if (usernameSelect && !usernameSelect.value) {
+ usernameSelect.value = secret.id;
+ } else if (passwordSelect) {
+ passwordSelect.value = secret.id;
+ }
+ closeQuickSecretModal();
+ if (window.CrankUi) {
+ window.CrankUi.success(
+ tKey('wizard.toast.quick_secret_created_body'),
+ tKey('wizard.toast.quick_secret_created_title')
+ );
+ }
+}
+
function escapeHtml(str) {
return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
}
@@ -1154,10 +1480,17 @@ function parseExecutionConfig(text) {
var value = parseStructuredText(text);
var retry = value.retry || value.retry_policy || null;
var headers = value.headers && typeof value.headers === 'object' ? value.headers : {};
+ var selectedUpstream = currentUpstream();
+ if (selectedUpstream && selectedUpstream.authMode === 'create') {
+ throw new Error(tKey('wizard.error.save_upstream_first'));
+ }
+ var authProfileRef = selectedUpstream && selectedUpstream.authProfileId
+ ? selectedUpstream.authProfileId
+ : (value.auth && value.auth.profile ? value.auth.profile : null);
var config = {
timeout_ms: Number(value.timeout_ms || 10000),
retry_policy: retry && retry.max_attempts ? { max_attempts: Number(retry.max_attempts) } : null,
- auth_profile_ref: value.auth && value.auth.profile ? value.auth.profile : null,
+ auth_profile_ref: authProfileRef,
headers: headers,
protocol_options: null,
};
@@ -1185,12 +1518,18 @@ function currentUpstream() {
var customUrl = textValue('new-upstream-url');
if (!customUrl) return null;
- var authHeaders = textValue('new-upstream-auth-headers');
+ var authMode = textValue('new-upstream-auth-mode') || 'none';
+ var authProfileId = authMode === 'existing' ? textValue('new-upstream-auth-profile') : null;
+ var authProfile = authProfileById(authProfileId);
return {
id: 'custom',
name: customName || 'custom-upstream',
url: customUrl,
- authHeaders: authHeaders,
+ staticHeaders: textValue('new-upstream-static-headers') || '{\n}',
+ authMode: authMode,
+ authProfileId: authProfileId || null,
+ authProfileName: authProfile ? authProfile.name : '',
+ authKind: authProfile ? authProfile.kind : null,
};
}
@@ -1230,7 +1569,7 @@ function buildTarget() {
base_url: upstream.url,
method: activeMethod ? activeMethod.dataset.method : 'POST',
path_template: pathTemplate,
- static_headers: parseHeaderMap(upstream.authHeaders),
+ static_headers: parseHeaderMap(upstream.staticHeaders),
};
}
@@ -1634,8 +1973,10 @@ function selectProtocol(protocol) {
updateWizardProtocolVisibility();
}
-function prefillUpstream(baseUrl, headers) {
- var known = upstreams.find(function(item) { return item.url === baseUrl; });
+function prefillUpstream(baseUrl, headers, authProfileRef) {
+ var known = upstreams.find(function(item) {
+ return item.url === baseUrl && (item.authProfileId || null) === (authProfileRef || null);
+ });
if (known) {
pickUpstream(known.id);
return;
@@ -1648,7 +1989,10 @@ function prefillUpstream(baseUrl, headers) {
if (form) form.style.display = '';
setValue('new-upstream-name', 'imported-upstream');
setValue('new-upstream-url', baseUrl);
- setValue('new-upstream-auth-headers', headers && Object.keys(headers).length ? JSON.stringify(headers, null, 2) : '{\n}');
+ setValue('new-upstream-static-headers', headers && Object.keys(headers).length ? JSON.stringify(headers, null, 2) : '{\n}');
+ setValue('new-upstream-auth-mode', authProfileRef ? 'existing' : 'none');
+ setValue('new-upstream-auth-profile', authProfileRef || '');
+ updateUpstreamAuthUi();
var valueEl = document.getElementById('upstream-combobox-value');
if (valueEl) {
@@ -1725,7 +2069,7 @@ function prefillWizardFromEdit(detail, versionDocument) {
var target = versionDocument.target || {};
if (target.kind === 'rest') {
- prefillUpstream(target.base_url, target.static_headers);
+ prefillUpstream(target.base_url, target.static_headers, versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null);
setValue('endpoint-path', target.path_template);
document.querySelectorAll('.method-card').forEach(function(card) {
card.classList.toggle('active', card.dataset.method === target.method);
@@ -1733,14 +2077,14 @@ function prefillWizardFromEdit(detail, versionDocument) {
} else if (target.kind === 'graphql') {
var endpoint = target.endpoint || '';
var parsedEndpoint = new URL(endpoint, window.location.origin);
- prefillUpstream(parsedEndpoint.origin, {});
+ prefillUpstream(parsedEndpoint.origin, {}, versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null);
setValue('endpoint-path', parsedEndpoint.pathname + parsedEndpoint.search);
setValue('gql-query-editor', target.query_template);
document.querySelectorAll('.gql-type-card').forEach(function(card) {
card.classList.toggle('active', card.dataset.gqlType === target.operation_type);
});
} else if (target.kind === 'grpc') {
- prefillUpstream(target.server_addr, {});
+ prefillUpstream(target.server_addr, {}, versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null);
var route = '/' + (target.package ? target.package + '.' : '') + target.service + '/' + target.method;
setValue('grpc-manual-route', route);
setValue('grpc-manual-req-type', '');
diff --git a/docs/secrets-auth-plan.md b/docs/secrets-auth-plan.md
index 4827322..7e071c5 100644
--- a/docs/secrets-auth-plan.md
+++ b/docs/secrets-auth-plan.md
@@ -247,5 +247,6 @@ DoD:
2. `feat/auth-profile-secret-resolution`
3. `feat/secrets-ui`
4. `feat/wizard-auth-selector`
+5. `feat/manual-regression-pass`
-Сейчас `feat/secrets-ui` уже закрыт: оператор может управлять encrypted secrets и видеть, какие `AuthProfile` на них ссылаются. Следующий шаг - убрать raw `Auth headers` из wizard primary flow и заменить их на нормальный auth selector и quick-create path для `Secret`/`AuthProfile`.
+Сейчас `feat/wizard-auth-selector` уже закрыт: wizard использует auth selector на шаге 2, сериализует `auth_profile_ref` в `execution_config`, умеет quick-create для `Secret` и `AuthProfile`, а raw `Auth headers` убраны из primary UX. Следующий шаг - ручной regression pass по REST / GraphQL / gRPC / WebSocket / SOAP и проверка связки `Secrets -> Auth Profiles -> Test Run / MCP call`.