diff --git a/src/lib/settings/categories/SecuritySettings.svelte b/src/lib/settings/categories/SecuritySettings.svelte index f666031..28c6150 100644 --- a/src/lib/settings/categories/SecuritySettings.svelte +++ b/src/lib/settings/categories/SecuritySettings.svelte @@ -3,7 +3,6 @@ import { getSetting, setSetting } from '../../adapters/settings-bridge'; import { storeSecret, getSecret, deleteSecret, listSecrets, hasKeyring, knownSecretKeys, SECRET_KEY_LABELS } from '../../adapters/secrets-bridge'; - // Secrets state let keyringAvailable = $state(false); let storedKeys = $state([]); let knownKeys = $state([]); @@ -13,13 +12,9 @@ let newSecretValue = $state(''); let secretsKeyDropdownOpen = $state(false); let secretsSaving = $state(false); - - // Branch policies state let branchPolicies = $state<{ pattern: string; action: 'block' | 'warn' }[]>([]); let newBranchPattern = $state(''); let newBranchAction = $state<'block' | 'warn'>('warn'); - - // Privacy & telemetry state let telemetryEnabled = $state(false); let retentionDays = $state(90); @@ -27,151 +22,88 @@ let newSecretKeyLabel = $derived(newSecretKey ? getSecretKeyLabel(newSecretKey) : 'Select key...'); onMount(async () => { - // Load secrets try { keyringAvailable = await hasKeyring(); - if (keyringAvailable) { - storedKeys = await listSecrets(); - knownKeys = await knownSecretKeys(); - } - } catch { - keyringAvailable = false; - } - - // Load branch policies - const rawPolicies = await getSetting('branch_policies'); - if (rawPolicies) { - try { branchPolicies = JSON.parse(rawPolicies); } catch { branchPolicies = []; } - } - - // Load privacy settings - const rawTelemetry = await getSetting('telemetry_enabled'); + if (keyringAvailable) { storedKeys = await listSecrets(); knownKeys = await knownSecretKeys(); } + } catch { keyringAvailable = false; } + const [rawPolicies, rawTelemetry, rawRetention] = await Promise.all([ + getSetting('branch_policies'), getSetting('telemetry_enabled'), getSetting('data_retention_days'), + ]); + if (rawPolicies) { try { branchPolicies = JSON.parse(rawPolicies); } catch { branchPolicies = []; } } telemetryEnabled = rawTelemetry === 'true'; - const rawRetention = await getSetting('data_retention_days'); retentionDays = rawRetention ? parseInt(rawRetention, 10) : 90; }); async function save(key: string, value: string) { try { await setSetting(key, value); } catch (e) { console.error(`Failed to save ${key}:`, e); } } - - // --- Secrets handlers --- - - function getSecretKeyLabel(key: string): string { - return SECRET_KEY_LABELS[key] ?? key; - } + function getSecretKeyLabel(key: string): string { return SECRET_KEY_LABELS[key] ?? key; } async function handleRevealSecret(key: string) { - if (revealedKey === key) { - revealedKey = null; - revealedValue = ''; - return; - } - try { - const val = await getSecret(key); - revealedKey = key; - revealedValue = val ?? ''; - } catch (e) { - console.error(`Failed to reveal secret '${key}':`, e); - } + if (revealedKey === key) { revealedKey = null; revealedValue = ''; return; } + try { const val = await getSecret(key); revealedKey = key; revealedValue = val ?? ''; } + catch (e) { console.error(`Failed to reveal secret '${key}':`, e); } } - async function handleSaveSecret() { if (!newSecretKey || !newSecretValue) return; secretsSaving = true; - try { - await storeSecret(newSecretKey, newSecretValue); - storedKeys = await listSecrets(); - newSecretKey = ''; - newSecretValue = ''; - revealedKey = null; - revealedValue = ''; - } catch (e) { - console.error('Failed to store secret:', e); - } finally { - secretsSaving = false; - } + try { await storeSecret(newSecretKey, newSecretValue); storedKeys = await listSecrets(); newSecretKey = ''; newSecretValue = ''; revealedKey = null; revealedValue = ''; } + catch (e) { console.error('Failed to store secret:', e); } + finally { secretsSaving = false; } } - async function handleDeleteSecret(key: string) { - try { - await deleteSecret(key); - storedKeys = await listSecrets(); - if (revealedKey === key) { - revealedKey = null; - revealedValue = ''; - } - } catch (e) { - console.error(`Failed to delete secret '${key}':`, e); - } + try { await deleteSecret(key); storedKeys = await listSecrets(); if (revealedKey === key) { revealedKey = null; revealedValue = ''; } } + catch (e) { console.error(`Failed to delete secret '${key}':`, e); } } - - // --- Branch policy handlers --- - function addBranchPolicy() { if (!newBranchPattern.trim()) return; branchPolicies = [...branchPolicies, { pattern: newBranchPattern.trim(), action: newBranchAction }]; - newBranchPattern = ''; - newBranchAction = 'warn'; + newBranchPattern = ''; newBranchAction = 'warn'; save('branch_policies', JSON.stringify(branchPolicies)); } - function removeBranchPolicy(idx: number) { branchPolicies = branchPolicies.filter((_, i) => i !== idx); save('branch_policies', JSON.stringify(branchPolicies)); } - - function handleClickOutside(e: MouseEvent) { - if (!(e.target as HTMLElement).closest('.custom-dropdown')) { - secretsKeyDropdownOpen = false; - } - } - - function handleKeydown(e: KeyboardEvent) { - if (e.key === 'Escape') secretsKeyDropdownOpen = false; - } + function handleClickOutside(e: MouseEvent) { if (!(e.target as HTMLElement).closest('.custom-dropdown')) secretsKeyDropdownOpen = false; } + function handleKeydown(e: KeyboardEvent) { if (e.key === 'Escape') secretsKeyDropdownOpen = false; } -

