code wiki / _hdl_build / nx_project_serve.nx
nx_project_serve.nx source
↩ module page · 738 lines · 30928 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><script>(function(){var t=sessionStorage.getItem('nsess');if(!t){location='/login?return=/project';return}fetch(location.pathname,{headers:{'X-Nishi-Session':t}}).then(function(r){return r.ok?r.text():null}).then(function(x){if(x){document.open();document.write(x);document.close()}else{location='/login?return=/project'}})})();</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
277func ps_today(out: *u8) -> i64 {
278 let secs: i64 = sys_now_realtime_sec()
279 let days: i64 = secs / PSV_MAGIC_86400
280 let ymd: *i64 = sys_mmap(32) as *i64
281 ps_civil(days, ymd)
282 var o: i64 = 0
283 o = pj_catn(out, o, ymd[0])
284 out[o] = 45 as u8
285 o = o + 1
286 if ymd[1] < 10 { out[o] = 48 as u8; o = o + 1 }
287 o = pj_catn(out, o, ymd[1])
288 out[o] = 45 as u8
289 o = o + 1
290 if ymd[2] < 10 { out[o] = 48 as u8; o = o + 1 }
291 o = pj_catn(out, o, ymd[2])
292 out[o] = 0 as u8
293 return o
294}
295
296// build a full HTTP response into resp; returns total bytes
297func ps_resp(resp: *u8, status: *u8, ctype: *u8, body: *u8, blen: i64) -> i64 {
298 var o: i64 = 0
299 o = pj_cat(resp, o, "HTTP/1.1 " as *u8)
300 o = pj_cat(resp, o, status)
301 o = pj_cat(resp, o, "\r\nContent-Type: " as *u8)
302 o = pj_cat(resp, o, ctype)
303 o = pj_cat(resp, o, "\r\nContent-Length: " as *u8)
304 o = pj_catn(resp, o, blen)
305 o = pj_cat(resp, o, "\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8)
306 var i: i64 = 0
307 while i < blen { resp[o] = body[i]; o = o + 1; i = i + 1 }
308 return o
309}
310
311func ps_redirect(resp: *u8, loc: *u8) -> i64 {
312 var o: i64 = 0
313 o = pj_cat(resp, o, "HTTP/1.1 302 Found\r\nLocation: " as *u8)
314 o = pj_cat(resp, o, loc)
315 o = pj_cat(resp, o, "\r\nContent-Length: 0\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8)
316 return o
317}
318
319func ps_text(resp: *u8, status: *u8, msg: *u8) -> i64 {
320 return ps_resp(resp, status, "text/plain; charset=utf-8" as *u8, msg, pj_slen(msg))
321}
322
323// ---- st slots: 0=ctx 1=confpath 2=idxpath 3=pagebuf 4=confbuf 5=idxbuf 6=tokbuf 7=pathbuf
324// 8=uid 9=uxbuf 10=handle 11=person 12=todaybuf 13=storebuf 14=pidbuf 15=demobuf 16=uidn ----
325func psv_st_new(ctx: *NxAuthContext, confpath: *u8, idxpath: *u8) -> *i64 {
326 let st: *i64 = sys_mmap(8 * 20) as *i64
327 st[0] = ctx as i64
328 st[1] = confpath as i64
329 st[2] = idxpath as i64
330 st[3] = sys_mmap(PSV_MAGIC_65536)
331 st[4] = sys_mmap(PSV_MAGIC_8192)
332 st[5] = sys_mmap(PSV_MAGIC_262144)
333 st[6] = sys_mmap(512)
334 st[7] = sys_mmap(PSV_MAGIC_1024)
335 st[8] = sys_mmap(64)
336 st[9] = sys_mmap(160)
337 st[10] = sys_mmap(128)
338 st[11] = sys_mmap(128)
339 st[12] = sys_mmap(32)
340 st[13] = sys_mmap(256)
341 st[14] = sys_mmap(128)
342 st[15] = sys_mmap(128)
343 st[16] = sys_mmap(16)
344 return st
345}
346
347func psv_handle(st: *i64, req: *u8, rn: i64, resp: *u8) -> i64 {
348 let path: *u8 = st[7] as *u8
349 var pl: i64 = hgw_path(req, rn, path, PSV_MAGIC_1024)
350 // strip the query string: the edge relays the FULL request-target (/project?b=x) and exact-match
351 // routing would 404 it (found live: WebFetch cache-busters). '?' truncates.
352 var qi: i64 = 0
353 var qgo: i64 = 1
354 while qgo == 1 {
355 if qi >= pl { qgo = 0 } else {
356 if path[qi] == (63 as u8) { path[qi] = 0 as u8; pl = qi; qgo = 0 } else { qi = qi + 1 }
357 }
358 }
359 let confbuf: *u8 = st[4] as *u8
360 let cl: i64 = ps_read_file(st[1] as *u8, confbuf, PSV_MAGIC_8192)
361 if cl <= 0 { return ps_text(resp, "503 Service Unavailable" as *u8, "project portal not configured" as *u8) }
362 let today: *u8 = st[12] as *u8
363 ps_today(today)
364 let pagebuf: *u8 = st[3] as *u8
365 let storebuf: *u8 = st[13] as *u8
366 // ---- /project/demo = the PUBLIC demo tenant (conf: `demo <pid> <person>`) ----
367 var isdemo: i64 = 0
368 if slk_eq(path, pl, "/project/demo" as *u8, 13) == 1 { isdemo = 1 }
369 if slk_eq(path, pl, "/project/demo/" as *u8, 14) == 1 { isdemo = 1 }
370 if isdemo == 1 {
371 let dpid: *u8 = st[14] as *u8
372 let dper: *u8 = st[15] as *u8
373 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) }
374 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) }
375 let dn: i64 = pj_html_build(storebuf, dpid, dper, today, pagebuf)
376 if dn < 0 { return ps_text(resp, "503 Service Unavailable" as *u8, "demo unavailable" as *u8) }
377 return ps_resp(resp, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, pagebuf, dn)
378 }
379 // ---- route shape: the picker root, or a per-project page /project/p/<pid> ----
380 var isroot: i64 = 0
381 if slk_eq(path, pl, "/project" as *u8, 8) == 1 { isroot = 1 }
382 if slk_eq(path, pl, "/project/" as *u8, 9) == 1 { isroot = 1 }
383 var ispage: i64 = 0
384 if isroot == 0 { if pl > 11 { if pj_starts(path, "/project/p/" as *u8) == 1 { ispage = 1 } } }
385 if isroot == 0 { if ispage == 0 { return ps_text(resp, "404 Not Found" as *u8, "not found" as *u8) } }
386 // ---- session resolution (fail-closed; token-less nav gets the sessionStorage bootstrap) ----
387 let tok: *u8 = st[6] as *u8
388 let tl: i64 = hgw_hdr_val(req, rn, "X-Nishi-Session:" as *u8, 16, tok, 512)
389 if tl == 0 { return ps_resp(resp, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, PSV_BOOTSTRAP, pj_slen(PSV_BOOTSTRAP)) }
390 let ctx: *NxAuthContext = st[0] as *NxAuthContext
391 let uid: *u8 = st[8] as *u8
392 let uidn: *i64 = st[16] as *i64
393 let handle: *u8 = st[10] as *u8
394 var hl: i64 = 0
395 let now: i64 = sys_now_realtime_sec()
396 if olg_whoami(ctx, tok, tl, now, uid, 64, uidn) == NX_MAUTH_OK {
397 let uxbuf: *u8 = st[9] as *u8
398 let uxn: i64 = ps_hex(uxbuf, 0, uid, uidn[0])
399 let idxbuf: *u8 = st[5] as *u8
400 let il: i64 = ps_read_file(st[2] as *u8, idxbuf, PSV_MAGIC_262144)
401 if il > 0 { hl = ps_idx_lookup(idxbuf, il, uxbuf, uxn, handle) }
402 }
403 if hl == 0 { return ps_redirect(resp, "/login?return=/project" as *u8) }
404 let person: *u8 = st[11] as *u8
405 ps_map(confbuf, cl, handle, person, 128)
406 // ---- the PICKER: projects where this person holds a membership record ----
407 if isroot == 1 {
408 let kn: i64 = ps_picker(confbuf, cl, person, pagebuf)
409 if kn >= 0 { return ps_resp(resp, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, pagebuf, kn) }
410 return ps_resp(resp, "403 Forbidden" as *u8, "text/html; charset=utf-8" as *u8, PSV_DENY, pj_slen(PSV_DENY))
411 }
412 // ---- per-project page: /project/p/<pid> (unknown pid = 404, non-member = 403) ----
413 let pidp: *u8 = ((path as i64) + 11) as *u8
414 if ps_pid_ok(pidp) == 0 { return ps_text(resp, "404 Not Found" as *u8, "not found" as *u8) }
415 if ps_proj_store(confbuf, cl, pidp, storebuf, 256) == 0 { return ps_text(resp, "404 Not Found" as *u8, "no such project" as *u8) }
416 let n2: i64 = pj_html_build(storebuf, pidp, person, today, pagebuf)
417 if n2 >= 0 { return ps_resp(resp, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, pagebuf, n2) }
418 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)) }
419 return ps_text(resp, "503 Service Unavailable" as *u8, "project store unavailable" as *u8)
420}
421
422// ---------------- SELFTEST (socket-free; REAL tokens against fresh auto-init keys) ----------------
423func psv_req(dst: *u8, path: *u8, tok: *u8) -> i64 {
424 var o: i64 = 0
425 o = pj_cat(dst, o, "GET " as *u8)
426 o = pj_cat(dst, o, path)
427 o = pj_cat(dst, o, " HTTP/1.1\r\nHost: t\r\n" as *u8)
428 if tok[0] != (0 as u8) {
429 o = pj_cat(dst, o, "X-Nishi-Session: " as *u8)
430 o = pj_cat(dst, o, tok)
431 o = pj_cat(dst, o, "\r\n" as *u8)
432 }
433 o = pj_cat(dst, o, "\r\n" as *u8)
434 return o
435}
436
437func psv_selftest() -> i64 {
438 let uniq: i64 = sys_now_us()
439 let base: *u8 = sys_mmap(256)
440 var bo: i64 = 0
441 bo = pj_cat(base, bo, "/tmp/psg_" as *u8)
442 bo = pj_catn(base, bo, uniq)
443 base[bo] = 0 as u8
444 let keys: *u8 = sys_mmap(256)
445 var o: i64 = pj_cat(keys, 0, base)
446 o = pj_cat(keys, o, "_keys.bin" as *u8)
447 keys[o] = 0 as u8
448 let astore: *u8 = sys_mmap(256)
449 o = pj_cat(astore, 0, base)
450 o = pj_cat(astore, o, "_astore.log" as *u8)
451 astore[o] = 0 as u8
452 let idxf: *u8 = sys_mmap(256)
453 o = pj_cat(idxf, 0, base)
454 o = pj_cat(idxf, o, "_idx.tsv" as *u8)
455 idxf[o] = 0 as u8
456 let conff: *u8 = sys_mmap(256)
457 o = pj_cat(conff, 0, base)
458 o = pj_cat(conff, o, "_serve.conf" as *u8)
459 conff[o] = 0 as u8
460 let planf: *u8 = sys_mmap(256)
461 o = pj_cat(planf, 0, base)
462 o = pj_cat(planf, o, "_plan.txt" as *u8)
463 planf[o] = 0 as u8
464 let projf: *u8 = sys_mmap(256)
465 o = pj_cat(projf, 0, base)
466 o = pj_cat(projf, o, "_proj.txt" as *u8)
467 projf[o] = 0 as u8
468 let prefix: *u8 = sys_mmap(256)
469 o = pj_cat(prefix, 0, base)
470 o = pj_cat(prefix, o, "_stA-" as *u8)
471 prefix[o] = 0 as u8
472 let prefB: *u8 = sys_mmap(256)
473 o = pj_cat(prefB, 0, base)
474 o = pj_cat(prefB, o, "_stB-" as *u8)
475 prefB[o] = 0 as u8
476 let planb: *u8 = sys_mmap(256)
477 o = pj_cat(planb, 0, base)
478 o = pj_cat(planb, o, "_planb.txt" as *u8)
479 planb[o] = 0 as u8
480 let projb: *u8 = sys_mmap(256)
481 o = pj_cat(projb, 0, base)
482 o = pj_cat(projb, o, "_projb.txt" as *u8)
483 projb[o] = 0 as u8
484 let outf: *u8 = sys_mmap(256)
485 o = pj_cat(outf, 0, base)
486 o = pj_cat(outf, o, "_run.out" as *u8)
487 outf[o] = 0 as u8
488
489 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)
490 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)
491 pj_wfile(planb, "TASK barn b1 amber 2027-01-01 60 3 - paint\n" as *u8)
492 pj_wfile(projb, "PROJ barn elder barn-paint\nMEMBER barn amber team -\n" as *u8)
493 let confc: *u8 = sys_mmap(PSV_MAGIC_1024)
494 o = 0
495 o = pj_cat(confc, o, "project fence " as *u8)
496 o = pj_cat(confc, o, prefix)
497 o = pj_cat(confc, o, "\nproject barn " as *u8)
498 o = pj_cat(confc, o, prefB)
499 o = pj_cat(confc, o, "\ndemo fence elder\nmap bo_test bo\n" as *u8)
500 confc[o] = 0 as u8
501 pj_wfile(conff, confc)
502
503 var pass: i64 = 0
504 // T1 the independent planner writes the task records for BOTH projects
505 let a1: *i64 = sys_mmap(8 * 8) as *i64
506 a1[0] = "load" as *u8 as i64
507 a1[1] = planf as i64
508 a1[2] = prefix as i64
509 a1[3] = "7301" as *u8 as i64
510 a1[4] = 0
511 let rc1: i64 = pj_run("_offc/nx_vizsla_plan.elf" as *u8, a1, outf)
512 let a1b: *i64 = sys_mmap(8 * 8) as *i64
513 a1b[0] = "load" as *u8 as i64
514 a1b[1] = planb as i64
515 a1b[2] = prefB as i64
516 a1b[3] = "7303" as *u8 as i64
517 a1b[4] = 0
518 let rc1b: i64 = pj_run("_offc/nx_vizsla_plan.elf" as *u8, a1b, outf)
519 let av: *i64 = sys_mmap(8 * 8) as *i64
520 av[0] = "pj" as *u8 as i64
521 av[1] = "load" as *u8 as i64
522 av[2] = projf as i64
523 av[3] = prefix as i64
524 av[4] = "7302" as *u8 as i64
525 let rc2: i64 = pj_cmd_load(5, av)
526 let avb: *i64 = sys_mmap(8 * 8) as *i64
527 avb[0] = "pj" as *u8 as i64
528 avb[1] = "load" as *u8 as i64
529 avb[2] = projb as i64
530 avb[3] = prefB as i64
531 avb[4] = "7304" as *u8 as i64
532 let rc2b: i64 = pj_cmd_load(5, avb)
533 var ok: i64 = 0
534 if rc1 == 0 { if rc1b == 0 { if rc2 == 0 { if rc2b == 0 { ok = 1 } } } }
535 pass = pass + pj_check(1, ok, "fixtures-loaded-2-projects" as *u8)
536 // T2 auth context on FRESH keys (load_or_init) + register + login = REAL tokens
537 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext
538 let crc: i64 = olg_ctx_setup(ctx, keys, astore, "nishi_site_admin" as *u8, 16, "Nishi site admin" as *u8, 16, 256, 2, 1)
539 let mn: *u8 = sys_mmap(PSV_MAGIC_1024)
540 let mnn: *i64 = sys_mmap(16) as *i64
541 let rr1: i64 = olg_register(ctx, "bo_test" as *u8, 7, "pw_bo_9931x" as *u8, 11, mn, PSV_MAGIC_1024, mnn)
542 let rr2: i64 = olg_register(ctx, "mallory_t" as *u8, 9, "pw_mal_442x" as *u8, 11, mn, PSV_MAGIC_1024, mnn)
543 let tokb: *u8 = sys_mmap(512)
544 let tbn: *i64 = sys_mmap(16) as *i64
545 let lr1: i64 = olg_login(ctx, "bo_test" as *u8, 7, "pw_bo_9931x" as *u8, 11, tokb, 512, tbn)
546 let tokm: *u8 = sys_mmap(512)
547 let tmn: *i64 = sys_mmap(16) as *i64
548 let lr2: i64 = olg_login(ctx, "mallory_t" as *u8, 9, "pw_mal_442x" as *u8, 11, tokm, 512, tmn)
549 tokb[tbn[0]] = 0 as u8
550 tokm[tmn[0]] = 0 as u8
551 ok = 0
552 if crc == 0 { if rr1 == 0 { if rr2 == 0 { if lr1 == 0 { if lr2 == 0 { ok = 1 } } } } }
553 pass = pass + pj_check(2, ok, "opaque-register-login" as *u8)
554 // build the uid->handle index from the REAL whoami uids
555 let uid: *u8 = sys_mmap(64)
556 let uidn: *i64 = sys_mmap(16) as *i64
557 let now: i64 = sys_now_realtime_sec()
558 let idxc: *u8 = sys_mmap(PSV_MAGIC_1024)
559 var io: i64 = 0
560 if olg_whoami(ctx, tokb, tbn[0], now, uid, 64, uidn) == NX_MAUTH_OK {
561 io = ps_hex(idxc, io, uid, uidn[0])
562 idxc[io] = 9 as u8
563 io = io + 1
564 io = pj_cat(idxc, io, "bo_test\n" as *u8)
565 }
566 if olg_whoami(ctx, tokm, tmn[0], now, uid, 64, uidn) == NX_MAUTH_OK {
567 io = ps_hex(idxc, io, uid, uidn[0])
568 idxc[io] = 9 as u8
569 io = io + 1
570 io = pj_cat(idxc, io, "mallory_t\n" as *u8)
571 }
572 idxc[io] = 0 as u8
573 pj_wfile(idxf, idxc)
574 // the handler state
575 let st: *i64 = psv_st_new(ctx, conff, idxf)
576 let req: *u8 = sys_mmap(PSV_MAGIC_16384)
577 let resp: *u8 = sys_mmap(PSV_MAGIC_131072)
578 let none: *u8 = sys_mmap(8)
579 none[0] = 0 as u8
580 // T3 token-less nav -> bootstrap 200
581 var rn: i64 = psv_req(req, "/project" as *u8, none)
582 var n: i64 = psv_handle(st, req, rn, resp)
583 ok = 0
584 if pj_contains(resp, n, "HTTP/1.1 200" as *u8) == 1 { if pj_contains(resp, n, "sessionStorage" as *u8) == 1 { ok = 1 } }
585 pass = pass + pj_check(3, ok, "bootstrap-no-token" as *u8)
586 // T4 garbage token -> 302 /login (fail-closed)
587 rn = psv_req(req, "/project" as *u8, "AAAAgarbagegarbage" as *u8)
588 n = psv_handle(st, req, rn, resp)
589 ok = 0
590 if pj_contains(resp, n, "HTTP/1.1 302" as *u8) == 1 { ok = 1 }
591 pass = pass + pj_check(4, ok, "neg-bad-token-302" as *u8)
592 // T5 the PICKER: bo sees fence (member) and NEVER barn (not a member)
593 rn = psv_req(req, "/project" as *u8, tokb)
594 n = psv_handle(st, req, rn, resp)
595 ok = 0
596 if pj_contains(resp, n, "HTTP/1.1 200" as *u8) == 1 {
597 if pj_contains(resp, n, "/project/p/fence" as *u8) == 1 {
598 if pj_contains(resp, n, "/project/p/barn" as *u8) == 0 { ok = 1 }
599 }
600 }
601 pass = pass + pj_check(5, ok, "picker-membership-scoped" as *u8)
602 // T6 per-project page: contractor scope holds on /project/p/fence
603 rn = psv_req(req, "/project/p/fence" as *u8, tokb)
604 n = psv_handle(st, req, rn, resp)
605 ok = 0
606 if pj_contains(resp, n, "HTTP/1.1 200" as *u8) == 1 {
607 if pj_contains(resp, n, "<td>f2</td>" as *u8) == 1 {
608 if pj_contains(resp, n, "<td>f1</td>" as *u8) == 0 {
609 if pj_contains(resp, n, "role=contractor" as *u8) == 1 { ok = 1 }
610 }
611 }
612 }
613 pass = pass + pj_check(6, ok, "page-contractor-scope" as *u8)
614 // T7 CROSS-PROJECT deny: bo on barn -> 403
615 rn = psv_req(req, "/project/p/barn" as *u8, tokb)
616 n = psv_handle(st, req, rn, resp)
617 ok = 0
618 if pj_contains(resp, n, "HTTP/1.1 403" as *u8) == 1 { ok = 1 }
619 pass = pass + pj_check(7, ok, "neg-cross-project-403" as *u8)
620 // T8 valid login, NO membership anywhere -> picker denies
621 rn = psv_req(req, "/project" as *u8, tokm)
622 n = psv_handle(st, req, rn, resp)
623 ok = 0
624 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 } }
625 pass = pass + pj_check(8, ok, "neg-no-membership-403" as *u8)
626 // T9 unknown pid -> 404 (no leak)
627 rn = psv_req(req, "/project/p/ghost" as *u8, tokb)
628 n = psv_handle(st, req, rn, resp)
629 ok = 0
630 if pj_contains(resp, n, "HTTP/1.1 404" as *u8) == 1 { ok = 1 }
631 pass = pass + pj_check(9, ok, "neg-unknown-pid-404" as *u8)
632 // T10 public demo: owner view shows all rows + the risk column/summary (f0 overdue by real today)
633 rn = psv_req(req, "/project/demo" as *u8, none)
634 n = psv_handle(st, req, rn, resp)
635 ok = 0
636 if pj_contains(resp, n, "HTTP/1.1 200" as *u8) == 1 {
637 if pj_contains(resp, n, "<td>f1</td>" as *u8) == 1 {
638 if pj_contains(resp, n, "OVERDUE" as *u8) == 1 {
639 if pj_contains(resp, n, "risk: overdue=" as *u8) == 1 { ok = 1 }
640 }
641 }
642 }
643 pass = pass + pj_check(10, ok, "public-demo-risk-column" as *u8)
644 // T11 unknown subpath -> 404
645 rn = psv_req(req, "/project/zzz" as *u8, none)
646 n = psv_handle(st, req, rn, resp)
647 ok = 0
648 if pj_contains(resp, n, "HTTP/1.1 404" as *u8) == 1 { ok = 1 }
649 pass = pass + pj_check(11, ok, "neg-404-subpath" as *u8)
650 // T12 query string stripped on the picker
651 rn = psv_req(req, "/project?b=cachebust" as *u8, tokb)
652 n = psv_handle(st, req, rn, resp)
653 ok = 0
654 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 } }
655 pass = pass + pj_check(12, ok, "query-string-stripped" as *u8)
656 // T13 pid traversal guard: dot/slash pids are refused
657 rn = psv_req(req, "/project/p/../etc" as *u8, tokb)
658 n = psv_handle(st, req, rn, resp)
659 ok = 0
660 if pj_contains(resp, n, "HTTP/1.1 404" as *u8) == 1 { ok = 1 }
661 pass = pass + pj_check(13, ok, "neg-pid-traversal-404" as *u8)
662
663 let rep: *u8 = sys_mmap(256)
664 var z: i64 = 0
665 z = pj_cat(rep, z, "NX-PROJECT-SERVE-GATE pass=" as *u8)
666 z = pj_catn(rep, z, pass)
667 z = pj_cat(rep, z, "/13 verdict=" as *u8)
668 if pass == 13 { z = pj_cat(rep, z, "GREEN\n" as *u8) } else { z = pj_cat(rep, z, "RED\n" as *u8) }
669 sys_write(1, rep, z)
670 if pass == 13 { return 0 }
671 return 1
672}
673
674// ---------------- DAEMON (sockets; the NAS runs this behind the gated proxy row) ----------------
675func psv_daemon(argc: i64, argv: *i64) -> i64 {
676 if argc < 7 {
677 pj_p("usage: nx_project_serve <port> <keys> <store> <budget> <conf> <idx> [m] [t] [p]\n" as *u8)
678 sys_exit(2)
679 return 2
680 }
681 let port: i64 = pj_atoi(argv[1] as *u8)
682 let keys_path: *u8 = argv[2] as *u8
683 let store_path: *u8 = argv[3] as *u8
684 let budget: i64 = pj_atoi(argv[4] as *u8)
685 let confpath: *u8 = argv[5] as *u8
686 let idxpath: *u8 = argv[6] as *u8
687 var m_cost: i64 = PSV_MAGIC_65536
688 if argc > 7 { m_cost = pj_atoi(argv[7] as *u8) }
689 var t_cost: i64 = 3
690 if argc > 8 { t_cost = pj_atoi(argv[8] as *u8) }
691 var p_cost: i64 = 4
692 if argc > 9 { p_cost = pj_atoi(argv[9] as *u8) }
693 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext
694 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 {
695 pj_p("CTX-INIT-FAIL\n" as *u8)
696 sys_exit(1)
697 return 1
698 }
699 let addr: *u8 = sys_mmap(16)
700 addr[0] = 2 as u8
701 addr[1] = 0 as u8
702 addr[2] = ((port >> 8) & 255) as u8
703 addr[3] = (port & 255) as u8
704 var zi: i64 = 4
705 while zi < 16 { addr[zi] = 0 as u8; zi = zi + 1 }
706 let lfd: i64 = sys_socket(2, 1, 0)
707 if lfd < 0 { pj_p("SOCKET-FAIL\n" as *u8); sys_exit(1); return 1 }
708 let optv: *u8 = sys_mmap(4)
709 optv[0] = 1 as u8
710 optv[1] = 0 as u8
711 optv[2] = 0 as u8
712 optv[3] = 0 as u8
713 sys_setsockopt(lfd, 1, 2, optv, 4)
714 if sys_bind(lfd, addr, 16) < 0 { pj_p("BIND-FAIL\n" as *u8); sys_exit(1); return 1 }
715 if sys_listen(lfd, 16) < 0 { pj_p("LISTEN-FAIL\n" as *u8); sys_exit(1); return 1 }
716 pj_p("PROJECT-GATEWAY-UP\n" as *u8)
717 let st: *i64 = psv_st_new(ctx, confpath, idxpath)
718 let req: *u8 = sys_mmap(PSV_MAGIC_16400)
719 let resp: *u8 = sys_mmap(PSV_MAGIC_131072)
720 pj_p("PSV-BUFS-OK\n" as *u8)
721 var served: i64 = 0
722 while served < budget {
723 let cfd: i64 = sys_accept(lfd)
724 if cfd >= 0 {
725 sys_set_socket_timeout(cfd, 5)
726 let rn: i64 = sys_read(cfd, req, PSV_MAGIC_16383)
727 if rn > 0 {
728 let n: i64 = psv_handle(st, req, rn, resp)
729 sys_write(cfd, resp, n)
730 }
731 sys_close(cfd)
732 }
733 served = served + 1
734 }
735 sys_close(lfd)
736 sys_exit(0)
737 return 0
738}