nx_wiki_admin_wiring.nx source
↩ module page · 558 lines · 24000 B
1// nx_wiki_admin_wiring.nx -- V-MODAUTH-7 per-site wiring for wiki admin routes.
2//
3// REWRITTEN 2026-05-27 from V1 cookie-based wiring to V-MODAUTH-6
4// SEALED API per NISHI_MODERN_AUTH_CHARTER §8. Composes
5// hub/nx_modern_auth_flow (V-MODAUTH-6) which itself composes
6// hub/nx_no_cookie_session (V-MODAUTH-5).
7//
8// NO COOKIES. NO Set-Cookie. NO Cookie: reads. Per C1 cardinal.
9//
10// Per-request flow (the hot path):
11// 1. Caller extracts X-Nishi-Session header from HTTP request
12// via nx_http_header_find
13// 2. Caller base64-decodes the header value to a 152-byte binary
14// session token
15// 3. Caller passes (NxAuthContext + token bytes + now_unix_s) to
16// nx_wiki_admin_handle
17// 4. nx_wiki_admin_handle calls nx_modern_auth_validate_session
18// 5. On success: emit 200/302 to protected content
19// On failure: emit 401 with hint "POST passphrase to /wiki/admin/login"
20//
21// COMPOSES per "avoid duplicate primitives":
22// hub/nx_modern_auth_flow V-MODAUTH-6 SEALED §8 API
23// nx_http_header_find HTTP header extraction
24//
25// COMPOSED BY:
26// wiki/nx_wiki_routes.nx dispatcher (updated to pass *NxAuthContext)
27//
28// SPEC REFERENCES:
29// NISHI_MODERN_AUTH_CHARTER §8 SEALED 5-fn API
30// NISHI_MODERN_AUTH_CHARTER §9.1 nishifamily.com wiring example
31//
32// V2 (2026-06-10): POST /wiki/admin/login is LIVE -- OPAQUE (RFC 9807, KAT-gated)
33// + argon2id KSF + account store + Ed25519 session mint. Flow:
34// POST /wiki/admin/login body "handle=<h>&passphrase=<p>" (form-urlencoded)
35// -> nx_modern_auth_login -> 200 {"token":"<base64 152B>"} | 401
36// GET /wiki/admin/* X-Nishi-Session: <base64 token> -> 200 | 401
37// The 503 LOGIN_NOT_READY path remains ONLY for an unarmed context (no
38// account store / oprf_seed wired at boot) -- honest, not a pretend-stub.
39// Admin REGISTRATION is out-of-band (ops provisioning via
40// nx_modern_auth_register; no public register endpoint on the admin realm).
41
42import "nx_syscalls.nx"
43import "nx_http_header_find.nx"
44import "nx_base64.nx"
45import "hub/nx_modern_auth_flow.nx"
46
47// ===== Sealed verdict surface (codes 1420-1439) =================================================
48const NX_WAW_OK: i64 = 0
49const NX_WAW_BAD_INPUT: i64 = 1420
50const NX_WAW_BUF_OVERFLOW: i64 = 1421
51const NX_WAW_AUTH_FAILED: i64 = 1422
52const NX_WAW_LOGIN_NOT_READY: i64 = 1423 // /wiki/admin/login waits for V-MODAUTH-2/4
53
54// ===== Named constants (M7) =================================================
55const NX_WAW_MAX_HEADER_SCAN_BYTES: i64 = 8192
56const NX_WAW_RESP_CAP: i64 = 4096
57const NX_WAW_HDR_SESSION_NAME_N: i64 = 15 // "X-Nishi-Session" (was 16 = NEVER matched;
58 // latent since V1 -- caught by nx_wiki_login_live_gate row 3)
59
60// HTTP method-kind constants per nx_http_io convention.
61const NX_WAW_METHOD_GET: i64 = 1
62const NX_WAW_METHOD_POST: i64 = 2
63
64// ===== Helpers =================================================
65
66func _waw_hdr_session_name() -> *u8 { return "X-Nishi-Session" as *u8 }
67
68func _waw_append(resp_buf: *u8, out_off: *i64, resp_cap: i64,
69 src: *u8, src_n: i64) -> i64 {
70 if out_off[0] + src_n > resp_cap { return 0 - NX_WAW_BUF_OVERFLOW }
71 var i: i64 = 0
72 while i < src_n {
73 if i >= NX_WAW_RESP_CAP { return 0 - NX_WAW_BUF_OVERFLOW }
74 resp_buf[out_off[0] + i] = src[i]
75 i = i + 1
76 }
77 out_off[0] = out_off[0] + src_n
78 return NX_WAW_OK
79}
80
81func _waw_append_z(resp_buf: *u8, out_off: *i64, resp_cap: i64,
82 src: *u8) -> i64 {
83 if (src as i64) == 0 { return 0 - NX_WAW_BAD_INPUT }
84 var n: i64 = 0
85 while n < NX_WAW_MAX_HEADER_SCAN_BYTES {
86 if src[n] == (0 as u8) {
87 return _waw_append(resp_buf, out_off, resp_cap, src, n)
88 }
89 n = n + 1
90 }
91 return 0 - NX_WAW_BUF_OVERFLOW
92}
93
94// ===== Response builders =================================================
95
96// 401 Unauthorized with hint to POST passphrase to /wiki/admin/login.
97// NO Set-Cookie. NO WWW-Authenticate Basic (per modern-research-auth
98// cardinal, Basic Auth is also old tech; use POST form).
99func _waw_emit_401(resp_buf: *u8, out_off: *i64, resp_cap: i64) -> i64 {
100 let response: *u8 = "HTTP/1.1 401 Unauthorized\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 198\r\n\r\n<!DOCTYPE html><html><body><h1>Authentication required</h1><p>POST your passphrase to <code>/wiki/admin/login</code>. Session tokens go in <code>X-Nishi-Session</code> header (NEVER cookies).</p></body></html>" as *u8
101 return _waw_append_z(resp_buf, out_off, resp_cap, response)
102}
103
104// 200 OK for the protected content placeholder; route-specific
105// rendering would replace this in V-MODAUTH-7b.
106func _waw_emit_authed_200(resp_buf: *u8, out_off: *i64, resp_cap: i64) -> i64 {
107 let response: *u8 = "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 64\r\n\r\n<!DOCTYPE html><html><body><h1>Admin dashboard</h1></body></html>" as *u8
108 return _waw_append_z(resp_buf, out_off, resp_cap, response)
109}
110
111// 503 Service Unavailable for /wiki/admin/login until M2 + M4 land.
112// Honest per M6 anti-pretend-stub: tell user explicitly.
113func _waw_emit_login_not_ready(resp_buf: *u8, out_off: *i64, resp_cap: i64) -> i64 {
114 let response: *u8 = "HTTP/1.1 503 Service Unavailable\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 232\r\n\r\n<!DOCTYPE html><html><body><h1>Login temporarily unavailable</h1><p>The OPAQUE PAKE login flow lands when V-MODAUTH-2 ships. Per NISHI_MODERN_AUTH_CHARTER section 13, that milestone composes nx_voprf + nx_argon2id (both substrate-resident).</p></body></html>" as *u8
115 return _waw_append_z(resp_buf, out_off, resp_cap, response)
116}
117
118// 200 with a single-key JSON body: {"<key>":"<val>"}. Shared by login/refresh
119// (key="token", val=base64 session token) and recover (key="mnemonic").
120func _waw_emit_json_kv(resp_buf: *u8, out_off: *i64, resp_cap: i64,
121 key: *u8, key_n: i64,
122 val: *u8, val_n: i64) -> i64 {
123 // body {"k":"v"} = key_n + val_n + 7 framing bytes ({ " " : " " })
124 // (the V1 login emitter overstated Content-Length by 2 -- fixed here)
125 let body_n: i64 = key_n + val_n + 7
126 let head: *u8 = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: " as *u8
127 let rc1: i64 = _waw_append_z(resp_buf, out_off, resp_cap, head)
128 if rc1 != NX_WAW_OK { return rc1 }
129 let dig: *u8 = sys_mmap(8)
130 var dn: i64 = 0
131 var v: i64 = body_n
132 if v >= 100 { dig[dn] = (48 + (v / 100)) as u8; dn = dn + 1; v = v % 100 }
133 dig[dn] = (48 + (v / 10)) as u8
134 dig[dn + 1] = (48 + (v % 10)) as u8
135 let rc2: i64 = _waw_append(resp_buf, out_off, resp_cap, dig, dn + 2)
136 if rc2 != NX_WAW_OK { return rc2 }
137 let rc3: i64 = _waw_append_z(resp_buf, out_off, resp_cap, "\r\n\r\n{\"" as *u8)
138 if rc3 != NX_WAW_OK { return rc3 }
139 let rc4: i64 = _waw_append(resp_buf, out_off, resp_cap, key, key_n)
140 if rc4 != NX_WAW_OK { return rc4 }
141 let rc5: i64 = _waw_append_z(resp_buf, out_off, resp_cap, "\":\"" as *u8)
142 if rc5 != NX_WAW_OK { return rc5 }
143 let rc6: i64 = _waw_append(resp_buf, out_off, resp_cap, val, val_n)
144 if rc6 != NX_WAW_OK { return rc6 }
145 return _waw_append_z(resp_buf, out_off, resp_cap, "\"}" as *u8)
146}
147
148// 200 with the freshly minted session token as JSON.
149func _waw_emit_login_ok(resp_buf: *u8, out_off: *i64, resp_cap: i64,
150 token_152: *u8) -> i64 {
151 let b64: *u8 = sys_mmap(256)
152 let b64_n: i64 = b64_encode(token_152, NX_MAUTH_SESSION_TOKEN_BYTES, b64)
153 if b64_n < 1 { return 0 - NX_WAW_BAD_INPUT }
154 return _waw_emit_json_kv(resp_buf, out_off, resp_cap, "token" as *u8, 5, b64, b64_n)
155}
156
157// 401 JSON for failed login POST (wrong passphrase / unknown handle --
158// SAME response for both per client-enumeration defense).
159func _waw_emit_login_401(resp_buf: *u8, out_off: *i64, resp_cap: i64) -> i64 {
160 let response: *u8 = "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nContent-Length: 26\r\n\r\n{\"error\":\"unauthorized\"}\r\n" as *u8
161 return _waw_append_z(resp_buf, out_off, resp_cap, response)
162}
163
164// ===== Login POST body parsing =================================================
165//
166// body: "handle=<h>&passphrase=<p>" (application/x-www-form-urlencoded;
167// '+' -> space, %XX -> byte). Body length from Content-Length header.
168
169func _waw_hex_nib(c: i64) -> i64 {
170 if c >= 48 { if c <= 57 { return c - 48 } }
171 if c >= 97 { if c <= 102 { return c - 87 } }
172 if c >= 65 { if c <= 70 { return c - 55 } }
173 return 0 - 1
174}
175
176// urldecode src[0..n) into out (cap out_cap); returns decoded length or negative.
177func _waw_urldecode(src: *u8, n: i64, out: *u8, out_cap: i64) -> i64 {
178 var i: i64 = 0
179 var o: i64 = 0
180 while i < n {
181 if o >= out_cap { return 0 - NX_WAW_BUF_OVERFLOW }
182 let c: i64 = src[i] as i64
183 if c == 43 { out[o] = 32 as u8; i = i + 1 } // '+'
184 if c == 37 { // '%'
185 if i + 3 > n { return 0 - NX_WAW_BAD_INPUT }
186 let hi: i64 = _waw_hex_nib(src[i + 1] as i64)
187 let lo: i64 = _waw_hex_nib(src[i + 2] as i64)
188 if hi < 0 { return 0 - NX_WAW_BAD_INPUT }
189 if lo < 0 { return 0 - NX_WAW_BAD_INPUT }
190 out[o] = ((hi << 4) | lo) as u8
191 i = i + 3
192 }
193 if c != 43 { if c != 37 { out[o] = c as u8; i = i + 1 } }
194 o = o + 1
195 }
196 return o
197}
198
199// find "name=" value slice inside form body; returns value length via out_n,
200// raw (still url-encoded) offset via out_off. 1 if found, 0 if not.
201func _waw_form_field(body: *u8, body_n: i64,
202 name: *u8, name_n: i64,
203 out_off: *i64, out_n: *i64) -> i64 {
204 var pos: i64 = 0
205 while pos < body_n {
206 // field starts at pos; check name match + '='
207 var m: i64 = 1
208 if pos + name_n + 1 > body_n { m = 0 }
209 if m == 1 {
210 var i: i64 = 0
211 while i < name_n {
212 if (body[pos + i] as i64) != (name[i] as i64) { m = 0; i = name_n }
213 if i < name_n { i = i + 1 }
214 }
215 }
216 if m == 1 { if (body[pos + name_n] as i64) != 61 { m = 0 } } // '='
217 // value ends at '&' or body end
218 var vend: i64 = pos
219 var scan: i64 = 1
220 while scan == 1 {
221 if vend >= body_n { scan = 0 }
222 if scan == 1 { if (body[vend] as i64) == 38 { scan = 0 } } // '&'
223 if scan == 1 { vend = vend + 1 }
224 }
225 if m == 1 {
226 out_off[0] = pos + name_n + 1
227 out_n[0] = vend - (pos + name_n + 1)
228 return 1
229 }
230 pos = vend + 1
231 }
232 return 0
233}
234
235// parse Content-Length header value -> integer (0 if absent/garbled).
236func _waw_content_length(http_req_buf: *u8, http_headers_end: i64) -> i64 {
237 let off_box: *i64 = sys_mmap(8) as *i64
238 let len_box: *i64 = sys_mmap(8) as *i64
239 off_box[0] = 0
240 len_box[0] = 0
241 let name: *u8 = "Content-Length" as *u8
242 if nx_http_header_find(http_req_buf, http_headers_end, name, 14, off_box, len_box) != NXHF_FOUND {
243 return 0
244 }
245 var v: i64 = 0
246 var i: i64 = 0
247 while i < len_box[0] {
248 let c: i64 = http_req_buf[off_box[0] + i] as i64
249 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } }
250 i = i + 1
251 }
252 if v < 0 { return 0 }
253 if v > 4096 { return 0 }
254 return v
255}
256
257// Handle POST /wiki/admin/login against an ARMED context.
258func _waw_handle_login_post(
259 ctx: *NxAuthContext,
260 http_req_buf: *u8, http_headers_end: i64,
261 resp_buf: *u8, out_off: *i64, resp_cap: i64,
262 out_resp_n: *i64
263) -> i64 {
264 let body_n: i64 = _waw_content_length(http_req_buf, http_headers_end)
265 let body: *u8 = (http_req_buf as i64 + http_headers_end) as *u8
266
267 var ok: i64 = 1
268 let h_off: *i64 = sys_mmap(8) as *i64
269 let h_n: *i64 = sys_mmap(8) as *i64
270 let p_off: *i64 = sys_mmap(8) as *i64
271 let p_n: *i64 = sys_mmap(8) as *i64
272 if body_n < 3 { ok = 0 }
273 if ok == 1 { if _waw_form_field(body, body_n, "handle" as *u8, 6, h_off, h_n) != 1 { ok = 0 } }
274 if ok == 1 { if _waw_form_field(body, body_n, "passphrase" as *u8, 10, p_off, p_n) != 1 { ok = 0 } }
275
276 let handle: *u8 = sys_mmap(NX_MAUTH_MAX_USER_HANDLE_LEN + 8)
277 var handle_n: i64 = 0
278 let pass: *u8 = sys_mmap(NX_MAUTH_MAX_PASSPHRASE_LEN + 8)
279 var pass_n: i64 = 0
280 if ok == 1 {
281 handle_n = _waw_urldecode((body as i64 + h_off[0]) as *u8, h_n[0],
282 handle, NX_MAUTH_MAX_USER_HANDLE_LEN)
283 if handle_n < 1 { ok = 0 }
284 }
285 if ok == 1 {
286 pass_n = _waw_urldecode((body as i64 + p_off[0]) as *u8, p_n[0],
287 pass, NX_MAUTH_MAX_PASSPHRASE_LEN)
288 if pass_n < 1 { ok = 0 }
289 }
290
291 var authed: i64 = 0
292 let token: *u8 = sys_mmap(NX_MAUTH_SESSION_TOKEN_BYTES)
293 if ok == 1 {
294 let tok_n: *i64 = sys_mmap(8) as *i64
295 let rc: i64 = nx_modern_auth_login(ctx, handle, handle_n, pass, pass_n,
296 token, NX_MAUTH_SESSION_TOKEN_BYTES, tok_n)
297 if rc == NX_MAUTH_OK { if tok_n[0] == NX_MAUTH_SESSION_TOKEN_BYTES { authed = 1 } }
298 }
299
300 if authed == 1 {
301 let rc_ok: i64 = _waw_emit_login_ok(resp_buf, out_off, resp_cap, token)
302 if rc_ok != NX_WAW_OK { return rc_ok }
303 out_resp_n[0] = out_off[0]
304 return NX_WAW_OK
305 }
306 let rc_401: i64 = _waw_emit_login_401(resp_buf, out_off, resp_cap)
307 if rc_401 != NX_WAW_OK { return rc_401 }
308 out_resp_n[0] = out_off[0]
309 return NX_WAW_OK
310}
311
312// Handle POST /wiki/admin/recover: body "handle=<h>&mnemonic=<24 words>&passphrase=<new>".
313// 200 {"mnemonic":"<NEW rotated 24 words>"} | 401 (same body for unknown handle /
314// wrong mnemonic: enumeration defense).
315func _waw_handle_recover_post(
316 ctx: *NxAuthContext,
317 http_req_buf: *u8, http_headers_end: i64,
318 resp_buf: *u8, out_off: *i64, resp_cap: i64,
319 out_resp_n: *i64
320) -> i64 {
321 let body_n: i64 = _waw_content_length(http_req_buf, http_headers_end)
322 let body: *u8 = (http_req_buf as i64 + http_headers_end) as *u8
323
324 var ok: i64 = 1
325 let h_off: *i64 = sys_mmap(8) as *i64
326 let h_n: *i64 = sys_mmap(8) as *i64
327 let m_off: *i64 = sys_mmap(8) as *i64
328 let m_n: *i64 = sys_mmap(8) as *i64
329 let p_off: *i64 = sys_mmap(8) as *i64
330 let p_n: *i64 = sys_mmap(8) as *i64
331 if body_n < 3 { ok = 0 }
332 if ok == 1 { if _waw_form_field(body, body_n, "handle" as *u8, 6, h_off, h_n) != 1 { ok = 0 } }
333 if ok == 1 { if _waw_form_field(body, body_n, "mnemonic" as *u8, 8, m_off, m_n) != 1 { ok = 0 } }
334 if ok == 1 { if _waw_form_field(body, body_n, "passphrase" as *u8, 10, p_off, p_n) != 1 { ok = 0 } }
335
336 let handle: *u8 = sys_mmap(NX_MAUTH_MAX_USER_HANDLE_LEN + 8)
337 var handle_n: i64 = 0
338 let mnemonic: *u8 = sys_mmap(NX_MAUTH_MAX_MNEMONIC_LEN + 8)
339 var mnemonic_n: i64 = 0
340 let pass: *u8 = sys_mmap(NX_MAUTH_MAX_PASSPHRASE_LEN + 8)
341 var pass_n: i64 = 0
342 if ok == 1 {
343 handle_n = _waw_urldecode((body as i64 + h_off[0]) as *u8, h_n[0],
344 handle, NX_MAUTH_MAX_USER_HANDLE_LEN)
345 if handle_n < 1 { ok = 0 }
346 }
347 if ok == 1 {
348 mnemonic_n = _waw_urldecode((body as i64 + m_off[0]) as *u8, m_n[0],
349 mnemonic, NX_MAUTH_MAX_MNEMONIC_LEN)
350 if mnemonic_n < 1 { ok = 0 }
351 }
352 if ok == 1 {
353 pass_n = _waw_urldecode((body as i64 + p_off[0]) as *u8, p_n[0],
354 pass, NX_MAUTH_MAX_PASSPHRASE_LEN)
355 if pass_n < 1 { ok = 0 }
356 }
357
358 var recovered: i64 = 0
359 let new_mn: *u8 = sys_mmap(NX_MAUTH_MAX_MNEMONIC_LEN + 8)
360 let new_mn_n: *i64 = sys_mmap(8) as *i64
361 new_mn_n[0] = 0
362 if ok == 1 {
363 let rc: i64 = nx_modern_auth_recover(ctx, handle, handle_n,
364 mnemonic, mnemonic_n,
365 pass, pass_n,
366 new_mn, NX_MAUTH_MAX_MNEMONIC_LEN, new_mn_n)
367 if rc == NX_MAUTH_OK { if new_mn_n[0] > 0 { recovered = 1 } }
368 }
369
370 if recovered == 1 {
371 let rc_ok: i64 = _waw_emit_json_kv(resp_buf, out_off, resp_cap,
372 "mnemonic" as *u8, 8, new_mn, new_mn_n[0])
373 if rc_ok != NX_WAW_OK { return rc_ok }
374 out_resp_n[0] = out_off[0]
375 return NX_WAW_OK
376 }
377 let rc_401: i64 = _waw_emit_login_401(resp_buf, out_off, resp_cap)
378 if rc_401 != NX_WAW_OK { return rc_401 }
379 out_resp_n[0] = out_off[0]
380 return NX_WAW_OK
381}
382
383// Handle POST /wiki/admin/refresh: valid X-Nishi-Session in -> fresh token out
384// (bumped expiry, no passphrase re-prompt). 200 {"token":...} | 401.
385func _waw_handle_refresh_post(
386 ctx: *NxAuthContext,
387 http_req_buf: *u8, http_headers_end: i64,
388 now_unix_s: i64,
389 resp_buf: *u8, out_off: *i64, resp_cap: i64,
390 out_resp_n: *i64
391) -> i64 {
392 let sess_off: *i64 = sys_mmap(8) as *i64
393 let sess_len: *i64 = sys_mmap(8) as *i64
394 sess_off[0] = 0
395 sess_len[0] = 0
396 var ok: i64 = 0
397 if nx_http_header_find(http_req_buf, http_headers_end,
398 _waw_hdr_session_name(), NX_WAW_HDR_SESSION_NAME_N,
399 sess_off, sess_len) == NXHF_FOUND { ok = 1 }
400 let new_tok: *u8 = sys_mmap(NX_MAUTH_SESSION_TOKEN_BYTES)
401 var refreshed: i64 = 0
402 if ok == 1 {
403 let token_ptr: *u8 = (http_req_buf as i64 + sess_off[0]) as *u8
404 var tok: *u8 = token_ptr
405 var tok_n: i64 = sess_len[0]
406 if tok_n != NX_MAUTH_SESSION_TOKEN_BYTES {
407 let dec: *u8 = sys_mmap(256)
408 let dec_n: i64 = b64_decode(token_ptr, sess_len[0], dec)
409 tok = dec
410 tok_n = dec_n
411 }
412 let new_n: *i64 = sys_mmap(8) as *i64
413 if nx_modern_auth_refresh_session(ctx, tok, tok_n, now_unix_s,
414 new_tok, NX_MAUTH_SESSION_TOKEN_BYTES,
415 new_n) == NX_MAUTH_OK { refreshed = 1 }
416 }
417 if refreshed == 1 {
418 let rc_ok: i64 = _waw_emit_login_ok(resp_buf, out_off, resp_cap, new_tok)
419 if rc_ok != NX_WAW_OK { return rc_ok }
420 out_resp_n[0] = out_off[0]
421 return NX_WAW_OK
422 }
423 let rc_401: i64 = _waw_emit_login_401(resp_buf, out_off, resp_cap)
424 if rc_401 != NX_WAW_OK { return rc_401 }
425 out_resp_n[0] = out_off[0]
426 return NX_WAW_OK
427}
428
429// path equality helper for the POST endpoint dispatch.
430func _waw_path_eq(url_path: *u8, url_path_n: i64, want: *u8, want_n: i64) -> i64 {
431 if url_path_n != want_n { return 0 }
432 var i: i64 = 0
433 while i < want_n {
434 if (url_path[i] as i64) != (want[i] as i64) { return 0 }
435 i = i + 1
436 }
437 return 1
438}
439
440// ===== Top-level wiring entry =================================================
441//
442// Inputs:
443// ctx V-MODAUTH-6 NxAuthContext (per-realm; caller-init at boot)
444// http_req_buf HTTP request bytes (parsed by nx_http_server)
445// http_headers_end byte offset where headers end
446// url_path request URL path
447// url_path_n length
448// method_kind 1 = GET, 2 = POST
449// now_unix_s epoch seconds (caller-supplied; substrate-pure)
450// resp_buf output: HTTP response bytes
451// resp_cap output capacity
452// out_resp_n receives bytes written
453//
454// Behavior:
455// POST /wiki/admin/login -> 503 LOGIN_NOT_READY (until V-MODAUTH-2/4)
456// GET /wiki/admin/* -> validate X-Nishi-Session; 200 if good, 401 if not
457// POST /wiki/admin/refresh -> 501 not-yet-wired (V+1 micro-arc)
458
459func nx_wiki_admin_handle(
460 ctx: *NxAuthContext,
461 http_req_buf: *u8, http_headers_end: i64,
462 url_path: *u8, url_path_n: i64,
463 method_kind: i64,
464 now_unix_s: i64,
465 resp_buf: *u8, resp_cap: i64,
466 out_resp_n: *i64
467) -> i64 {
468 if (ctx as i64) == 0 { return 0 - NX_WAW_BAD_INPUT }
469 if ctx.valid != 1 { return 0 - NX_WAW_BAD_INPUT }
470 if (resp_buf as i64) == 0 { return 0 - NX_WAW_BAD_INPUT }
471 if (out_resp_n as i64) == 0 { return 0 - NX_WAW_BAD_INPUT }
472 if resp_cap < NX_WAW_RESP_CAP { return 0 - NX_WAW_BAD_INPUT }
473 out_resp_n[0] = 0
474
475 let out_off: *i64 = (sys_mmap(8)) as *i64
476 out_off[0] = 0
477
478 // POST endpoint dispatch: login / recover need an ARMED OPAQUE context
479 // (otherwise the honest 503); refresh needs only the session keys.
480 if method_kind == NX_WAW_METHOD_POST {
481 var is_login: i64 = _waw_path_eq(url_path, url_path_n, "/wiki/admin/login" as *u8, 17)
482 var is_recover: i64 = _waw_path_eq(url_path, url_path_n, "/wiki/admin/recover" as *u8, 19)
483 var is_refresh: i64 = _waw_path_eq(url_path, url_path_n, "/wiki/admin/refresh" as *u8, 19)
484 if is_refresh == 1 {
485 return _waw_handle_refresh_post(ctx, http_req_buf, http_headers_end,
486 now_unix_s,
487 resp_buf, out_off, resp_cap, out_resp_n)
488 }
489 var needs_armed: i64 = 0
490 if is_login == 1 { needs_armed = 1 }
491 if is_recover == 1 { needs_armed = 1 }
492 if needs_armed == 1 {
493 var armed: i64 = 1
494 if ctx.account_store == 0 { armed = 0 }
495 if (ctx.opaque_skS_32 as i64) == 0 { armed = 0 }
496 if armed == 0 {
497 let rc_lnr: i64 = _waw_emit_login_not_ready(resp_buf, out_off, resp_cap)
498 if rc_lnr != NX_WAW_OK { return rc_lnr }
499 out_resp_n[0] = out_off[0]
500 return NX_WAW_OK
501 }
502 if is_login == 1 {
503 return _waw_handle_login_post(ctx, http_req_buf, http_headers_end,
504 resp_buf, out_off, resp_cap, out_resp_n)
505 }
506 return _waw_handle_recover_post(ctx, http_req_buf, http_headers_end,
507 resp_buf, out_off, resp_cap, out_resp_n)
508 }
509 }
510
511 // Extract X-Nishi-Session header (no cookies; never).
512 let sess_off: *i64 = (sys_mmap(8)) as *i64
513 let sess_len: *i64 = (sys_mmap(8)) as *i64
514 sess_off[0] = 0
515 sess_len[0] = 0
516 let rc_find: i64 = nx_http_header_find(http_req_buf, http_headers_end,
517 _waw_hdr_session_name(),
518 NX_WAW_HDR_SESSION_NAME_N,
519 sess_off, sess_len)
520
521 // No session header -> 401.
522 if rc_find != NXHF_FOUND {
523 let rc_401: i64 = _waw_emit_401(resp_buf, out_off, resp_cap)
524 if rc_401 != NX_WAW_OK { return rc_401 }
525 out_resp_n[0] = out_off[0]
526 return NX_WAW_OK
527 }
528
529 // Header carries base64(152B token) (what /wiki/admin/login returns).
530 // Raw 152-byte binary also accepted (CLI clients on the LAN).
531 let token_ptr: *u8 = (http_req_buf as i64 + sess_off[0]) as *u8
532 var tok: *u8 = token_ptr
533 var tok_n: i64 = sess_len[0]
534 if tok_n != NX_MAUTH_SESSION_TOKEN_BYTES {
535 let dec: *u8 = sys_mmap(256)
536 let dec_n: i64 = b64_decode(token_ptr, sess_len[0], dec)
537 tok = dec
538 tok_n = dec_n
539 }
540 let rc_val: i64 = nx_modern_auth_validate_session(
541 ctx,
542 tok, tok_n,
543 now_unix_s,
544 (0 as i64) as *u8, 0, (0 as i64) as *i64)
545
546 if rc_val != NX_MAUTH_OK {
547 let rc_401b: i64 = _waw_emit_401(resp_buf, out_off, resp_cap)
548 if rc_401b != NX_WAW_OK { return rc_401b }
549 out_resp_n[0] = out_off[0]
550 return NX_WAW_OK
551 }
552
553 // Authed.
554 let rc_200: i64 = _waw_emit_authed_200(resp_buf, out_off, resp_cap)
555 if rc_200 != NX_WAW_OK { return rc_200 }
556 out_resp_n[0] = out_off[0]
557 return NX_WAW_OK
558}