Secrets

- + {keyringAvailable ? 'System keyring available' : 'System keyring unavailable'}
- {#if !keyringAvailable} -
+
System keyring not available. Secrets cannot be stored securely.
{:else} {#if storedKeys.length > 0} -
+
{#each storedKeys as key} -
+
- {getSecretKeyLabel(key)} - {key} + {getSecretKeyLabel(key)} + {key}
-
- {#if revealedKey === key} - - {:else} - {'\u25CF'.repeat(8)} - {/if} +
+ {#if revealedKey === key} + {:else}{'\u25CF'.repeat(8)}{/if}
-
- -
@@ -179,58 +111,53 @@ {/each}
{/if} - -
-
-
- {#if secretsKeyDropdownOpen} - - - + +
{/if}
-

Branch Policies Pro

{#if branchPolicies.length > 0} -
+
{#each branchPolicies as policy, idx} -
- {policy.pattern} - {policy.action} -
{/each}
{/if} -
-
- { newBranchPattern = (e.target as HTMLInputElement).value; }} /> +
+
+ { newBranchPattern = (e.target as HTMLInputElement).value; }} />
@@ -241,7 +168,6 @@
-

Privacy & Telemetry

@@ -276,145 +202,75 @@ .hint { font-size: 0.625rem; color: var(--ctp-overlay0); } .toggle-row { display: flex; align-items: center; justify-content: space-between; cursor: pointer; } - .toggle { - position: relative; width: 2rem; height: 1.125rem; border: none; border-radius: 0.5625rem; - background: var(--ctp-surface1); cursor: pointer; transition: background 0.2s; padding: 0; flex-shrink: 0; - } + .toggle { position: relative; width: 2rem; height: 1.125rem; border: none; border-radius: 0.5625rem; background: var(--ctp-surface1); cursor: pointer; transition: background 0.2s; padding: 0; flex-shrink: 0; } .toggle.on { background: var(--ctp-blue); } - .thumb { - position: absolute; top: 0.125rem; left: 0.125rem; width: 0.875rem; height: 0.875rem; - border-radius: 50%; background: var(--ctp-text); transition: transform 0.2s; - } + .thumb { position: absolute; top: 0.125rem; left: 0.125rem; width: 0.875rem; height: 0.875rem; border-radius: 50%; background: var(--ctp-text); transition: transform 0.2s; } .toggle.on .thumb { transform: translateX(0.875rem); } .num-row { display: flex; align-items: center; gap: 0.25rem; } - .num { - width: 4.5rem; padding: 0.375rem 0.625rem; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); - border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.8rem; -moz-appearance: textfield; - } + .num { width: 4.5rem; padding: 0.375rem 0.625rem; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.8rem; -moz-appearance: textfield; } .num::-webkit-inner-spin-button, .num::-webkit-outer-spin-button { -webkit-appearance: none; } .unit { font-size: 0.7rem; color: var(--ctp-overlay0); } - .pro { - display: inline-block; font-size: 0.5625rem; padding: 0.0625rem 0.3125rem; background: var(--ctp-peach); - color: var(--ctp-base); border-radius: 0.125rem; font-weight: 700; text-transform: uppercase; - letter-spacing: 0.04em; vertical-align: middle; margin-left: 0.375rem; - } - + .pro { display: inline-block; font-size: 0.5625rem; padding: 0.0625rem 0.3125rem; background: var(--ctp-peach); color: var(--ctp-base); border-radius: 0.125rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; vertical-align: middle; margin-left: 0.375rem; } .seg-group { display: flex; border-radius: 0.25rem; overflow: hidden; border: 1px solid var(--ctp-surface1); } - .seg-btn { - flex: 1; padding: 0.25rem 0.5rem; border: none; background: var(--ctp-surface0); - color: var(--ctp-overlay1); font-size: 0.7rem; font-weight: 500; cursor: pointer; transition: background 0.12s, color 0.12s; - } + .seg-btn { flex: 1; padding: 0.25rem 0.5rem; border: none; background: var(--ctp-surface0); color: var(--ctp-overlay1); font-size: 0.7rem; font-weight: 500; cursor: pointer; transition: background 0.12s, color 0.12s; } .seg-btn:not(:last-child) { border-right: 1px solid var(--ctp-surface1); } .seg-btn:hover { background: var(--ctp-surface1); color: var(--ctp-subtext1); } .seg-btn.active { background: color-mix(in srgb, var(--ctp-blue) 20%, var(--ctp-surface0)); color: var(--ctp-blue); font-weight: 600; } - - .btn-primary { - padding: 0.3125rem 0.875rem; background: var(--ctp-blue); color: var(--ctp-base); border: none; - border-radius: 0.25rem; font-size: 0.78rem; font-weight: 600; cursor: pointer; white-space: nowrap; transition: opacity 0.15s; - } + .btn-primary { padding: 0.3125rem 0.875rem; background: var(--ctp-blue); color: var(--ctp-base); border: none; border-radius: 0.25rem; font-size: 0.78rem; font-weight: 600; cursor: pointer; white-space: nowrap; transition: opacity 0.15s; } .btn-primary:hover { opacity: 0.9; } .btn-primary:disabled { opacity: 0.4; cursor: not-allowed; } - /* --- Secrets --- */ + /* Shared row patterns */ + .text-input { padding: 0.375rem 0.625rem; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.8rem; } + .text-input:focus { border-color: var(--ctp-blue); outline: none; } + .text-input:disabled { opacity: 0.4; cursor: not-allowed; } + .text-input.mono { font-family: var(--term-font-family, monospace); } + .flex-fill { flex: 1; min-width: 0; } + .item-list { display: flex; flex-direction: column; gap: 0.375rem; margin-bottom: 0.625rem; } + .item-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.625rem; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); border-radius: 0.375rem; transition: border-color 0.15s; } + .item-row:hover { border-color: var(--ctp-surface2); } + .item-name { font-size: 0.78rem; font-weight: 600; color: var(--ctp-text); white-space: nowrap; } + .item-sub { font-size: 0.625rem; color: var(--ctp-overlay0); font-family: var(--term-font-family, monospace); } + .item-mono { flex: 1; font-size: 0.78rem; font-family: var(--term-font-family, monospace); color: var(--ctp-text); } + .row-actions { display: flex; gap: 0.25rem; flex-shrink: 0; } + .icon-btn { display: flex; align-items: center; justify-content: center; width: 1.75rem; height: 1.75rem; background: transparent; border: 1px solid var(--ctp-surface1); border-radius: 0.25rem; color: var(--ctp-overlay1); cursor: pointer; transition: color 0.15s, background 0.15s, border-color 0.15s; } + .icon-btn:hover { color: var(--ctp-text); background: var(--ctp-surface0); border-color: var(--ctp-surface2); } + .icon-btn.danger:hover { color: var(--ctp-red); background: color-mix(in srgb, var(--ctp-red) 8%, transparent); border-color: color-mix(in srgb, var(--ctp-red) 30%, var(--ctp-surface1)); } + .add-form { padding: 0.5rem 0.625rem; background: var(--ctp-mantle); border: 1px dashed var(--ctp-surface1); border-radius: 0.375rem; } + .add-row { display: flex; gap: 0.375rem; align-items: stretch; } + + /* Secrets-specific */ .secrets-status { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.625rem; } - .keyring-indicator { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } - .keyring-indicator.available { background: var(--ctp-green); box-shadow: 0 0 4px color-mix(in srgb, var(--ctp-green) 50%, transparent); } - .keyring-indicator.unavailable { background: var(--ctp-red); box-shadow: 0 0 4px color-mix(in srgb, var(--ctp-red) 50%, transparent); } + .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } + .dot.available { background: var(--ctp-green); box-shadow: 0 0 4px color-mix(in srgb, var(--ctp-green) 50%, transparent); } + .dot.unavailable { background: var(--ctp-red); box-shadow: 0 0 4px color-mix(in srgb, var(--ctp-red) 50%, transparent); } .keyring-label { font-size: 0.75rem; color: var(--ctp-subtext0); } - - .secrets-warning { - display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.625rem; - background: color-mix(in srgb, var(--ctp-red) 8%, var(--ctp-surface0)); - border: 1px solid color-mix(in srgb, var(--ctp-red) 30%, var(--ctp-surface1)); - border-radius: 0.375rem; color: var(--ctp-red); font-size: 0.75rem; line-height: 1.4; - } - .secrets-warning svg { flex-shrink: 0; } - - .secrets-list { display: flex; flex-direction: column; gap: 0.375rem; margin-bottom: 0.625rem; } - .secret-row { - display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.625rem; - background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); - border-radius: 0.375rem; transition: border-color 0.15s; - } - .secret-row:hover { border-color: var(--ctp-surface2); } + .warning-box { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.625rem; background: color-mix(in srgb, var(--ctp-red) 8%, var(--ctp-surface0)); border: 1px solid color-mix(in srgb, var(--ctp-red) 30%, var(--ctp-surface1)); border-radius: 0.375rem; color: var(--ctp-red); font-size: 0.75rem; line-height: 1.4; } + .warning-box svg { flex-shrink: 0; } .secret-info { display: flex; flex-direction: column; gap: 0.0625rem; min-width: 0; flex-shrink: 0; } - .secret-key-name { font-size: 0.78rem; font-weight: 600; color: var(--ctp-text); white-space: nowrap; } - .secret-key-id { font-size: 0.625rem; color: var(--ctp-overlay0); font-family: var(--term-font-family, monospace); } - .secret-value-area { flex: 1; min-width: 0; display: flex; align-items: center; } - .secret-masked { color: var(--ctp-overlay0); font-size: 0.75rem; letter-spacing: 0.1em; } - .secret-value-input { - width: 100%; padding: 0.25rem 0.5rem; background: var(--ctp-base); border: 1px solid var(--ctp-surface1); - border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.75rem; font-family: var(--term-font-family, monospace); - } - .secret-actions { display: flex; gap: 0.25rem; flex-shrink: 0; } - .secret-btn { - display: flex; align-items: center; justify-content: center; width: 1.75rem; height: 1.75rem; - background: transparent; border: 1px solid var(--ctp-surface1); border-radius: 0.25rem; - color: var(--ctp-overlay1); cursor: pointer; transition: color 0.15s, background 0.15s, border-color 0.15s; - } - .secret-btn:hover { color: var(--ctp-text); background: var(--ctp-surface0); border-color: var(--ctp-surface2); } - .secret-btn-danger:hover { - color: var(--ctp-red); background: color-mix(in srgb, var(--ctp-red) 8%, transparent); - border-color: color-mix(in srgb, var(--ctp-red) 30%, var(--ctp-surface1)); - } - .secret-add-form { padding: 0.5rem 0.625rem; background: var(--ctp-mantle); border: 1px dashed var(--ctp-surface1); border-radius: 0.375rem; } - .secret-add-row { display: flex; gap: 0.375rem; align-items: stretch; } - .secret-key-dropdown { min-width: 10rem; flex-shrink: 0; } - .secret-key-hint { font-size: 0.625rem; color: var(--ctp-overlay0); font-family: var(--term-font-family, monospace); margin-left: auto; padding-left: 0.5rem; } - .secret-value-new { - flex: 1; min-width: 0; padding: 0.375rem 0.625rem; background: var(--ctp-surface0); - border: 1px solid var(--ctp-surface1); border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.8rem; - } - .secret-value-new:disabled { opacity: 0.4; cursor: not-allowed; } - .secret-value-new:focus { border-color: var(--ctp-blue); outline: none; } + .secret-val { flex: 1; min-width: 0; display: flex; align-items: center; } + .masked { color: var(--ctp-overlay0); font-size: 0.75rem; letter-spacing: 0.1em; } + .val-input { width: 100%; padding: 0.25rem 0.5rem; background: var(--ctp-base); border: 1px solid var(--ctp-surface1); border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.75rem; font-family: var(--term-font-family, monospace); } - /* --- Dropdown (custom select) --- */ + /* Dropdown */ .custom-dropdown { position: relative; } - .dropdown-trigger { - display: flex; align-items: center; gap: 0.5rem; width: 100%; padding: 0.375rem 0.625rem; - background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); border-radius: 0.25rem; - color: var(--ctp-text); font-size: 0.8rem; cursor: pointer; text-align: left; height: 100%; - } - .dropdown-trigger:hover { border-color: var(--ctp-surface2); } - .dropdown-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .dropdown-arrow { color: var(--ctp-overlay0); font-size: 0.7rem; flex-shrink: 0; } - .dropdown-menu { - position: absolute; top: calc(100% + 0.25rem); left: 0; min-width: 100%; width: max-content; - max-height: 22.5rem; overflow-y: auto; background: var(--ctp-mantle); border: 1px solid var(--ctp-surface1); - border-radius: 0.25rem; box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.4); z-index: 100; padding: 0.25rem 0; - } - .dropdown-option { - display: flex; align-items: center; gap: 0.5rem; width: 100%; padding: 0.3125rem 0.625rem; - background: transparent; border: none; color: var(--ctp-subtext1); font-size: 0.8rem; cursor: pointer; text-align: left; white-space: nowrap; - } - .dropdown-option:hover { background: var(--ctp-surface0); color: var(--ctp-text); } - .dropdown-option.active { background: var(--ctp-surface0); color: var(--ctp-text); font-weight: 600; } - .dropdown-option-label { flex: 1; } - .dropdown-empty { display: block; padding: 0.375rem 0.625rem; font-size: 0.75rem; color: var(--ctp-overlay0); font-style: italic; } + .key-dropdown { min-width: 10rem; flex-shrink: 0; } + .dd-trigger { display: flex; align-items: center; gap: 0.5rem; width: 100%; padding: 0.375rem 0.625rem; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.8rem; cursor: pointer; text-align: left; height: 100%; } + .dd-trigger:hover { border-color: var(--ctp-surface2); } + .dd-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .dd-arrow { color: var(--ctp-overlay0); font-size: 0.7rem; flex-shrink: 0; } + .dd-menu { position: absolute; top: calc(100% + 0.25rem); left: 0; min-width: 100%; width: max-content; max-height: 22.5rem; overflow-y: auto; background: var(--ctp-mantle); border: 1px solid var(--ctp-surface1); border-radius: 0.25rem; box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.4); z-index: 100; padding: 0.25rem 0; } + .dd-opt { display: flex; align-items: center; gap: 0.5rem; width: 100%; padding: 0.3125rem 0.625rem; background: transparent; border: none; color: var(--ctp-subtext1); font-size: 0.8rem; cursor: pointer; text-align: left; white-space: nowrap; } + .dd-opt:hover { background: var(--ctp-surface0); color: var(--ctp-text); } + .dd-opt.active { background: var(--ctp-surface0); color: var(--ctp-text); font-weight: 600; } + .dd-opt-label { flex: 1; } + .dd-opt-hint { font-size: 0.625rem; color: var(--ctp-overlay0); font-family: var(--term-font-family, monospace); margin-left: auto; padding-left: 0.5rem; } + .dd-empty { display: block; padding: 0.375rem 0.625rem; font-size: 0.75rem; color: var(--ctp-overlay0); font-style: italic; } - /* --- Branch Policies --- */ - .policy-list { display: flex; flex-direction: column; gap: 0.375rem; margin-bottom: 0.625rem; } - .policy-row { - display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.625rem; - background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); - border-radius: 0.375rem; transition: border-color 0.15s; - } - .policy-row:hover { border-color: var(--ctp-surface2); } - .policy-pattern { flex: 1; font-size: 0.78rem; font-family: var(--term-font-family, monospace); color: var(--ctp-text); } - .policy-action { - font-size: 0.625rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; - padding: 0.125rem 0.375rem; border-radius: 0.125rem; - } - .policy-action.warn { background: color-mix(in srgb, var(--ctp-yellow) 15%, transparent); color: var(--ctp-yellow); } - .policy-action.block { background: color-mix(in srgb, var(--ctp-red) 15%, transparent); color: var(--ctp-red); } - .policy-add-form { padding: 0.5rem 0.625rem; background: var(--ctp-mantle); border: 1px dashed var(--ctp-surface1); border-radius: 0.375rem; } - .policy-add-row { display: flex; gap: 0.375rem; align-items: stretch; } - .policy-input { - flex: 1; min-width: 0; padding: 0.375rem 0.625rem; background: var(--ctp-surface0); - border: 1px solid var(--ctp-surface1); border-radius: 0.25rem; color: var(--ctp-text); - font-size: 0.8rem; font-family: var(--term-font-family, monospace); - } - .policy-input:focus { border-color: var(--ctp-blue); outline: none; } + /* Branch policy badge */ + .badge { font-size: 0.625rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; padding: 0.125rem 0.375rem; border-radius: 0.125rem; } + .badge.warn { background: color-mix(in srgb, var(--ctp-yellow) 15%, transparent); color: var(--ctp-yellow); } + .badge.block { background: color-mix(in srgb, var(--ctp-red) 15%, transparent); color: var(--ctp-red); }