38 lines
5.5 KiB
JavaScript
38 lines
5.5 KiB
JavaScript
import http from 'node:http';import fs from 'node:fs';import crypto from 'node:crypto';import {pathToFileURL} from 'node:url';
|
|
import {ZERO,hash,validate,reply} from './contract.mjs';
|
|
const need=(v,c)=>{if(!v)throw Error(c);};
|
|
export function authenticate(raw,config,now=Date.now()){
|
|
if(typeof raw!=='string'||!raw.startsWith('Bearer '))return null;const digest=hash(raw.slice(7));
|
|
return config.clients?.find(c=>/^[a-f0-9]{64}$/.test(c.key_sha256||'')&&crypto.timingSafeEqual(Buffer.from(digest,'hex'),Buffer.from(c.key_sha256,'hex'))&&Date.parse(c.expires_at)>now)||null;
|
|
}
|
|
async function json(url,options={}){const r=await fetch(url,{...options,redirect:'error',signal:AbortSignal.timeout(55000)});need(r.ok,'ZERO_UPSTREAM_UNAVAILABLE');return r.json();}
|
|
export async function zeroQuestion(text,{get=json}={}){
|
|
const before=await get('http://127.0.0.1:3931/status');need(before.brain_id===ZERO&&before.integrity==='BRAIN_VERIFIED'&&before.node_role==='REPLICA_READ_ONLY'&&before.brain_write_allowed===false,'ZERO_IDENTITY_OR_ROLE_INVALID');
|
|
const r=await get('http://127.0.0.1:3931/ask',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({text})});
|
|
need(r.schema==='guanghu.zeropoint-channel-ask-receipt/v1'&&r.no_state_change===true&&r.brain_turn?.speaker?.brain_id===ZERO&&r.brain_turn.brain_revision===before.revision&&r.brain_turn.no_state_change===true&&r.brain_turn.input_sha256===hash(text)&&r.brain_turn.reply_sha256===hash(r.spoken)&&r.brain_turn.reply===r.spoken&&r.brain_turn?.outcome==='BRAIN_REPLY_ACCEPTED'&&typeof r.spoken==='string'&&r.spoken.trim(),'ZERO_REPLY_WITHHELD');
|
|
const after=await get('http://127.0.0.1:3931/status');need(after.brain_id===before.brain_id&&after.integrity==='BRAIN_VERIFIED','ZERO_IDENTITY_CHANGED');
|
|
need(after.node_role==='REPLICA_READ_ONLY'&&after.brain_write_allowed===false&&after.revision===before.revision,'ZERO_SOURCE_CHANGED');
|
|
return {brain_id:ZERO,revision:before.revision,answer:r.spoken,no_state_change:true};
|
|
}
|
|
export function createReceiver({config,ask=zeroQuestion,rules=async()=>{const r=await json('http://127.0.0.1:3922/v1/manifest');need(typeof r.version==='string'&&/^[a-f0-9]{64}$/.test(r.bundle_sha256),'RULE_RELEASE_INVALID');return {version:r.version,bundle_sha256:r.bundle_sha256,bundle_url:r.bundle_url,automatic_adoption:false};},now=Date.now,capacity=256}={}){
|
|
const messages=new Map(),buckets=new Map();let active=0;
|
|
const send=(res,status,data)=>{res.writeHead(status,{'content-type':'application/json; charset=utf-8','cache-control':'no-store','x-content-type-options':'nosniff'});res.end(JSON.stringify(data));};
|
|
const server=http.createServer(async(req,res)=>{try{
|
|
if(req.method==='GET'&&req.url==='/health')return send(res,200,{state:'GLP_RECEIVER_READY',profile:'FEISHU_ZERO_READONLY_1',production_feishu_connected:false,no_execution_authority:true});
|
|
if(req.method!=='POST'||req.url!=='/v1/glp')return send(res,404,{error:'ROUTE_NOT_REGISTERED'});
|
|
const c=authenticate(req.headers.authorization,config);if(!c)return send(res,401,{error:'GLP_AUTH_REQUIRED'});
|
|
if(!String(req.headers['content-type']||'').startsWith('application/json'))return send(res,415,{error:'JSON_REQUIRED'});
|
|
const chunks=[];let size=0;for await(const b of req){size+=b.length;if(size>24000)return send(res,413,{error:'GLP_BODY_TOO_LARGE'});chunks.push(b);}
|
|
let input,m;try{input=JSON.parse(Buffer.concat(chunks));m=validate(input,c,now());}catch(e){return send(res,400,{error:/^[A-Z_]+$/.test(e.message)?e.message:'GLP_JSON_INVALID'});}
|
|
const k=c.id+':'+m.message_id,digest=hash(input),old=messages.get(k);if(old){if(old.digest!==digest)return send(res,409,{error:'GLP_MESSAGE_ID_REBOUND'});const r=await old.promise;return send(res,r.status,r.value);}
|
|
for(const [id,v] of messages)if(v.expires<=now())messages.delete(id);
|
|
const bucket=buckets.get(c.id)||{start:now(),count:0};if(now()-bucket.start>=60000){bucket.start=now();bucket.count=0;}if(bucket.count>=12||active>=2)return send(res,429,{error:'GLP_CAPACITY_LIMIT'});if(messages.size>=capacity)return send(res,503,{error:'GLP_REPLAY_CACHE_FULL'});bucket.count++;buckets.set(c.id,bucket);active++;
|
|
const ack=reply(m,'ACK',{status:'ACK-RECEIVED'});const promise=(async()=>{try{
|
|
const result=m.message_type==='REQUEST'?await ask(m.payload.content):await rules();
|
|
const response=reply(m,'RESPONSE',result),receipt=reply(m,'RECEIPT',{status:'ACK-EXECUTED',operation:m.message_type==='REQUEST'?'READ_ONLY_ZERO_QUESTION':'RULE_VERSION_QUERY',no_neural_state_write:true,no_execution_authority:true});
|
|
return {status:200,value:{schema:'guanghu.glp-readonly-exchange/v1',request_id:m.message_id,ack,response,...(m.control.receipt_required?{receipt}:{}),no_execution_authority:true}};
|
|
}catch{return {status:502,value:{error:'GLP_UPSTREAM_UNAVAILABLE',request_id:m.message_id,ack:reply(m,'ACK',{status:'ACK-RECEIVED'}),receipt:reply(m,'RECEIPT',{status:'ACK-FAILED',reply_withheld:true}),no_execution_authority:true}};}finally{active--;}})();
|
|
messages.set(k,{digest,promise,expires:Date.parse(m.control.expires_at)});const r=await promise;send(res,r.status,r.value);
|
|
}catch{if(!res.headersSent)send(res,503,{error:'GLP_RECEIVER_UNAVAILABLE'});else res.destroy();}});server.requestTimeout=65000;server.headersTimeout=10000;return server;
|
|
}
|
|
if(process.argv[1]&&import.meta.url===pathToFileURL(process.argv[1]).href){const file=process.env.GUANGHU_GLP_CONFIG;need(file,'GLP_CONFIG_REQUIRED');createReceiver({config:JSON.parse(fs.readFileSync(file))}).listen(Number(process.env.GUANGHU_GLP_PORT||3926),'127.0.0.1');}
|