Settings: temporary UAT secret-unmask gate (SLAB_UAT_UNMASK_SECRETS)

Owner-directed, LAN/HTTP dev-test only. When the env flag is set,
GET /api/v1/settings returns decrypted secret values (string) instead of
{configured: bool}; the UI prefills and shows them in clear text. Default
off -> SETTINGS-75 masking and the existing tests stay intact. Revert
before any non-UAT deployment.
This commit is contained in:
2026-09-20 07:38:38 -07:00
parent e51753b6d0
commit 34ee7eff60
2 changed files with 49 additions and 37 deletions
+14
View File
@@ -59,10 +59,24 @@ def get_settings() -> dict[str, Any]:
result: dict[str, Any] = {}
result["woo_base_url"] = os.environ.get("WOO_BASE_URL", "")
# TEMPORARY UAT override (owner-directed, LAN/HTTP only): when set, secrets are
# returned as their decrypted plaintext so they can be inspected on the dev-test
# box. Default OFF -> SETTINGS-75 masking intact. Remove before prod.
unmask = os.environ.get("SLAB_UAT_UNMASK_SECRETS") == "1"
for k in sorted(ALL_KEYS):
row = rows.get(k, {})
if k in SECRET_KEYS:
enc = row.get("value_enc")
if unmask:
if enc:
try:
result[k] = decrypt(enc)
except Exception:
result[k] = ""
else:
result[k] = ""
else:
if enc:
try:
decrypt(enc) # verify roundtrip possible
+28 -30
View File
@@ -2,12 +2,14 @@
import { onMount } from 'svelte';
interface SecretState { configured: boolean }
// Masked mode: object {configured}. Unmasked UAT mode (SLAB_UAT_UNMASK_SECRETS): plain string.
type Secret = SecretState | string;
interface Settings {
woo_base_url: string;
woo_wp_username: string | null;
woo_app_password: SecretState;
inference_base_url: SecretState;
inference_api_key: SecretState;
woo_app_password: Secret;
inference_base_url: Secret;
inference_api_key: Secret;
inference_model: string | null;
inference_enabled: boolean;
content_llm_enabled: boolean;
@@ -24,6 +26,11 @@
// local edit buffers (write-only for secrets)
let form: any = {};
// Unmasked UAT mode returns the secret as a plain string; masked mode returns {configured}.
const isUnmasked = (s: Secret) => typeof s === 'string';
const secretValue = (s: Secret): string => (isUnmasked(s) ? (s as string) : '');
const secretConfigured = (s: Secret): boolean => (isUnmasked(s) ? (s as string).length > 0 : s.configured);
async function load() {
loading = true;
try {
@@ -39,10 +46,10 @@
brand_voice: settings.brand_voice || '',
geo_context: settings.geo_context || '',
woo_create_status: settings.woo_create_status || 'draft',
// secrets: only send if user types
woo_app_password: '',
inference_base_url: '',
inference_api_key: '',
// secrets: prefill when unmasked (UAT), else blank (masked, write-only)
woo_app_password: secretValue(settings.woo_app_password),
inference_base_url: secretValue(settings.inference_base_url),
inference_api_key: secretValue(settings.inference_api_key),
};
}
} catch (e) {
@@ -77,10 +84,10 @@
if (res.ok) {
settings = await res.json();
message = 'Saved';
// clear secret inputs after save
form.woo_app_password = '';
form.inference_base_url = '';
form.inference_api_key = '';
// refresh secret buffers from the (possibly unmasked) server state
form.woo_app_password = secretValue(settings.woo_app_password);
form.inference_base_url = secretValue(settings.inference_base_url);
form.inference_api_key = secretValue(settings.inference_api_key);
} else {
const err = await res.json();
message = err.error?.message || 'Save failed';
@@ -124,18 +131,18 @@
</div>
<div>
<label>Woo App Password {settings.woo_app_password.configured ? '(configured)' : '(not set)'}</label>
<input type="password" bind:value={form.woo_app_password} placeholder={settings.woo_app_password.configured ? '••••••••' : ''} />
<label>Woo App Password {secretConfigured(settings.woo_app_password) ? '(configured)' : '(not set)'}</label>
<input type={isUnmasked(settings.woo_app_password) ? 'text' : 'password'} bind:value={form.woo_app_password} placeholder={isUnmasked(settings.woo_app_password) ? '' : '••••••••'} />
</div>
<div>
<label>Inference Base URL {settings.inference_base_url.configured ? '(configured)' : '(not set)'}</label>
<input bind:value={form.inference_base_url} placeholder={settings.inference_base_url.configured ? '••••••••' : ''} />
<label>Inference Base URL {secretConfigured(settings.inference_base_url) ? '(configured)' : '(not set)'}</label>
<input bind:value={form.inference_base_url} placeholder={isUnmasked(settings.inference_base_url) ? '' : '••••••••'} />
</div>
<div>
<label>Inference API Key {settings.inference_api_key.configured ? '(configured)' : '(not set)'}</label>
<input type="password" bind:value={form.inference_api_key} placeholder={settings.inference_api_key.configured ? '••••••••' : ''} />
<label>Inference API Key {secretConfigured(settings.inference_api_key) ? '(configured)' : '(not set)'}</label>
<input type={isUnmasked(settings.inference_api_key) ? 'text' : 'password'} bind:value={form.inference_api_key} placeholder={isUnmasked(settings.inference_api_key) ? '' : '••••••••'} />
</div>
<div>
@@ -182,17 +189,8 @@
<style>
.settings { max-width: 600px; color: #eee; }
.settings label { display: block; margin: 8px 0 4px; }
.settings input,
.settings select {
color: #eee;
background: #1c1c1c;
width: 100%;
padding: 6px;
}
.settings option {
color: #eee;
background: #1c1c1c;
}
.settings button { margin-top: 16px; }
label { display: block; margin: 8px 0 4px; }
input, select, option { color: #eee; background: #1c1c1c; width: 100%; padding: 6px; }
option { color: #eee; background: #1c1c1c; }
button { margin-top: 16px; }
</style>