fix: 修复云端聊天鉴权及容器兼容并支持3200原地升级

This commit is contained in:
NathanAlex7
2026-09-14 11:53:29 +08:00
parent af1a1027d7
commit c556172a1d
11 changed files with 97 additions and 13 deletions
+35 -5
View File
@@ -4,10 +4,10 @@ const path = require('node:path');
const vm = require('node:vm');
const ts = require('typescript');
const root = path.resolve(__dirname, '..');
function load(relative, overrides = {}) {
function load(relative, overrides = {}, globals = {}) {
const compiled = ts.transpileModule(fs.readFileSync(path.join(root,relative),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2020}}).outputText;
const exports = {};
vm.runInNewContext(compiled,{exports,require:id=>overrides[id]||require(id),process,fetch:global.fetch,Headers,Response,AbortController,TextDecoder,TextEncoder,setTimeout,clearTimeout,URL});
vm.runInNewContext(compiled,{exports,require:id=>overrides[id]||require(id),process,fetch:global.fetch,Headers,Response,AbortController,TextDecoder,TextEncoder,setTimeout,clearTimeout,URL,...globals});
return exports;
}
const {ChatEventDecoder,readChatStream} = load('src/lib/chat-stream.ts');
@@ -44,19 +44,49 @@ function responder(chunks) {return async()=>new Response(new ReadableStream({sta
const NextResponse = class extends Response {static json(data,init){return new Response(JSON.stringify(data),{...init,headers:{'Content-Type':'application/json'}});}};
const oldFetch=global.fetch, oldBase=process.env.RISK_AGENT_BACKEND_URL, oldCore=process.env.CORE_BACKEND_URL;
const savedEnv = Object.fromEntries(['NODE_ENV','XINGBANG_REQUIRE_AUTH','XINGBANG_AUTH_USERS'].map(k=>[k,process.env[k]]));
try {
process.env.NODE_ENV='development';delete process.env.XINGBANG_REQUIRE_AUTH;delete process.env.XINGBANG_AUTH_USERS;
delete process.env.CORE_BACKEND_URL;process.env.RISK_AGENT_BACKEND_URL='http://127.0.0.1:8003';
let target, options;
const upstream=new Response(new ReadableStream({start(c){c.enqueue(encode(event('token',{content:'stream'})));}}),{headers:{'Content-Type':'text/event-stream'}});
global.fetch=async(url,opts)=>{target=url;options=opts;return upstream;};
const route=load('src/app/api/core/[...path]/route.ts',{'next/server':{NextResponse}});
const request={nextUrl:{hostname:'127.0.0.1',search:'?id=abc'},headers:new Headers(),method:'POST',arrayBuffer:async()=>encode('{}'),signal:new AbortController().signal};
const request={nextUrl:{hostname:'0.0.0.0',search:'?id=abc'},headers:new Headers({Host:'127.0.0.1:3000'}),method:'POST',arrayBuffer:async()=>encode('{}'),signal:new AbortController().signal};
const response=await route.POST(request,{params:{path:['chat']}});
assert.equal(target,'http://127.0.0.1:8003/api/chat?id=abc');assert.equal(response.body,upstream.body);assert.equal(options.signal,request.signal);
const reader=response.body.getReader();assert.ok((await reader.read()).value.length);await reader.cancel();
assert.equal((await route.GET({...request,nextUrl:{hostname:'example.org'}},{params:{path:['chat']}})).status,403);
assert.equal((await route.GET({...request,headers:new Headers({Host:'example.org'})},{params:{path:['chat']}})).status,403);
assert.equal((await route.GET({...request,headers:new Headers({Host:'localhost:3000',Origin:'https://evil.example'})},{params:{path:['chat']}})).status,403);
assert.equal((await route.GET(request,{params:{path:['chat','..']}})).status,404);
assert.equal((await route.GET(request,{params:{path:['unknown']}})).status,404);
} finally {global.fetch=oldFetch;if(oldBase===undefined)delete process.env.RISK_AGENT_BACKEND_URL;else process.env.RISK_AGENT_BACKEND_URL=oldBase;if(oldCore===undefined)delete process.env.CORE_BACKEND_URL;else process.env.CORE_BACKEND_URL=oldCore;}
process.env.NODE_ENV='production';
assert.equal((await route.GET(request,{params:{path:['sessions']}})).status,401);
for (const identity of [{id:'a',role:'admin'},{id:'r',role:'reviewer'},{id:'m',role:'manager'},{id:'p',role:'admin',preview:true},{}]) {
let forwarded=false;
global.fetch=async(url,opts)=>{
if(url.endsWith('/access/context')) {assert.equal(opts.headers.Authorization,'Bearer test-admin');return Response.json(identity);}
forwarded=true;return new Response(event('done',{content:'ok'}),{headers:{'Content-Type':'text/event-stream'}});
};
const secured=load('src/app/api/core/[...path]/route.ts',{'next/server':{NextResponse}});
const r=await secured.POST({...request,headers:new Headers({Host:'cloud.example:3200',Authorization:'Bearer test-admin'})},{params:{path:['chat']}});
const permitted=identity.role==='admin'&&!identity.preview&&!!identity.id;
assert.equal(r.status,permitted?200:403);assert.equal(forwarded,permitted);
}
for (const status of [401,403,500]) {
global.fetch=async()=>new Response('{}',{status});
const secured=load('src/app/api/core/[...path]/route.ts',{'next/server':{NextResponse}});
assert.equal((await secured.GET({...request,headers:new Headers({Authorization:'Bearer invalid'})},{params:{path:['sessions']}})).status,status===500?503:status);
}
let forwardedHeaders;
global.fetch=async(_url,opts)=>{forwardedHeaders=opts.headers;return Response.json({sessions:[]});};
const {coreFetch}=load('src/lib/core-fetch.ts',{}, {window:{sessionStorage:{getItem:key=>key==='xingbang-access-token'?'existing-token':null}}});
await coreFetch('/api/core/sessions',{headers:{'Content-Type':'application/json'}});
assert.equal(forwardedHeaders.get('Authorization'),'Bearer existing-token');
assert.equal(forwardedHeaders.get('Content-Type'),'application/json');
assert.ok(source.includes('{fetcher:fetch}'));assert.ok(source.includes('coreFetch as fetch'));
const {newRequestId}=load('src/lib/request-id.ts',{}, {crypto:{getRandomValues:require('node:crypto').webcrypto.getRandomValues.bind(require('node:crypto').webcrypto)}});
assert.match(newRequestId(),/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/);assert.notEqual(newRequestId(),newRequestId());
} finally {for(const [k,v] of Object.entries(savedEnv)){if(v===undefined)delete process.env[k];else process.env[k]=v;}global.fetch=oldFetch;if(oldBase===undefined)delete process.env.RISK_AGENT_BACKEND_URL;else process.env.RISK_AGENT_BACKEND_URL=oldBase;if(oldCore===undefined)delete process.env.CORE_BACKEND_URL;else process.env.CORE_BACKEND_URL=oldCore;}
console.log('PASS: all SSE split boundaries, CRLF, multiline, final-only, empty, timeout, premature EOF, errors, streaming proxy and local-only guard');
})().catch(error=>{console.error(error);process.exitCode=1;});