diff --git a/SECURITY.md b/SECURITY.md index 225dbdb..063f804 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,4 +8,4 @@ Streaming is relayed through [`us.proxy.sune.chat`](https://github.com/sune-org/ **What it doesn't do:** no prompt logging (your messages are held in memory only while the run is in progress; the generated reply stays in memory for 90 minutes after it ends so you can reconnect, and nothing is written to disk), no reading your chats, no storing keys, no third-party sharing. -**What I see:** an [ntfy](https://ntfy.sh) ping when a run ends/fails, containing only: run ID, `[provider/model]`, duration, error message (if any), and the sune's non-empty generation settings (temperature, top_p, top_k, frequency/repetition penalty, min_p, top_a, max tokens, reasoning effort, show thoughts, image output/aspect ratio/size). No prompts (system or master prompts included), no responses, no IP, no key. Source is public — audit it [here](https://github.com/sune-org/us.proxy.sune.chat). +**What I see:** an [ntfy](https://ntfy.sh) ping when a run ends/fails, containing only: the run's start time (the timestamp half of its ID, not the secret half), `[provider/model]`, duration, error message (if any), and the sune's non-empty generation settings (temperature, top_p, top_k, frequency/repetition penalty, min_p, top_a, max tokens, reasoning effort, show thoughts, image output/aspect ratio/size). No prompts (system or master prompts included), no responses, no IP, no key. Source is public — audit it [here](https://github.com/sune-org/us.proxy.sune.chat). diff --git a/src/streaming.js b/src/streaming.js index 0336e54..aff90b1 100644 --- a/src/streaming.js +++ b/src/streaming.js @@ -53,7 +53,6 @@ export const buildBody=()=>{ you're elsewhere, reconnect on drops, and get picked back up after a reload. A streaming message carries stream:{seq} (the last chunk it holds) until it's finished. */ const live=new Map(); -const wsUrl=rid=>`${WS_URL}?rid=${encodeURIComponent(rid)}`; const threadKey=tid=>(el.threadRepoInput.value.trim().startsWith('gh://')?'rem_t_':'t_')+tid; const saveLive=()=>localStorage.setItem(LIVE_KEY,JSON.stringify([...live.values()].map(s=>[s.rid,s.tid,s.key]))); const onScreen=s=>window.state.currentThreadId===s.tid&&window.state.messages.includes(s.msg); @@ -102,8 +101,9 @@ function finish(s,note=''){ // Stops the run server-side too; without an open socket a throwaway one carries the stop function sendStop(s){ - if(s.ws?.readyState===1)return s.ws.send('{"type":"stop"}'); - const w=new WebSocket(wsUrl(s.rid));w.onopen=()=>{w.send('{"type":"stop"}');w.close()} + const m=JSON.stringify({type:'stop',rid:s.rid}); + if(s.ws?.readyState===1)return s.ws.send(m); + const w=new WebSocket(WS_URL);w.onopen=()=>{w.send(m);w.close()} } // For a deleted message or thread: stop without saving anything @@ -130,8 +130,8 @@ function onMessage(s,m){ function connect(s){ if(s.done)return;clearTimeout(s.timer); - const ws=s.ws=new WebSocket(wsUrl(s.rid)); - ws.onopen=()=>ws.send(s.req||JSON.stringify({type:'resume',after:s.msg.stream.seq})); + const ws=s.ws=new WebSocket(WS_URL); + ws.onopen=()=>ws.send(s.req||JSON.stringify({type:'resume',rid:s.rid,after:s.msg.stream.seq})); ws.onmessage=e=>{if(s.ws!==ws)return;let m;try{m=JSON.parse(e.data)}catch{return}onMessage(s,m)}; ws.onclose=()=>{ if(s.ws!==ws||s.done)return;s.ws=null;retry(s) @@ -165,7 +165,7 @@ export function startReply(msg){ const apiKey=provider==='openai'?USER.apiKeyOpenAI:provider==='google'?USER.apiKeyGoogle:provider==='claude'?USER.apiKeyClaude:USER.apiKeyOpenRouter; const s=track(msg,state.currentThreadId,threadKey(state.currentThreadId)); if(!apiKey){msg.content[0].text=window.localDemoReply();return finish(s)} - s.req=JSON.stringify({type:'begin',provider,apiKey,body:buildBody()}); + s.req=JSON.stringify({type:'begin',rid:s.rid,provider,apiKey,body:buildBody()}); if(s.req.length>MAX_REQ-(1<<20))return finish(s,'\n\nThis request is over the server’s 100 MB limit.'); connect(s);syncBtn() } diff --git a/src/utils.js b/src/utils.js index 028fbe0..fb127a7 100644 --- a/src/utils.js +++ b/src/utils.js @@ -8,7 +8,8 @@ export const positionPopover = (a, p) => { p.style.top = `${r.bottom + p.offsetHeight + 4 > window.innerHeight ? r.top - p.offsetHeight - 4 : r.bottom + 4}px`; p.style.left = `${Math.max(8, Math.min(r.right - p.offsetWidth, window.innerWidth - p.offsetWidth - 8))}px`; }; -export const sid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 6); +// Reply ids double as the proxy's run id, so after the ms timestamp come 128 random bits +export const sid = () => Date.now().toString(36) + '-' + [...crypto.getRandomValues(new Uint8Array(16))].map(b => b.toString(16).padStart(2, '0')).join(''); export const fmtSize = b => { const u = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0, x = b;