mirror of
https://github.com/sune-org/sune.git
synced 2026-10-29 04:54:51 +00:00
Make reply ids unguessable and keep them out of the socket URL
Reply ids, which double as the proxy's run id, are now the same base36 ms timestamp followed by a dash and 128 random bits from crypto.getRandomValues, so a run can't be found by guessing. Saved replies keep their fields and the timestamp prefix. The id is sent inside begin, resume and stop instead of the URL, and SECURITY.md now says ntfy pings carry only the timestamp half. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -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 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).
|
||||||
|
|||||||
+6
-6
@@ -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
|
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. */
|
stream:{seq} (the last chunk it holds) until it's finished. */
|
||||||
const live=new Map();
|
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 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 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);
|
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
|
// Stops the run server-side too; without an open socket a throwaway one carries the stop
|
||||||
function sendStop(s){
|
function sendStop(s){
|
||||||
if(s.ws?.readyState===1)return s.ws.send('{"type":"stop"}');
|
const m=JSON.stringify({type:'stop',rid:s.rid});
|
||||||
const w=new WebSocket(wsUrl(s.rid));w.onopen=()=>{w.send('{"type":"stop"}');w.close()}
|
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
|
// For a deleted message or thread: stop without saving anything
|
||||||
@@ -130,8 +130,8 @@ function onMessage(s,m){
|
|||||||
|
|
||||||
function connect(s){
|
function connect(s){
|
||||||
if(s.done)return;clearTimeout(s.timer);
|
if(s.done)return;clearTimeout(s.timer);
|
||||||
const ws=s.ws=new WebSocket(wsUrl(s.rid));
|
const ws=s.ws=new WebSocket(WS_URL);
|
||||||
ws.onopen=()=>ws.send(s.req||JSON.stringify({type:'resume',after:s.msg.stream.seq}));
|
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.onmessage=e=>{if(s.ws!==ws)return;let m;try{m=JSON.parse(e.data)}catch{return}onMessage(s,m)};
|
||||||
ws.onclose=()=>{
|
ws.onclose=()=>{
|
||||||
if(s.ws!==ws||s.done)return;s.ws=null;retry(s)
|
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 apiKey=provider==='openai'?USER.apiKeyOpenAI:provider==='google'?USER.apiKeyGoogle:provider==='claude'?USER.apiKeyClaude:USER.apiKeyOpenRouter;
|
||||||
const s=track(msg,state.currentThreadId,threadKey(state.currentThreadId));
|
const s=track(msg,state.currentThreadId,threadKey(state.currentThreadId));
|
||||||
if(!apiKey){msg.content[0].text=window.localDemoReply();return finish(s)}
|
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.');
|
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()
|
connect(s);syncBtn()
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -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.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`;
|
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 => {
|
export const fmtSize = b => {
|
||||||
const u = ['B', 'KB', 'MB', 'GB', 'TB'];
|
const u = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
let i = 0, x = b;
|
let i = 0, x = b;
|
||||||
|
|||||||
Reference in New Issue
Block a user