Skip to content

Commit d46e214

Browse files
committed
Ignore redacted dashboard admin keys
1 parent e09d718 commit d46e214

2 files changed

Lines changed: 22 additions & 5 deletions

File tree

src/dashboard.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,14 @@ const initialConfig=${scriptJson(initialConfig)};
4949
let cfg=initialConfig, dirty=false;
5050
const $=id=>document.getElementById(id); const esc=v=>String(v??'').replace(/[&<>"']/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch])); const toast=t=>{ $('toast').textContent=t; $('toast').classList.add('show'); setTimeout(()=>$('toast').classList.remove('show'),2500); };
5151
const expiredIds=new Set();
52-
function fullBridgeKey(){const raw=($('bridgeApiKey')?.value||'').trim(); if(!raw)return ''; return raw.startsWith('sk-')?raw:'sk-'+raw;}
53-
function displayBridgeKey(key){return key?.startsWith('sk-')?key.slice(3):(key||'')}
54-
function auth(){const key=cfg?.bridgeApiKey||localStorage.getItem('bridgeApiKey')||fullBridgeKey()||''; return key?{'authorization':'Bearer '+key,'content-type':'application/json'}:{'content-type':'application/json'}}
52+
function isRedactedSecret(value){const v=String(value||'').trim(); return !v||v==='[REDACTED]'||v==='sk-[REDACTED]'||v.includes('…')||v.includes('...');}
53+
function fullBridgeKey(){const raw=($('bridgeApiKey')?.value||'').trim(); if(!raw||isRedactedSecret(raw))return ''; return raw.startsWith('sk-')?raw:'sk-'+raw;}
54+
function displayBridgeKey(key){return isRedactedSecret(key)?'':(key?.startsWith('sk-')?key.slice(3):(key||''))}
55+
function authKey(value){return isRedactedSecret(value)?'':String(value||'').trim();}
56+
function auth(){const key=authKey(cfg?.bridgeApiKey)||authKey(localStorage.getItem('bridgeApiKey'))||fullBridgeKey()||''; return key?{'authorization':'Bearer '+key,'content-type':'application/json'}:{'content-type':'application/json'}}
5557
function duplicateCredentialMessage(e){const text=String(e?.message||e||''); return text.includes('duplicate_commandcode_api_key')||text.includes('Duplicate CommandCode API key')?'이미 등록된 키입니다. 다른 CommandCode API key를 입력해주세요.':null;}
5658
function preventDuplicateVisibleKey(input){const value=input.value.trim(); if(!value)return false; const dup=Array.from(document.querySelectorAll('[data-ckey]')).some(el=>el!==input&&el.value.trim()===value); if(!dup)return false; const i=+input.dataset.ckey; input.value=''; if(cfg?.credentials?.[i])cfg.credentials[i].apiKey=''; toast('이미 등록된 키입니다. 입력하지 않았습니다.'); return true;}
57-
function syncBridgeKey(){const el=$('bridgeApiKey'); if(!el)return; const configured=cfg?.bridgeApiKey||''; const stored=localStorage.getItem('bridgeApiKey')||''; const key=configured||stored; if(configured&&stored!==configured)localStorage.setItem('bridgeApiKey',configured); if(document.activeElement!==el) el.value=displayBridgeKey(key); const wrap=$('bridgeKeyWrap'); if(wrap) wrap.style.display=$('bindHost')?.value==='0.0.0.0'?'grid':'none';}
59+
function syncBridgeKey(){const el=$('bridgeApiKey'); if(!el)return; const configured=authKey(cfg?.bridgeApiKey); const stored=authKey(localStorage.getItem('bridgeApiKey')); const key=configured||stored; if(configured&&stored!==configured)localStorage.setItem('bridgeApiKey',configured); if(document.activeElement!==el) el.value=displayBridgeKey(key); const wrap=$('bridgeKeyWrap'); if(wrap) wrap.style.display=$('bindHost')?.value==='0.0.0.0'?'grid':'none';}
5860
function setDirty(v=true){dirty=v; $('dirtyText').textContent=v?'pending changes · restart required':'no pending changes'; updateRestart();}
5961
function updateRestart(){const online=$('online').textContent==='online'; $('restart').disabled=online&&!dirty; $('restart').classList.toggle('active',!$('restart').disabled)}
6062
async function fetchJson(path,opt={}){const init={...opt,cache:'no-store',headers:{...auth(),...(opt.headers||{})}}; try{const r=await fetch(path,init); if(!r.ok) throw new Error(await r.text()); return r.json();}catch(e){if(location.hostname&&!location.port){const r=await fetch('http://'+location.hostname+':9992'+path,init); if(!r.ok) throw new Error(await r.text()); return r.json();}throw e;}}

tests/dashboard-ui.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,22 @@ describe("dashboard UI", () => {
116116
bridgeApiKey: "test-admin-token",
117117
});
118118

119-
expect(html).toContain("cfg?.bridgeApiKey||localStorage.getItem('bridgeApiKey')");
119+
expect(html).toContain("authKey(cfg?.bridgeApiKey)||authKey(localStorage.getItem('bridgeApiKey'))");
120120
expect(html).toContain("'authorization':'Bearer '+key");
121121
});
122+
123+
it("does not treat redacted admin API key values as usable write credentials", () => {
124+
const html = dashboardHtml({
125+
server: { host: "0.0.0.0", port: 9992 },
126+
routing: { policy: "daily_burn_priority", maxInFlightPerCredential: 4 },
127+
credentials: [],
128+
models: [],
129+
bridgeApiKey: "[REDACTED]",
130+
});
131+
132+
expect(html).toContain("function isRedactedSecret");
133+
expect(html).toContain("authKey(cfg?.bridgeApiKey)");
134+
expect(html).toContain("const configured=authKey(cfg?.bridgeApiKey)");
135+
expect(html).toContain("return isRedactedSecret(key)?''");
136+
});
122137
});

0 commit comments

Comments
 (0)