code wiki / _hdl_build / nx_docportal_admin_daemon.nx
nx_docportal_admin_daemon.nx source
↩ module page · 742 lines · 47765 B
1// nx_docportal_admin_daemon.nx -- the admin.<domain> DOCUMENT-PORTAL daemon, on the CANONICAL ecosystem auth
2// (the SAME Modern Auth nishifamily.com/status uses: OPAQUE-3DH + Argon2id + NO-COOKIE Ed25519 session). Operator
3// 2026-06-28: "match it to what we have in the nishi ecosystem and team." So this does NOT invent a token scheme --
4// it COMPOSES nx_status_daemon's proven auth (sd_* request parsing + nx_modern_auth_login + nx_sa_validate) and
5// wraps the proven nx_docportal_admin_lib router da_handle:
6// GET /admin -> the no-cookie login + upload SPA shell (public)
7// POST /admin/login -> handle+passphrase -> nx_modern_auth_login -> 200 {"token":...} | 401
8// * /admin/* (token) -> X-Nishi-Session validates (nx_sa_validate) ? da_handle at STAFF level : 401
9// A loopback HTTP daemon the sites daemon reverse-proxies to (behind the SNI router admin.<d> row); the public
10// TLS + cert are the operator-gated deploy step. Pure router dad_handle = bytes-in/bytes-out (the gate drives it
11// in-process, no socket). license_tier: ORIGINAL
12import "nx_docportal_admin_lib.nx"
13import "nx_status_daemon.nx"
14import "hub/nx_modern_auth_flow.nx"
15import "nx_syscalls.nx"
16import "nx_invite_token.nx" // invite-token store: inv_issue / inv_check / inv_consume
17import "nx_opaque_login.nx" // olg_register (OPAQUE one-shot over nx_modern_auth_register)
18import "nx_docportal_search_serve.nx" // R2 dss_serve: PUBLIC sovereign seg_store-native /search (no tsv, no derived .idx)
19import "nx_tool_registry.nx" // API-FIRST: the service self-registers nishi_search/nishi_doc at startup
20import "nx_multipart.nx" // RFC 7578 multipart/form-data parser (real <input type=file> uploads)
21
22// Rule 11: the accept-failure backoff is a named constant, not a number buried in the loop.
23// 50ms is short enough that a transient accept error costs nothing perceptible and long enough that
24// a PERSISTENT one cannot spin a core -- 20 retries/sec instead of millions.
25const DAD_ACCEPT_BACKOFF_MS: i64 = 50
26// Rule 11: how stale the web-shard handle may get, in seconds. The refresh reopens a 1.59GB shard
27// on any manifest change, so it must NOT run per request. 10s bounds index staleness while keeping
28// the reopen off the hot path entirely when the crawler is active.
29const DAD_REFRESH_MIN_SEC: i64 = 10
30const DAD_REQCAP: i64 = 65536
31const DAD_OUTCAP: i64 = 524288
32
33// the no-cookie admin SPA: log in -> token in sessionStorage -> upload form posts to /admin/upload with the
34// X-Nishi-Session header (NEVER a cookie). Minimal Web-API binding only (fetch/FileReader are JS-only). (public UI)
35func dad_shell(out: *u8) -> i64 {
36 // Build the HTML body FIRST into a temp buffer so the header can carry Content-Length. sites.elf's BUFFERED
37 // reverse proxy delimits the upstream body by Content-Length; a close-delimited response (no length) makes the
38 // proxy hop fail (-4) even though the daemon serves a direct client fine. Mirrors the JSON paths + the mgmt API.
39 let body: *u8 = sys_mmap(DAD_OUTCAP)
40 var b: i64 = 0
41 b = sd_cat(body, b, "<!DOCTYPE html><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Nishi document portal</title>" as *u8)
42 b = sd_cat(body, b, "<style>body{font-family:-apple-system,Segoe UI,sans-serif;max-width:720px;margin:6vh auto;padding:0 20px;color:#1c1c1e}input,select{width:100%;padding:9px;margin:.4rem 0;box-sizing:border-box;border:1px solid #ccc;border-radius:7px}button{padding:10px 18px;border:0;border-radius:7px;background:#0a6;color:#fff;font-size:1rem}.row{display:flex;gap:14px}.row label{font-size:.9rem}.e{color:#b00;min-height:1.2em}</style>" as *u8)
43 // NISHI-FIRST (2026-07-05 doctrine): the login is a REAL <form> -- native POST works with ZERO JS
44 // (nishi browser / any no-JS client); the JS below upgrades it to the SPA flow for third-party
45 // browsers (the last-mile shim). Hidden ui=1 rides ONLY the native submit -> server answers HTML.
46 b = sd_cat(body, b, "<div id=login><h2>🔒 Nishi document portal</h2><form id=lf method=post action=/admin/login><input type=hidden name=ui value=1><input id=h name=handle placeholder=\"handle\" autocomplete=username autofocus><input id=p name=passphrase type=password placeholder=\"passphrase\" autocomplete=current-password><button type=submit id=b>Sign in</button></form><p id=e class=e></p></div>" as *u8)
47 b = sd_cat(body, b, "<div id=up hidden><h2>Upload a document</h2><input id=dom placeholder=\"domain (e.g. andelinwest.com)\"><input id=f type=file>" as *u8)
48 b = sd_cat(body, b, "<div class=row><label><input type=radio name=vis value=public checked> Public (visitor-facing)</label><label><input type=radio name=vis value=private> Private (client vault)</label></div>" as *u8)
49 b = sd_cat(body, b, "<div class=row><label><input type=checkbox id=ps checked> List in onsite search</label><label><input type=checkbox id=ab> Allow AI to publish blog posts</label></div>" as *u8)
50 b = sd_cat(body, b, "<button id=u>Upload</button><p id=r class=e></p></div>" as *u8)
51 b = sd_cat(body, b, "<script>var L=document.getElementById('login'),U=document.getElementById('up'),E=document.getElementById('e');" as *u8)
52 b = sd_cat(body, b, "document.getElementById('lf').onsubmit=function(){E.textContent='';var b='handle='+encodeURIComponent(h.value)+'&passphrase='+encodeURIComponent(p.value);fetch('/admin/login',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:b}).then(function(r){if(r.ok){return r.json()}throw 0}).then(function(j){sessionStorage.nx_sess=j.token;L.hidden=true;U.hidden=false}).catch(function(){E.textContent='Wrong handle or passphrase.'});return false};" as *u8)
53 b = sd_cat(body, b, "document.getElementById('u').onclick=function(){var R=document.getElementById('r');R.textContent='';var fr=new FileReader();fr.onload=function(){var vis=document.querySelector('input[name=vis]:checked').value;var q='/admin/upload?domain='+encodeURIComponent(dom.value)+'&visibility='+vis+'&pub_search='+(document.getElementById('ps').checked?1:0)+'&ai_blog='+(document.getElementById('ab').checked?1:0);fetch(q,{method:'POST',headers:{'X-Nishi-Session':sessionStorage.nx_sess||''},body:fr.result}).then(function(r){return r.text()}).then(function(t){R.style.color='#0a6';R.textContent=t}).catch(function(){R.textContent='Upload failed.'})};fr.readAsText(document.getElementById('f').files[0])};</script>" as *u8)
54 var o: i64 = 0
55 o = sd_cat(out, o, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\nContent-Length: " as *u8)
56 o = sd_catn(out, o, b)
57 o = sd_cat(out, o, "\r\n\r\n" as *u8)
58 var i: i64 = 0
59 while i < b { out[o] = body[i]; o = o + 1; i = i + 1 }
60 return o
61}
62
63// Per-domain invite-token store path = "<storefile>.invites" (storefile = argv[3], carried on
64// ctx.account_store). Each realm/domain has its OWN storefile, so its invite tokens are isolated
65// by construction (per-domain isolation). DERIVED (not a new argv slot) => the daemon argv contract
66// <port> <keysfile> <storefile> <realm> <budget> stays unchanged (backward-compatible).
67func dad_invite_path(ctx: *NxAuthContext, out: *u8) -> i64 {
68 let sp: *u8 = ctx.account_store as *u8
69 var i: i64 = 0
70 while sp[i] != (0 as u8) { out[i] = sp[i]; i = i + 1 }
71 let suf: *u8 = ".invites" as *u8
72 var j: i64 = 0
73 while suf[j] != (0 as u8) { out[i] = suf[j]; i = i + 1; j = j + 1 }
74 out[i] = 0 as u8
75 return i
76}
77
78// ==== NISHI-FIRST no-JS session plumbing (2026-07-05 doctrine: nishi os/browser first; JS = the
79// third-party last-mile shim). The no-cookie C1 cardinal is PRESERVED: no Set-Cookie ever -- the no-JS
80// path carries the Ed25519 session token in the ?s= query of same-portal links (short-TTL, realm-scoped,
81// SAME validator). The query-session helpers now live in the SHARED nx_site_auth (nx_sa_validate_qs /
82// nx_sa_qs_raw / nx_sa_tok_urlenc) so mail/siteedit/every surface inherit them -- DRY, one auth home. ====
83
84// the no-JS signed-in LANDING page (HTML answer to a form login): links carry the session in ?s=.
85func dad_landing(out: *u8, b64: *u8, b64n: i64) -> i64 {
86 let body: *u8 = sys_mmap(DAD_OUTCAP)
87 var b: i64 = 0
88 b = sd_cat(body, b, "<!DOCTYPE html><meta charset=utf-8><title>Signed in</title><body style=\"font-family:sans-serif;max-width:720px;margin:6vh auto\"><h2>Signed in</h2><p>No-JS session active (nishi-first). Continue to:</p><p><a href=\"/admin/ui?s=" as *u8)
89 b = nx_sa_tok_urlenc(b64, b64n, body, b)
90 b = sd_cat(body, b, "\">the document portal</a></p>" as *u8)
91 var o: i64 = 0
92 o = sd_cat(out, o, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\nContent-Length: " as *u8)
93 o = sd_catn(out, o, b)
94 o = sd_cat(out, o, "\r\n\r\n" as *u8)
95 var i: i64 = 0
96 while i < b { out[o] = body[i]; o = o + 1; i = i + 1 }
97 return o
98}
99
100// the no-JS admin UI: shows the authenticated handle + a paste-text upload FORM (multipart file
101// pickers are the next rung; textarea covers the nishi browser + any no-JS client TODAY).
102// ---- session-uid -> handle map (the no-cookie session carries the 32-byte uid HASH, not the plaintext
103// handle; without this the authed page shows binary hash bytes in "Signed in as"). Mirrors the mail portal's
104// .mailmap: login/register append "MAP <64hex-uid> <handle>\n" to "<storefile>.uidmap" (append-only, rule 13),
105// /admin/ui resolves hash->handle. Same OPAQUE realm -> the docportal now shows the handle like the mail portal.
106func dad_hex32(src: *u8, out: *u8) -> i64 {
107 let hx: *u8 = "0123456789abcdef" as *u8
108 var i: i64 = 0
109 while i < 32 { out[i * 2] = hx[((src[i] as i64) >> 4) & 15]; out[i * 2 + 1] = hx[(src[i] as i64) & 15]; i = i + 1 }
110 out[64] = 0 as u8
111 return 64
112}
113func dad_uidmap_path(ctx: *NxAuthContext, out: *u8) -> i64 {
114 let sp: *u8 = ctx.account_store as *u8
115 var i: i64 = 0
116 while sp[i] != (0 as u8) { out[i] = sp[i]; i = i + 1 }
117 let suf: *u8 = ".uidmap" as *u8
118 var j: i64 = 0
119 while suf[j] != (0 as u8) { out[i] = suf[j]; i = i + 1; j = j + 1 }
120 out[i] = 0 as u8
121 return i
122}
123func dad_map_put(path: *u8, hexuid: *u8, h: *u8, hn: i64) -> i64 {
124 let row: *u8 = sys_mmap(256)
125 var o: i64 = sd_cat(row, 0, "MAP " as *u8)
126 var i: i64 = 0
127 while i < 64 { row[o] = hexuid[i]; o = o + 1; i = i + 1 }
128 row[o] = 32 as u8; o = o + 1
129 i = 0
130 while i < hn { row[o] = h[i]; o = o + 1; i = i + 1 }
131 row[o] = 10 as u8; o = o + 1
132 let fd: i64 = sys_openat_append(path, 0x1a4)
133 if fd < 0 { return 0 - 1 }
134 sys_write(fd, row, o)
135 sys_close(fd)
136 return 0
137}
138func dad_map_get(path: *u8, hexuid: *u8, out: *u8, cap: i64) -> i64 {
139 let szp: *i64 = sys_mmap(16) as *i64
140 szp[0] = 0
141 let b: *u8 = sys_read_file(path, szp)
142 if (b as i64) == 0 { sys_munmap(szp as *u8, 16); return 0 }
143 let n: i64 = szp[0]
144 var best: i64 = 0
145 var cur: i64 = 0
146 while cur < n {
147 var le: i64 = cur
148 while le < n && (b[le] as i64) != 10 { le = le + 1 }
149 if le - cur > 69 {
150 if (b[cur] as i64) == 77 {
151 var m: i64 = 1
152 var k: i64 = 0
153 while k < 64 { if b[cur + 4 + k] != hexuid[k] { m = 0; k = 64 } else { k = k + 1 } }
154 if m == 1 {
155 var w: i64 = 0
156 var s: i64 = cur + 69
157 while s < le { if w < cap - 1 { out[w] = b[s]; w = w + 1 } s = s + 1 }
158 out[w] = 0 as u8
159 best = w
160 }
161 }
162 }
163 cur = le + 1
164 }
165 // FREE the buffer sys_read_file mmap'd (cap 4 GiB + 16) -- it is NEVER freed by sys_read_file (the caller owns
166 // it), so this per-request map lookup was leaking the whole (append-only, growing) map file resident every call
167 // = the measured 27 GB docportal RSS (the F-class sys_read_file pattern). munmap bounds it to zero residual.
168 sys_munmap(b, 4294967312)
169 sys_munmap(szp as *u8, 16)
170 return best
171}
172
173func dad_ui_page(out: *u8, handle: *u8, hn: i64, sraw: *u8, sn: i64) -> i64 {
174 let body: *u8 = sys_mmap(DAD_OUTCAP)
175 var b: i64 = 0
176 b = sd_cat(body, b, "<!DOCTYPE html><meta charset=utf-8><title>Nishi document portal</title><body style=\"font-family:sans-serif;max-width:720px;margin:6vh auto\"><h2>Document portal</h2><p>Signed in as <b>" as *u8)
177 var i: i64 = 0
178 while i < hn { body[b] = handle[i]; b = b + 1; i = i + 1 }
179 b = sd_cat(body, b, "</b> (no-JS mode)</p><form method=post action=\"/admin/uploadtext?s=" as *u8)
180 i = 0
181 while i < sn { body[b] = sraw[i]; b = b + 1; i = i + 1 }
182 b = sd_cat(body, b, "\"><p><input name=domain value=\"andelinwest.com\" style=\"width:100%;padding:8px\"></p><p><select name=visibility><option value=public>Public (visitor-facing)</option><option value=private>Private (client vault)</option></select> <label><input type=checkbox name=pub_search value=1 checked> List in onsite search</label> <label><input type=checkbox name=ai_blog value=1> Allow AI blog</label></p><p><textarea name=text rows=12 style=\"width:100%\" placeholder=\"paste the document text here\"></textarea></p><button type=submit>Upload text</button></form>" as *u8)
183 // REAL file upload (multipart/form-data): a standard <input type=file>. Any file, binary-safe.
184 b = sd_cat(body, b, "<hr><h3>Or upload a file</h3><form enctype=multipart/form-data method=post action=\"/admin/uploadfile?s=" as *u8)
185 i = 0
186 while i < sn { body[b] = sraw[i]; b = b + 1; i = i + 1 }
187 b = sd_cat(body, b, "\"><p><input name=domain value=\"andelinwest.com\" style=\"width:100%;padding:8px\"></p><p><select name=visibility><option value=public>Public (visitor-facing)</option><option value=private>Private (client vault)</option></select> <label><input type=checkbox name=pub_search value=1 checked> List in onsite search</label></p><p><input type=file name=file></p><button type=submit>Upload file</button></form>" as *u8)
188 var o: i64 = 0
189 o = sd_cat(out, o, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\nContent-Length: " as *u8)
190 o = sd_catn(out, o, b)
191 o = sd_cat(out, o, "\r\n\r\n" as *u8)
192 i = 0
193 while i < b { out[o] = body[i]; o = o + 1; i = i + 1 }
194 return o
195}
196
197// no-JS upload shim: form-urlencoded fields (domain/visibility/pub_search/ai_blog/text) -> the
198// CANONICAL /admin/upload request (query params on the request line + raw text body) -> da_handle.
199// One storage path, two front doors (rule 15/19: same engine, no contract fork).
200func dad_uploadtext(ctx: *NxAuthContext, req: *u8, req_n: i64, subject: *u8, out: *u8) -> i64 {
201 let body_off: i64 = sd_body_off(req, req_n)
202 let body: *u8 = ((req as i64) + body_off) as *u8
203 let body_n: i64 = req_n - body_off
204 let f_off: *i64 = sys_mmap(8) as *i64
205 let f_len: *i64 = sys_mmap(8) as *i64
206 // domain
207 let domb: *u8 = sys_mmap(256)
208 var dom_n: i64 = 0
209 if sd_form_field(body, body_n, "domain" as *u8, 6, f_off, f_len) == 1 {
210 dom_n = sd_urldecode(((body as i64) + f_off[0]) as *u8, f_len[0], domb, 255)
211 }
212 if dom_n < 1 { return sd_cat(out, 0, "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nConnection: close\r\nContent-Length: 14\r\n\r\nmissing domain" as *u8) }
213 // visibility (default public)
214 let visb: *u8 = sys_mmap(64)
215 var vis_n: i64 = 0
216 if sd_form_field(body, body_n, "visibility" as *u8, 10, f_off, f_len) == 1 {
217 vis_n = sd_urldecode(((body as i64) + f_off[0]) as *u8, f_len[0], visb, 63)
218 }
219 if vis_n < 1 { visb[0] = 112 as u8; visb[1] = 117 as u8; visb[2] = 98 as u8; visb[3] = 108 as u8; visb[4] = 105 as u8; visb[5] = 99 as u8; vis_n = 6 }
220 // checkboxes: present -> 1, absent -> 0
221 var ps: i64 = 0
222 if sd_form_field(body, body_n, "pub_search" as *u8, 10, f_off, f_len) == 1 { ps = 1 }
223 var ab: i64 = 0
224 if sd_form_field(body, body_n, "ai_blog" as *u8, 7, f_off, f_len) == 1 { ab = 1 }
225 // the document text
226 if sd_form_field(body, body_n, "text" as *u8, 4, f_off, f_len) != 1 { return sd_cat(out, 0, "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nConnection: close\r\nContent-Length: 12\r\n\r\nmissing text" as *u8) }
227 let txt: *u8 = sys_mmap(DAD_REQCAP)
228 let txt_n: i64 = sd_urldecode(((body as i64) + f_off[0]) as *u8, f_len[0], txt, DAD_REQCAP - 1)
229 if txt_n < 1 { return sd_cat(out, 0, "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nConnection: close\r\nContent-Length: 10\r\n\r\nempty text" as *u8) }
230 // synthesize the canonical upload request
231 let syn: *u8 = sys_mmap(DAD_REQCAP + 4096)
232 var s: i64 = 0
233 s = sd_cat(syn, s, "POST /admin/upload?domain=" as *u8)
234 var i: i64 = 0
235 while i < dom_n { syn[s] = domb[i]; s = s + 1; i = i + 1 }
236 s = sd_cat(syn, s, "&visibility=" as *u8)
237 i = 0
238 while i < vis_n { syn[s] = visb[i]; s = s + 1; i = i + 1 }
239 s = sd_cat(syn, s, "&pub_search=" as *u8)
240 if ps == 1 { syn[s] = 49 as u8 } else { syn[s] = 48 as u8 }
241 s = s + 1
242 s = sd_cat(syn, s, "&ai_blog=" as *u8)
243 if ab == 1 { syn[s] = 49 as u8 } else { syn[s] = 48 as u8 }
244 s = s + 1
245 s = sd_cat(syn, s, " HTTP/1.1\r\n\r\n" as *u8)
246 i = 0
247 while i < txt_n { syn[s] = txt[i]; s = s + 1; i = i + 1 }
248 let dctx: *DaCtx = sys_mmap(16) as *DaCtx
249 dctx.user_level = 2
250 dctx.subject = subject
251 return da_handle(dctx, syn, s, out)
252}
253
254// ===== REAL multipart/form-data FILE upload (standards-correct: what an <input type=file> submits; binary-
255// safe, any file type). Composes the shipped nx_multipart parser + feeds the extracted file bytes into the
256// SAME da_handle /admin/upload store path the paste-text shim uses (one storage engine, three front doors). =====
257
258// extract the boundary token from the request's Content-Type header. returns length (0 if none).
259func dad_ct_boundary(req: *u8, req_n: i64, out: *u8) -> i64 {
260 let off: *i64 = sys_mmap(8) as *i64
261 let ln: *i64 = sys_mmap(8) as *i64
262 if nx_http_header_find(req, req_n, "Content-Type" as *u8, 12, off, ln) != NXHF_FOUND { return 0 }
263 let v: *u8 = ((req as i64) + off[0]) as *u8
264 let vn: i64 = ln[0]
265 let key: *u8 = "boundary=" as *u8
266 var bstart: i64 = 0 - 1
267 var i: i64 = 0
268 while i + 9 <= vn {
269 var m: i64 = 1
270 var j: i64 = 0
271 while j < 9 { if (v[i+j] as i64) != (key[j] as i64) { m = 0; j = 9 } else { j = j + 1 } }
272 if m == 1 { bstart = i + 9; i = vn } else { i = i + 1 }
273 }
274 if bstart < 0 { return 0 }
275 var w: i64 = 0
276 var k: i64 = bstart
277 while k < vn { let c: i64 = v[k] as i64; if c == 59 { k = vn } else { if c != 34 { out[w] = c as u8; w = w + 1 } k = k + 1 } }
278 out[w] = 0 as u8
279 return w
280}
281
282// extract a `key` value (e.g. name=" or filename=") from a part's header block body[hoff..hoff+hlen).
283// returns value length (0 if absent). key includes the trailing quote char it opens on.
284func dad_cd_extract(body: *u8, hoff: i64, hlen: i64, key: *u8, out: *u8) -> i64 {
285 var kl: i64 = 0
286 while key[kl] != (0 as u8) { kl = kl + 1 }
287 let end: i64 = hoff + hlen
288 var start: i64 = 0 - 1
289 var i: i64 = hoff
290 while i + kl <= end {
291 var m: i64 = 1
292 var j: i64 = 0
293 while j < kl { if (body[i+j] as i64) != (key[j] as i64) { m = 0; j = kl } else { j = j + 1 } }
294 if m == 1 { start = i + kl; i = end } else { i = i + 1 }
295 }
296 if start < 0 { return 0 }
297 var w: i64 = 0
298 var k: i64 = start
299 while k < end { if (body[k] as i64) == 34 { k = end } else { out[w] = body[k]; w = w + 1; k = k + 1 } }
300 out[w] = 0 as u8
301 return w
302}
303
304const DAD_MAXPARTS: i64 = 16
305
306// parse the multipart body -> pull the `file` part bytes + the domain/visibility/pub_search/ai_blog text
307// parts -> synthesize the canonical /admin/upload request -> da_handle (STAFF). Returns response length.
308func dad_uploadfile(ctx: *NxAuthContext, req: *u8, req_n: i64, subject: *u8, out: *u8) -> i64 {
309 let boundary: *u8 = sys_mmap(256)
310 let blen: i64 = dad_ct_boundary(req, req_n, boundary)
311 if blen <= 0 { return sd_cat(out, 0, "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nConnection: close\r\nContent-Length: 21\r\n\r\nno multipart boundary" as *u8) }
312 let body_off: i64 = sd_body_off(req, req_n)
313 let body: *u8 = ((req as i64) + body_off) as *u8
314 let body_n: i64 = req_n - body_off
315 let parts: *MultipartPart = sys_mmap(32 * DAD_MAXPARTS) as *MultipartPart
316 let np: i64 = multipart_parse(body, body_n, boundary, blen, parts, DAD_MAXPARTS)
317 if np <= 0 { return sd_cat(out, 0, "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nConnection: close\r\nContent-Length: 19\r\n\r\nmalformed multipart" as *u8) }
318 let domb: *u8 = sys_mmap(256); var dom_n: i64 = 0
319 let visb: *u8 = sys_mmap(64); var vis_n: i64 = 0
320 var ps: i64 = 0
321 var ab: i64 = 0
322 var file_off: i64 = 0 - 1
323 var file_len: i64 = 0
324 let nmb: *u8 = sys_mmap(128)
325 var i: i64 = 0
326 while i < np {
327 let p: *MultipartPart = ((parts as i64) + i * 32) as *MultipartPart
328 let nm_n: i64 = dad_cd_extract(body, p.headers_off, p.headers_len, "name=\"" as *u8, nmb)
329 if nm_n == 4 { if sd_starts(nmb, 4, "file" as *u8) == 1 { file_off = p.body_off; file_len = p.body_len } }
330 if nm_n == 6 { if sd_starts(nmb, 6, "domain" as *u8) == 1 { var w: i64 = 0; while w < p.body_len { domb[w] = body[p.body_off + w]; w = w + 1 } domb[p.body_len] = 0 as u8; dom_n = p.body_len } }
331 if nm_n == 10 { if sd_starts(nmb, 10, "visibility" as *u8) == 1 { var w2: i64 = 0; while w2 < p.body_len { visb[w2] = body[p.body_off + w2]; w2 = w2 + 1 } visb[p.body_len] = 0 as u8; vis_n = p.body_len } }
332 if nm_n == 10 { if sd_starts(nmb, 10, "pub_search" as *u8) == 1 { ps = 1 } }
333 if nm_n == 7 { if sd_starts(nmb, 7, "ai_blog" as *u8) == 1 { ab = 1 } }
334 i = i + 1
335 }
336 if file_len <= 0 { return sd_cat(out, 0, "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nConnection: close\r\nContent-Length: 18\r\n\r\nno file part found" as *u8) }
337 if dom_n <= 0 { return sd_cat(out, 0, "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nConnection: close\r\nContent-Length: 14\r\n\r\nmissing domain" as *u8) }
338 if vis_n <= 0 { visb[0] = 112 as u8; visb[1] = 117 as u8; visb[2] = 98 as u8; visb[3] = 108 as u8; visb[4] = 105 as u8; visb[5] = 99 as u8; vis_n = 6 }
339 // synthesize the canonical /admin/upload request (query metadata + raw file body) -> the SAME store path
340 let syn: *u8 = sys_mmap(body_n + 4096)
341 var s: i64 = sd_cat(syn, 0, "POST /admin/upload?domain=" as *u8)
342 var q: i64 = 0
343 while q < dom_n { syn[s] = domb[q]; s = s + 1; q = q + 1 }
344 s = sd_cat(syn, s, "&visibility=" as *u8)
345 q = 0
346 while q < vis_n { syn[s] = visb[q]; s = s + 1; q = q + 1 }
347 s = sd_cat(syn, s, "&pub_search=" as *u8)
348 if ps == 1 { syn[s] = 49 as u8 } else { syn[s] = 48 as u8 }
349 s = s + 1
350 s = sd_cat(syn, s, "&ai_blog=" as *u8)
351 if ab == 1 { syn[s] = 49 as u8 } else { syn[s] = 48 as u8 }
352 s = s + 1
353 s = sd_cat(syn, s, " HTTP/1.1\r\n\r\n" as *u8)
354 var f: i64 = 0
355 while f < file_len { syn[s] = body[file_off + f]; s = s + 1; f = f + 1 }
356 let dctx: *DaCtx = sys_mmap(16) as *DaCtx
357 dctx.user_level = 2
358 dctx.subject = subject
359 return da_handle(dctx, syn, s, out)
360}
361
362// ---- THE ROUTER: request bytes -> response bytes (no socket). The gate drives this directly. ----
363func dad_handle(ctx: *NxAuthContext, req: *u8, req_n: i64, out: *u8) -> i64 {
364 let poff: *i64 = sys_mmap(8) as *i64; let plen: *i64 = sys_mmap(8) as *i64
365 poff[0] = 0; plen[0] = 0
366 sd_find_path(req, req_n, poff, plen)
367 let path: *u8 = ((req as i64) + poff[0]) as *u8
368 let pn: i64 = plen[0]
369 let is_post: i64 = (req[0] == 80 as u8) as i64
370 var o: i64 = 0
371 // PUBLIC sovereign onsite /search (no auth, seg_store-native, NO tsv) -- matched FIRST, before the /admin chain.
372 // Domain from the Host header -> dss_serve queries dp-<Host>-pub- (this daemon fronts andelinwest.com).
373 if sd_starts(path, pn, "/search" as *u8) == 1 {
374 let hostbuf: *u8 = sys_mmap(256)
375 dsv_host(req, req_n, hostbuf, 255)
376 return dss_serve(hostbuf, req, req_n, out)
377 }
378 // PUBLIC /doc?cid=<cid> document view (the search loop's click-through; -pub- shard only). Boundary-guarded:
379 // exactly "/doc" or "/doc?..." -- a raw prefix would also swallow /doctor-style paths (path includes the query).
380 if sd_starts(path, pn, "/doc" as *u8) == 1 {
381 var docb: i64 = 0
382 if pn == 4 { docb = 1 }
383 if pn > 4 { if path[4] == (63 as u8) { docb = 1 } }
384 if docb == 1 {
385 let hostbuf2: *u8 = sys_mmap(256)
386 dsv_host(req, req_n, hostbuf2, 255)
387 return dsv_doc_serve(hostbuf2, req, req_n, out)
388 }
389 }
390 // API-FIRST surface: versioned public JSON (CORS, structured errors). Same engine, machine contract --
391 // the HTML SERP above is just one client of it. Boundary-guarded like /doc.
392 if sd_starts(path, pn, "/api/search" as *u8) == 1 {
393 var apib: i64 = 0
394 if pn == 11 { apib = 1 }
395 if pn > 11 { if path[11] == (63 as u8) { apib = 1 } }
396 if apib == 1 {
397 let hostbuf3: *u8 = sys_mmap(256)
398 dsv_host(req, req_n, hostbuf3, 255)
399 return dss_api_search(hostbuf3, req, req_n, out)
400 }
401 }
402 if sd_starts(path, pn, "/api/doc" as *u8) == 1 {
403 var apid: i64 = 0
404 if pn == 8 { apid = 1 }
405 if pn > 8 { if path[8] == (63 as u8) { apid = 1 } }
406 if apid == 1 {
407 let hostbuf4: *u8 = sys_mmap(256)
408 dsv_host(req, req_n, hostbuf4, 255)
409 return dss_api_doc(hostbuf4, req, req_n, out)
410 }
411 }
412 if sd_starts(path, pn, "/api/suggest" as *u8) == 1 {
413 var apis: i64 = 0
414 if pn == 12 { apis = 1 }
415 if pn > 12 { if path[12] == (63 as u8) { apis = 1 } }
416 if apis == 1 {
417 let hostbuf5: *u8 = sys_mmap(256)
418 dsv_host(req, req_n, hostbuf5, 255)
419 return dss_api_suggest(hostbuf5, req, req_n, out)
420 }
421 }
422 // machine-readable OpenAPI 3.1 contract (enterprise: client codegen / Swagger / contract testing).
423 // Exact "/api/openapi.json" or with a query string.
424 if sd_starts(path, pn, "/api/openapi.json" as *u8) == 1 {
425 var apio: i64 = 0
426 if pn == 17 { apio = 1 }
427 if pn > 17 { if path[17] == (63 as u8) { apio = 1 } }
428 if apio == 1 {
429 let hostbuf6: *u8 = sys_mmap(256)
430 dsv_host(req, req_n, hostbuf6, 255)
431 return dss_api_openapi(hostbuf6, out)
432 }
433 }
434 if sd_starts(path, pn, "/admin/register" as *u8) == 1 {
435 // INVITE-GATED OPAQUE self-registration: a remote firm presents a SECRET single-use, expiring
436 // invite token (minted out-of-band by inv_issue) instead of a guessable handle. Matched BEFORE
437 // the "/admin/" session gate (which is a prefix of this path) and as a sibling of /admin/login.
438 // Gate shape mirrors nx_lan_signup_routes.lsd_route POST /register -- swapping its LAN-IP gate
439 // for the inv_check token gate. On success: olg_register (OPAQUE) -> inv_consume -> 200 mnemonic.
440 if is_post == 1 {
441 let body_off: i64 = sd_body_off(req, req_n)
442 let body: *u8 = ((req as i64) + body_off) as *u8
443 let body_n: i64 = req_n - body_off
444 let hoff: *i64 = sys_mmap(8) as *i64; let hn: *i64 = sys_mmap(8) as *i64
445 let poff2: *i64 = sys_mmap(8) as *i64; let pnn: *i64 = sys_mmap(8) as *i64
446 let ioff: *i64 = sys_mmap(8) as *i64; let inn: *i64 = sys_mmap(8) as *i64
447 var got: i64 = 0
448 if sd_form_field(body, body_n, "handle" as *u8, 6, hoff, hn) == 1 {
449 if sd_form_field(body, body_n, "pw" as *u8, 2, poff2, pnn) == 1 {
450 if sd_form_field(body, body_n, "invite" as *u8, 6, ioff, inn) == 1 { got = 1 }
451 }
452 }
453 var done: i64 = 0
454 if got == 1 {
455 let hbuf: *u8 = sys_mmap(256); let pbuf: *u8 = sys_mmap(512); let ibuf: *u8 = sys_mmap(128)
456 let h_dec: i64 = sd_urldecode(((body as i64) + hoff[0]) as *u8, hn[0], hbuf, 255)
457 let p_dec: i64 = sd_urldecode(((body as i64) + poff2[0]) as *u8, pnn[0], pbuf, 511)
458 let i_dec: i64 = sd_urldecode(((body as i64) + ioff[0]) as *u8, inn[0], ibuf, 127)
459 if h_dec > 0 { if p_dec > 0 { if i_dec > 0 {
460 let invpath: *u8 = sys_mmap(512)
461 dad_invite_path(ctx, invpath)
462 let now_s: i64 = sys_now_realtime_sec()
463 // THE GATE: token must be a live, realm-scoped, unconsumed invite (level > 0).
464 let lvl: i64 = inv_check(invpath, ibuf, i_dec, ctx.realm_id, ctx.realm_id_n, now_s)
465 if lvl <= 0 {
466 o = sd_cat(out, 0, "HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: 37\r\n\r\n{\"error\":\"invalid or expired invite\"}" as *u8)
467 done = 1
468 } else {
469 let mn: *u8 = sys_mmap(512); let mnn: *i64 = sys_mmap(16) as *i64
470 if olg_register(ctx, hbuf, h_dec, pbuf, p_dec, mn, 512, mnn) == NX_MAUTH_OK {
471 // uidmap: record uid-hash -> handle at register so /admin/ui greets by name
472 let uidh2: *u8 = sys_mmap(32)
473 if nx_ncs_derive_user_id_hash(ctx.realm_id, ctx.realm_id_n, hbuf, h_dec, uidh2) == NX_NCS_OK {
474 let uhex2: *u8 = sys_mmap(72); dad_hex32(uidh2, uhex2)
475 let ump2: *u8 = sys_mmap(512); dad_uidmap_path(ctx, ump2)
476 dad_map_put(ump2, uhex2, hbuf, h_dec)
477 }
478 inv_consume(invpath, ibuf, i_dec, now_s) // single-use: burn the invite
479 o = sd_cat(out, 0, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: " as *u8)
480 o = sd_catn(out, o, 15 + mnn[0])
481 o = sd_cat(out, o, "\r\n\r\n{\"mnemonic\":\"" as *u8)
482 var z: i64 = 0; while z < mnn[0] { out[o] = mn[z]; o = o + 1; z = z + 1 }
483 o = sd_cat(out, o, "\"}" as *u8)
484 done = 1
485 } else {
486 o = sd_cat(out, 0, "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: 27\r\n\r\n{\"error\":\"register failed\"}" as *u8)
487 done = 1
488 }
489 }
490 } } }
491 }
492 if done == 0 { o = sd_cat(out, 0, "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: 27\r\n\r\n{\"error\":\"register failed\"}" as *u8) }
493 } else { o = dad_shell(out) }
494 } else { if sd_starts(path, pn, "/admin/login" as *u8) == 1 {
495 if is_post == 1 {
496 let body_off: i64 = sd_body_off(req, req_n)
497 let body: *u8 = ((req as i64) + body_off) as *u8
498 let body_n: i64 = req_n - body_off
499 let hoff: *i64 = sys_mmap(8) as *i64; let hn: *i64 = sys_mmap(8) as *i64
500 let poff2: *i64 = sys_mmap(8) as *i64; let pnn: *i64 = sys_mmap(8) as *i64
501 var got: i64 = 0
502 if sd_form_field(body, body_n, "handle" as *u8, 6, hoff, hn) == 1 {
503 if sd_form_field(body, body_n, "passphrase" as *u8, 10, poff2, pnn) == 1 { got = 1 }
504 }
505 // ui=1 rides ONLY the native <form> submit (nishi-first no-JS path) -> answer HTML, not JSON.
506 var uiflag: i64 = 0
507 let uoff: *i64 = sys_mmap(8) as *i64
508 let un: *i64 = sys_mmap(8) as *i64
509 if sd_form_field(body, body_n, "ui" as *u8, 2, uoff, un) == 1 { uiflag = 1 }
510 var ok: i64 = 0
511 if got == 1 {
512 let hbuf: *u8 = sys_mmap(256); let pbuf: *u8 = sys_mmap(512)
513 let h_dec: i64 = sd_urldecode(((body as i64) + hoff[0]) as *u8, hn[0], hbuf, 255)
514 let p_dec: i64 = sd_urldecode(((body as i64) + poff2[0]) as *u8, pnn[0], pbuf, 511)
515 if h_dec > 0 { if p_dec > 0 {
516 let tok: *u8 = sys_mmap(NX_MAUTH_SESSION_TOKEN_BYTES)
517 let tok_n: *i64 = sys_mmap(8) as *i64; tok_n[0] = 0
518 if nx_modern_auth_login(ctx, hbuf, h_dec, pbuf, p_dec, tok, NX_MAUTH_SESSION_TOKEN_BYTES, tok_n) == NX_MAUTH_OK {
519 // uidmap: the session token carries the uid HASH -> record hash->handle so /admin/ui greets by name
520 let uidh: *u8 = sys_mmap(32)
521 if nx_ncs_derive_user_id_hash(ctx.realm_id, ctx.realm_id_n, hbuf, h_dec, uidh) == NX_NCS_OK {
522 let uhex: *u8 = sys_mmap(72); dad_hex32(uidh, uhex)
523 let ump: *u8 = sys_mmap(512); dad_uidmap_path(ctx, ump)
524 dad_map_put(ump, uhex, hbuf, h_dec)
525 }
526 let b64: *u8 = sys_mmap(256); let b64_n: i64 = b64_encode(tok, NX_MAUTH_SESSION_TOKEN_BYTES, b64)
527 if uiflag == 1 { o = dad_landing(out, b64, b64_n) }
528 else {
529 o = sd_cat(out, o, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: " as *u8)
530 o = sd_catn(out, o, 12 + b64_n)
531 o = sd_cat(out, o, "\r\n\r\n{\"token\":\"" as *u8)
532 var z: i64 = 0; while z < b64_n { out[o] = b64[z]; o = o + 1; z = z + 1 }
533 o = sd_cat(out, o, "\"}" as *u8)
534 }
535 ok = 1
536 }
537 } }
538 }
539 if ok == 0 {
540 if uiflag == 1 { o = sd_cat(out, 0, "HTTP/1.1 401 Unauthorized\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\nContent-Length: 106\r\n\r\n<!DOCTYPE html><meta charset=utf-8><p>Wrong handle or passphrase.</p><p><a href=\"/admin\">Try again</a></p>" as *u8) }
541 else { o = sd_emit_401_json(out) }
542 }
543 } else { o = dad_shell(out) }
544 } else { if sd_starts(path, pn, "/admin/ui" as *u8) == 1 {
545 // NISHI-FIRST no-JS admin page: session from the ?s= query (same Ed25519 validator) -> the
546 // handle-greeting + paste-upload form. Renders + navigates in the nishi browser TODAY.
547 let hb: *u8 = sys_mmap(256)
548 let hbn: *i64 = sys_mmap(8) as *i64
549 let now_ui: i64 = sys_now_realtime_sec()
550 if nx_sa_validate_qs(ctx, path, pn, now_ui, hb, 255, hbn) == NX_MAUTH_OK {
551 let voff: *i64 = sys_mmap(8) as *i64
552 let vlen: i64 = nx_sa_qs_raw(path, pn, voff)
553 let sp: *u8 = ((path as i64) + voff[0]) as *u8
554 // resolve the session uid HASH -> plaintext handle (else "Signed in as" shows raw hash bytes)
555 var disp_h: *u8 = hb
556 var disp_n: i64 = hbn[0]
557 if hbn[0] == 32 {
558 let uhex: *u8 = sys_mmap(72); dad_hex32(hb, uhex)
559 let ump: *u8 = sys_mmap(512); dad_uidmap_path(ctx, ump)
560 let rhb: *u8 = sys_mmap(128)
561 let rhn: i64 = dad_map_get(ump, uhex, rhb, 127)
562 if rhn > 0 { disp_h = rhb; disp_n = rhn }
563 }
564 o = dad_ui_page(out, disp_h, disp_n, sp, vlen)
565 } else { o = sd_emit_401_json(out) }
566 } else { if sd_starts(path, pn, "/admin/uploadtext" as *u8) == 1 {
567 // NISHI-FIRST no-JS upload: auth via ?s= OR the header, then the form->canonical-request shim.
568 let now_ut: i64 = sys_now_realtime_sec()
569 let hb2: *u8 = sys_mmap(256)
570 let hn2: *i64 = sys_mmap(8) as *i64
571 hn2[0] = 0
572 var authed: i64 = 0
573 if nx_sa_validate_qs(ctx, path, pn, now_ut, hb2, 255, hn2) == NX_MAUTH_OK { authed = 1 }
574 if authed == 0 { if nx_sa_validate_handle(ctx, req, req_n, now_ut, hb2, 255, hn2) == NX_MAUTH_OK { authed = 1 } }
575 if authed == 1 {
576 let subhex2: *u8 = sys_mmap(72); subhex2[0] = 0 as u8
577 if hn2[0] == 32 { dad_hex32(hb2, subhex2) }
578 if is_post == 1 { o = dad_uploadtext(ctx, req, req_n, subhex2, out) }
579 else { o = sd_emit_401_json(out) }
580 } else { o = sd_emit_401_json(out) }
581 } else { if sd_starts(path, pn, "/admin/uploadfile" as *u8) == 1 {
582 // REAL multipart/form-data file upload (what an <input type=file> submits): auth via ?s= OR header,
583 // then dad_uploadfile parses the parts + feeds the file bytes into the SAME /admin/upload store path.
584 let now_uf: i64 = sys_now_realtime_sec()
585 let hb3: *u8 = sys_mmap(256)
586 let hn3: *i64 = sys_mmap(8) as *i64
587 hn3[0] = 0
588 var authed_f: i64 = 0
589 if nx_sa_validate_qs(ctx, path, pn, now_uf, hb3, 255, hn3) == NX_MAUTH_OK { authed_f = 1 }
590 if authed_f == 0 { if nx_sa_validate_handle(ctx, req, req_n, now_uf, hb3, 255, hn3) == NX_MAUTH_OK { authed_f = 1 } }
591 if authed_f == 1 {
592 let subhex3: *u8 = sys_mmap(72); subhex3[0] = 0 as u8
593 if hn3[0] == 32 { dad_hex32(hb3, subhex3) }
594 if is_post == 1 { o = dad_uploadfile(ctx, req, req_n, subhex3, out) }
595 else { o = sd_emit_401_json(out) }
596 } else { o = sd_emit_401_json(out) }
597 } else { if sd_starts(path, pn, "/admin/" as *u8) == 1 {
598 // session-gated: the /admin/<action> paths (upload/policy/whoami) validate the no-cookie token, then route
599 // to da_handle at STAFF level (admin.<d> = the management capability). Unauthenticated -> 401 BEFORE
600 // da_handle ever runs (deny-by-default, two layers). Bare /admin falls through to the public login shell.
601 let now_s: i64 = sys_now_realtime_sec()
602 // P2: emit the session SUBJECT via nx_sa_validate_handle (verified to accept EXACTLY the same sessions as
603 // nx_sa_validate -- same sig/MAC + TTL + realm check, computed before + independent of the subject copy).
604 // The non-null hb/hbn + hb-cap>=32 are REQUIRED (else deny / null-deref). Same ?s= buffer pattern used above.
605 let hb: *u8 = sys_mmap(256)
606 let hbn: *i64 = sys_mmap(8) as *i64
607 hbn[0] = 0
608 if nx_sa_validate_handle(ctx, req, req_n, now_s, hb, 255, hbn) == NX_MAUTH_OK {
609 let dctx: *DaCtx = sys_mmap(16) as *DaCtx
610 dctx.user_level = 2
611 let subhex: *u8 = sys_mmap(72); subhex[0] = 0 as u8
612 if hbn[0] == 32 { dad_hex32(hb, subhex) }
613 dctx.subject = subhex
614 o = da_handle(dctx, req, req_n, out)
615 } else { o = sd_emit_401_json(out) }
616 } else { o = dad_shell(out) } } } } } }
617 return o
618}
619
620func main(argc: i64, argv: *i64) -> i64 {
621 if argc < 6 { sys_write(2, "usage: nx_docportal_admin_daemon <port> <keysfile> <storefile> <realm> <budget>\n" as *u8, 79); return 1 }
622 let port: i64 = sd_atoi(argv[1] as *u8)
623 let keysfile: *u8 = argv[2] as *u8
624 let storefile: *u8 = argv[3] as *u8
625 let realm: *u8 = argv[4] as *u8
626 let budget: i64 = sd_atoi(argv[5] as *u8)
627 let realm_n: i64 = sd_len(realm)
628
629 // fail-fast: arm the realm context at startup (Rule 20), exactly like nx_status_daemon
630 let oprf_seed: *u8 = sys_mmap(32); let akp: *u8 = sys_mmap(32); let akb: *u8 = sys_mmap(33)
631 let edp: *u8 = sys_mmap(32); let edb: *u8 = sys_mmap(32)
632 sys_write(1, "DBG start: args parsed\n" as *u8, 23)
633 if nx_uas_server_keys_load_or_init(keysfile, oprf_seed, akp, akb, edp, edb) != NX_UAS_OK { sys_write(2, "FATAL: server-key bundle\n" as *u8, 24); return 2 }
634 sys_write(1, "DBG: server-keys OK\n" as *u8, 20)
635 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext
636 if nx_auth_context_init(ctx, realm, realm_n, realm, realm_n, storefile as i64, oprf_seed, edp, edb, 900, 8192, 1, 1, 5, 1) != NX_MAUTH_OK { sys_write(2, "FATAL: context init\n" as *u8, 20); return 3 }
637 sys_write(1, "DBG: context init OK\n" as *u8, 21)
638
639 // API-FIRST: announce the search API in the ecosystem tool registry (idempotent check-first; the NAS
640 // CWD is nishihost/, so this writes the LIVE knowledge/toolreg- shard -> /api/tools + MCP tools/list
641 // discover nishi_search with zero adapter -- the "services register at their own startup" pattern).
642 let trp: *i64 = sys_mmap(16) as *i64
643 let trl: *i64 = sys_mmap(16) as *i64
644 if tool_get("nishi_search" as *u8, trp, trl) != 1 {
645 tool_register("nishi_search" as *u8, "Sovereign nishi search: a domain's docs + site pages, the nishi library (Host nishifamily.com), and the crawled open web (scope=web). Versioned JSON, CORS, structured errors." as *u8, "GET /api/search?q=<terms>[&scope=web] (per-domain via Host)" as *u8, "GREEN" as *u8)
646 }
647 if tool_get("nishi_doc" as *u8, trp, trl) != 1 {
648 tool_register("nishi_doc" as *u8, "Fetch a public document by content id from the nishi search corpus (JSON; text bounded + truncation-flagged)." as *u8, "GET /api/doc?cid=<cid>[&scope=web]" as *u8, "GREEN" as *u8)
649 }
650
651 let addr: *u8 = sys_mmap(16)
652 if nx_http_server_addr_loopback(addr, port) != 16 { sys_write(2, "FATAL: addr_loopback != 16\n" as *u8, 27); return 4 }
653 sys_write(1, "DBG: addr_loopback OK\n" as *u8, 22)
654 let lv: *i64 = sys_mmap(8) as *i64
655 let lfd: i64 = nx_http_server_listen(addr, 64, lv)
656 if lfd < 0 { sys_write(2, "FATAL: listen failed (port busy or denied)\n" as *u8, 43); return 4 }
657 sys_write(1, "nx_docportal_admin_daemon listening loopback (admin.<domain> behind the SNI router)\n" as *u8, 83)
658
659 // SERVE-SCALE: pre-warm the web-shard handle in the PARENT so every forked child inherits it COW --
660 // per-request ss_open on the bulk-CC shard measured ~0.7s/query without this. Refreshed per accept
661 // below (stat-signature check, ~free; reopen only when new segments actually ship).
662 if dss_web_cache_refresh() == 1 { sys_write(1, "web shard handle pre-warmed\n" as *u8, 28) }
663
664 let req: *u8 = sys_mmap(DAD_REQCAP)
665 let out: *u8 = sys_mmap(DAD_OUTCAP)
666 let av: *i64 = sys_mmap(8) as *i64 // HOISTED out of the loop -- reused each accept (was leaking a page/iter)
667 let wst: *i64 = sys_mmap(16) as *i64 // HOISTED -- child reap status, reused
668 // Seeded from the pre-warm above, not from zero: the shard was JUST opened, so the first
669 // request must not immediately reopen it. Without this seed the gate would let exactly one
670 // full 1.59GB reopen through on the very first query after every restart -- the slowest
671 // possible moment, right when the daemon is already cold.
672 var last_refresh: i64 = sys_now_realtime_sec()
673 var served: i64 = 0
674 while served < budget {
675 let cfd: i64 = nx_http_server_accept_one(lfd, av)
676 if cfd < 0 {
677 // ACCEPT BACKOFF (2026-08-01). This branch retried INSTANTLY, so any persistent accept
678 // error became a 100pct-CPU busy loop while the socket stayed LISTENING -- which every
679 // port-probe health check reads as a healthy daemon. 50ms costs nothing on the happy
680 // path (this branch is not taken when accepts succeed) and turns a spin into a
681 // survivable retry. It also stops a failing daemon from eating its 20M `served` budget
682 // in minutes.
683 sys_sleep_ms(DAD_ACCEPT_BACKOFF_MS)
684 served = served + 1
685 }
686 if cfd >= 0 {
687 // PER-REQUEST TRACE (2026-08-01). This daemon emitted NOTHING after `pre-warmed`, so when
688 // it wedged there was no way to tell which stage died: accept, refresh, fork, or handle.
689 // I spent an hour narrowing it from the outside with CPU counters and fd counts and still
690 // could not say. Three cheap writes make the next occurrence self-diagnosing.
691 // ★★★★★A DAEMON THAT LOGS ONLY ITS STARTUP CAN ONLY EVER TELL YOU IT STARTED.
692 sys_write(1, "REQ accepted\n" as *u8, 13)
693 // ROOT-CAUSE FIX (2026-08-01), found by the trace two lines above.
694 // The wedge was HERE: the log read accepted=52, refresh-ok=51, done=51 -- the daemon
695 // accepted a request and never returned from dss_web_cache_refresh(). That call compares
696 // a manifest signature and, on ANY change, does ss_close + ss_open2 over the whole web
697 // shard: 94 segments, 1.59GB. It ran ON EVERY ACCEPTED REQUEST.
698 // WHY IT ONLY BIT NOW: the manifest used to be frozen because the crawler was committing
699 // nothing (the segments=0 poisoned-segid bug I root-fixed earlier tonight). With the
700 // crawler working the shard grows again -- 87 segments at session start, 94 now -- so the
701 // cached handle is invalidated constantly and a full reopen lands on the request path.
702 // ★★★★★A CORRECT FIX IN ONE ORGAN CAN EXPOSE A LATENT O(N) COST IN ANOTHER.
703 // THE GATE: at most one refresh per DAD_REFRESH_MIN_SEC. Freshness is bounded by that
704 // interval instead of by request arrival, so a growing shard can no longer put a
705 // 1.59GB reopen in front of every query. Serving a few seconds of slightly stale index
706 // is strictly better than not serving at all.
707 let now_s: i64 = sys_now_realtime_sec()
708 if now_s - last_refresh >= DAD_REFRESH_MIN_SEC {
709 dss_web_cache_refresh()
710 last_refresh = now_s
711 sys_write(1, "REQ refresh-ok\n" as *u8, 15)
712 } else {
713 sys_write(1, "REQ refresh-skip\n" as *u8, 17)
714 }
715 // FORK-PER-REQUEST (the durable leak fix): dad_handle mmaps DAD_OUTCAP (512 KB) + boxes INTERNALLY on
716 // every request and frees NONE -- the measured ~50 MB/s -> 27 GB RSS leak (budget=20M means it never
717 // self-recycles). A child that handles then sys_exit(0) reclaims EVERY page BY CONSTRUCTION; the parent
718 // reaps it with a blocking wait4 (no zombies -- the nx_mp_serve leak class avoided). Functionally safe:
719 // sessions are file-validated (no in-mem state), upload writes hit shared fds so they persist, and the
720 // parent's ctx/req/out stay pristine (the child mutates only COW copies). Leak fixed -> process stays
721 // small -> fork stays cheap. Requests serialize, which is fine for a loopback admin daemon.
722 let kid: i64 = sys_fork()
723 if kid == 0 {
724 let om: *i64 = sys_mmap(8) as *i64; let opo: *i64 = sys_mmap(8) as *i64; let opl: *i64 = sys_mmap(8) as *i64
725 let ocl: *i64 = sys_mmap(8) as *i64; let obo: *i64 = sys_mmap(8) as *i64; let orn: *i64 = sys_mmap(8) as *i64
726 let rrc: i64 = nx_http_server_read_request(cfd, req, DAD_REQCAP, om, opo, opl, ocl, obo, orn)
727 if rrc == NXS_OK {
728 let oN: i64 = dad_handle(ctx, req, orn[0], out)
729 nx_http_server_send_response_nokeep_close(cfd, out, oN)
730 }
731 sys_close(cfd)
732 sys_exit(0)
733 }
734 sys_close(cfd)
735 if kid > 0 { sys_wait4(kid, wst, 0) }
736 sys_write(1, "REQ done\n" as *u8, 9)
737 served = served + 1
738 }
739 }
740 sys_close(lfd)
741 return 0
742}