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}