code wiki / (root) / nx_legal_portal.nx

nx_legal_portal.nx source

↩ module page · 356 lines · 16292 B

1// nx_legal_portal.nx -- LEGAL RUNG D6: the Client Workspace portal request handler. 2// 3// module: nishi-core.legal.portal 4// capability: LEGAL_PORTAL 5// 6// The composition spine that ties the gated doc-signing logic cores into ONE 7// bytes-in/bytes-out HTTP request handler (the proven nx_status_daemon sd_handle 8// pattern): a thin socket loop can wrap it later, but the logic + the gate need 9// no socket and touch nothing live. It routes a tenant's requests over: 10// D1 nx_doc_vault -- a doc is served only if THIS tenant's vault owns it. 11// D2 nx_doc_envelope -- envelope list + status (signed N of M). 12// D3 nx_doc_serve -- open-in-app-of-choice document bytes. 13// D4 nx_doc_annotate -- the "what's still needed" checklist readiness. 14// D0 nx_legal_compliance-- the regime is SURFACED to the client: a REQUIRES_WET 15// instrument is told "e-sign not legally valid" rather 16// than silently offered for signing (never-void in UX). 17// nx_authz -- /portal (client level 1) vs /staff (lawyer level 2), 18// deny-by-default; an unlisted resource is 404. 19// Tenant isolation: one NxPortalCtx = one tenant's private stores, so a request 20// can never reach another tenant's envelopes/docs. 21// 22// READ spine only -- the stateful POST mutations (sign/annotate) ARE the already- 23// gated logic cores (D2/D4); this organ is the routing + authz + serving layer 24// over them. Wiring it behind a live daemon (nx_andelinwest_daemon :8453) is a 25// separate, operator-gated deploy step. 26// 27// Composes: nx_doc_envelope, nx_doc_serve, nx_doc_annotate, nx_doc_vault, 28// nx_legal_compliance. The /portal-vs-/staff level check is the same 29// longest-prefix deny-by-default rule as nx_authz, inlined here over two disjoint 30// prefixes to keep the compile unit lean. license_tier: ORIGINAL 31// lineage_id: nishi_legal_portal_d6 32import "nx_syscalls.nx" 33import "nx_doc_envelope.nx" 34import "nx_doc_serve.nx" 35import "nx_doc_annotate.nx" 36import "nx_doc_vault.nx" 37import "nx_legal_compliance.nx" 38const K_MAGIC_8192: i64 = 8192 39const K_MAGIC_1024: i64 = 1024 40const K_MAGIC_16384: i64 = 16384 41 42// One tenant's portal context: its private workflow stores + the requesting 43// user's authz level. Caller (the daemon, or the gate) populates it. 44struct NxPortalCtx { 45 user_level: i64, // 1 = client, 2 = lawyer/staff (from nx_authz) 46 eflat: *i64, // envelope store (D2) 47 ne: i64, 48 rflat: *i64, // recipient store (D2) 49 nr: i64, 50 anflat: *i64, // annotation store (D4) 51 na: i64, 52 vflat: *i64, // document vault (D1) 53 vc: i64, 54 doc_id: i64, // the one document this tenant can serve (gate simplification) 55 doc_bytes: *u8, 56 doc_len: i64, 57 doc_fmt: i64, 58 env_ids: *i64, // the tenant's envelope ids (for listing) 59 env_count: i64 60} 61 62func lp_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 63func lp_cat(out: *u8, o: i64, s: *u8) -> i64 { var k: i64 = 0; while s[k] != (0 as u8) { out[o] = s[k]; o = o + 1; k = k + 1 } return o } 64func lp_num(out: *u8, o: i64, v: i64) -> i64 { 65 if v == 0 { out[o] = 48; return o + 1 } 66 var m: i64 = v 67 if m < 0 { out[o] = 45; o = o + 1; m = 0 - m } 68 let t: *u8 = sys_mmap(28); var k: i64 = 0 69 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 70 var i: i64 = 0; while i < k { out[o] = t[k - 1 - i]; o = o + 1; i = i + 1 } 71 return o 72} 73 74// exact match of p[0..pl] vs a C-string literal. 75func lp_eq(p: *u8, pl: i64, lit: *u8) -> i64 { 76 let ll: i64 = lp_slen(lit) 77 if pl != ll { return 0 } 78 var i: i64 = 0 79 while i < pl { if p[i] != lit[i] { return 0 } i = i + 1 } 80 return 1 81} 82// does p[0..pl] start with lit? 83func lp_prefix(p: *u8, pl: i64, lit: *u8) -> i64 { 84 let ll: i64 = lp_slen(lit) 85 if pl < ll { return 0 } 86 var i: i64 = 0 87 while i < ll { if p[i] != lit[i] { return 0 } i = i + 1 } 88 return 1 89} 90// extract the integer value of "<key>=NNN" from the request bytes, or -1. 91func lp_query_int(req: *u8, n: i64, key: *u8) -> i64 { 92 let kl: i64 = lp_slen(key) 93 var i: i64 = 0 94 while i + kl + 1 <= n { 95 var m: i64 = 1 96 var j: i64 = 0 97 while j < kl { if req[i + j] != key[j] { m = 0; break } j = j + 1 } 98 if m == 1 { 99 if req[i + kl] == 61 { 100 var v: i64 = 0 101 var k: i64 = i + kl + 1 102 var any: i64 = 0 103 var go: i64 = 1 104 while go == 1 { 105 if k >= n { go = 0 } else { 106 let c: i64 = req[k] as i64 107 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); any = 1; k = k + 1 } else { go = 0 } } else { go = 0 } 108 } 109 } 110 if any == 1 { return v } 111 } 112 } 113 i = i + 1 114 } 115 return 0 - 1 116} 117 118// build a text/plain response (status line + nosniff + Content-Length + body). 119func lp_text_response(out: *u8, status: *u8, body: *u8, body_len: i64) -> i64 { 120 var o: i64 = 0 121 o = lp_cat(out, o, "HTTP/1.1 ") 122 o = lp_cat(out, o, status) 123 o = lp_cat(out, o, "\r\nContent-Type: text/plain; charset=utf-8\r\nX-Content-Type-Options: nosniff\r\nContent-Length: ") 124 o = lp_num(out, o, body_len) 125 o = lp_cat(out, o, "\r\n\r\n") 126 var i: i64 = 0 127 while i < body_len { out[o] = body[i]; o = o + 1; i = i + 1 } 128 return o 129} 130 131// ---- the pure router: request bytes in, response bytes out. ---- 132func lp_handle(pctx: *NxPortalCtx, req: *u8, n: i64, out: *u8) -> i64 { 133 // ---- extract the request path (after the first space, up to space or '?') ---- 134 var ps: i64 = 0 135 while ps < n { if req[ps] == 32 { break } ps = ps + 1 } 136 ps = ps + 1 137 var pe: i64 = ps 138 var sc: i64 = 1 139 while sc == 1 { 140 if pe >= n { sc = 0 } else { 141 let c: i64 = req[pe] as i64 142 if c == 32 { sc = 0 } else { if c == 63 { sc = 0 } else { pe = pe + 1 } } 143 } 144 } 145 let path_len: i64 = pe - ps 146 let pathbuf: *u8 = sys_mmap(512) 147 var pi: i64 = 0 148 while pi < path_len { pathbuf[pi] = req[ps + pi]; pi = pi + 1 } 149 pathbuf[path_len] = 0 as u8 150 151 // ---- authz: /portal needs level 1, /staff needs level 2 (deny-by-default) ---- 152 var need: i64 = 0 153 if lp_prefix(pathbuf, path_len, "/portal" as *u8) == 1 { need = 1 } 154 if lp_prefix(pathbuf, path_len, "/staff" as *u8) == 1 { need = 2 } 155 if need == 0 { return lp_text_response(out, "404 Not Found" as *u8, "no such resource" as *u8, 16) } 156 if pctx.user_level < need { return lp_text_response(out, "403 Forbidden" as *u8, "insufficient access level" as *u8, 25) } 157 158 // ---- GET /portal/envelopes : list the tenant's envelopes + status ---- 159 if lp_eq(pathbuf, path_len, "/portal/envelopes" as *u8) == 1 { 160 let body: *u8 = sys_mmap(K_MAGIC_8192) 161 var bo: i64 = 0 162 bo = lp_cat(body, bo, "ENVELOPES\n" as *u8) 163 let eids: *i64 = pctx.env_ids 164 var ei: i64 = 0 165 while ei < pctx.env_count { 166 let eid: i64 = eids[ei] 167 bo = lp_cat(body, bo, "env " as *u8) 168 bo = lp_num(body, bo, eid) 169 bo = lp_cat(body, bo, " status " as *u8) 170 bo = lp_cat(body, bo, nx_env_status_name(nx_env_status(pctx.eflat, pctx.ne, eid))) 171 bo = lp_cat(body, bo, " signed " as *u8) 172 bo = lp_num(body, bo, nx_env_signed_count(pctx.rflat, pctx.nr, eid)) 173 bo = lp_cat(body, bo, " of " as *u8) 174 bo = lp_num(body, bo, nx_env_required_signers(pctx.rflat, pctx.nr, eid)) 175 bo = lp_cat(body, bo, "\n" as *u8) 176 ei = ei + 1 177 } 178 return lp_text_response(out, "200 OK" as *u8, body, bo) 179 } 180 181 // ---- GET /portal/status?env=E : one envelope, with never-void surfacing ---- 182 if lp_eq(pathbuf, path_len, "/portal/status" as *u8) == 1 { 183 let env: i64 = lp_query_int(req, n, "env" as *u8) 184 let st: i64 = nx_env_status(pctx.eflat, pctx.ne, env) 185 if st < 0 { return lp_text_response(out, "404 Not Found" as *u8, "no such envelope" as *u8, 16) } 186 let rg: i64 = nx_env_regime(pctx.eflat, pctx.ne, env) 187 let body: *u8 = sys_mmap(K_MAGIC_1024) 188 var bo: i64 = 0 189 bo = lp_cat(body, bo, "ENVELOPE " as *u8) 190 bo = lp_num(body, bo, env) 191 bo = lp_cat(body, bo, " status " as *u8) 192 bo = lp_cat(body, bo, nx_env_status_name(st)) 193 bo = lp_cat(body, bo, " regime " as *u8) 194 bo = lp_cat(body, bo, nx_legal_regime_name(rg)) 195 bo = lp_cat(body, bo, " signed " as *u8) 196 bo = lp_num(body, bo, nx_env_signed_count(pctx.rflat, pctx.nr, env)) 197 bo = lp_cat(body, bo, " of " as *u8) 198 bo = lp_num(body, bo, nx_env_required_signers(pctx.rflat, pctx.nr, env)) 199 if rg == RG_REQUIRES_WET { 200 bo = lp_cat(body, bo, " NOTE wet signature required (e-sign not legally valid for this instrument)" as *u8) 201 } 202 return lp_text_response(out, "200 OK" as *u8, body, bo) 203 } 204 205 // ---- GET /portal/checklist?doc=X : the "what's still needed" readiness (D4) ---- 206 if lp_eq(pathbuf, path_len, "/portal/checklist" as *u8) == 1 { 207 let doc: i64 = lp_query_int(req, n, "doc" as *u8) 208 let rem: i64 = nx_ann_checklist_remaining(pctx.anflat, pctx.na, doc, 1) 209 let body: *u8 = sys_mmap(256) 210 var bo: i64 = 0 211 bo = lp_cat(body, bo, "CHECKLIST doc " as *u8) 212 bo = lp_num(body, bo, doc) 213 bo = lp_cat(body, bo, " remaining " as *u8) 214 bo = lp_num(body, bo, rem) 215 if rem == 0 { bo = lp_cat(body, bo, " READY" as *u8) } else { bo = lp_cat(body, bo, " NOT_READY" as *u8) } 216 return lp_text_response(out, "200 OK" as *u8, body, bo) 217 } 218 219 // ---- GET /portal/doc?id=X : serve the document IF this tenant's vault owns it ---- 220 if lp_eq(pathbuf, path_len, "/portal/doc" as *u8) == 1 { 221 let id: i64 = lp_query_int(req, n, "id" as *u8) 222 if id != pctx.doc_id { return lp_text_response(out, "404 Not Found" as *u8, "no such document" as *u8, 16) } 223 if nx_vault_current_idx(pctx.vflat, pctx.vc, id) < 0 { return lp_text_response(out, "404 Not Found" as *u8, "no such document" as *u8, 16) } 224 return nx_doc_serve_response(out, pctx.doc_bytes, pctx.doc_len, pctx.doc_fmt, nx_doc_default_disposition(pctx.doc_fmt), "document" as *u8) 225 } 226 227 // ---- GET /staff/matters : lawyer-level view (authz already enforced level 2) ---- 228 if lp_eq(pathbuf, path_len, "/staff/matters" as *u8) == 1 { 229 let body: *u8 = sys_mmap(256) 230 var bo: i64 = 0 231 bo = lp_cat(body, bo, "STAFF MATTERS lawyer view envelopes " as *u8) 232 bo = lp_num(body, bo, pctx.env_count) 233 return lp_text_response(out, "200 OK" as *u8, body, bo) 234 } 235 236 // ---- known prefix, but no matching endpoint ---- 237 return lp_text_response(out, "404 Not Found" as *u8, "no such resource" as *u8, 16) 238} 239 240// ---- read an HTTP request (until the CRLFCRLF header terminator, or EOF). ---- 241func lp_read_request(cfd: i64, buf: *u8, cap: i64) -> i64 { 242 var n: i64 = 0 243 var done: i64 = 0 244 while done == 0 { 245 if n >= cap - 1 { done = 1 } else { 246 let r: i64 = sys_read(cfd, (buf as i64 + n) as *u8, cap - 1 - n) 247 if r <= 0 { done = 1 } else { 248 n = n + r 249 var i: i64 = 0 250 var found: i64 = 0 251 while i + 3 < n { 252 if buf[i] == 13 { if buf[i + 1] == 10 { if buf[i + 2] == 13 { if buf[i + 3] == 10 { found = 1; break } } } } 253 i = i + 1 254 } 255 if found == 1 { done = 1 } 256 } 257 } 258 } 259 return n 260} 261 262// ---- serve ONE connection: read request -> lp_handle -> write the response. ---- 263// The bridge from a real socket to the pure router; the daemon + the live gate 264// share it so they prove the same code path. Caller owns + closes cfd. 265func lp_serve_conn(pctx: *NxPortalCtx, cfd: i64) -> i64 { 266 let req: *u8 = sys_mmap(K_MAGIC_8192) 267 let out: *u8 = sys_mmap(K_MAGIC_16384) 268 let rn: i64 = lp_read_request(cfd, req, K_MAGIC_8192) 269 if rn <= 0 { return 0 - 1 } 270 let on: i64 = lp_handle(pctx, req, rn, out) 271 var w: i64 = 0 272 while w < on { 273 let k: i64 = sys_write(cfd, (out as i64 + w) as *u8, on - w) 274 if k <= 0 { return 0 - 1 } 275 w = w + k 276 } 277 return on 278} 279 280// ---- extract the request path into pathbuf (null-terminated); returns its length. ---- 281func lp_pathlen(req: *u8, n: i64, pathbuf: *u8) -> i64 { 282 var ps: i64 = 0 283 while ps < n { if req[ps] == 32 { break } ps = ps + 1 } 284 ps = ps + 1 285 var pe: i64 = ps 286 var sc: i64 = 1 287 while sc == 1 { 288 if pe >= n { sc = 0 } else { 289 let c: i64 = req[pe] as i64 290 if c == 32 { sc = 0 } else { if c == 63 { sc = 0 } else { pe = pe + 1 } } 291 } 292 } 293 let path_len: i64 = pe - ps 294 var pi: i64 = 0 295 while pi < path_len { pathbuf[pi] = req[ps + pi]; pi = pi + 1 } 296 pathbuf[path_len] = 0 as u8 297 return path_len 298} 299 300// ---- POST router: the MUTATING actions (annotate / check) = the interactive markup. ---- 301// Composes D4 (nx_ann_add / nx_ann_resolve); pctx.na is advanced in place, ncap bounds 302// the add. The CALLER persists (lp_boot_save) after a successful mutation. Params are 303// read from the request (query string or body) via lp_query_int. 304func lp_handle_post(pctx: *NxPortalCtx, ncap: i64, req: *u8, n: i64, out: *u8) -> i64 { 305 let pathbuf: *u8 = sys_mmap(512) 306 let path_len: i64 = lp_pathlen(req, n, pathbuf) 307 var need: i64 = 0 308 if lp_prefix(pathbuf, path_len, "/portal" as *u8) == 1 { need = 1 } 309 if lp_prefix(pathbuf, path_len, "/staff" as *u8) == 1 { need = 2 } 310 if need == 0 { return lp_text_response(out, "404 Not Found" as *u8, "no such resource" as *u8, 16) } 311 if pctx.user_level < need { return lp_text_response(out, "403 Forbidden" as *u8, "insufficient access level" as *u8, 25) } 312 313 // POST /portal/annotate doc=&ann=&author=&typ=&req= -> add a note/comment/checklist item 314 if lp_eq(pathbuf, path_len, "/portal/annotate" as *u8) == 1 { 315 let doc: i64 = lp_query_int(req, n, "doc" as *u8) 316 let ann: i64 = lp_query_int(req, n, "ann" as *u8) 317 let author: i64 = lp_query_int(req, n, "author" as *u8) 318 let atype: i64 = lp_query_int(req, n, "typ" as *u8) 319 let reqd: i64 = lp_query_int(req, n, "req" as *u8) 320 if doc < 0 { return lp_text_response(out, "400 Bad Request" as *u8, "missing doc" as *u8, 11) } 321 if ann < 0 { return lp_text_response(out, "400 Bad Request" as *u8, "missing ann" as *u8, 11) } 322 var fl: i64 = 0 323 if reqd == 1 { fl = ANF_REQUIRED } 324 var ty: i64 = atype 325 if ty < 0 { ty = AN_NOTE } 326 var au: i64 = author 327 if au < 0 { au = 0 } 328 let newna: i64 = nx_ann_add(pctx.anflat, pctx.na, ncap, 0, ann, doc, 1, ty, au, 0, fl, 0) 329 if newna < 0 { return lp_text_response(out, "507 Insufficient Storage" as *u8, "store full" as *u8, 10) } 330 pctx.na = newna 331 let body: *u8 = sys_mmap(256) 332 var bo: i64 = 0 333 bo = lp_cat(body, bo, "ANNOTATED doc " as *u8) 334 bo = lp_num(body, bo, doc) 335 bo = lp_cat(body, bo, " ann " as *u8) 336 bo = lp_num(body, bo, ann) 337 bo = lp_cat(body, bo, " total " as *u8) 338 bo = lp_num(body, bo, pctx.na) 339 return lp_text_response(out, "200 OK" as *u8, body, bo) 340 } 341 342 // POST /portal/check ann= -> mark a checklist item done 343 if lp_eq(pathbuf, path_len, "/portal/check" as *u8) == 1 { 344 let ann: i64 = lp_query_int(req, n, "ann" as *u8) 345 if ann < 0 { return lp_text_response(out, "400 Bad Request" as *u8, "missing ann" as *u8, 11) } 346 let r: i64 = nx_ann_resolve(pctx.anflat, pctx.na, 0, ann, AS_DONE, 0, 0) 347 if r != ANR_OK { return lp_text_response(out, "404 Not Found" as *u8, "no such item" as *u8, 12) } 348 let body: *u8 = sys_mmap(256) 349 var bo: i64 = 0 350 bo = lp_cat(body, bo, "CHECKED ann " as *u8) 351 bo = lp_num(body, bo, ann) 352 return lp_text_response(out, "200 OK" as *u8, body, bo) 353 } 354 355 return lp_text_response(out, "404 Not Found" as *u8, "no such resource" as *u8, 16) 356}