Stiglo je novo izdanje. Osvježi stranu.
Pravo na uvid u kod. Otvoren protokol omogućava nezavisne operatere i interoperabilnost. Manifest
Izvor
4d7883a894b3
- server.js
- lib/auth.js
- lib/build.js
- lib/cards.js
- lib/db.js
- lib/gradovi.js
- lib/icons.js
- lib/imena.js
- lib/jezik.js
- lib/kartapodaci.js
- lib/lekcije.js
- lib/md.js
- lib/mehanika.js
- lib/mreza.js
- lib/pages.js
- lib/registar.js
- lib/routes.js
- lib/schema2.js
- lib/views.js
- seed/content.mjs
- seed/seed.mjs
- public/app.js
- public/style.css
1 import crypto from 'node:crypto';
2 import fs from 'node:fs';
3 import path from 'node:path';
4 import {sha} from './auth.js';
5 import {checkName,normalName,validOrigin} from './imena.js';
6 import {dinar} from './db.js';
7 export const canonical=v=>v===null||typeof v!=='object'?JSON.stringify(v):Array.isArray(v)?'['+v.map(canonical).join(',')+']':'{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+canonical(v[k])).join(',')+'}';
8 export const verify=(value,sig,key)=>{try{return crypto.verify(null,Buffer.from(canonical(value)),key,Buffer.from(sig,'base64url'));}catch{return false;}};
9 export const record=r=>({name:r.name,service_id:r.service_id,owner_key:'clan:'+r.owner_id,origin:r.origin,sequence:r.sequence,expires_at:r.expires_at,policy_version:r.policy_version});
10 export const logEntry=r=>({seq:r.seq,at:r.at,action:r.action,name:r.name,actor_id:r.actor_id,payload:typeof r.payload==='string'?JSON.parse(r.payload):r.payload});
11 export function verifyChain(rows,keys){let prev='';for(const r of rows){const hash=sha(prev+canonical(logEntry(r)));if(r.prev_hash!==prev||r.hash!==hash||!verify({hash},r.sig,keys[r.key_id]))return false;prev=r.hash;}return true;}
12 export function registry(db){
13 let cache=null,cacheAt=0;
14 function key(){let k=db.get('SELECT * FROM reg_keys WHERE retired_at IS NULL ORDER BY created_at DESC LIMIT 1');if(!k)k=createKey();return {...k,private_key:fs.readFileSync(path.join(db.dataDir,`registar-kljuc-${k.key_id}.pem`),'utf8')};}
15 function createKey(){const pair=crypto.generateKeyPairSync('ed25519'),pub=pair.publicKey.export({type:'spki',format:'pem'}),id=sha(pub).slice(0,16);fs.writeFileSync(path.join(db.dataDir,`registar-kljuc-${id}.pem`),pair.privateKey.export({type:'pkcs8',format:'pem'}),{mode:0o600});db.run('INSERT INTO reg_keys VALUES(?,?,?,NULL)',id,pub,Date.now());return db.get('SELECT * FROM reg_keys WHERE key_id=?',id);}
16 function sign(value,k=key()){return crypto.sign(null,Buffer.from(canonical(value)),k.private_key).toString('base64url');}
17 function signed(value){const k=key();return {payload:value,key_id:k.key_id,signature:sign(value,k)};}
18 function log(action,name,actor,payload={}){const prev=db.get('SELECT seq,hash FROM reg_log ORDER BY seq DESC LIMIT 1'),k=key(),e={seq:(prev?.seq||0)+1,at:Date.now(),action,name,actor_id:actor,payload};const hash=sha((prev?.hash||'')+canonical(e));db.run('INSERT INTO reg_log VALUES(?,?,?,?,?,?,?,?,?,?)',e.seq,e.at,action,name,actor,canonical(payload),prev?.hash||'',hash,sign({hash},k),k.key_id);cache=null;return e.seq;}
19 function seal(name){const r=db.get('SELECT * FROM names WHERE name=?',name),k=key();db.run('UPDATE names SET sig=?,key_id=? WHERE name=?',sign(record(r),k),k.key_id,name);cache=null;return db.get('SELECT * FROM names WHERE name=?',name);}
20 function create(owner,value,{system=false,status='izdato',kind='licno',origin='',net='glavna',trust='neprovjereno',at=Date.now()}={}){
21 const n=checkName(db,value,system),seq=log(status,n.name,owner);
22 db.run('INSERT INTO names VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',n.name,n.skeleton,owner,kind,net,origin||`https://www.yugoslavia.forum/u/${db.get('SELECT handle FROM users WHERE id=?',owner).handle}`,sha(n.name).slice(0,16),status,trust,seq,at+(status==='rezervisano'?2:365)*86400000,1,at,at,'','');return seal(n.name);
23 }
24 function reserve(owner,value,net='glavna',review=false){return db.tx(()=>{const count=db.get('SELECT COUNT(*) n FROM names WHERE owner_id=?',owner).n;if(count>=6)throw Error('Najviše šest imena po članu.');if(count>=3){charge(owner,30,'ime');}return create(owner,value,{status:review?'pregled':'rezervisano',net:net==='proba'?'proba':'glavna'});});}
25 function charge(owner,amount,reason){const u=db.get('SELECT dinar FROM users WHERE id=?',owner),treasury=db.get("SELECT id FROM users WHERE handle='blagajna'");if(!u||u.dinar<amount)throw Error('Nema dovoljno probnih bodova.');dinar(db,owner,-amount,reason);dinar(db,treasury.id,amount,reason);}
26 function change(owner,name,action,opts={}){return db.tx(()=>{const r=db.get('SELECT * FROM names WHERE name=?',normalName(name));if(!r||r.owner_id!==owner)throw Error('Ime nije vaše.');if(r.kind!=='licno')throw Error('Potreban je dvojni potpis.');if(action==='izdaj'&&r.status!=='rezervisano')throw Error('Ime nije rezervisano.');if(r.status==='rezervisano'&&r.expires_at<Date.now())throw Error('Rezervacija je istekla.');if(r.expires_at<Date.now()-30*86400000)throw Error('Rok za oporavak je istekao.');if(opts.origin&&!validOrigin(opts.origin))throw Error('Porijeklo mora biti javna HTTPS adresa.');if(action==='obnovi'&&r.status!=='izdato')throw Error('Ime mora biti izdato prije obnove.');if(action==='obnovi'){const own=db.all('SELECT name FROM names WHERE owner_id=? ORDER BY created_at,name',owner);if(own.findIndex(n=>n.name===r.name)>=3)charge(owner,30,'obnova');}if(!['izdaj','obnovi'].includes(action))throw Error('Nepoznata radnja.');const seq=log(action,r.name,owner);db.run("UPDATE names SET status='izdato',origin=?,trust=?,expires_at=?,sequence=?,updated_at=? WHERE name=?",opts.origin||r.origin,opts.origin?'neprovjereno':r.trust,Date.now()+365*86400000,seq,Date.now(),r.name);return seal(r.name);});}
27 function resolve(value,net='glavna'){let name;try{name=normalName(value);}catch{return {ok:false,state:'nedostaje'};}const r=db.get('SELECT * FROM names WHERE name=?',name);if(!r)return {ok:false,state:'nedostaje'};let state=r.net==='proba'&&net!=='proba'?'proba':['opozvano','sporno','rezervisano','pregled'].includes(r.status)?r.status:r.expires_at<Date.now()?'isteklo':'ok';const k=db.get('SELECT public_key FROM reg_keys WHERE key_id=?',r.key_id);if(state==='ok'&&!verify(record(r),r.sig,k?.public_key))state='opozvano';return {ok:state==='ok',state,record:record(r),signature:r.sig,key_id:r.key_id,trust:r.trust,net:r.net,snapshot:snapshot()};}
28 function snapshot(){if(cache&&Date.now()-cacheAt<30000)return cache;const k=key(),payload={sequence:db.get('SELECT COALESCE(MAX(seq),0) n FROM reg_log').n,issued_at:Date.now(),expires_at:Date.now()+3600000,key_id:k.key_id,records:db.all('SELECT * FROM names ORDER BY name').map(r=>({...record(r),signature:r.sig,key_id:r.key_id,status:r.status,net:r.net,trust:r.trust}))};cache={...payload,signature:sign(payload,k)};cacheAt=Date.now();return cache;}
29 function rotate(){const old=key();db.run('UPDATE reg_keys SET retired_at=? WHERE key_id=?',Date.now(),old.key_id);const fresh=createKey();logWithOld(fresh,old);cache=null;return fresh;}
30 function logWithOld(fresh,old){const prev=db.get('SELECT seq,hash FROM reg_log ORDER BY seq DESC LIMIT 1'),e={seq:(prev?.seq||0)+1,at:Date.now(),action:'zamjena-kljuca',name:'',actor_id:0,payload:{key_id:fresh.key_id,public_key:fresh.public_key}},hash=sha((prev?.hash||'')+canonical(e));db.run('INSERT INTO reg_log VALUES(?,?,?,?,?,?,?,?,?,?)',e.seq,e.at,e.action,'',0,canonical(e.payload),prev?.hash||'',hash,sign({hash},old),old.key_id);}
31 return {key,sign,signed,log,seal,create,reserve,change,charge,resolve,snapshot,rotate};
32 }
33