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}