code wiki / _hdl_build / nx_siteedit_daemon.nx

nx_siteedit_daemon.nx source

↩ module page · 733 lines · 46064 B

1// nx_siteedit_daemon.nx -- the SITE-VISUALS editor for admin.<domain>/site: the client edits their 2// site's .site BLUEPRINT (title/hero/cards/trust/footer = the site's DATA), previews the regenerated 3// whole-site at /preview/, and publishes it to the live docroot -- all through the SAME canonical 4// Modern Auth realm as the doc portal (same keysfile/storefile/realm => ONE client account works on 5// both; login happens against POST /admin/login on the same host, this daemon only VALIDATES tokens). 6// GET /site -> editor SPA shell (public shell; every action below needs the session) 7// GET /site/config -> the current .site blueprint bytes 8// POST /site/save -> body = the new blueprint; VALIDATED (title|/hero|/card|/footer| present, 9// bounded, no NUL) -> prior config preserved as <config>.v<epoch> (additive, 10// rule 13) -> atomic rename into place. Never a silent partial write. 11// POST /site/preview -> sb_build_site(config, <docroot>/preview, domain) -> site visible at 12// https://<domain>/preview/ (served hot; visitors' pages untouched) 13// POST /site/publish -> preview-build FIRST (proves the config builds), then live docroot pages 14// (.prev-index.html backup kept for instant revert) -- the client's call. 15// Composes: nx_site_build (sb_build_site, gated 8/8) + nx_status_daemon sd_* + Modern Auth validate. 16// Loopback HTTP daemon behind proxy_routes row `admin.<d> /site <port>`. Every response carries 17// Content-Length (the buffered-proxy contract). license_tier: ORIGINAL 18import "nx_status_daemon.nx" 19import "hub/nx_modern_auth_flow.nx" 20import "nx_syscalls.nx" 21import "_hdl_build/nx_site_build.nx" 22import "nx_cms_visual_builder.nx" 23import "nx_siteedit_history.nx" 24const SED_MAGIC_3600: i64 = 3600 25const SED_MAGIC_86400: i64 = 86400 26const SED_MAGIC_8192: i64 = 8192 27 28const SED_REQCAP: i64 = 65536 29const SED_OUTCAP: i64 = 262144 30const SED_CFGCAP: i64 = 32768 31 32// full HTTP response with Content-Length 33func sed_resp(out: *u8, status: *u8, ctype: *u8, body: *u8, blen: i64) -> i64 { 34 var o: i64 = 0 35 o = sd_cat(out, o, "HTTP/1.1 " as *u8) 36 o = sd_cat(out, o, status) 37 o = sd_cat(out, o, "\r\nContent-Type: " as *u8) 38 o = sd_cat(out, o, ctype) 39 o = sd_cat(out, o, "\r\nConnection: close\r\nContent-Length: " as *u8) 40 o = sd_catn(out, o, blen) 41 o = sd_cat(out, o, "\r\n\r\n" as *u8) 42 var i: i64 = 0 43 while i < blen { out[o] = body[i]; o = o + 1; i = i + 1 } 44 return o 45} 46 47// blueprint sanity: bounded, no NUL, and the load-bearing lines exist. 1 = ok. 48func sed_cfg_ok(b: *u8, n: i64) -> i64 { 49 if n < 20 { return 0 } 50 if n > SED_CFGCAP { return 0 } 51 var i: i64 = 0 52 while i < n { if (b[i] as i64) == 0 { return 0 } i = i + 1 } 53 var has_title: i64 = 0 54 var has_hero: i64 = 0 55 var has_card: i64 = 0 56 var has_footer: i64 = 0 57 var ls: i64 = 0 58 i = 0 59 while i <= n { 60 var eol: i64 = 0 61 if i == n { eol = 1 } 62 if i < n { if (b[i] as i64) == 10 { eol = 1 } } 63 if eol == 1 { 64 if sb_lm(b, ls, i, "title" as *u8, 5) >= 0 { has_title = 1 } 65 if sb_lm(b, ls, i, "hero" as *u8, 4) >= 0 { has_hero = 1 } 66 if sb_lm(b, ls, i, "card" as *u8, 4) >= 0 { has_card = 1 } 67 if sb_lm(b, ls, i, "footer" as *u8, 6) >= 0 { has_footer = 1 } 68 ls = i + 1 69 } 70 i = i + 1 71 } 72 if has_title == 1 { if has_hero == 1 { if has_card == 1 { if has_footer == 1 { return 1 } } } } 73 return 0 74} 75 76// write b[0..n) to <path>.new then atomic-rename over <path>. 0 ok / -1 fail. 77func sed_write_atomic(path: *u8, b: *u8, n: i64) -> i64 { 78 let np: *u8 = sys_mmap(600) 79 var o: i64 = sd_cat(np, 0, path) 80 o = sd_cat(np, o, ".new" as *u8) 81 np[o] = 0 as u8 82 let fd: i64 = sys_openat_wr(np, 0x1a4) 83 if fd < 0 { return 0 - 1 } 84 let w: i64 = sys_write(fd, b, n) 85 sys_close(fd) 86 if w != n { return 0 - 1 } 87 if sys_renameat(np, path) != 0 { return 0 - 1 } 88 return 0 89} 90 91// preserve the current config as <path>.v<epoch> (additive history). best-effort. 92func sed_version_keep(path: *u8) -> i64 { 93 let szp: *i64 = sys_mmap(16) as *i64 94 szp[0] = 0 95 let cur: *u8 = sys_read_file(path, szp) 96 if (cur as i64) == 0 { return 0 } 97 let vp: *u8 = sys_mmap(600) 98 var o: i64 = sd_cat(vp, 0, path) 99 o = sd_cat(vp, o, ".v" as *u8) 100 o = sd_catn(vp, o, sys_now_realtime_sec()) 101 vp[o] = 0 as u8 102 let fd: i64 = sys_openat_wr(vp, 0x1a4) 103 if fd < 0 { return 0 - 1 } 104 sys_write(fd, cur, szp[0]) 105 sys_close(fd) 106 return 0 107} 108 109// keep a .prev-index.html copy of the live homepage before publish (instant manual revert). 110func sed_backup_index(docroot: *u8) -> i64 { 111 let ip: *u8 = sys_mmap(600) 112 var o: i64 = sd_cat(ip, 0, docroot) 113 o = sd_cat(ip, o, "/index.html" as *u8) 114 ip[o] = 0 as u8 115 let szp: *i64 = sys_mmap(16) as *i64 116 szp[0] = 0 117 let cur: *u8 = sys_read_file(ip, szp) 118 if (cur as i64) == 0 { return 0 } 119 let bp: *u8 = sys_mmap(600) 120 o = sd_cat(bp, 0, docroot) 121 o = sd_cat(bp, o, "/.prev-index.html" as *u8) 122 bp[o] = 0 as u8 123 let fd: i64 = sys_openat_wr(bp, 0x1a4) 124 if fd < 0 { return 0 - 1 } 125 sys_write(fd, cur, szp[0]) 126 sys_close(fd) 127 return 0 128} 129 130// the editor SPA (public shell; actions carry X-Nishi-Session; login posts to /admin/login = the 131// doc-portal daemon on the SAME host, so ONE client account drives both portals). 132// `body` is a CALLER-OWNED scratch buffer, allocated once in main() and reused for every request. 133// It used to be sys_mmap(SED_OUTCAP) here, i.e. 256 KB of address space per request that this in-process 134// daemon never reclaims. MEASURED 2026-08-09: 40 GET /site connections cost 9,472 kB of VSZ (~237 kB each), 135// which is this allocation. The same pattern remains in sed_landing/editor_page/editor_raw/result_page/ 136// history_page -- they are colder paths and get the same treatment once this one is proven by re-measurement. 137func sed_shell(domain: *u8, out: *u8, body: *u8) -> i64 { 138 var b: i64 = 0 139 b = sd_cat(body, b, "<!DOCTYPE html><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Site editor &mdash; " as *u8) 140 b = sd_cat(body, b, domain) 141 b = sd_cat(body, b, "</title><style>body{font-family:-apple-system,Segoe UI,sans-serif;max-width:900px;margin:5vh auto;padding:0 20px;color:#1c1c1e}input,textarea{width:100%;padding:9px;margin:.35rem 0;box-sizing:border-box;border:1px solid #ccc;border-radius:7px;font-family:ui-monospace,Consolas,monospace}textarea{min-height:340px}button{padding:9px 16px;border:0;border-radius:7px;background:#2a4d8f;color:#fff;font-size:.95rem;margin:.2rem .3rem .2rem 0;cursor:pointer}#pub{background:#0a6}.e{color:#b00;min-height:1.2em}.ok{color:#0a6}.leg{background:#f6f6f9;border:1px solid #dcdce2;border-radius:8px;padding:.8rem 1rem;font-size:.85rem}code{background:#eee;padding:1px 4px;border-radius:3px}</style>" as *u8) 142 b = sd_cat(body, b, "<div id=login><h2>&#128736; Site editor</h2><p>Sign in with your portal account (same as the document portal).</p><form id=lf method=post action=/site/login><input type=hidden name=ui value=1><input id=h name=handle placeholder=handle autocomplete=username autofocus><input id=p name=passphrase type=password placeholder=passphrase autocomplete=current-password><button type=submit id=b>Sign in</button></form><p id=e class=e></p></div>" as *u8) 143 b = sd_cat(body, b, "<div id=ed hidden><h2>Edit your site</h2><div class=leg>One line per element, fields separated by <code>|</code>:<br><code>title|Browser tab title</code> &middot; <code>header|Brand|Button text|#link</code> &middot; <code>hero|Big headline|Subtext|Button|#link</code> &middot; <code>card|Service name|One-line description</code> (each card becomes a page) &middot; <code>step|What happens</code> &middot; <code>sig|A trust point</code> &middot; <code>footer|Firm, location, phone</code></div>" as *u8) 144 b = sd_cat(body, b, "<textarea id=cfg spellcheck=false></textarea><br><button id=sv>Save</button><button id=pv>Preview</button><button id=pub>Publish live</button><p id=r class=e></p><p id=plink></p></div>" as *u8) 145 b = sd_cat(body, b, "<script>var L=document.getElementById('login'),ED=document.getElementById('ed'),E=document.getElementById('e'),R=document.getElementById('r'),PL=document.getElementById('plink');function H(){return{'X-Nishi-Session':sessionStorage.nx_sess||''}}" as *u8) 146 b = sd_cat(body, b, "function loadCfg(){fetch('/site/config',{headers:H()}).then(function(r){if(!r.ok)throw 0;return r.text()}).then(function(t){document.getElementById('cfg').value=t;L.hidden=true;ED.hidden=false}).catch(function(){E.textContent='Session invalid: sign in.'})}" as *u8) 147 b = sd_cat(body, b, "document.getElementById('lf').onsubmit=function(){E.textContent='';var q='handle='+encodeURIComponent(h.value)+'&passphrase='+encodeURIComponent(p.value);fetch('/site/login',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:q}).then(function(r){if(r.ok)return r.json();throw 0}).then(function(j){sessionStorage.nx_sess=j.token;loadCfg()}).catch(function(){E.textContent='Wrong handle or passphrase.'});return false};" as *u8) 148 b = sd_cat(body, b, "function act(p,okmsg){R.textContent='';PL.innerHTML='';fetch(p,{method:'POST',headers:H(),body:document.getElementById('cfg').value}).then(function(r){return r.text().then(function(t){return{ok:r.ok,t:t}})}).then(function(x){R.className=x.ok?'ok':'e';R.textContent=x.t;if(x.ok&&p=='/site/preview'){PL.innerHTML='<a href=\"/preview/\" target=_blank>Open the preview &rarr;</a>'}if(x.ok&&p=='/site/publish'){PL.innerHTML='<a href=\"/\" target=_blank>See it live &rarr;</a>'}}).catch(function(){R.className='e';R.textContent='Request failed.'})}" as *u8) 149 b = sd_cat(body, b, "document.getElementById('sv').onclick=function(){act('/site/save')};document.getElementById('pv').onclick=function(){act('/site/save');setTimeout(function(){act('/site/preview')},400)};document.getElementById('pub').onclick=function(){if(confirm('Publish these changes to the LIVE site?')){act('/site/save');setTimeout(function(){act('/site/publish')},400)}};</script>" as *u8) 150 return sed_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, body, b) 151} 152 153// ===== NISHI-FIRST no-JS site editor (2026-07-05 doctrine). Real forms carrying the no-cookie session in 154// ?s= (shared nx_sa_validate_qs); the client edits + previews + publishes with ZERO JS. Login mints on 155// the SHARED realm ctx (same account as the doc portal). NO Set-Cookie (C1 preserved). ===== 156 157// escape & and < for safe embedding of the config into a <textarea>. returns new offset. 158func sed_esc(src: *u8, n: i64, out: *u8, start: i64) -> i64 { 159 var o: i64 = start 160 var i: i64 = 0 161 while i < n { 162 let c: i64 = src[i] as i64 163 if c == 38 { out[o] = 38 as u8; out[o+1] = 97 as u8; out[o+2] = 109 as u8; out[o+3] = 112 as u8; out[o+4] = 59 as u8; o = o + 5 } 164 else { if c == 60 { out[o] = 38 as u8; out[o+1] = 108 as u8; out[o+2] = 116 as u8; out[o+3] = 59 as u8; o = o + 4 } 165 else { out[o] = c as u8; o = o + 1 } } 166 i = i + 1 167 } 168 return o 169} 170 171// splice raw (%-encoded) session token into body at offset b; returns new offset. 172func sed_splice(body: *u8, b: i64, sraw: *u8, srn: i64) -> i64 { 173 var o: i64 = b 174 var i: i64 = 0 175 while i < srn { body[o] = sraw[i]; o = o + 1; i = i + 1 } 176 return o 177} 178 179// no-JS landing after a form login -> link into the editor carrying the session in ?s=. 180func sed_landing(out: *u8, b64: *u8, b64n: i64, body: *u8) -> i64 { 181 var b: i64 = 0 182 b = sd_cat(body, b, "<!DOCTYPE html><meta charset=utf-8><title>Signed in</title><body style=\"font-family:sans-serif;max-width:900px;margin:6vh auto;padding:0 20px\"><h2>&#128736; Signed in</h2><p>No-JS site editor (nishi-first).</p><p><a href=\"/site/ed?s=" as *u8) 183 b = nx_sa_tok_urlenc(b64, b64n, body, b) 184 b = sd_cat(body, b, "\">Open the editor &rarr;</a></p>" as *u8) 185 return sed_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, body, b) 186} 187 188// the VISUAL no-JS editor (R-CANVAS v1, default at /site/ed): the blueprint as a per-field FORM 189// (nx_cms_visual_builder) -- field-level WYSIWYG editing, zero client JS; Save posts to /site/vsave where 190// the server REBUILDS the blueprint and runs the SAME validation/versioning as every other save path. 191func sed_editor_page(cfgpath: *u8, sraw: *u8, srn: i64, out: *u8, body: *u8) -> i64 { 192 var b: i64 = 0 193 b = sd_cat(body, b, "<!DOCTYPE html><html lang=\"en\"><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Site editor</title><body style=\"font-family:sans-serif;max-width:900px;margin:4vh auto;padding:0 20px;color:rgb(28,28,30)\"><h2>Edit your site</h2><p style=\"font-size:.85rem;color:rgb(90,96,115)\">Every field below is a piece of your live site. Save, then Preview, then Publish. Prefer the raw one-line-per-element view? <a href=\"/site/edraw?s=" as *u8) 194 b = sed_splice(body, b, sraw, srn) 195 b = sd_cat(body, b, "\">Open raw mode</a>.</p>" as *u8) 196 let szp: *i64 = sys_mmap(16) as *i64 197 szp[0] = 0 198 let cur: *u8 = sys_read_file(cfgpath, szp) 199 var cn: i64 = 0 200 if (cur as i64) != 0 { cn = szp[0] } 201 b = vb_form_page(cur, cn, sraw, srn, body, b) 202 // Preview + Publish forms (operate on the SAVED config; same endpoints as the raw flow) 203 b = sd_cat(body, b, "<form method=post action=\"/site/preview2?s=" as *u8) 204 b = sed_splice(body, b, sraw, srn) 205 b = sd_cat(body, b, "\" style=\"display:inline\"><button type=submit style=\"padding:9px 16px;border:0;border-radius:7px;background:rgb(85,85,102);color:rgb(255,255,255)\">Preview</button></form> " as *u8) 206 b = sd_cat(body, b, "<form method=post action=\"/site/publish2?s=" as *u8) 207 b = sed_splice(body, b, sraw, srn) 208 b = sd_cat(body, b, "\" style=\"display:inline\" onsubmit=\"return confirm('Publish to the LIVE site?')\"><button type=submit style=\"padding:9px 16px;border:0;border-radius:7px;background:rgb(0,170,102);color:rgb(255,255,255)\">Publish live</button></form>" as *u8) 209 b = sd_cat(body, b, "<p style=\"margin-top:16px;font-size:.85rem\"><a href=\"/site/history?s=" as *u8) 210 b = sed_splice(body, b, sraw, srn) 211 b = sd_cat(body, b, "\">&#8630; Revision history</a> &middot; every save is a restorable snapshot.</p>" as *u8) 212 return sed_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, body, b) 213} 214 215// the RAW no-JS editor page (fallback at /site/edraw): current blueprint in a <textarea> + Save form; 216// Preview + Publish forms operate on the SAVED config (same as the JS flow). All carry ?s=. 217func sed_editor_raw(cfgpath: *u8, sraw: *u8, srn: i64, out: *u8, body: *u8) -> i64 { 218 var b: i64 = 0 219 b = sd_cat(body, b, "<!DOCTYPE html><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Site editor</title><body style=\"font-family:sans-serif;max-width:900px;margin:4vh auto;padding:0 20px\"><h2>Edit your site</h2><div style=\"background:#f6f6f9;border:1px solid #dcdce2;border-radius:8px;padding:.8rem 1rem;font-size:.85rem\">One line per element, fields separated by <code>|</code>: title| header| hero| card| step| sig| footer|</div>" as *u8) 220 // Save form (textarea prefilled with the current config) 221 b = sd_cat(body, b, "<form method=post action=\"/site/save2?s=" as *u8) 222 b = sed_splice(body, b, sraw, srn) 223 b = sd_cat(body, b, "\"><textarea name=cfg spellcheck=false style=\"width:100%;min-height:340px;font-family:ui-monospace,Consolas,monospace;padding:9px;border:1px solid #ccc;border-radius:7px\">" as *u8) 224 let szp: *i64 = sys_mmap(16) as *i64 225 szp[0] = 0 226 let cur: *u8 = sys_read_file(cfgpath, szp) 227 if (cur as i64) != 0 { b = sed_esc(cur, szp[0], body, b) } 228 b = sd_cat(body, b, "</textarea><p><button type=submit style=\"padding:9px 16px;border:0;border-radius:7px;background:#2a4d8f;color:#fff\">Save</button></p></form>" as *u8) 229 // Preview + Publish forms (no body; build from the SAVED config) 230 b = sd_cat(body, b, "<form method=post action=\"/site/preview2?s=" as *u8) 231 b = sed_splice(body, b, sraw, srn) 232 b = sd_cat(body, b, "\" style=\"display:inline\"><button type=submit style=\"padding:9px 16px;border:0;border-radius:7px;background:#556;color:#fff\">Preview</button></form> " as *u8) 233 b = sd_cat(body, b, "<form method=post action=\"/site/publish2?s=" as *u8) 234 b = sed_splice(body, b, sraw, srn) 235 b = sd_cat(body, b, "\" style=\"display:inline\" onsubmit=\"return confirm('Publish to the LIVE site?')\"><button type=submit style=\"padding:9px 16px;border:0;border-radius:7px;background:#0a6;color:#fff\">Publish live</button></form>" as *u8) 236 return sed_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, body, b) 237} 238 239// no-JS action RESULT page: the outcome + links back to the editor / preview / live. 240func sed_result_page(msg: *u8, msglen: i64, sraw: *u8, srn: i64, out: *u8, body: *u8) -> i64 { 241 var b: i64 = 0 242 b = sd_cat(body, b, "<!DOCTYPE html><meta charset=utf-8><title>Site editor</title><body style=\"font-family:sans-serif;max-width:900px;margin:5vh auto;padding:0 20px\"><p>" as *u8) 243 var i: i64 = 0 244 while i < msglen { body[b] = msg[i]; b = b + 1; i = i + 1 } 245 b = sd_cat(body, b, "</p><p><a href=\"/site/ed?s=" as *u8) 246 b = sed_splice(body, b, sraw, srn) 247 b = sd_cat(body, b, "\">&larr; Editor</a> &nbsp; <a href=\"/preview/\" target=_blank>Preview</a> &nbsp; <a href=\"/\" target=_blank>Live site</a></p>" as *u8) 248 return sed_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, body, b) 249} 250 251// ---- THE ROUTER: request bytes -> response bytes (the gate drives this directly). ---- 252// "N seconds/minutes/hours/days ago" for a past epoch (revision-history readability). 253func sed_relage(now: i64, past: i64, body: *u8, b: i64) -> i64 { 254 var d: i64 = now - past 255 if d < 0 { d = 0 } 256 var o: i64 = b 257 if d < 60 { o = sd_catn(body, o, d); o = sd_cat(body, o, " seconds ago" as *u8) } 258 else { if d < SED_MAGIC_3600 { o = sd_catn(body, o, d / 60); o = sd_cat(body, o, " minutes ago" as *u8) } 259 else { if d < SED_MAGIC_86400 { o = sd_catn(body, o, d / SED_MAGIC_3600); o = sd_cat(body, o, " hours ago" as *u8) } 260 else { o = sd_catn(body, o, d / SED_MAGIC_86400); o = sd_cat(body, o, " days ago" as *u8) } } } 261 return o 262} 263 264// GET /site/history -- browse every saved snapshot (sh_list) newest-first, each with view + restore. 265func sed_history_page(cfgpath: *u8, sraw: *u8, srn: i64, out: *u8, body: *u8) -> i64 { 266 var b: i64 = 0 267 b = sd_cat(body, b, "<!DOCTYPE html><html lang=\"en\"><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Revision history</title><body style=\"font-family:sans-serif;max-width:900px;margin:4vh auto;padding:0 20px;color:rgb(28,28,30)\"><h2>Revision history</h2><p style=\"font-size:.85rem;color:rgb(90,96,115)\">Every save keeps a snapshot. Restore is additive &mdash; the current version is snapshotted first, so any restore is itself undoable.</p>" as *u8) 268 let eps: *i64 = sys_mmap(8 * 300) 269 let cnt: i64 = sh_list(cfgpath, eps, 300) 270 sh_sort_desc(eps, cnt) 271 let now: i64 = sys_now_realtime_sec() 272 if cnt == 0 { b = sd_cat(body, b, "<p>No saved versions yet. Edit and Save to build history.</p>" as *u8) } 273 var i: i64 = 0 274 while i < cnt { 275 let ep: i64 = eps[i] 276 b = sd_cat(body, b, "<div style=\"border:1px solid rgb(224,224,230);border-radius:8px;padding:10px 14px;margin:8px 0;display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap\"><span>Saved " as *u8) 277 b = sed_relage(now, ep, body, b) 278 b = sd_cat(body, b, " <span style=\"color:rgb(150,155,170);font-size:.8rem\">(v" as *u8) 279 b = sd_catn(body, b, ep) 280 b = sd_cat(body, b, ")</span></span><span><a style=\"margin-right:12px\" href=\"/site/vview?s=" as *u8) 281 b = sed_splice(body, b, sraw, srn) 282 b = sd_cat(body, b, "&v=" as *u8) 283 b = sd_catn(body, b, ep) 284 b = sd_cat(body, b, "\">view</a><form method=post style=\"display:inline\" action=\"/site/restore?s=" as *u8) 285 b = sed_splice(body, b, sraw, srn) 286 b = sd_cat(body, b, "&v=" as *u8) 287 b = sd_catn(body, b, ep) 288 b = sd_cat(body, b, "\"><button type=submit style=\"padding:7px 13px;border:0;border-radius:6px;background:rgb(85,85,102);color:rgb(255,255,255)\">Restore</button></form></span></div>" as *u8) 289 i = i + 1 290 } 291 b = sd_cat(body, b, "<p style=\"margin-top:18px\"><a href=\"/site/ed?s=" as *u8) 292 b = sed_splice(body, b, sraw, srn) 293 b = sd_cat(body, b, "\">&larr; Back to editor</a></p></body></html>" as *u8) 294 return sed_resp(out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, body, b) 295} 296 297// POST /site/restore?s=&v=<epoch> -- restore a snapshot. ADDITIVE: snapshot CURRENT first (sed_version_keep), 298// validate the snapshot, then atomic-write it as the working config. A mis-restore is itself undoable. 299func sed_do_restore(cfgpath: *u8, path: *u8, pn: i64, sraw: *u8, srn: i64, out: *u8, body: *u8) -> i64 { 300 let msg: *u8 = sys_mmap(300) 301 var ml: i64 = 0 302 let ep: i64 = sh_qs_epoch(path, pn) 303 if ep <= 0 { ml = sd_cat(msg, 0, "Restore failed: no version selected." as *u8); return sed_result_page(msg, ml, sraw, srn, out, body) } 304 let vp: *u8 = sys_mmap(700) 305 sh_version_path(cfgpath, ep, vp) 306 let szp: *i64 = sys_mmap(16) as *i64 307 szp[0] = 0 308 let content: *u8 = sys_read_file(vp, szp) 309 if (content as i64) == 0 { ml = sd_cat(msg, 0, "Restore failed: that snapshot no longer exists." as *u8); return sed_result_page(msg, ml, sraw, srn, out, body) } 310 if sed_cfg_ok(content, szp[0]) == 0 { ml = sd_cat(msg, 0, "Restore refused: the snapshot is not a valid site config." as *u8); return sed_result_page(msg, ml, sraw, srn, out, body) } 311 sed_version_keep(cfgpath) 312 if sed_write_atomic(cfgpath, content, szp[0]) == 0 { 313 ml = sd_cat(msg, 0, "Restored (your previous state was snapshotted first). Preview, then Publish." as *u8) 314 } else { ml = sd_cat(msg, 0, "Restore failed writing the config." as *u8) } 315 return sed_result_page(msg, ml, sraw, srn, out, body) 316} 317 318// GET /site/vview?s=&v=<epoch> -- see a snapshot's raw blueprint before restoring (text/plain). 319func sed_view_version(cfgpath: *u8, path: *u8, pn: i64, out: *u8) -> i64 { 320 let eb: *u8 = sys_mmap(64) 321 let ep: i64 = sh_qs_epoch(path, pn) 322 if ep <= 0 { let en: i64 = sd_cat(eb, 0, "no version selected" as *u8); return sed_resp(out, "400 Bad Request" as *u8, "text/plain; charset=utf-8" as *u8, eb, en) } 323 let vp: *u8 = sys_mmap(700) 324 sh_version_path(cfgpath, ep, vp) 325 let szp: *i64 = sys_mmap(16) as *i64 326 szp[0] = 0 327 let content: *u8 = sys_read_file(vp, szp) 328 if (content as i64) == 0 { let en2: i64 = sd_cat(eb, 0, "snapshot not found" as *u8); return sed_resp(out, "404 Not Found" as *u8, "text/plain; charset=utf-8" as *u8, eb, en2) } 329 return sed_resp(out, "200 OK" as *u8, "text/plain; charset=utf-8" as *u8, content, szp[0]) 330} 331 332// NOTE: this scratch is called `scratch`, NOT `body`. sed_handle already binds a local `body` that points 333// INTO the request buffer; naming the parameter `body` let that local shadow it, so sed_shell received a 334// pointer into the 64 KB request as its 256 KB scratch and wrote past it. It compiled clean and SIGSEGV'd at 335// runtime -- a shadowed parameter is invisible to the type checker. 336func sed_handle(ctx: *NxAuthContext, domain: *u8, cfgpath: *u8, docroot: *u8, req: *u8, req_n: i64, out: *u8, scratch: *u8) -> i64 { 337 let poff: *i64 = sys_mmap(8) as *i64 338 let plen: *i64 = sys_mmap(8) as *i64 339 poff[0] = 0 340 plen[0] = 0 341 sd_find_path(req, req_n, poff, plen) 342 let path: *u8 = ((req as i64) + poff[0]) as *u8 343 let pn: i64 = plen[0] 344 let is_post: i64 = (req[0] == 80 as u8) as i64 345 var o: i64 = 0 346 347 // ===== NISHI-FIRST no-JS routes (early-return; ?s= session, mint on the shared realm). Matched BEFORE 348 // the header-gated /site/ actions so their /site/ prefix doesn't fall into them. ===== 349 if sd_starts(path, pn, "/site/login" as *u8) == 1 { 350 if is_post == 1 { 351 let body_off: i64 = sd_body_off(req, req_n) 352 let fb: *u8 = ((req as i64) + body_off) as *u8 353 let fbn: i64 = req_n - body_off 354 let hoff: *i64 = sys_mmap(8) as *i64 355 let hnf: *i64 = sys_mmap(8) as *i64 356 let poff2: *i64 = sys_mmap(8) as *i64 357 let pnf: *i64 = sys_mmap(8) as *i64 358 var got: i64 = 0 359 if sd_form_field(fb, fbn, "handle" as *u8, 6, hoff, hnf) == 1 { 360 if sd_form_field(fb, fbn, "passphrase" as *u8, 10, poff2, pnf) == 1 { got = 1 } 361 } 362 var uiflag: i64 = 0 363 let uoff: *i64 = sys_mmap(8) as *i64 364 let un: *i64 = sys_mmap(8) as *i64 365 if sd_form_field(fb, fbn, "ui" as *u8, 2, uoff, un) == 1 { uiflag = 1 } 366 var ok: i64 = 0 367 if got == 1 { 368 let hbuf: *u8 = sys_mmap(256) 369 let pbuf: *u8 = sys_mmap(512) 370 let hsrc: *u8 = ((fb as i64) + hoff[0]) as *u8 371 let psrc: *u8 = ((fb as i64) + poff2[0]) as *u8 372 let h_dec: i64 = sd_urldecode(hsrc, hnf[0], hbuf, 255) 373 let p_dec: i64 = sd_urldecode(psrc, pnf[0], pbuf, 511) 374 if h_dec > 0 { if p_dec > 0 { 375 let tok: *u8 = sys_mmap(NX_MAUTH_SESSION_TOKEN_BYTES) 376 let tok_n: *i64 = sys_mmap(8) as *i64 377 tok_n[0] = 0 378 if nx_modern_auth_login(ctx, hbuf, h_dec, pbuf, p_dec, tok, NX_MAUTH_SESSION_TOKEN_BYTES, tok_n) == NX_MAUTH_OK { 379 let b64: *u8 = sys_mmap(256) 380 let b64_n: i64 = b64_encode(tok, NX_MAUTH_SESSION_TOKEN_BYTES, b64) 381 if uiflag == 1 { o = sed_landing(out, b64, b64_n, scratch) } 382 else { 383 o = sd_cat(out, o, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: " as *u8) 384 o = sd_catn(out, o, 12 + b64_n) 385 o = sd_cat(out, o, "\r\n\r\n{\"token\":\"" as *u8) 386 var z: i64 = 0 387 while z < b64_n { out[o] = b64[z]; o = o + 1; z = z + 1 } 388 o = sd_cat(out, o, "\"}" as *u8) 389 } 390 ok = 1 391 } 392 } } 393 } 394 if ok == 0 { 395 if uiflag == 1 { o = sd_cat(out, 0, "HTTP/1.1 401 Unauthorized\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\nContent-Length: 101\r\n\r\n<!DOCTYPE html><meta charset=utf-8><p>Wrong handle or passphrase.</p><p><a href=\"/\">Try again</a></p>" as *u8) } 396 else { o = sd_emit_401_json(out) } 397 } 398 } else { o = sed_shell(domain, out, scratch) } 399 return o 400 } 401 // NOTE: /site/edraw MUST be matched before /site/ed (prefix). 402 if sd_starts(path, pn, "/site/edraw" as *u8) == 1 { 403 let now_er: i64 = sys_now_realtime_sec() 404 let hbr: *u8 = sys_mmap(128) 405 let hbrn: *i64 = sys_mmap(8) as *i64 406 if nx_sa_validate_qs(ctx, path, pn, now_er, hbr, 128, hbrn) == NX_MAUTH_OK { 407 let voffr: *i64 = sys_mmap(8) as *i64 408 let srnr: i64 = nx_sa_qs_raw(path, pn, voffr) 409 let srawr: *u8 = ((path as i64) + voffr[0]) as *u8 410 return sed_editor_raw(cfgpath, srawr, srnr, out, scratch) 411 } 412 return sd_emit_401_json(out) 413 } 414 if sd_starts(path, pn, "/site/ed" as *u8) == 1 { 415 let now_e: i64 = sys_now_realtime_sec() 416 let hb: *u8 = sys_mmap(128) 417 let hbn: *i64 = sys_mmap(8) as *i64 418 if nx_sa_validate_qs(ctx, path, pn, now_e, hb, 128, hbn) == NX_MAUTH_OK { 419 let voff: *i64 = sys_mmap(8) as *i64 420 let srn: i64 = nx_sa_qs_raw(path, pn, voff) 421 let sraw: *u8 = ((path as i64) + voff[0]) as *u8 422 return sed_editor_page(cfgpath, sraw, srn, out, scratch) 423 } 424 return sd_emit_401_json(out) 425 } 426 // VISUAL editor save: rebuild the blueprint from the posted form fields, then the SAME validation, 427 // versioning, and atomic write as save2 (the safety story is shared, not forked). 428 if sd_starts(path, pn, "/site/vsave" as *u8) == 1 { 429 if is_post == 1 { 430 let now_vs: i64 = sys_now_realtime_sec() 431 let hbv: *u8 = sys_mmap(128) 432 let hbvn: *i64 = sys_mmap(8) as *i64 433 if nx_sa_validate_qs(ctx, path, pn, now_vs, hbv, 128, hbvn) == NX_MAUTH_OK { 434 let voffv: *i64 = sys_mmap(8) as *i64 435 let srnv: i64 = nx_sa_qs_raw(path, pn, voffv) 436 let srawv: *u8 = ((path as i64) + voffv[0]) as *u8 437 let body_offv: i64 = sd_body_off(req, req_n) 438 let fbv: *u8 = ((req as i64) + body_offv) as *u8 439 let fbvn: i64 = req_n - body_offv 440 let cfgv: *u8 = sys_mmap(SED_OUTCAP) 441 let v_len: i64 = vb_rebuild(fbv, fbvn, cfgv) 442 let msgv: *u8 = sys_mmap(256) 443 var mlv: i64 = 0 444 if v_len > 0 { 445 if sed_cfg_ok(cfgv, v_len) == 1 { 446 sed_version_keep(cfgpath) 447 if sed_write_atomic(cfgpath, cfgv, v_len) == 0 { 448 mlv = sd_cat(msgv, 0, "Saved from the visual editor (previous version preserved). Preview, then Publish." as *u8) 449 } else { mlv = sd_cat(msgv, 0, "Save failed writing the config." as *u8) } 450 } else { mlv = sd_cat(msgv, 0, "Refused: the site needs a title, a hero headline, at least one card, and a footer." as *u8) } 451 } else { mlv = sd_cat(msgv, 0, "Refused: no fields submitted." as *u8) } 452 return sed_result_page(msgv, mlv, srawv, srnv, out, scratch) 453 } 454 return sd_emit_401_json(out) 455 } 456 return sd_emit_401_json(out) 457 } 458 // Revision history: browse (GET), restore (POST, additive), view a snapshot (GET). All session-gated like save. 459 if sd_starts(path, pn, "/site/history" as *u8) == 1 { 460 let now_h: i64 = sys_now_realtime_sec() 461 let hbh: *u8 = sys_mmap(128) 462 let hbhn: *i64 = sys_mmap(8) as *i64 463 if nx_sa_validate_qs(ctx, path, pn, now_h, hbh, 128, hbhn) == NX_MAUTH_OK { 464 let voffh: *i64 = sys_mmap(8) as *i64 465 let srnh: i64 = nx_sa_qs_raw(path, pn, voffh) 466 let srawh: *u8 = ((path as i64) + voffh[0]) as *u8 467 return sed_history_page(cfgpath, srawh, srnh, out, scratch) 468 } 469 return sd_emit_401_json(out) 470 } 471 if sd_starts(path, pn, "/site/restore" as *u8) == 1 { 472 if is_post == 1 { 473 let now_r2: i64 = sys_now_realtime_sec() 474 let hbr2: *u8 = sys_mmap(128) 475 let hbr2n: *i64 = sys_mmap(8) as *i64 476 if nx_sa_validate_qs(ctx, path, pn, now_r2, hbr2, 128, hbr2n) == NX_MAUTH_OK { 477 let voffr2: *i64 = sys_mmap(8) as *i64 478 let srnr2: i64 = nx_sa_qs_raw(path, pn, voffr2) 479 let srawr2: *u8 = ((path as i64) + voffr2[0]) as *u8 480 return sed_do_restore(cfgpath, path, pn, srawr2, srnr2, out, scratch) 481 } 482 return sd_emit_401_json(out) 483 } 484 return sd_emit_401_json(out) 485 } 486 if sd_starts(path, pn, "/site/vview" as *u8) == 1 { 487 let now_vv: i64 = sys_now_realtime_sec() 488 let hbvv: *u8 = sys_mmap(128) 489 let hbvvn: *i64 = sys_mmap(8) as *i64 490 if nx_sa_validate_qs(ctx, path, pn, now_vv, hbvv, 128, hbvvn) == NX_MAUTH_OK { 491 return sed_view_version(cfgpath, path, pn, out) 492 } 493 return sd_emit_401_json(out) 494 } 495 if sd_starts(path, pn, "/site/save2" as *u8) == 1 { 496 if is_post == 1 { 497 let now_s2: i64 = sys_now_realtime_sec() 498 let hb: *u8 = sys_mmap(128) 499 let hbn: *i64 = sys_mmap(8) as *i64 500 if nx_sa_validate_qs(ctx, path, pn, now_s2, hb, 128, hbn) == NX_MAUTH_OK { 501 let voff: *i64 = sys_mmap(8) as *i64 502 let srn: i64 = nx_sa_qs_raw(path, pn, voff) 503 let sraw: *u8 = ((path as i64) + voff[0]) as *u8 504 let body_off: i64 = sd_body_off(req, req_n) 505 let fb: *u8 = ((req as i64) + body_off) as *u8 506 let fbn: i64 = req_n - body_off 507 let coff: *i64 = sys_mmap(8) as *i64 508 let cnf: *i64 = sys_mmap(8) as *i64 509 let msg: *u8 = sys_mmap(256) 510 var ml: i64 = 0 511 if sd_form_field(fb, fbn, "cfg" as *u8, 3, coff, cnf) == 1 { 512 let csrc: *u8 = ((fb as i64) + coff[0]) as *u8 513 let cfgb: *u8 = sys_mmap(SED_CFGCAP) 514 let c_dec: i64 = sd_urldecode(csrc, cnf[0], cfgb, SED_CFGCAP - 1) 515 if sed_cfg_ok(cfgb, c_dec) == 1 { 516 sed_version_keep(cfgpath) 517 if sed_write_atomic(cfgpath, cfgb, c_dec) == 0 { 518 ml = sd_cat(msg, 0, "Saved (previous version preserved). Preview, then Publish." as *u8) 519 } else { ml = sd_cat(msg, 0, "Save failed writing the config." as *u8) } 520 } else { ml = sd_cat(msg, 0, "Refused: keep title| hero| a card| and footer| lines. Nothing changed." as *u8) } 521 } else { ml = sd_cat(msg, 0, "Refused: no config submitted." as *u8) } 522 return sed_result_page(msg, ml, sraw, srn, out, scratch) 523 } 524 return sd_emit_401_json(out) 525 } 526 return sd_emit_401_json(out) 527 } 528 if sd_starts(path, pn, "/site/preview2" as *u8) == 1 { 529 if is_post == 1 { 530 let now_p2: i64 = sys_now_realtime_sec() 531 let hb: *u8 = sys_mmap(128) 532 let hbn: *i64 = sys_mmap(8) as *i64 533 if nx_sa_validate_qs(ctx, path, pn, now_p2, hb, 128, hbn) == NX_MAUTH_OK { 534 let voff: *i64 = sys_mmap(8) as *i64 535 let srn: i64 = nx_sa_qs_raw(path, pn, voff) 536 let sraw: *u8 = ((path as i64) + voff[0]) as *u8 537 let pd: *u8 = sys_mmap(600) 538 var po: i64 = sd_cat(pd, 0, docroot) 539 po = sd_cat(pd, po, "/preview" as *u8) 540 pd[po] = 0 as u8 541 let nc: i64 = sb_build_site(cfgpath, pd, domain, 0) 542 let msg: *u8 = sys_mmap(256) 543 var ml: i64 = 0 544 if nc >= 1 { ml = sd_cat(msg, 0, "Preview built at /preview/ (live pages untouched)." as *u8) } else { ml = sd_cat(msg, 0, "Preview failed: the config did not build." as *u8) } 545 return sed_result_page(msg, ml, sraw, srn, out, scratch) 546 } 547 return sd_emit_401_json(out) 548 } 549 return sd_emit_401_json(out) 550 } 551 if sd_starts(path, pn, "/site/publish2" as *u8) == 1 { 552 if is_post == 1 { 553 let now_pub: i64 = sys_now_realtime_sec() 554 let hb: *u8 = sys_mmap(128) 555 let hbn: *i64 = sys_mmap(8) as *i64 556 if nx_sa_validate_qs(ctx, path, pn, now_pub, hb, 128, hbn) == NX_MAUTH_OK { 557 let voff: *i64 = sys_mmap(8) as *i64 558 let srn: i64 = nx_sa_qs_raw(path, pn, voff) 559 let sraw: *u8 = ((path as i64) + voff[0]) as *u8 560 let pd2: *u8 = sys_mmap(600) 561 var po2: i64 = sd_cat(pd2, 0, docroot) 562 po2 = sd_cat(pd2, po2, "/preview" as *u8) 563 pd2[po2] = 0 as u8 564 let nc2: i64 = sb_build_site(cfgpath, pd2, domain, 0) 565 let msg: *u8 = sys_mmap(256) 566 var ml: i64 = 0 567 if nc2 >= 1 { 568 sed_backup_index(docroot) 569 let nc3: i64 = sb_build_site(cfgpath, docroot, domain, 0) 570 if nc3 >= 1 { ml = sd_cat(msg, 0, "Published live (previous homepage kept as .prev-index.html)." as *u8) } else { ml = sd_cat(msg, 0, "Publish failed building the live docroot." as *u8) } 571 } else { ml = sd_cat(msg, 0, "Publish refused: the config did not build cleanly. Live untouched." as *u8) } 572 return sed_result_page(msg, ml, sraw, srn, out, scratch) 573 } 574 return sd_emit_401_json(out) 575 } 576 return sd_emit_401_json(out) 577 } 578 579 // every /site/<action> is session-gated; bare /site serves the public shell. 580 var action: i64 = 0 581 if pn > 5 { if sd_starts(path, pn, "/site/" as *u8) == 1 { action = 1 } } 582 if action == 1 { 583 let now_s: i64 = sys_now_realtime_sec() 584 if nx_sa_validate(ctx, req, req_n, now_s) == NX_MAUTH_OK { 585 let frag: *u8 = sys_mmap(SED_OUTCAP) 586 if sd_starts(path, pn, "/site/config" as *u8) == 1 { 587 let szp: *i64 = sys_mmap(16) as *i64 588 szp[0] = 0 589 let cur: *u8 = sys_read_file(cfgpath, szp) 590 if (cur as i64) != 0 { 591 o = sed_resp(out, "200 OK" as *u8, "text/plain; charset=utf-8" as *u8, cur, szp[0]) 592 } else { 593 let fb: i64 = sd_cat(frag, 0, "no config yet" as *u8) 594 o = sed_resp(out, "404 Not Found" as *u8, "text/plain" as *u8, frag, fb) 595 } 596 } else { if sd_starts(path, pn, "/site/save" as *u8) == 1 { 597 var saved: i64 = 0 598 if is_post == 1 { 599 let body_off: i64 = sd_body_off(req, req_n) 600 let body: *u8 = ((req as i64) + body_off) as *u8 601 let body_n: i64 = req_n - body_off 602 if sed_cfg_ok(body, body_n) == 1 { 603 sed_version_keep(cfgpath) 604 if sed_write_atomic(cfgpath, body, body_n) == 0 { 605 var fb2: i64 = sd_cat(frag, 0, "SAVED " as *u8) 606 fb2 = sd_catn(frag, fb2, body_n) 607 fb2 = sd_cat(frag, fb2, " bytes (previous version preserved). Now Preview, then Publish." as *u8) 608 o = sed_resp(out, "200 OK" as *u8, "text/plain" as *u8, frag, fb2) 609 saved = 1 610 } 611 } 612 } 613 if saved == 0 { 614 let fb3: i64 = sd_cat(frag, 0, "REFUSED: config must keep title| hero| at least one card| and footer| lines (bounded, no NUL). Nothing was changed." as *u8) 615 o = sed_resp(out, "400 Bad Request" as *u8, "text/plain" as *u8, frag, fb3) 616 } 617 } else { if sd_starts(path, pn, "/site/preview" as *u8) == 1 { 618 var prev_ok: i64 = 0 619 if is_post == 1 { 620 let pd: *u8 = sys_mmap(600) 621 var po: i64 = sd_cat(pd, 0, docroot) 622 po = sd_cat(pd, po, "/preview" as *u8) 623 pd[po] = 0 as u8 624 let nc: i64 = sb_build_site(cfgpath, pd, domain, 0) 625 if nc >= 1 { 626 var fb4: i64 = sd_cat(frag, 0, "PREVIEW BUILT: index + " as *u8) 627 fb4 = sd_catn(frag, fb4, nc) 628 fb4 = sd_cat(frag, fb4, " service pages at /preview/ (live pages untouched)." as *u8) 629 o = sed_resp(out, "200 OK" as *u8, "text/plain" as *u8, frag, fb4) 630 prev_ok = 1 631 } 632 } 633 if prev_ok == 0 { 634 let fb5: i64 = sd_cat(frag, 0, "PREVIEW FAILED: the config did not build. Live pages untouched." as *u8) 635 o = sed_resp(out, "400 Bad Request" as *u8, "text/plain" as *u8, frag, fb5) 636 } 637 } else { if sd_starts(path, pn, "/site/publish" as *u8) == 1 { 638 var pub_ok: i64 = 0 639 if is_post == 1 { 640 // prove the config builds (preview dir), THEN touch the live docroot. 641 let pd2: *u8 = sys_mmap(600) 642 var po2: i64 = sd_cat(pd2, 0, docroot) 643 po2 = sd_cat(pd2, po2, "/preview" as *u8) 644 pd2[po2] = 0 as u8 645 let nc2: i64 = sb_build_site(cfgpath, pd2, domain, 0) 646 if nc2 >= 1 { 647 sed_backup_index(docroot) 648 let nc3: i64 = sb_build_site(cfgpath, docroot, domain, 0) 649 if nc3 >= 1 { 650 var fb6: i64 = sd_cat(frag, 0, "PUBLISHED LIVE: index + " as *u8) 651 fb6 = sd_catn(frag, fb6, nc3) 652 fb6 = sd_cat(frag, fb6, " service pages + sitemap + robots. Previous homepage kept as .prev-index.html." as *u8) 653 o = sed_resp(out, "200 OK" as *u8, "text/plain" as *u8, frag, fb6) 654 pub_ok = 1 655 } 656 } 657 } 658 if pub_ok == 0 { 659 let fb7: i64 = sd_cat(frag, 0, "PUBLISH REFUSED: the config did not build cleanly. Live pages untouched." as *u8) 660 o = sed_resp(out, "400 Bad Request" as *u8, "text/plain" as *u8, frag, fb7) 661 } 662 } else { 663 let fb8: i64 = sd_cat(frag, 0, "no such site action" as *u8) 664 o = sed_resp(out, "404 Not Found" as *u8, "text/plain" as *u8, frag, fb8) 665 } } } } 666 } else { o = sd_emit_401_json(out) } 667 } else { o = sed_shell(domain, out, scratch) } 668 return o 669} 670 671func main(argc: i64, argv: *i64) -> i64 { 672 if argc < 9 { sys_write(2, "usage: nx_siteedit_daemon <port> <keysfile> <storefile> <realm> <domain> <configpath> <docroot> <budget>\n" as *u8, 105); return 1 } 673 let port: i64 = sd_atoi(argv[1] as *u8) 674 let keysfile: *u8 = argv[2] as *u8 675 let storefile: *u8 = argv[3] as *u8 676 let realm: *u8 = argv[4] as *u8 677 let domain: *u8 = argv[5] as *u8 678 let cfgpath: *u8 = argv[6] as *u8 679 let docroot: *u8 = argv[7] as *u8 680 let budget: i64 = sd_atoi(argv[8] as *u8) 681 let realm_n: i64 = sd_len(realm) 682 683 let oprf_seed: *u8 = sys_mmap(32) 684 let akp: *u8 = sys_mmap(32) 685 let akb: *u8 = sys_mmap(33) 686 let edp: *u8 = sys_mmap(32) 687 let edb: *u8 = sys_mmap(32) 688 if nx_uas_server_keys_load_or_init(keysfile, oprf_seed, akp, akb, edp, edb) != NX_UAS_OK { sys_write(2, "FATAL: server-key bundle\n" as *u8, 24); return 2 } 689 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext 690 if nx_auth_context_init(ctx, realm, realm_n, realm, realm_n, storefile as i64, oprf_seed, edp, edb, 900, SED_MAGIC_8192, 1, 1, 5, 1) != NX_MAUTH_OK { sys_write(2, "FATAL: context init\n" as *u8, 20); return 3 } 691 692 let addr: *u8 = sys_mmap(16) 693 if nx_http_server_addr_loopback(addr, port) != 16 { sys_write(2, "FATAL: addr_loopback != 16\n" as *u8, 27); return 4 } 694 let lv: *i64 = sys_mmap(8) as *i64 695 let lfd: i64 = nx_http_server_listen(addr, 64, lv) 696 if lfd < 0 { sys_write(2, "FATAL: listen failed (port busy or denied)\n" as *u8, 43); return 4 } 697 sys_write(1, "nx_siteedit_daemon listening loopback (admin.<domain>/site behind the SNI router)\n" as *u8, 83) 698 699 let req: *u8 = sys_mmap(SED_REQCAP) 700 let out: *u8 = sys_mmap(SED_OUTCAP) 701 let bodybuf: *u8 = sys_mmap(SED_OUTCAP) 702 // VSZ LEAK FIX (measured 2026-08-09). This daemon serves IN-PROCESS -- it does not fork per connection -- 703 // so every sys_mmap inside the accept loop is address space that is never reclaimed. There is no munmap 704 // in this runtime: mmap IS the allocator, which is correct for a one-shot organ and for a fork-per-request 705 // CHILD (the mapping dies with the process) and a slow leak in a daemon that runs forever. 706 // These seven are 8-byte OUT-PARAMS that each iteration overwrites, so one allocation each serves every 707 // connection. Each sys_mmap(8) still costs a whole 4096-byte page of VSZ, so the seven were leaking ~28 KB 708 // per connection -- matching the measured +1,480 kB/3min. `req` and `out` above were already hoisted, which 709 // is what the loop was always meant to look like. 710 let av: *i64 = sys_mmap(8) as *i64 711 let om: *i64 = sys_mmap(8) as *i64 712 let opo: *i64 = sys_mmap(8) as *i64 713 let opl: *i64 = sys_mmap(8) as *i64 714 let ocl: *i64 = sys_mmap(8) as *i64 715 let obo: *i64 = sys_mmap(8) as *i64 716 let orn: *i64 = sys_mmap(8) as *i64 717 var served: i64 = 0 718 while served < budget { 719 let cfd: i64 = nx_http_server_accept_one(lfd, av) 720 if cfd < 0 { served = served + 1 } 721 if cfd >= 0 { 722 let rrc: i64 = nx_http_server_read_request(cfd, req, SED_REQCAP, om, opo, opl, ocl, obo, orn) 723 if rrc == NXS_OK { 724 let oN: i64 = sed_handle(ctx, domain, cfgpath, docroot, req, orn[0], out, bodybuf) 725 nx_http_server_send_response_nokeep_close(cfd, out, oN) 726 } 727 sys_close(cfd) 728 served = served + 1 729 } 730 } 731 sys_close(lfd) 732 return 0 733}