code wiki / _hdl_build / nx_sitegen_studio_daemon.nx

nx_sitegen_studio_daemon.nx source

↩ module page · 284 lines · 15703 B

1// nx_sitegen_studio_daemon.nx -- the CAPABILITY-ADAPTIVE WYSIWYG studio for nishifamily.com/webdev, BEHIND the 2// CANONICAL Modern Auth (the SINGLE ecosystem-wide login: OPAQUE-3DH + Argon2id KSF + BIP39 recovery + NO-COOKIE 3// Ed25519 session, 152B X-Nishi-Session header). Operator: "OPAQUE login for all nishifamily.com/* sites." This 4// composes the ONE shared gate nx_sa_validate -- NOT cookies, NOT nx_cms_admin (that argon2id+cookie path is the 5// pre-charter CMS holdout). A loopback HTTP daemon the sites daemon reverse-proxies /webdev* to. 6// 7// The router is a PURE FUNCTION stu_handle(ctx, req, n, out) -> out_n (request bytes in, response bytes out, no 8// socket); the socket loop is a thin shell; the gate drives it IN-PROCESS (sovereign, no curl/shell). Routes: 9// POST /webdev/login -> handle=<h>&passphrase=<p> -> nx_modern_auth_login -> 200 {"token":"<b64 152B>"}|401 10// POST /webdev/build -> X-Nishi-Session validates (nx_sa_validate) ? se_build_tier(body) : 401 (privileged) 11// GET /webdev/guided|blocks|freeform -> the tier surface (its /build fetch carries X-Nishi-Session) 12// GET /webdev (+ any) -> the no-cookie SPA shell: login -> token in sessionStorage -> the tier picker 13// Context armed at startup (nx_uas_server_keys_load_or_init + nx_auth_context_init); the admin is provisioned 14// out-of-band by nx_modauth_arm (this daemon never registers). argv: [1]=port [2]=keysfile [3]=storefile 15// [4]=realm [5]=budget. Composes nx_site_auth + nx_modern_auth_flow + nx_studio_tiers (se_build_tier) + 16// nx_http_server + nx_base64. license_tier: ORIGINAL 17import "nx_syscalls.nx" 18import "nx_http_server.nx" 19import "nx_site_auth.nx" 20import "hub/nx_modern_auth_flow.nx" 21import "nx_base64.nx" 22import "nx_studio_tiers.nx" 23const STU_MAGIC_262144: i64 = 262144 24const STU_MAGIC_262143: i64 = 262143 25const STU_MAGIC_8192: i64 = 8192 26 27const STU_REQCAP: i64 = 131072 28const STU_OUTCAP: i64 = 524288 29 30func stu_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != 0 as u8 { n = n + 1 } return n } 31func stu_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != 0 as u8 { d[o + i] = s[i]; i = i + 1 } return o + i } 32func stu_atoi(s: *u8) -> i64 { var v: i64 = 0; var i: i64 = 0; while s[i] != 0 as u8 { let c: i64 = s[i] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } i = i + 1 } return v } 33func stu_catn(d: *u8, o: i64, v: i64) -> i64 { 34 let t: *u8 = sys_mmap(24); var m: i64 = v; var k: i64 = 0 35 var w: i64 = o 36 if m < 0 { d[w] = 45 as u8; w = w + 1; m = 0 - m } 37 if m == 0 { t[0] = 48 as u8; k = 1 } 38 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 39 var i: i64 = 0 40 while i < k { d[w] = t[k - 1 - i]; w = w + 1; i = i + 1 } 41 return w 42} 43func stu_starts(req: *u8, n: i64, s: *u8) -> i64 { 44 let sl: i64 = stu_len(s) 45 if n < sl { return 0 } 46 var i: i64 = 0 47 while i < sl { if req[i] != s[i] { return 0 } i = i + 1 } 48 return 1 49} 50func stu_find_path(req: *u8, n: i64, off_box: *i64, len_box: *i64) -> i64 { 51 var sp1: i64 = 0 52 var f1: i64 = 0 53 while f1 == 0 { if sp1 >= n { f1 = 1 } else { if (req[sp1] as i64) == 32 { f1 = 1 } else { sp1 = sp1 + 1 } } } 54 let start: i64 = sp1 + 1 55 var e: i64 = start 56 var f2: i64 = 0 57 while f2 == 0 { if e >= n { f2 = 1 } else { if (req[e] as i64) == 32 { f2 = 1 } else { e = e + 1 } } } 58 off_box[0] = start 59 len_box[0] = e - start 60 if e <= start { return 0 } 61 return 1 62} 63func stu_body_off(req: *u8, n: i64) -> i64 { 64 var i: i64 = 0 65 while i + 4 <= n { 66 if (req[i] as i64) == 13 { if (req[i + 1] as i64) == 10 { if (req[i + 2] as i64) == 13 { if (req[i + 3] as i64) == 10 { return i + 4 } } } } 67 i = i + 1 68 } 69 return n 70} 71func stu_hexnib(c: i64) -> i64 { 72 if c >= 48 { if c <= 57 { return c - 48 } } 73 if c >= 97 { if c <= 102 { return c - 87 } } 74 if c >= 65 { if c <= 70 { return c - 55 } } 75 return 0 - 1 76} 77func stu_urldecode(src: *u8, n: i64, out: *u8, out_cap: i64) -> i64 { 78 var i: i64 = 0 79 var o: i64 = 0 80 while i < n { 81 if o >= out_cap { return 0 - 1 } 82 let c: i64 = src[i] as i64 83 if c == 43 { out[o] = 32 as u8; i = i + 1 } 84 if c == 37 { 85 if i + 3 > n { return 0 - 1 } 86 let hi: i64 = stu_hexnib(src[i + 1] as i64) 87 let lo: i64 = stu_hexnib(src[i + 2] as i64) 88 if hi < 0 { return 0 - 1 } 89 if lo < 0 { return 0 - 1 } 90 out[o] = ((hi << 4) | lo) as u8 91 i = i + 3 92 } 93 if c != 43 { if c != 37 { out[o] = c as u8; i = i + 1 } } 94 o = o + 1 95 } 96 return o 97} 98func stu_form_field(body: *u8, body_n: i64, name: *u8, name_n: i64, out_off: *i64, out_n: *i64) -> i64 { 99 var pos: i64 = 0 100 while pos < body_n { 101 var m: i64 = 1 102 if pos + name_n + 1 > body_n { m = 0 } 103 if m == 1 { 104 var i: i64 = 0 105 while i < name_n { 106 if (body[pos + i] as i64) != (name[i] as i64) { m = 0; i = name_n } 107 if i < name_n { i = i + 1 } 108 } 109 } 110 if m == 1 { if (body[pos + name_n] as i64) != 61 { m = 0 } } 111 var vend: i64 = pos 112 var scan: i64 = 1 113 while scan == 1 { 114 if vend >= body_n { scan = 0 } 115 if scan == 1 { if (body[vend] as i64) == 38 { scan = 0 } } 116 if scan == 1 { vend = vend + 1 } 117 } 118 if m == 1 { 119 out_off[0] = pos + name_n + 1 120 out_n[0] = vend - (pos + name_n + 1) 121 return 1 122 } 123 pos = vend + 1 124 } 125 return 0 126} 127func stu_emit_401(out: *u8) -> i64 { 128 return stu_cat(out, 0, "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: 24\r\n\r\n{\"error\":\"unauthorized\"}" as *u8) 129} 130// serve a studio asset (web_assets/_studio/<file>) as 200 text/html 131func stu_serve(out: *u8, file: *u8) -> i64 { 132 let buf: *u8 = sys_mmap(STU_MAGIC_262144) 133 let n: i64 = st_slurp(file, buf, STU_MAGIC_262143) 134 if n < 0 { return stu_cat(out, 0, "HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\nContent-Length: 0\r\n\r\n" as *u8) } 135 var o: i64 = stu_cat(out, 0, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\nContent-Length: " as *u8) 136 o = stu_catn(out, o, n); o = stu_cat(out, o, "\r\n\r\n" as *u8) 137 var z: i64 = 0 138 while z < n { out[o] = buf[z]; o = o + 1; z = z + 1 } 139 return o 140} 141// the no-cookie SPA shell: login -> token in sessionStorage -> reveal the tier picker (links carry no secret; 142// the editors read sessionStorage.nx_sess and send it as X-Nishi-Session on /webdev/build). 143func stu_shell(out: *u8) -> i64 { 144 var o: i64 = 0 145 o = stu_cat(out, o, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\n\r\n" as *u8) 146 o = stu_cat(out, o, "<!DOCTYPE html><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Nishi Studio</title>" as *u8) 147 o = stu_cat(out, o, "<style>body{font-family:-apple-system,Segoe UI,sans-serif;max-width:760px;margin:7vh auto;padding:0 20px;color:#1c1c1e}#login{max-width:330px}input{width:100%;padding:10px;margin:.45rem 0;box-sizing:border-box;border:1px solid #ccc;border-radius:7px}button{padding:10px 18px;border:0;border-radius:7px;background:#1a6dff;color:#fff;font-size:1rem}.e{color:#b00;min-height:1.2em}a.tile{display:block;border:1px solid #d7deea;border-radius:10px;padding:16px 18px;margin:10px 0;text-decoration:none;color:#13315c;font-weight:600}</style>" as *u8) 148 o = stu_cat(out, o, "<div id=login><h2>&#128274; Nishi Studio</h2><input id=h placeholder=\"handle\" autocomplete=username autofocus><input id=p type=password placeholder=\"passphrase\" autocomplete=current-password><button id=b>Sign in</button><p id=e class=e></p></div>" as *u8) 149 o = stu_cat(out, o, "<div id=pick hidden><h2>How do you want to build?</h2><a class=tile href=\"webdev/guided\">Guided &mdash; answer a few questions</a><a class=tile href=\"webdev/blocks\">Blocks &mdash; arrange sections</a><a class=tile href=\"webdev/freeform\">Free-form &mdash; place anything on a canvas</a></div>" as *u8) 150 o = stu_cat(out, o, "<script>var L=document.getElementById('login'),P=document.getElementById('pick'),E=document.getElementById('e');function ok(){L.hidden=true;P.hidden=false}" as *u8) 151 o = stu_cat(out, o, "document.getElementById('b').onclick=function(){E.textContent='';var b='handle='+encodeURIComponent(document.getElementById('h').value)+'&passphrase='+encodeURIComponent(document.getElementById('p').value);fetch('webdev/login',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:b}).then(function(r){if(r.ok){return r.json()}throw 0}).then(function(j){sessionStorage.nx_sess=j.token;ok()}).catch(function(){E.textContent='Wrong handle or passphrase.'})};if(sessionStorage.nx_sess){ok()}</script>" as *u8) 152 return o 153} 154 155// THE ROUTER: pure function, request bytes -> response bytes (no socket). gate drives this directly. 156func stu_handle(ctx: *NxAuthContext, req: *u8, req_n: i64, out: *u8) -> i64 { 157 let poff: *i64 = sys_mmap(8) as *i64 158 let plen: *i64 = sys_mmap(8) as *i64 159 poff[0] = 0 160 plen[0] = 0 161 stu_find_path(req, req_n, poff, plen) 162 let path: *u8 = ((req as i64) + poff[0]) as *u8 163 let pn: i64 = plen[0] 164 let is_post: i64 = (req[0] == 80 as u8) as i64 165 var o: i64 = 0 166 167 if stu_starts(path, pn, "/webdev/login" as *u8) == 1 { 168 if is_post == 1 { 169 let body_off: i64 = stu_body_off(req, req_n) 170 let body: *u8 = ((req as i64) + body_off) as *u8 171 let body_n: i64 = req_n - body_off 172 let hoff: *i64 = sys_mmap(8) as *i64 173 let hn: *i64 = sys_mmap(8) as *i64 174 let poff2: *i64 = sys_mmap(8) as *i64 175 let pnn: *i64 = sys_mmap(8) as *i64 176 var got: i64 = 0 177 if stu_form_field(body, body_n, "handle" as *u8, 6, hoff, hn) == 1 { 178 if stu_form_field(body, body_n, "passphrase" as *u8, 10, poff2, pnn) == 1 { got = 1 } 179 } 180 var ok: i64 = 0 181 if got == 1 { 182 let hbuf: *u8 = sys_mmap(256) 183 let pbuf: *u8 = sys_mmap(512) 184 let h_dec: i64 = stu_urldecode(((body as i64) + hoff[0]) as *u8, hn[0], hbuf, 255) 185 let p_dec: i64 = stu_urldecode(((body as i64) + poff2[0]) as *u8, pnn[0], pbuf, 511) 186 if h_dec > 0 { if p_dec > 0 { 187 let tok: *u8 = sys_mmap(NX_MAUTH_SESSION_TOKEN_BYTES) 188 let tok_n: *i64 = sys_mmap(8) as *i64 189 tok_n[0] = 0 190 if nx_modern_auth_login(ctx, hbuf, h_dec, pbuf, p_dec, tok, NX_MAUTH_SESSION_TOKEN_BYTES, tok_n) == NX_MAUTH_OK { 191 let b64: *u8 = sys_mmap(256) 192 let b64_n: i64 = b64_encode(tok, NX_MAUTH_SESSION_TOKEN_BYTES, b64) 193 o = stu_cat(out, o, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: " as *u8) 194 o = stu_catn(out, o, 12 + b64_n) 195 o = stu_cat(out, o, "\r\n\r\n{\"token\":\"" as *u8) 196 var z: i64 = 0 197 while z < b64_n { out[o] = b64[z]; o = o + 1; z = z + 1 } 198 o = stu_cat(out, o, "\"}" as *u8) 199 ok = 1 200 } 201 } } 202 } 203 if ok == 0 { o = stu_emit_401(out) } 204 } else { o = stu_shell(out) } 205 } else { if stu_starts(path, pn, "/webdev/build" as *u8) == 1 { 206 let now_s: i64 = sys_now_realtime_sec() 207 if nx_sa_validate(ctx, req, req_n, now_s) == NX_MAUTH_OK { 208 let body_off: i64 = stu_body_off(req, req_n) 209 let body: *u8 = ((req as i64) + body_off) as *u8 210 let body_n: i64 = req_n - body_off 211 let bbuf: *u8 = sys_mmap(STU_OUTCAP) 212 let bn: i64 = se_build_tier(body, body_n, bbuf, STU_OUTCAP - 1) 213 if bn > 0 { 214 o = stu_cat(out, o, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\nContent-Length: " as *u8) 215 o = stu_catn(out, o, bn); o = stu_cat(out, o, "\r\n\r\n" as *u8) 216 var z: i64 = 0 217 while z < bn { out[o] = bbuf[z]; o = o + 1; z = z + 1 } 218 } else { 219 let rb: *u8 = "<!doctype html><meta charset=utf-8><p style='font:15px sans-serif;color:#a11'>Refused: not UX-compliant (needs CTA + onsite search + trust + 1-3 steps).</p>" as *u8 220 o = stu_cat(out, o, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\nContent-Length: " as *u8) 221 o = stu_catn(out, o, stu_len(rb)); o = stu_cat(out, o, "\r\n\r\n" as *u8); o = stu_cat(out, o, rb) 222 } 223 } else { o = stu_emit_401(out) } 224 } else { if stu_starts(path, pn, "/webdev/guided" as *u8) == 1 { 225 o = stu_serve(out, "web_assets/_studio/editor_guided.html" as *u8) 226 } else { if stu_starts(path, pn, "/webdev/blocks" as *u8) == 1 { 227 o = stu_serve(out, "web_assets/_studio/editor.html" as *u8) 228 } else { if stu_starts(path, pn, "/webdev/freeform" as *u8) == 1 { 229 o = stu_serve(out, "web_assets/_studio/editor_freeform.html" as *u8) 230 } else { o = stu_shell(out) } } } } } 231 return o 232} 233 234func main(argc: i64, argv: *i64) -> i64 { 235 if argc < 6 { sys_write(2, "usage: nx_sitegen_studio_daemon <port> <keysfile> <storefile> <realm> <budget>\n" as *u8, 78); return 1 } 236 let port: i64 = stu_atoi(argv[1] as *u8) 237 let keysfile: *u8 = argv[2] as *u8 238 let storefile: *u8 = argv[3] as *u8 239 let realm: *u8 = argv[4] as *u8 240 let budget: i64 = stu_atoi(argv[5] as *u8) 241 let realm_n: i64 = stu_len(realm) 242 243 let oprf_seed: *u8 = sys_mmap(32) 244 let akp: *u8 = sys_mmap(32) 245 let akb: *u8 = sys_mmap(33) 246 let edp: *u8 = sys_mmap(32) 247 let edb: *u8 = sys_mmap(32) 248 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 } 249 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext 250 if nx_auth_context_init(ctx, realm, realm_n, realm, realm_n, storefile as i64, oprf_seed, edp, edb, 900, STU_MAGIC_8192, 1, 1, 5, 1) != NX_MAUTH_OK { sys_write(2, "FATAL: context init\n" as *u8, 20); return 3 } 251 252 let addr: *u8 = sys_mmap(16) 253 if nx_http_server_addr_loopback(addr, port) != 16 { return 4 } 254 let lv: *i64 = sys_mmap(8) as *i64 255 let lfd: i64 = nx_http_server_listen(addr, 64, lv) 256 if lfd < 0 { return 4 } 257 sys_write(1, "nx_sitegen_studio_daemon (OPAQUE modern-auth) listening\n" as *u8, 56) 258 259 let req: *u8 = sys_mmap(STU_REQCAP) 260 let out: *u8 = sys_mmap(STU_OUTCAP) 261 var served: i64 = 0 262 while served < budget { 263 let av: *i64 = sys_mmap(8) as *i64 264 let cfd: i64 = nx_http_server_accept_one(lfd, av) 265 if cfd < 0 { served = served + 1 } 266 if cfd >= 0 { 267 let om: *i64 = sys_mmap(8) as *i64 268 let opo: *i64 = sys_mmap(8) as *i64 269 let opl: *i64 = sys_mmap(8) as *i64 270 let ocl: *i64 = sys_mmap(8) as *i64 271 let obo: *i64 = sys_mmap(8) as *i64 272 let orn: *i64 = sys_mmap(8) as *i64 273 let rrc: i64 = nx_http_server_read_request(cfd, req, STU_REQCAP, om, opo, opl, ocl, obo, orn) 274 if rrc == NXS_OK { 275 let oo: i64 = stu_handle(ctx, req, orn[0], out) 276 nx_http_server_send_response_nokeep_close(cfd, out, oo) 277 } 278 sys_close(cfd) 279 served = served + 1 280 } 281 } 282 sys_close(lfd) 283 return 0 284}