code wiki / _hdl_build / nx_project_serve.nx

nx_project_serve.nx source

↩ module page · 746 lines · 33373 B

1// nx_project_serve.nx -- the /project GATEWAY core (pure request->response; sockets live in the _daemon 2// wrapper, the gate drives THIS core with real tokens and no sockets -- the relate_serve idiom). 3// Sits behind the sites.elf proxy row `nishifamily.com /project 8028 gated` (fail-closed 302 on down). 4// Per request: resolve the OPAQUE no-cookie X-Nishi-Session token -> uid (olg_whoami, realm 5// nishi_site_admin, the SAME keys/store the /login minter uses) -> handle (uid-hex over the login 6// daemon's index file) -> person (data-driven map rows in serve.conf) -> the ROLE-SCOPED project page 7// via pj_html_build (owner/team all tasks, contractor own-only, non-member 403 DENY -- fail-closed). 8// Token-less top-level navigation -> the sessionStorage bootstrap (a bare 302 would loop: no-cookie navs 9// carry no header). Present-but-invalid token -> 302 /login. /project/demo -> the public demo tenant. 10// serve.conf (hot, per-request): `store <prefix>` `pid <id>` `demo <person>` `map <handle> <person>`. 11// license_tier: ORIGINAL 12import "nx_opaque_login.nx" 13import "nx_hub_gw_decide.nx" 14import "nx_site_lock_lib.nx" 15import "nx_project.nx" 16const PSV_MAGIC_719468: i64 = 719468 17const PSV_MAGIC_146097: i64 = 146097 18const PSV_MAGIC_146096: i64 = 146096 19const PSV_MAGIC_1460: i64 = 1460 20const PSV_MAGIC_36524: i64 = 36524 21const PSV_MAGIC_86400: i64 = 86400 22const PSV_MAGIC_65536: i64 = 65536 23const PSV_MAGIC_8192: i64 = 8192 24const PSV_MAGIC_262144: i64 = 262144 25const PSV_MAGIC_1024: i64 = 1024 26const PSV_MAGIC_16384: i64 = 16384 27const PSV_MAGIC_131072: i64 = 131072 28const PSV_MAGIC_16400: i64 = 16400 29const PSV_MAGIC_16383: i64 = 16383 30 31const PSV_BOOTSTRAP: *u8 = "<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'><title>Nishi Project</title></head><body><main style='max-width:48rem;margin:8vh auto;padding:1.5rem;font:1.1rem/1.6 system-ui,sans-serif'><h1>Nishi Project</h1><p id='project-status' role='status' aria-live='polite'>Opening your project…</p><button id='project-retry' type='button' hidden>Retry</button><details><summary>Release notes</summary><p>Session recovery · 8 September 2026. Access and connection failures retain your session. Sign-in returns to the project you requested.</p></details><noscript>Enable JavaScript to use the existing secure session handoff.</noscript></main><script>(function(){var status=document.getElementById('project-status');var retry=document.getElementById('project-retry');var destination=location.pathname+location.search+location.hash;function show(message){status.textContent=message;retry.hidden=false;retry.disabled=false}function signin(){location.assign('/login?return='+encodeURIComponent(destination))}async function load(){retry.disabled=true;retry.hidden=true;status.textContent='Opening your project…';var token;try{token=sessionStorage.getItem('nsess')}catch(e){show('Browser session storage is unavailable. Enable it for this site, then retry.');return}if(!token){signin();return}try{var r=await fetch(location.pathname+location.search,{headers:{'X-Nishi-Session':token},credentials:'omit',cache:'no-store',redirect:'manual'});if(r.type==='opaqueredirect'||r.status===401){signin();return}if(r.status===403){show('Access denied for this project. Your session has been kept. Ask the project owner to check your membership.');return}if(!r.ok){show('Project service returned HTTP '+r.status+'. Your session has been kept. Retry when the service is available.');return}if(!(r.headers.get('content-type')||'').toLowerCase().includes('text/html')){show('The project service returned an unexpected response. Your session has been kept.');return}var html=await r.text();if(!html){show('The project service returned an empty page. Your session has been kept.');return}document.open();document.write(html);document.close()}catch(e){show('Unable to reach the project service. Your session has been kept. Check your connection and retry.')}}retry.addEventListener('click',load);return load()})();</script></body></html>" 32const PSV_DENY: *u8 = "<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'><title>Nishi Project - no access</title></head><body><h1>Not a member of this project</h1><p>Your login is valid, but this account has no membership record on the project. Ask the project owner to add you.</p><p><a href='/login'>switch account</a></p></body></html>" 33 34// leak-free bounded read into a REUSED caller buffer; returns bytes (>=0) or -1 (hub gw discipline) 35func ps_read_file(path: *u8, out: *u8, cap: i64) -> i64 { 36 let fd: i64 = sys_openat_rd(path) 37 if fd < 0 { return 0 - 1 } 38 var total: i64 = 0 39 var go: i64 = 1 40 while go == 1 { 41 let base: i64 = out as i64 42 let tail: *u8 = (base + total) as *u8 43 let nr: i64 = sys_read(fd, tail, cap - total) 44 if nr <= 0 { go = 0 } 45 if nr > 0 { total = total + nr } 46 if total >= cap { go = 0 } 47 } 48 sys_close(fd) 49 return total 50} 51 52func ps_hex(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { 53 let hx: *u8 = "0123456789abcdef" as *u8 54 var o: i64 = off 55 var i: i64 = 0 56 while i < n { 57 let c: i64 = (src[i] as i64) & 0xff 58 dst[o] = hx[(c >> 4) & 15] 59 dst[o + 1] = hx[c & 15] 60 o = o + 2 61 i = i + 1 62 } 63 return o 64} 65 66// uidhex -> handle from the login daemon's index (field0=uidhex field1=handle); returns handle len or 0 67func ps_idx_lookup(idxbuf: *u8, idxlen: i64, uidhex: *u8, uxn: i64, out_h: *u8) -> i64 { 68 let fs: *i64 = sys_mmap(8) as *i64 69 let fe: *i64 = sys_mmap(8) as *i64 70 var ls: i64 = 0 71 var found: i64 = 0 72 while ls < idxlen { 73 let le: i64 = slk_line_end(idxbuf, idxlen, ls) 74 if found == 0 { if le > ls { if idxbuf[ls] != (35 as u8) { 75 if slk_field(idxbuf, ls, le, 0, fs, fe) == 1 { 76 if slk_eq(slk_at(idxbuf, fs[0]), fe[0] - fs[0], uidhex, uxn) == 1 { 77 if slk_field(idxbuf, ls, le, 1, fs, fe) == 1 { 78 var o: i64 = 0 79 let hl: i64 = fe[0] - fs[0] 80 while o < hl { if o < 127 { out_h[o] = idxbuf[fs[0] + o] } o = o + 1 } 81 out_h[o] = 0 as u8 82 found = hl 83 } 84 } 85 } 86 } } } 87 ls = le + 1 88 } 89 return found 90} 91 92// conf pair: first line `<key> <v1> <v2>` -> both tokens; 1 if found (e.g. `demo <pid> <person>`) 93func ps_conf_pair(b: *u8, n: i64, key: *u8, out1: *u8, out2: *u8) -> i64 { 94 out1[0] = 0 as u8 95 out2[0] = 0 as u8 96 let t0: *u8 = sys_mmap(128) 97 var i: i64 = 0 98 var found: i64 = 0 99 while i < n { 100 var e: i64 = i 101 var go: i64 = 1 102 while go == 1 { 103 if e >= n { go = 0 } else { 104 if b[e] == (10 as u8) { go = 0 } else { e = e + 1 } 105 } 106 } 107 if found == 0 { 108 var p: i64 = pj_tok(b, i, e, t0, 128) 109 if pj_eq(t0, key) == 1 { 110 p = pj_tok(b, p, e, out1, 128) 111 p = pj_tok(b, p, e, out2, 128) 112 if out1[0] != (0 as u8) { if out2[0] != (0 as u8) { found = 1 } } 113 } 114 } 115 i = e + 1 116 } 117 return found 118} 119 120// project row `project <pid> <store-prefix>` -> store prefix for pid; 1 if found 121func ps_proj_store(b: *u8, n: i64, pid: *u8, out: *u8, cap: i64) -> i64 { 122 out[0] = 0 as u8 123 let t0: *u8 = sys_mmap(128) 124 let t1: *u8 = sys_mmap(128) 125 var i: i64 = 0 126 var found: i64 = 0 127 while i < n { 128 var e: i64 = i 129 var go: i64 = 1 130 while go == 1 { 131 if e >= n { go = 0 } else { 132 if b[e] == (10 as u8) { go = 0 } else { e = e + 1 } 133 } 134 } 135 if found == 0 { 136 var p: i64 = pj_tok(b, i, e, t0, 128) 137 if pj_eq(t0, "project" as *u8) == 1 { 138 p = pj_tok(b, p, e, t1, 128) 139 if pj_eq(t1, pid) == 1 { 140 p = pj_tok(b, p, e, out, cap) 141 if out[0] != (0 as u8) { found = 1 } 142 } 143 } 144 } 145 i = e + 1 146 } 147 return found 148} 149 150// pid charset guard: 1..40 of [a-z0-9_-] (fail-closed; the path segment is caller data) 151func ps_pid_ok(s: *u8) -> i64 { 152 var i: i64 = 0 153 while s[i] != (0 as u8) { 154 let c: i64 = s[i] as i64 155 var okc: i64 = 0 156 if c >= 97 { if c <= 122 { okc = 1 } } 157 if c >= 48 { if c <= 57 { okc = 1 } } 158 if c == 95 { okc = 1 } 159 if c == 45 { okc = 1 } 160 if okc == 0 { return 0 } 161 i = i + 1 162 } 163 if i < 1 { return 0 } 164 if i > 40 { return 0 } 165 return 1 166} 167 168// the PICKER page: every `project` conf row where person is a member -> a link card. 169// returns page len, or -2 when person belongs to no project (deny). 170func ps_picker(confbuf: *u8, cl: i64, person: *u8, hb: *u8) -> i64 { 171 let t0: *u8 = sys_mmap(128) 172 let t1: *u8 = sys_mmap(128) 173 let t2: *u8 = sys_mmap(256) 174 var o: i64 = 0 175 o = pj_head(hb, o, "Nishi Projects" as *u8) 176 o = pj_cat(hb, o, "<h1>Nishi Projects</h1><p class='meta'>signed in as " as *u8) 177 o = pj_cat(hb, o, person) 178 o = pj_cat(hb, o, " - you see only projects where you hold a membership record</p>" as *u8) 179 var nproj: i64 = 0 180 var i: i64 = 0 181 while i < cl { 182 var e: i64 = i 183 var go: i64 = 1 184 while go == 1 { 185 if e >= cl { go = 0 } else { 186 if confbuf[e] == (10 as u8) { go = 0 } else { e = e + 1 } 187 } 188 } 189 var p: i64 = pj_tok(confbuf, i, e, t0, 128) 190 if pj_eq(t0, "project" as *u8) == 1 { 191 p = pj_tok(confbuf, p, e, t1, 128) 192 p = pj_tok(confbuf, p, e, t2, 256) 193 if t1[0] != (0 as u8) { if t2[0] != (0 as u8) { 194 let stg: *i64 = pj_st_new() 195 if pj_gather(t2, t1, stg) == 0 { if stg[12] == 1 { 196 let role: i64 = pj_roleof(stg, person) 197 if role != 0 { 198 o = pj_cat(hb, o, "<a class='card' href='/project/p/" as *u8) 199 o = pj_cat(hb, o, t1) 200 o = pj_cat(hb, o, "'>" as *u8) 201 o = pj_cat(hb, o, stg[14] as *u8) 202 o = pj_cat(hb, o, " <span class='meta'>(" as *u8) 203 o = pj_cat(hb, o, t1) 204 o = pj_cat(hb, o, " - your role: " as *u8) 205 o = pj_cat(hb, o, role as *u8) 206 o = pj_cat(hb, o, ")</span></a>" as *u8) 207 nproj = nproj + 1 208 } 209 } } 210 } } 211 } 212 i = e + 1 213 } 214 if nproj == 0 { return 0 - 2 } 215 o = pj_foot(hb, o) 216 return o 217} 218 219// map row `map <handle> <person>`; absent -> person = handle (natural handle==person default) 220func ps_map(b: *u8, n: i64, handle: *u8, out: *u8, cap: i64) -> i64 { 221 let t0: *u8 = sys_mmap(128) 222 let t1: *u8 = sys_mmap(128) 223 var i: i64 = 0 224 var found: i64 = 0 225 while i < n { 226 var e: i64 = i 227 var go: i64 = 1 228 while go == 1 { 229 if e >= n { go = 0 } else { 230 if b[e] == (10 as u8) { go = 0 } else { e = e + 1 } 231 } 232 } 233 if found == 0 { 234 var p: i64 = pj_tok(b, i, e, t0, 128) 235 if pj_eq(t0, "map" as *u8) == 1 { 236 p = pj_tok(b, p, e, t1, 128) 237 if pj_eq(t1, handle) == 1 { 238 p = pj_tok(b, p, e, out, cap) 239 if out[0] != (0 as u8) { found = 1 } 240 } 241 } 242 } 243 i = e + 1 244 } 245 if found == 0 { 246 var k: i64 = 0 247 var go2: i64 = 1 248 while go2 == 1 { 249 out[k] = handle[k] 250 if handle[k] == (0 as u8) { go2 = 0 } else { k = k + 1 } 251 } 252 } 253 return found 254} 255 256// epoch-days -> civil (Howard Hinnant algorithm, mirrors the proven planner math) 257func ps_civil(z0: i64, out: *i64) -> i64 { 258 var z: i64 = z0 + PSV_MAGIC_719468 259 var era: i64 = z / PSV_MAGIC_146097 260 if z < 0 { era = (z - PSV_MAGIC_146096) / PSV_MAGIC_146097 } 261 let doe: i64 = z - era * PSV_MAGIC_146097 262 let yoe: i64 = (doe - doe / PSV_MAGIC_1460 + doe / PSV_MAGIC_36524 - doe / PSV_MAGIC_146096) / 365 263 let y0: i64 = yoe + era * 400 264 let doy: i64 = doe - (365 * yoe + yoe / 4 - yoe / 100) 265 let mp: i64 = (5 * doy + 2) / 153 266 let d: i64 = doy - (153 * mp + 2) / 5 + 1 267 var m: i64 = mp + 3 268 if mp >= 10 { m = mp - 9 } 269 var y: i64 = y0 270 if m <= 2 { y = y0 + 1 } 271 out[0] = y 272 out[1] = m 273 out[2] = d 274 return 0 275} 276 277// LEAK ROOT-FIX 2026-08-17: this ran on EVERY request and did sys_mmap(32) per call, never unmapped -- 278// one PAGE (4096B) leaked per request. Measured on the live daemon: worst_committed_kb creeping ~264KB 279// (=64 pages = 64 probe requests) per resmon beat, 2.55GB total, swap 690->720 permil. Scratch is now a 280// static pointer, mmapped ONCE on first use (the estate's sanctioned static-POINTER idiom) -- leak-free 281// by construction and one syscall cheaper per request. 282static ps_ymd_scratch: *i64 283 284func ps_today(out: *u8) -> i64 { 285 let secs: i64 = sys_now_realtime_sec() 286 let days: i64 = secs / PSV_MAGIC_86400 287 if (ps_ymd_scratch as i64) == 0 { ps_ymd_scratch = sys_mmap(32) as *i64 } 288 let ymd: *i64 = ps_ymd_scratch 289 ps_civil(days, ymd) 290 var o: i64 = 0 291 o = pj_catn(out, o, ymd[0]) 292 out[o] = 45 as u8 293 o = o + 1 294 if ymd[1] < 10 { out[o] = 48 as u8; o = o + 1 } 295 o = pj_catn(out, o, ymd[1]) 296 out[o] = 45 as u8 297 o = o + 1 298 if ymd[2] < 10 { out[o] = 48 as u8; o = o + 1 } 299 o = pj_catn(out, o, ymd[2]) 300 out[o] = 0 as u8 301 return o 302} 303 304// build a full HTTP response into resp; returns total bytes 305func ps_resp(resp: *u8, status: *u8, ctype: *u8, body: *u8, blen: i64) -> i64 { 306 var o: i64 = 0 307 o = pj_cat(resp, o, "HTTP/1.1 " as *u8) 308 o = pj_cat(resp, o, status) 309 o = pj_cat(resp, o, "\r\nContent-Type: " as *u8) 310 o = pj_cat(resp, o, ctype) 311 o = pj_cat(resp, o, "\r\nContent-Length: " as *u8) 312 o = pj_catn(resp, o, blen) 313 o = pj_cat(resp, o, "\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8) 314 var i: i64 = 0 315 while i < blen { resp[o] = body[i]; o = o + 1; i = i + 1 } 316 return o 317} 318 319func ps_redirect(resp: *u8, loc: *u8) -> i64 { 320 var o: i64 = 0 321 o = pj_cat(resp, o, "HTTP/1.1 302 Found\r\nLocation: " as *u8) 322 o = pj_cat(resp, o, loc) 323 o = pj_cat(resp, o, "\r\nContent-Length: 0\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8) 324 return o 325} 326 327func ps_text(resp: *u8, status: *u8, msg: *u8) -> i64 { 328 return ps_resp(resp, status, "text/plain; charset=utf-8" as *u8, msg, pj_slen(msg)) 329} 330 331// ---- st slots: 0=ctx 1=confpath 2=idxpath 3=pagebuf 4=confbuf 5=idxbuf 6=tokbuf 7=pathbuf 332// 8=uid 9=uxbuf 10=handle 11=person 12=todaybuf 13=storebuf 14=pidbuf 15=demobuf 16=uidn ---- 333func psv_st_new(ctx: *NxAuthContext, confpath: *u8, idxpath: *u8) -> *i64 { 334 let st: *i64 = sys_mmap(8 * 20) as *i64 335 st[0] = ctx as i64 336 st[1] = confpath as i64 337 st[2] = idxpath as i64 338 st[3] = sys_mmap(PSV_MAGIC_65536) 339 st[4] = sys_mmap(PSV_MAGIC_8192) 340 st[5] = sys_mmap(PSV_MAGIC_262144) 341 st[6] = sys_mmap(512) 342 st[7] = sys_mmap(PSV_MAGIC_1024) 343 st[8] = sys_mmap(64) 344 st[9] = sys_mmap(160) 345 st[10] = sys_mmap(128) 346 st[11] = sys_mmap(128) 347 st[12] = sys_mmap(32) 348 st[13] = sys_mmap(256) 349 st[14] = sys_mmap(128) 350 st[15] = sys_mmap(128) 351 st[16] = sys_mmap(16) 352 return st 353} 354 355func psv_handle(st: *i64, req: *u8, rn: i64, resp: *u8) -> i64 { 356 let path: *u8 = st[7] as *u8 357 var pl: i64 = hgw_path(req, rn, path, PSV_MAGIC_1024) 358 // strip the query string: the edge relays the FULL request-target (/project?b=x) and exact-match 359 // routing would 404 it (found live: WebFetch cache-busters). '?' truncates. 360 var qi: i64 = 0 361 var qgo: i64 = 1 362 while qgo == 1 { 363 if qi >= pl { qgo = 0 } else { 364 if path[qi] == (63 as u8) { path[qi] = 0 as u8; pl = qi; qgo = 0 } else { qi = qi + 1 } 365 } 366 } 367 let confbuf: *u8 = st[4] as *u8 368 let cl: i64 = ps_read_file(st[1] as *u8, confbuf, PSV_MAGIC_8192) 369 if cl <= 0 { return ps_text(resp, "503 Service Unavailable" as *u8, "project portal not configured" as *u8) } 370 let today: *u8 = st[12] as *u8 371 ps_today(today) 372 let pagebuf: *u8 = st[3] as *u8 373 let storebuf: *u8 = st[13] as *u8 374 // ---- /project/demo = the PUBLIC demo tenant (conf: `demo <pid> <person>`) ---- 375 var isdemo: i64 = 0 376 if slk_eq(path, pl, "/project/demo" as *u8, 13) == 1 { isdemo = 1 } 377 if slk_eq(path, pl, "/project/demo/" as *u8, 14) == 1 { isdemo = 1 } 378 if isdemo == 1 { 379 let dpid: *u8 = st[14] as *u8 380 let dper: *u8 = st[15] as *u8 381 if ps_conf_pair(confbuf, cl, "demo" as *u8, dpid, dper) == 0 { return ps_text(resp, "404 Not Found" as *u8, "no public demo configured" as *u8) } 382 if ps_proj_store(confbuf, cl, dpid, storebuf, 256) == 0 { return ps_text(resp, "503 Service Unavailable" as *u8, "demo project not in conf" as *u8) } 383 let dn: i64 = pj_html_build(storebuf, dpid, dper, today, pagebuf) 384 if dn < 0 { return ps_text(resp, "503 Service Unavailable" as *u8, "demo unavailable" as *u8) } 385 return ps_resp(resp, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, pagebuf, dn) 386 } 387 // ---- route shape: the picker root, or a per-project page /project/p/<pid> ---- 388 var isroot: i64 = 0 389 if slk_eq(path, pl, "/project" as *u8, 8) == 1 { isroot = 1 } 390 if slk_eq(path, pl, "/project/" as *u8, 9) == 1 { isroot = 1 } 391 var ispage: i64 = 0 392 if isroot == 0 { if pl > 11 { if pj_starts(path, "/project/p/" as *u8) == 1 { ispage = 1 } } } 393 if isroot == 0 { if ispage == 0 { return ps_text(resp, "404 Not Found" as *u8, "not found" as *u8) } } 394 // ---- session resolution (fail-closed; token-less nav gets the sessionStorage bootstrap) ---- 395 let tok: *u8 = st[6] as *u8 396 let tl: i64 = hgw_hdr_val(req, rn, "X-Nishi-Session:" as *u8, 16, tok, 512) 397 if tl == 0 { return ps_resp(resp, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, PSV_BOOTSTRAP, pj_slen(PSV_BOOTSTRAP)) } 398 let ctx: *NxAuthContext = st[0] as *NxAuthContext 399 let uid: *u8 = st[8] as *u8 400 let uidn: *i64 = st[16] as *i64 401 let handle: *u8 = st[10] as *u8 402 var hl: i64 = 0 403 let now: i64 = sys_now_realtime_sec() 404 if olg_whoami(ctx, tok, tl, now, uid, 64, uidn) == NX_MAUTH_OK { 405 let uxbuf: *u8 = st[9] as *u8 406 let uxn: i64 = ps_hex(uxbuf, 0, uid, uidn[0]) 407 let idxbuf: *u8 = st[5] as *u8 408 let il: i64 = ps_read_file(st[2] as *u8, idxbuf, PSV_MAGIC_262144) 409 if il > 0 { hl = ps_idx_lookup(idxbuf, il, uxbuf, uxn, handle) } 410 } 411 if hl == 0 { return ps_redirect(resp, "/login?return=/project" as *u8) } 412 let person: *u8 = st[11] as *u8 413 ps_map(confbuf, cl, handle, person, 128) 414 // ---- the PICKER: projects where this person holds a membership record ---- 415 if isroot == 1 { 416 let kn: i64 = ps_picker(confbuf, cl, person, pagebuf) 417 if kn >= 0 { return ps_resp(resp, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, pagebuf, kn) } 418 return ps_resp(resp, "403 Forbidden" as *u8, "text/html; charset=utf-8" as *u8, PSV_DENY, pj_slen(PSV_DENY)) 419 } 420 // ---- per-project page: /project/p/<pid> (unknown pid = 404, non-member = 403) ---- 421 let pidp: *u8 = ((path as i64) + 11) as *u8 422 if ps_pid_ok(pidp) == 0 { return ps_text(resp, "404 Not Found" as *u8, "not found" as *u8) } 423 if ps_proj_store(confbuf, cl, pidp, storebuf, 256) == 0 { return ps_text(resp, "404 Not Found" as *u8, "no such project" as *u8) } 424 let n2: i64 = pj_html_build(storebuf, pidp, person, today, pagebuf) 425 if n2 >= 0 { return ps_resp(resp, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, pagebuf, n2) } 426 if n2 == 0 - 2 { return ps_resp(resp, "403 Forbidden" as *u8, "text/html; charset=utf-8" as *u8, PSV_DENY, pj_slen(PSV_DENY)) } 427 return ps_text(resp, "503 Service Unavailable" as *u8, "project store unavailable" as *u8) 428} 429 430// ---------------- SELFTEST (socket-free; REAL tokens against fresh auto-init keys) ---------------- 431func psv_req(dst: *u8, path: *u8, tok: *u8) -> i64 { 432 var o: i64 = 0 433 o = pj_cat(dst, o, "GET " as *u8) 434 o = pj_cat(dst, o, path) 435 o = pj_cat(dst, o, " HTTP/1.1\r\nHost: t\r\n" as *u8) 436 if tok[0] != (0 as u8) { 437 o = pj_cat(dst, o, "X-Nishi-Session: " as *u8) 438 o = pj_cat(dst, o, tok) 439 o = pj_cat(dst, o, "\r\n" as *u8) 440 } 441 o = pj_cat(dst, o, "\r\n" as *u8) 442 return o 443} 444 445func psv_selftest() -> i64 { 446 let uniq: i64 = sys_now_us() 447 let base: *u8 = sys_mmap(256) 448 var bo: i64 = 0 449 bo = pj_cat(base, bo, "/tmp/psg_" as *u8) 450 bo = pj_catn(base, bo, uniq) 451 base[bo] = 0 as u8 452 let keys: *u8 = sys_mmap(256) 453 var o: i64 = pj_cat(keys, 0, base) 454 o = pj_cat(keys, o, "_keys.bin" as *u8) 455 keys[o] = 0 as u8 456 let astore: *u8 = sys_mmap(256) 457 o = pj_cat(astore, 0, base) 458 o = pj_cat(astore, o, "_astore.log" as *u8) 459 astore[o] = 0 as u8 460 let idxf: *u8 = sys_mmap(256) 461 o = pj_cat(idxf, 0, base) 462 o = pj_cat(idxf, o, "_idx.tsv" as *u8) 463 idxf[o] = 0 as u8 464 let conff: *u8 = sys_mmap(256) 465 o = pj_cat(conff, 0, base) 466 o = pj_cat(conff, o, "_serve.conf" as *u8) 467 conff[o] = 0 as u8 468 let planf: *u8 = sys_mmap(256) 469 o = pj_cat(planf, 0, base) 470 o = pj_cat(planf, o, "_plan.txt" as *u8) 471 planf[o] = 0 as u8 472 let projf: *u8 = sys_mmap(256) 473 o = pj_cat(projf, 0, base) 474 o = pj_cat(projf, o, "_proj.txt" as *u8) 475 projf[o] = 0 as u8 476 let prefix: *u8 = sys_mmap(256) 477 o = pj_cat(prefix, 0, base) 478 o = pj_cat(prefix, o, "_stA-" as *u8) 479 prefix[o] = 0 as u8 480 let prefB: *u8 = sys_mmap(256) 481 o = pj_cat(prefB, 0, base) 482 o = pj_cat(prefB, o, "_stB-" as *u8) 483 prefB[o] = 0 as u8 484 let planb: *u8 = sys_mmap(256) 485 o = pj_cat(planb, 0, base) 486 o = pj_cat(planb, o, "_planb.txt" as *u8) 487 planb[o] = 0 as u8 488 let projb: *u8 = sys_mmap(256) 489 o = pj_cat(projb, 0, base) 490 o = pj_cat(projb, o, "_projb.txt" as *u8) 491 projb[o] = 0 as u8 492 let outf: *u8 = sys_mmap(256) 493 o = pj_cat(outf, 0, base) 494 o = pj_cat(outf, o, "_run.out" as *u8) 495 outf[o] = 0 as u8 496 497 pj_wfile(planf, "TASK fence f0 amber 2026-07-01 30 1 - permits\nTASK fence f1 amber 2026-07-20 200 3 - posts\nTASK fence f2 bo 2026-07-18 100 4 - rails\nTASK fence f3 bo 2026-07-19 80 2 f2 gates\nDONE fence f1 2026-07-08\n" as *u8) 498 pj_wfile(projf, "PROJ fence elder fence-build\nMEMBER fence amber team 240\nMEMBER fence bo contractor 120\nWCONF fence 120 1\n" as *u8) 499 pj_wfile(planb, "TASK barn b1 amber 2027-01-01 60 3 - paint\n" as *u8) 500 pj_wfile(projb, "PROJ barn elder barn-paint\nMEMBER barn amber team -\n" as *u8) 501 let confc: *u8 = sys_mmap(PSV_MAGIC_1024) 502 o = 0 503 o = pj_cat(confc, o, "project fence " as *u8) 504 o = pj_cat(confc, o, prefix) 505 o = pj_cat(confc, o, "\nproject barn " as *u8) 506 o = pj_cat(confc, o, prefB) 507 o = pj_cat(confc, o, "\ndemo fence elder\nmap bo_test bo\n" as *u8) 508 confc[o] = 0 as u8 509 pj_wfile(conff, confc) 510 511 var pass: i64 = 0 512 // T1 the independent planner writes the task records for BOTH projects 513 let a1: *i64 = sys_mmap(8 * 8) as *i64 514 a1[0] = "load" as *u8 as i64 515 a1[1] = planf as i64 516 a1[2] = prefix as i64 517 a1[3] = "7301" as *u8 as i64 518 a1[4] = 0 519 let rc1: i64 = pj_run("_offc/nx_vizsla_plan.elf" as *u8, a1, outf) 520 let a1b: *i64 = sys_mmap(8 * 8) as *i64 521 a1b[0] = "load" as *u8 as i64 522 a1b[1] = planb as i64 523 a1b[2] = prefB as i64 524 a1b[3] = "7303" as *u8 as i64 525 a1b[4] = 0 526 let rc1b: i64 = pj_run("_offc/nx_vizsla_plan.elf" as *u8, a1b, outf) 527 let av: *i64 = sys_mmap(8 * 8) as *i64 528 av[0] = "pj" as *u8 as i64 529 av[1] = "load" as *u8 as i64 530 av[2] = projf as i64 531 av[3] = prefix as i64 532 av[4] = "7302" as *u8 as i64 533 let rc2: i64 = pj_cmd_load(5, av) 534 let avb: *i64 = sys_mmap(8 * 8) as *i64 535 avb[0] = "pj" as *u8 as i64 536 avb[1] = "load" as *u8 as i64 537 avb[2] = projb as i64 538 avb[3] = prefB as i64 539 avb[4] = "7304" as *u8 as i64 540 let rc2b: i64 = pj_cmd_load(5, avb) 541 var ok: i64 = 0 542 if rc1 == 0 { if rc1b == 0 { if rc2 == 0 { if rc2b == 0 { ok = 1 } } } } 543 pass = pass + pj_check(1, ok, "fixtures-loaded-2-projects" as *u8) 544 // T2 auth context on FRESH keys (load_or_init) + register + login = REAL tokens 545 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext 546 let crc: i64 = olg_ctx_setup(ctx, keys, astore, "nishi_site_admin" as *u8, 16, "Nishi site admin" as *u8, 16, 256, 2, 1) 547 let mn: *u8 = sys_mmap(PSV_MAGIC_1024) 548 let mnn: *i64 = sys_mmap(16) as *i64 549 let rr1: i64 = olg_register(ctx, "bo_test" as *u8, 7, "pw_bo_9931x" as *u8, 11, mn, PSV_MAGIC_1024, mnn) 550 let rr2: i64 = olg_register(ctx, "mallory_t" as *u8, 9, "pw_mal_442x" as *u8, 11, mn, PSV_MAGIC_1024, mnn) 551 let tokb: *u8 = sys_mmap(512) 552 let tbn: *i64 = sys_mmap(16) as *i64 553 let lr1: i64 = olg_login(ctx, "bo_test" as *u8, 7, "pw_bo_9931x" as *u8, 11, tokb, 512, tbn) 554 let tokm: *u8 = sys_mmap(512) 555 let tmn: *i64 = sys_mmap(16) as *i64 556 let lr2: i64 = olg_login(ctx, "mallory_t" as *u8, 9, "pw_mal_442x" as *u8, 11, tokm, 512, tmn) 557 tokb[tbn[0]] = 0 as u8 558 tokm[tmn[0]] = 0 as u8 559 ok = 0 560 if crc == 0 { if rr1 == 0 { if rr2 == 0 { if lr1 == 0 { if lr2 == 0 { ok = 1 } } } } } 561 pass = pass + pj_check(2, ok, "opaque-register-login" as *u8) 562 // build the uid->handle index from the REAL whoami uids 563 let uid: *u8 = sys_mmap(64) 564 let uidn: *i64 = sys_mmap(16) as *i64 565 let now: i64 = sys_now_realtime_sec() 566 let idxc: *u8 = sys_mmap(PSV_MAGIC_1024) 567 var io: i64 = 0 568 if olg_whoami(ctx, tokb, tbn[0], now, uid, 64, uidn) == NX_MAUTH_OK { 569 io = ps_hex(idxc, io, uid, uidn[0]) 570 idxc[io] = 9 as u8 571 io = io + 1 572 io = pj_cat(idxc, io, "bo_test\n" as *u8) 573 } 574 if olg_whoami(ctx, tokm, tmn[0], now, uid, 64, uidn) == NX_MAUTH_OK { 575 io = ps_hex(idxc, io, uid, uidn[0]) 576 idxc[io] = 9 as u8 577 io = io + 1 578 io = pj_cat(idxc, io, "mallory_t\n" as *u8) 579 } 580 idxc[io] = 0 as u8 581 pj_wfile(idxf, idxc) 582 // the handler state 583 let st: *i64 = psv_st_new(ctx, conff, idxf) 584 let req: *u8 = sys_mmap(PSV_MAGIC_16384) 585 let resp: *u8 = sys_mmap(PSV_MAGIC_131072) 586 let none: *u8 = sys_mmap(8) 587 none[0] = 0 as u8 588 // T3 token-less nav -> bootstrap 200 589 var rn: i64 = psv_req(req, "/project" as *u8, none) 590 var n: i64 = psv_handle(st, req, rn, resp) 591 ok = 0 592 if pj_contains(resp, n, "HTTP/1.1 200" as *u8) == 1 { if pj_contains(resp, n, "sessionStorage" as *u8) == 1 { ok = 1 } } 593 pass = pass + pj_check(3, ok, "bootstrap-no-token" as *u8) 594 // T4 garbage token -> 302 /login (fail-closed) 595 rn = psv_req(req, "/project" as *u8, "AAAAgarbagegarbage" as *u8) 596 n = psv_handle(st, req, rn, resp) 597 ok = 0 598 if pj_contains(resp, n, "HTTP/1.1 302" as *u8) == 1 { ok = 1 } 599 pass = pass + pj_check(4, ok, "neg-bad-token-302" as *u8) 600 // T5 the PICKER: bo sees fence (member) and NEVER barn (not a member) 601 rn = psv_req(req, "/project" as *u8, tokb) 602 n = psv_handle(st, req, rn, resp) 603 ok = 0 604 if pj_contains(resp, n, "HTTP/1.1 200" as *u8) == 1 { 605 if pj_contains(resp, n, "/project/p/fence" as *u8) == 1 { 606 if pj_contains(resp, n, "/project/p/barn" as *u8) == 0 { ok = 1 } 607 } 608 } 609 pass = pass + pj_check(5, ok, "picker-membership-scoped" as *u8) 610 // T6 per-project page: contractor scope holds on /project/p/fence 611 rn = psv_req(req, "/project/p/fence" as *u8, tokb) 612 n = psv_handle(st, req, rn, resp) 613 ok = 0 614 if pj_contains(resp, n, "HTTP/1.1 200" as *u8) == 1 { 615 if pj_contains(resp, n, "<td>f2</td>" as *u8) == 1 { 616 if pj_contains(resp, n, "<td>f1</td>" as *u8) == 0 { 617 if pj_contains(resp, n, "role=contractor" as *u8) == 1 { ok = 1 } 618 } 619 } 620 } 621 pass = pass + pj_check(6, ok, "page-contractor-scope" as *u8) 622 // T7 CROSS-PROJECT deny: bo on barn -> 403 623 rn = psv_req(req, "/project/p/barn" as *u8, tokb) 624 n = psv_handle(st, req, rn, resp) 625 ok = 0 626 if pj_contains(resp, n, "HTTP/1.1 403" as *u8) == 1 { ok = 1 } 627 pass = pass + pj_check(7, ok, "neg-cross-project-403" as *u8) 628 // T8 valid login, NO membership anywhere -> picker denies 629 rn = psv_req(req, "/project" as *u8, tokm) 630 n = psv_handle(st, req, rn, resp) 631 ok = 0 632 if pj_contains(resp, n, "HTTP/1.1 403" as *u8) == 1 { if pj_contains(resp, n, "Not a member" as *u8) == 1 { ok = 1 } } 633 pass = pass + pj_check(8, ok, "neg-no-membership-403" as *u8) 634 // T9 unknown pid -> 404 (no leak) 635 rn = psv_req(req, "/project/p/ghost" as *u8, tokb) 636 n = psv_handle(st, req, rn, resp) 637 ok = 0 638 if pj_contains(resp, n, "HTTP/1.1 404" as *u8) == 1 { ok = 1 } 639 pass = pass + pj_check(9, ok, "neg-unknown-pid-404" as *u8) 640 // T10 public demo: owner view shows all rows + the risk column/summary (f0 overdue by real today) 641 rn = psv_req(req, "/project/demo" as *u8, none) 642 n = psv_handle(st, req, rn, resp) 643 ok = 0 644 if pj_contains(resp, n, "HTTP/1.1 200" as *u8) == 1 { 645 if pj_contains(resp, n, "<td>f1</td>" as *u8) == 1 { 646 if pj_contains(resp, n, "OVERDUE" as *u8) == 1 { 647 if pj_contains(resp, n, "risk: overdue=" as *u8) == 1 { ok = 1 } 648 } 649 } 650 } 651 pass = pass + pj_check(10, ok, "public-demo-risk-column" as *u8) 652 // T11 unknown subpath -> 404 653 rn = psv_req(req, "/project/zzz" as *u8, none) 654 n = psv_handle(st, req, rn, resp) 655 ok = 0 656 if pj_contains(resp, n, "HTTP/1.1 404" as *u8) == 1 { ok = 1 } 657 pass = pass + pj_check(11, ok, "neg-404-subpath" as *u8) 658 // T12 query string stripped on the picker 659 rn = psv_req(req, "/project?b=cachebust" as *u8, tokb) 660 n = psv_handle(st, req, rn, resp) 661 ok = 0 662 if pj_contains(resp, n, "HTTP/1.1 200" as *u8) == 1 { if pj_contains(resp, n, "/project/p/fence" as *u8) == 1 { ok = 1 } } 663 pass = pass + pj_check(12, ok, "query-string-stripped" as *u8) 664 // T13 pid traversal guard: dot/slash pids are refused 665 rn = psv_req(req, "/project/p/../etc" as *u8, tokb) 666 n = psv_handle(st, req, rn, resp) 667 ok = 0 668 if pj_contains(resp, n, "HTTP/1.1 404" as *u8) == 1 { ok = 1 } 669 pass = pass + pj_check(13, ok, "neg-pid-traversal-404" as *u8) 670 671 let rep: *u8 = sys_mmap(256) 672 var z: i64 = 0 673 z = pj_cat(rep, z, "NX-PROJECT-SERVE-GATE pass=" as *u8) 674 z = pj_catn(rep, z, pass) 675 z = pj_cat(rep, z, "/13 verdict=" as *u8) 676 if pass == 13 { z = pj_cat(rep, z, "GREEN\n" as *u8) } else { z = pj_cat(rep, z, "RED\n" as *u8) } 677 sys_write(1, rep, z) 678 if pass == 13 { return 0 } 679 return 1 680} 681 682// ---------------- DAEMON (sockets; the NAS runs this behind the gated proxy row) ---------------- 683func psv_daemon(argc: i64, argv: *i64) -> i64 { 684 if argc < 7 { 685 pj_p("usage: nx_project_serve <port> <keys> <store> <budget> <conf> <idx> [m] [t] [p]\n" as *u8) 686 sys_exit(2) 687 return 2 688 } 689 let port: i64 = pj_atoi(argv[1] as *u8) 690 let keys_path: *u8 = argv[2] as *u8 691 let store_path: *u8 = argv[3] as *u8 692 let budget: i64 = pj_atoi(argv[4] as *u8) 693 let confpath: *u8 = argv[5] as *u8 694 let idxpath: *u8 = argv[6] as *u8 695 var m_cost: i64 = PSV_MAGIC_65536 696 if argc > 7 { m_cost = pj_atoi(argv[7] as *u8) } 697 var t_cost: i64 = 3 698 if argc > 8 { t_cost = pj_atoi(argv[8] as *u8) } 699 var p_cost: i64 = 4 700 if argc > 9 { p_cost = pj_atoi(argv[9] as *u8) } 701 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext 702 if olg_ctx_setup(ctx, keys_path, store_path, "nishi_site_admin" as *u8, 16, "Nishi site admin" as *u8, 16, m_cost, t_cost, p_cost) != 0 { 703 pj_p("CTX-INIT-FAIL\n" as *u8) 704 sys_exit(1) 705 return 1 706 } 707 let addr: *u8 = sys_mmap(16) 708 addr[0] = 2 as u8 709 addr[1] = 0 as u8 710 addr[2] = ((port >> 8) & 255) as u8 711 addr[3] = (port & 255) as u8 712 var zi: i64 = 4 713 while zi < 16 { addr[zi] = 0 as u8; zi = zi + 1 } 714 let lfd: i64 = sys_socket(2, 1, 0) 715 if lfd < 0 { pj_p("SOCKET-FAIL\n" as *u8); sys_exit(1); return 1 } 716 let optv: *u8 = sys_mmap(4) 717 optv[0] = 1 as u8 718 optv[1] = 0 as u8 719 optv[2] = 0 as u8 720 optv[3] = 0 as u8 721 sys_setsockopt(lfd, 1, 2, optv, 4) 722 if sys_bind(lfd, addr, 16) < 0 { pj_p("BIND-FAIL\n" as *u8); sys_exit(1); return 1 } 723 if sys_listen(lfd, 16) < 0 { pj_p("LISTEN-FAIL\n" as *u8); sys_exit(1); return 1 } 724 pj_p("PROJECT-GATEWAY-UP\n" as *u8) 725 let st: *i64 = psv_st_new(ctx, confpath, idxpath) 726 let req: *u8 = sys_mmap(PSV_MAGIC_16400) 727 let resp: *u8 = sys_mmap(PSV_MAGIC_131072) 728 pj_p("PSV-BUFS-OK\n" as *u8) 729 var served: i64 = 0 730 while served < budget { 731 let cfd: i64 = sys_accept(lfd) 732 if cfd >= 0 { 733 sys_set_socket_timeout(cfd, 5) 734 let rn: i64 = sys_read(cfd, req, PSV_MAGIC_16383) 735 if rn > 0 { 736 let n: i64 = psv_handle(st, req, rn, resp) 737 sys_write(cfd, resp, n) 738 } 739 sys_close(cfd) 740 } 741 served = served + 1 742 } 743 sys_close(lfd) 744 sys_exit(0) 745 return 0 746}