code wiki / (root) / nx_relate_serve.nx

nx_relate_serve.nx source

↩ module page · 1336 lines · 73958 B

1// nx_relate_serve.nx -- the INTERACTIVE handler for the Relationship OS (turns /relate from a static view into a 2// live app). GET renders the PERSISTED graph (from seg_store); POST /add-entity + /add-edge parse a form, SAVE to the 3// store, and the next GET shows the new record. Sovereign "pure core + deferred socket I/O" discipline (email-stack 4// pattern): this organ gates the interactive CORE (parse request -> read/write the persisted store -> render) against 5// a real store, NO sockets -- the accept-loop + public deploy (route /relate -> this daemon) is the ops shell after. 6// Self-contained store (canon_cid + seg_store), same proven pattern as nx_relate_store. 7// PURE CORE (no main): rv_handle (request->response), rv_read_req/rv_write_all (fd shells), store save/count, 8// rv_selftest (the offline gate body). Consumers: _hdl_build/nx_relate_serve_gate (offline), nx_relate_live_gate 9// (real loopback socket), nx_relate_daemon (deployable accept-loop). 10// license_tier: ORIGINAL 11import "nx_syscalls.nx" 12import "nx_canon_cid.nx" 13import "nx_seg_store.nx" 14import "nx_rebac.nx" // the shared ReBAC plane: friends/groups on /relate are rb_ tuples in THIS same store 15 16// R2 AUTH surfaces (pure -- the daemon shell does the olg_whoami crypto and passes the verdict in): 17// token-less browser nav gets the sessionStorage bootstrap (the PROVEN /project pattern: nsess -> re-fetch with 18// X-Nishi-Session -> render in place; no token -> /login?return=/relate). NO data ever renders unauthed. 19const RV_BOOTSTRAP: *u8 = "<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'><title>Nishi Relationship OS</title></head><body><script>(function(){var t=sessionStorage.getItem('nsess');if(!t){location='/login?return=/relate';return}fetch(location.pathname,{headers:{'X-Nishi-Session':t}}).then(function(r){return r.ok?r.text():null}).then(function(x){if(x){document.open();document.write(x);document.close()}else{location='/login?return=/relate'}})})();</script></body></html>" 20 21func p(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 22func pn(v: i64) -> i64 { 23 var m: i64 = v; if m < 0 { p("-" as *u8); m = 0 - m } 24 let t: *u8 = sys_mmap(24); var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 } 25 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 26 let o: *u8 = sys_mmap(24); var i: i64 = 0; while i < k { o[i] = t[k-1-i]; i = i + 1 } sys_write(1, o, k); return 0 27} 28func rv_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 29func rv_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[off+i] = s[i]; i = i + 1 } return off + i } 30func rv_catn(dst: *u8, off: i64, v: i64) -> i64 { 31 var o: i64 = off; var m: i64 = v 32 let t: *u8 = sys_mmap(28); var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 } 33 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 34 var i: i64 = 0; while i < k { dst[o + i] = t[k - 1 - i]; i = i + 1 } 35 return o + k 36} 37func rv_kmemeq(b: *u8, off: i64, n: i64, s: *u8) -> i64 { 38 if rv_slen(s) != n { return 0 } 39 var i: i64 = 0; while i < n { if b[off + i] != s[i] { return 0 } i = i + 1 } 40 return 1 41} 42func rv_iskind(b: *u8, koff: i64, kl: i64, pfx: *u8) -> i64 { if kl <= 4 { return 0 } return rv_kmemeq(b, koff, 4, pfx) } 43// HTML-escape cat for USER-STORED data. Without this, a stored <script> would run in every viewer's session -- 44// and once auth ships (sess synchronizer token embedded in the page) stored XSS = token theft. Escape by default. 45func rv_cat_esc(dst: *u8, off: i64, s: *u8) -> i64 { 46 var o: i64 = off 47 var i: i64 = 0 48 while s[i] != (0 as u8) { 49 let c: i64 = s[i] as i64 50 var lit: i64 = 1 51 if c == 38 { o = rv_cat(dst, o, "&amp;" as *u8); lit = 0 } 52 if c == 60 { o = rv_cat(dst, o, "&lt;" as *u8); lit = 0 } 53 if c == 62 { o = rv_cat(dst, o, "&gt;" as *u8); lit = 0 } 54 if c == 34 { o = rv_cat(dst, o, "&quot;" as *u8); lit = 0 } 55 if c == 39 { o = rv_cat(dst, o, "&apos;" as *u8); lit = 0 } 56 if lit == 1 { dst[o] = s[i]; o = o + 1 } 57 i = i + 1 58 } 59 return o 60} 61// emit the hidden `sess` synchronizer field (b64 alphabet = attr-safe) when a token is present; no-op when empty. 62func rv_sess_field(dst: *u8, off: i64, tok: *u8) -> i64 { 63 var o: i64 = off 64 if tok[0] != (0 as u8) { 65 o = rv_cat(dst, o, "<input type='hidden' name='sess' value='" as *u8) 66 o = rv_cat(dst, o, tok) 67 o = rv_cat(dst, o, "'>" as *u8) 68 } 69 return o 70} 71func rv_field(rec: *u8, rl: i64, want: *u8, out: *u8, cap: i64) -> i64 { 72 if rl < 8 { return 0 } 73 let nf: i64 = ss_r32(rec, 4) 74 var off: i64 = 8; var fi: i64 = 0 75 while fi < nf { 76 if off + 8 > rl { return 0 } 77 let kl: i64 = ss_r32(rec, off); let koff: i64 = off + 4 78 let vl: i64 = ss_r32(rec, koff + kl); let voff: i64 = koff + kl + 4 79 if rv_kmemeq(rec, koff, kl, want) == 1 { 80 var t: i64 = 0 81 while t < vl { if t < cap - 1 { out[t] = rec[voff + t] } t = t + 1 } 82 if t > cap - 1 { t = cap - 1 } 83 out[t] = 0 as u8; return 1 84 } 85 off = voff + vl; fi = fi + 1 86 } 87 return 0 88} 89// substring search 90func rv_has(buf: *u8, n: i64, needle: *u8) -> i64 { 91 var i: i64 = 0 92 while i < n { var k: i64 = 0; var hit: i64 = 1 93 while needle[k] != (0 as u8) { if i + k >= n { hit = 0; k = 0 - 1; break } if buf[i+k] != needle[k] { hit = 0; break } k = k + 1 } 94 if hit == 1 { if k > 0 { return 1 } } i = i + 1 } 95 return 0 96} 97 98// ---- store write (same proven pattern as nx_relate_store) ---- 99func rv_save_entity(w: *i64, etype: *u8, name: *u8, tenant: *u8) -> i64 { 100 let keys: *i64 = sys_mmap(8 * 8) as *i64; let vals: *i64 = sys_mmap(8 * 8) as *i64 101 keys[0]="kind" as *u8 as i64; keys[1]="type" as *u8 as i64; keys[2]="name" as *u8 as i64; keys[3]="tenant" as *u8 as i64 102 vals[0]="ent" as *u8 as i64; vals[1]=etype as i64; vals[2]=name as i64; vals[3]=tenant as i64 103 let enc: *u8 = sys_mmap(2048); let el: i64 = canon_encode(keys, vals, 4, enc) 104 let cid: *u8 = sys_mmap(96); cid_of(enc, el, cid) 105 let key: *u8 = sys_mmap(128); var ko: i64 = 0; ko = rv_cat(key, ko, "ent:" as *u8); ko = rv_cat(key, ko, cid); key[ko] = 0 as u8 106 return ss_add(w, 1, key, enc, el) 107} 108func rv_save_edge(w: *i64, from: *u8, rel: *u8, to: *u8, ctx: *u8, role: *u8) -> i64 { 109 let keys: *i64 = sys_mmap(8 * 8) as *i64; let vals: *i64 = sys_mmap(8 * 8) as *i64 110 keys[0]="kind" as *u8 as i64; keys[1]="from" as *u8 as i64; keys[2]="rel" as *u8 as i64; keys[3]="to" as *u8 as i64; keys[4]="ctx" as *u8 as i64; keys[5]="role" as *u8 as i64 111 vals[0]="edg" as *u8 as i64; vals[1]=from as i64; vals[2]=rel as i64; vals[3]=to as i64; vals[4]=ctx as i64; vals[5]=role as i64 112 let enc: *u8 = sys_mmap(2048); let el: i64 = canon_encode(keys, vals, 6, enc) 113 let cid: *u8 = sys_mmap(96); cid_of(enc, el, cid) 114 let key: *u8 = sys_mmap(128); var ko: i64 = 0; ko = rv_cat(key, ko, "edg:" as *u8); ko = rv_cat(key, ko, cid); key[ko] = 0 as u8 115 return ss_add(w, 1, key, enc, el) 116} 117// persist a single entity (own commit) 118func rv_persist_entity(prefix: *u8, etype: *u8, name: *u8, tenant: *u8) -> i64 { 119 let w: *i64 = ss_begin() 120 rv_save_entity(w, etype, name, tenant) 121 return ss_commit(prefix, w, sys_now_us()) 122} 123func rv_atoi(s: *u8) -> i64 { 124 var v: i64 = 0 125 var i: i64 = 0 126 while s[i] != (0 as u8) { 127 let d: i64 = (s[i] as i64) - 48 128 if d >= 0 { if d <= 9 { v = v * 10 + d } } 129 i = i + 1 130 } 131 return v 132} 133func rv_seq(a: *u8, b: *u8) -> i64 { 134 var i: i64 = 0 135 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 136 if b[i] != (0 as u8) { return 0 } 137 return 1 138} 139// stage vocab MIRROR of nx_crm stage_prob/stage_open (import would collide on shared helpers); 140// the selftest cross-checks the recomputed forecast against the nx_crm_store oracle (122500) = drift guard. 141func rv_stage_prob(s: *u8) -> i64 { 142 if rv_seq(s, "Lead" as *u8) == 1 { return 10 } 143 if rv_seq(s, "Qualified" as *u8) == 1 { return 25 } 144 if rv_seq(s, "Proposal" as *u8) == 1 { return 50 } 145 if rv_seq(s, "Negotiation" as *u8) == 1 { return 75 } 146 if rv_seq(s, "ClosedWon" as *u8) == 1 { return 100 } 147 if rv_seq(s, "ClosedLost" as *u8) == 1 { return 0 } 148 return 0 - 1 149} 150func rv_stage_open(s: *u8) -> i64 { 151 if rv_seq(s, "ClosedWon" as *u8) == 1 { return 0 } 152 if rv_seq(s, "ClosedLost" as *u8) == 1 { return 0 } 153 return 1 154} 155// deal + gift writers (record-format CONTRACT: keep field sets in sync with nx_crm_store cs_save_deal/cs_save_gift; 156// `tenant` is an ADDITIVE field for the ACL wall -- readers look fields up by name, absent = founding-tenant default) 157func rv_save_deal(w: *i64, name: *u8, account: *u8, amount: i64, stage: *u8, tenant: *u8) -> i64 { 158 if rv_stage_prob(stage) < 0 { return 0 - 1 } 159 if amount < 0 { return 0 - 1 } 160 let ab: *u8 = sys_mmap(32) 161 var ao: i64 = 0 162 ao = rv_catn(ab, ao, amount) 163 ab[ao] = 0 as u8 164 let keys: *i64 = sys_mmap(8 * 8) as *i64 165 let vals: *i64 = sys_mmap(8 * 8) as *i64 166 keys[0]="kind" as *u8 as i64; keys[1]="name" as *u8 as i64; keys[2]="account" as *u8 as i64; keys[3]="amount" as *u8 as i64; keys[4]="stage" as *u8 as i64; keys[5]="tenant" as *u8 as i64 167 vals[0]="dea" as *u8 as i64; vals[1]=name as i64; vals[2]=account as i64; vals[3]=ab as i64; vals[4]=stage as i64; vals[5]=tenant as i64 168 let enc: *u8 = sys_mmap(2048) 169 let el: i64 = canon_encode(keys, vals, 6, enc) 170 let cid: *u8 = sys_mmap(96) 171 cid_of(enc, el, cid) 172 let key: *u8 = sys_mmap(128) 173 var ko: i64 = 0 174 ko = rv_cat(key, ko, "dea:" as *u8) 175 ko = rv_cat(key, ko, cid) 176 key[ko] = 0 as u8 177 return ss_add(w, 1, key, enc, el) 178} 179func rv_save_gift(w: *i64, donor: *u8, fund: *u8, amount: i64, gtype: *u8) -> i64 { 180 if amount < 0 { return 0 - 1 } 181 if gtype[0] == (0 as u8) { return 0 - 1 } 182 let ab: *u8 = sys_mmap(32) 183 var ao: i64 = 0 184 ao = rv_catn(ab, ao, amount) 185 ab[ao] = 0 as u8 186 let keys: *i64 = sys_mmap(8 * 8) as *i64 187 let vals: *i64 = sys_mmap(8 * 8) as *i64 188 keys[0]="kind" as *u8 as i64; keys[1]="donor" as *u8 as i64; keys[2]="fund" as *u8 as i64; keys[3]="amount" as *u8 as i64; keys[4]="gtype" as *u8 as i64 189 vals[0]="giv" as *u8 as i64; vals[1]=donor as i64; vals[2]=fund as i64; vals[3]=ab as i64; vals[4]=gtype as i64 190 let enc: *u8 = sys_mmap(2048) 191 let el: i64 = canon_encode(keys, vals, 5, enc) 192 let cid: *u8 = sys_mmap(96) 193 cid_of(enc, el, cid) 194 let key: *u8 = sys_mmap(128) 195 var ko: i64 = 0 196 ko = rv_cat(key, ko, "giv:" as *u8) 197 ko = rv_cat(key, ko, cid) 198 key[ko] = 0 as u8 199 return ss_add(w, 1, key, enc, el) 200} 201func rv_persist_deal(prefix: *u8, name: *u8, account: *u8, amount: i64, stage: *u8, tenant: *u8) -> i64 { 202 let w: *i64 = ss_begin() 203 if rv_save_deal(w, name, account, amount, stage, tenant) != 0 { return 0 - 1 } 204 return ss_commit(prefix, w, sys_now_us()) 205} 206// share vocab MIRROR of nx_share sh_itype_ok (helper-collision blocks the import; record shape = the contract) 207func rv_itype_ok(t: *u8) -> i64 { 208 if rv_seq(t, "job" as *u8) == 1 { return 1 } 209 if rv_seq(t, "resource" as *u8) == 1 { return 1 } 210 if rv_seq(t, "intro" as *u8) == 1 { return 1 } 211 return 0 212} 213// share writer (record-format CONTRACT: keep field set in sync with nx_share sh_save; tenant ADDITIVE for the wall) 214// a: [0]=itype [1]=title [2]=from [3]=to [4]=note [5]=reqs-or-empty [6]=tenant 215func rv_save_share(w: *i64, a: *i64) -> i64 { 216 if rv_itype_ok(a[0] as *u8) == 0 { return 0 - 1 } 217 let keys: *i64 = sys_mmap(8 * 8) as *i64 218 let vals: *i64 = sys_mmap(8 * 8) as *i64 219 keys[0]="kind" as *u8 as i64; keys[1]="itype" as *u8 as i64; keys[2]="title" as *u8 as i64 220 keys[3]="from" as *u8 as i64; keys[4]="to" as *u8 as i64; keys[5]="note" as *u8 as i64; keys[6]="reqs" as *u8 as i64; keys[7]="tenant" as *u8 as i64 221 vals[0]="shr" as *u8 as i64; vals[1]=a[0]; vals[2]=a[1]; vals[3]=a[2]; vals[4]=a[3]; vals[5]=a[4]; vals[6]=a[5]; vals[7]=a[6] 222 let enc: *u8 = sys_mmap(4096) 223 let el: i64 = canon_encode(keys, vals, 8, enc) 224 let cid: *u8 = sys_mmap(96) 225 cid_of(enc, el, cid) 226 let key: *u8 = sys_mmap(128) 227 var ko: i64 = 0 228 ko = rv_cat(key, ko, "shr:" as *u8) 229 ko = rv_cat(key, ko, cid) 230 key[ko] = 0 as u8 231 return ss_add(w, 1, key, enc, el) 232} 233// ---- ACL memberships (the nx_crm_acl tenant-wall model, PERSISTED): kind "acl" = handle/tenant/role/grantor. 234// Roles: 2 member / 3 manager / 4 admin. DENY-BY-DEFAULT: no record = no access; the WALL BEATS ADMIN (a role in 235// tenant A grants NOTHING in tenant B). Grants are written by a tenant ADMIN (role 4); the very FIRST grant on an 236// EMPTY acl plane may be SELF-issued by an authed+registered user (single-household bootstrap; grantor recorded). 237func rv_save_acl(w: *i64, handle: *u8, tenant: *u8, role: i64, grantor: *u8) -> i64 { 238 if role < 2 { return 0 - 1 } 239 if role > 4 { return 0 - 1 } 240 if handle[0] == (0 as u8) { return 0 - 1 } 241 if tenant[0] == (0 as u8) { return 0 - 1 } 242 let rb: *u8 = sys_mmap(16) 243 let ro: i64 = rv_catn(rb, 0, role) 244 rb[ro] = 0 as u8 245 let keys: *i64 = sys_mmap(8 * 8) as *i64 246 let vals: *i64 = sys_mmap(8 * 8) as *i64 247 keys[0]="kind" as *u8 as i64; keys[1]="handle" as *u8 as i64; keys[2]="tenant" as *u8 as i64; keys[3]="role" as *u8 as i64; keys[4]="grantor" as *u8 as i64 248 vals[0]="acl" as *u8 as i64; vals[1]=handle as i64; vals[2]=tenant as i64; vals[3]=rb as i64; vals[4]=grantor as i64 249 let enc: *u8 = sys_mmap(2048) 250 let el: i64 = canon_encode(keys, vals, 5, enc) 251 let cid: *u8 = sys_mmap(96) 252 cid_of(enc, el, cid) 253 let key: *u8 = sys_mmap(128) 254 var ko: i64 = 0 255 ko = rv_cat(key, ko, "acl:" as *u8) 256 ko = rv_cat(key, ko, cid) 257 key[ko] = 0 as u8 258 return ss_add(w, 1, key, enc, el) 259} 260// does ANY acl record exist? (the bootstrap window: 0 = first-grant self-issue allowed) 261func rv_acl_any(prefix: *u8) -> i64 { 262 let segp: *i64 = sys_mmap(16) as *i64 263 let ns: i64 = ss_manifest_dyn(prefix, segp) // uncapped: sees the WHOLE store 264 let segs: *i64 = segp[0] as *i64 265 var s: i64 = 0 266 while s < ns { 267 let path: *u8 = sys_mmap(512) 268 var po: i64 = 0 269 po = rv_cat(path, po, prefix); po = rv_cat(path, po, segs[s] as *u8); po = rv_cat(path, po, ".docs" as *u8); path[po] = 0 as u8 270 let szp: *i64 = sys_mmap(16) as *i64 271 let b: *u8 = ss_readall(path, szp) 272 let sz: i64 = szp[0] 273 var j: i64 = 0 274 while j + 9 <= sz { 275 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4 276 if b[j] == 1 { if rv_iskind(b, koff, kl, "acl:" as *u8) == 1 { return 1 } } 277 j = voff + vl 278 } 279 s = s + 1 280 } 281 return 0 282} 283// load the caller's memberships into (tenants[i], roles[i]); returns n (max 16). LAST record per tenant WINS 284// (append-only store -> role changes = re-grants). Empty handle -> 0 = deny-all (authed but unmapped/ungranted). 285func rv_acl_load(prefix: *u8, handle: *u8, tenants: *i64, roles: *i64) -> i64 { 286 var n: i64 = 0 287 if handle[0] == (0 as u8) { return 0 } 288 let segp: *i64 = sys_mmap(16) as *i64 289 let ns: i64 = ss_manifest_dyn(prefix, segp) // uncapped: sees the WHOLE store 290 let segs: *i64 = segp[0] as *i64 291 let fh: *u8 = sys_mmap(256) 292 let ftn: *u8 = sys_mmap(256) 293 let fr: *u8 = sys_mmap(64) 294 var s: i64 = 0 295 while s < ns { 296 let path: *u8 = sys_mmap(512) 297 var po: i64 = 0 298 po = rv_cat(path, po, prefix); po = rv_cat(path, po, segs[s] as *u8); po = rv_cat(path, po, ".docs" as *u8); path[po] = 0 as u8 299 let szp: *i64 = sys_mmap(16) as *i64 300 let b: *u8 = ss_readall(path, szp) 301 let sz: i64 = szp[0] 302 var j: i64 = 0 303 while j + 9 <= sz { 304 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4 305 if b[j] == 1 { 306 if rv_iskind(b, koff, kl, "acl:" as *u8) == 1 { 307 let rec: *u8 = (b as i64 + voff) as *u8 308 fh[0] = 0 as u8; ftn[0] = 0 as u8; fr[0] = 0 as u8 309 rv_field(rec, vl, "handle" as *u8, fh, 256) 310 rv_field(rec, vl, "tenant" as *u8, ftn, 256) 311 rv_field(rec, vl, "role" as *u8, fr, 64) 312 if rv_seq(fh, handle) == 1 { 313 let rl: i64 = rv_atoi(fr) 314 var hit: i64 = 0 - 1 315 var q: i64 = 0 316 while q < n { if rv_seq(tenants[q] as *u8, ftn) == 1 { hit = q; q = n } q = q + 1 } 317 if hit >= 0 { roles[hit] = rl } else { 318 if n < 16 { 319 let tc: *u8 = sys_mmap(256) 320 var tq: i64 = 0 321 tq = rv_cat(tc, tq, ftn) 322 tc[tq] = 0 as u8 323 tenants[n] = tc as i64 324 roles[n] = rl 325 n = n + 1 326 } 327 } 328 } 329 } 330 } 331 j = voff + vl 332 } 333 s = s + 1 334 } 335 return n 336} 337// records created before the tenant wall carry no tenant field -> they belong to the FOUNDING tenant. 338func rv_ten_default(t: *u8) -> i64 { 339 if t[0] == (0 as u8) { let o: i64 = rv_cat(t, 0, "andelinwest" as *u8); t[o] = 0 as u8 } 340 return 0 341} 342// caller's role in `ten` from the grants bundle gr (null = legacy OPEN mode -> full role). 0 = none. 343// gr layout: [0]=handle *u8, [1]=n, [2]=tenants *i64, [3]=roles *i64 344func rv_role_of(gr: *i64, ten: *u8) -> i64 { 345 if (gr as i64) == 0 { return 4 } 346 let n: i64 = gr[1] 347 let ts: *i64 = gr[2] as *i64 348 let rs: *i64 = gr[3] as *i64 349 var i: i64 = 0 350 while i < n { if rv_seq(ts[i] as *u8, ten) == 1 { return rs[i] } i = i + 1 } 351 return 0 352} 353func rv_vis(gr: *i64, ten: *u8) -> i64 { if rv_role_of(gr, ten) >= 2 { return 1 } return 0 } 354func rv_amt_ok(gr: *i64, ten: *u8) -> i64 { if rv_role_of(gr, ten) >= 3 { return 1 } return 0 } 355// write-gate for a POST: parse tenant from the body into tb (defaulted), require role>=2. 1 allow / 0 deny. 356func rv_wcheck(gr: *i64, body: *u8, blen: i64, tb: *u8) -> i64 { 357 if rv_form_get(body, blen, "tenant" as *u8, tb, 128) == 0 { tb[0] = 0 as u8 } 358 rv_ten_default(tb) 359 if (gr as i64) == 0 { return 1 } 360 if rv_role_of(gr, tb) >= 2 { return 1 } 361 return 0 362} 363func rv_emit_403(out: *u8) -> i64 { 364 let body: *u8 = "{\"type\":\"about:blank\",\"title\":\"Forbidden\",\"status\":403,\"detail\":\"your session is valid but you hold no role on that tenant (deny-by-default; the tenant wall beats admin)\"}" as *u8 365 var o: i64 = 0 366 o = rv_cat(out, o, "HTTP/1.1 403 Forbidden\r\nContent-Type: application/problem+json\r\nConnection: close\r\nContent-Length: " as *u8) 367 o = rv_catn(out, o, rv_slen(body)) 368 o = rv_cat(out, o, "\r\n\r\n" as *u8) 369 o = rv_cat(out, o, body) 370 return o 371} 372 373// count entity records 374func rv_count_ent(prefix: *u8) -> i64 { 375 let segp: *i64 = sys_mmap(16) as *i64; let ns: i64 = ss_manifest_dyn(prefix, segp); let segs: *i64 = segp[0] as *i64 // uncapped 376 var cnt: i64 = 0; var s: i64 = 0 377 while s < ns { 378 let path: *u8 = sys_mmap(512); var po: i64 = 0 379 po = rv_cat(path, po, prefix); po = rv_cat(path, po, segs[s] as *u8); po = rv_cat(path, po, ".docs" as *u8); path[po] = 0 as u8 380 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0] 381 var j: i64 = 0 382 while j + 9 <= sz { 383 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4 384 if b[j] == 1 { if rv_iskind(b, koff, kl, "ent:" as *u8) == 1 { cnt = cnt + 1 } } 385 j = voff + vl 386 } 387 s = s + 1 388 } 389 return cnt 390} 391 392// display a "type:id" tuple ref as just the id (HTML-escaped): "user:rose" -> "rose". 393func rv_disp(dst: *u8, off: i64, s: *u8) -> i64 { 394 var i: i64 = 0 395 while s[i] != (0 as u8) { if (s[i] as i64) == 58 { return rv_cat_esc(dst, off, (s as i64 + i + 1) as *u8) } i = i + 1 } 396 return rv_cat_esc(dst, off, s) 397} 398// build "user:<handle>" into out (NUL-term); returns length. the caller's own identity as a ReBAC subject. 399func rv_userref(out: *u8, handle: *u8) -> i64 { 400 var o: i64 = 0 401 o = rv_cat(out, o, "user:" as *u8) 402 o = rv_cat(out, o, handle) 403 out[o] = 0 as u8 404 return o 405} 406// SECURITY: a handle/group name must be [a-zA-Z0-9_-], 1..64 -- so it can NEVER inject ':' (forge a type) or '#' 407// (forge a userset) into a tuple ref. Reject everything else. 1 = ok, 0 = reject. 408func rv_tok_ok(s: *u8) -> i64 { 409 if s[0] == (0 as u8) { return 0 } 410 var i: i64 = 0 411 while s[i] != (0 as u8) { 412 let c: i64 = s[i] as i64 413 var ok: i64 = 0 414 if c >= 97 { if c <= 122 { ok = 1 } } 415 if c >= 65 { if c <= 90 { ok = 1 } } 416 if c >= 48 { if c <= 57 { ok = 1 } } 417 if c == 95 { ok = 1 } 418 if c == 45 { ok = 1 } 419 if ok == 0 { return 0 } 420 if i >= 64 { return 0 } 421 i = i + 1 422 } 423 return 1 424} 425// build "group:<name>" into out (NUL-term); returns length. 426func rv_groupref(out: *u8, name: *u8) -> i64 { 427 var o: i64 = 0 428 o = rv_cat(out, o, "group:" as *u8) 429 o = rv_cat(out, o, name) 430 out[o] = 0 as u8 431 return o 432} 433 434// ---- render the persisted graph as HTML into out; returns length ---- 435// tok (never null; may be EMPTY) = the caller's validated session token, embedded as a hidden `sess` field in 436// every form so plain zero-JS form POSTs stay authenticated (synchronizer-token = CSRF-resistant by construction; 437// same exposure class as the sessionStorage copy the bootstrap already holds). Empty tok = no field (legacy/open). 438// gr = the caller's grants bundle ([0]=handle,[1]=n,[2]=tenants,[3]=roles) or NULL = legacy OPEN (unfiltered). 439// Every section renders ONLY records whose tenant the caller holds a role on (deny-by-default tenant wall); 440// giving AMOUNTS additionally need manager+ (field-level security, the nx_crm_acl minrole=3 rule). 441func rv_render_tok(prefix: *u8, out: *u8, cap: i64, tok: *u8, gr: *i64) -> i64 { 442 var o: i64 = 0 443 o = rv_cat(out, o, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\n" as *u8) 444 o = rv_cat(out, o, "<html><head><meta charset='utf-8'><title>Nishi Relationship OS</title><style>body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;background:rgb(15,16,22);color:rgb(228,230,235);max-width:760px;margin:5vh auto;padding:0 20px}h1{font-size:24px}h2{font-size:14px;text-transform:uppercase;letter-spacing:.4px;color:rgb(150,200,255);margin-top:24px}.row{padding:8px 0;border-bottom:1px solid rgb(34,38,48)}.mut{color:rgb(150,155,165);font-size:13px}form{margin-top:10px}input,select{background:rgb(26,30,40);color:rgb(228,230,235);border:1px solid rgb(48,54,66);border-radius:8px;padding:8px;margin-right:6px}button{background:rgb(40,110,210);color:white;border:0;border-radius:8px;padding:9px 14px;font-weight:600}</style></head><body>" as *u8) 445 o = rv_cat(out, o, "<h1>Nishi Relationship OS</h1><div class='mut'>Live from your own append-only store -- add a person and it persists, on your hardware.</div>" as *u8) 446 447 // walk records 448 let segp: *i64 = sys_mmap(16) as *i64; let ns: i64 = ss_manifest_dyn(prefix, segp); let segs: *i64 = segp[0] as *i64 // uncapped 449 let f1: *u8 = sys_mmap(256); let f2: *u8 = sys_mmap(256); let f3: *u8 = sys_mmap(256) 450 let f4: *u8 = sys_mmap(256); let f5: *u8 = sys_mmap(256); let ft: *u8 = sys_mmap(256) 451 o = rv_cat(out, o, "<h2>People and entities</h2>" as *u8) 452 var s: i64 = 0 453 while s < ns { 454 let path: *u8 = sys_mmap(512); var po: i64 = 0 455 po = rv_cat(path, po, prefix); po = rv_cat(path, po, segs[s] as *u8); po = rv_cat(path, po, ".docs" as *u8); path[po] = 0 as u8 456 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0] 457 var j: i64 = 0 458 while j + 9 <= sz { 459 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4 460 if b[j] == 1 { 461 if rv_iskind(b, koff, kl, "ent:" as *u8) == 1 { 462 let rec: *u8 = (b as i64 + voff) as *u8 463 f3[0] = 0 as u8 464 rv_field(rec, vl, "name" as *u8, f1, 256); rv_field(rec, vl, "type" as *u8, f2, 256); rv_field(rec, vl, "tenant" as *u8, f3, 256) 465 rv_ten_default(f3) 466 if rv_vis(gr, f3) == 1 { 467 o = rv_cat(out, o, "<div class='row'><b>" as *u8); o = rv_cat_esc(out, o, f1) 468 o = rv_cat(out, o, "</b> <span class='mut'>" as *u8); o = rv_cat_esc(out, o, f2); o = rv_cat(out, o, " &middot; " as *u8); o = rv_cat_esc(out, o, f3); o = rv_cat(out, o, "</span></div>" as *u8) 469 } 470 } 471 } 472 j = voff + vl 473 } 474 s = s + 1 475 } 476 // relationships 477 o = rv_cat(out, o, "<h2>Relationships</h2>" as *u8) 478 s = 0 479 while s < ns { 480 let path: *u8 = sys_mmap(512); var po: i64 = 0 481 po = rv_cat(path, po, prefix); po = rv_cat(path, po, segs[s] as *u8); po = rv_cat(path, po, ".docs" as *u8); path[po] = 0 as u8 482 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0] 483 var j: i64 = 0 484 while j + 9 <= sz { 485 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4 486 if b[j] == 1 { 487 if rv_iskind(b, koff, kl, "edg:" as *u8) == 1 { 488 let rec: *u8 = (b as i64 + voff) as *u8 489 ft[0] = 0 as u8 490 rv_field(rec, vl, "tenant" as *u8, ft, 256) 491 rv_ten_default(ft) 492 if rv_vis(gr, ft) == 1 { 493 rv_field(rec, vl, "from" as *u8, f1, 256); rv_field(rec, vl, "rel" as *u8, f2, 256); rv_field(rec, vl, "to" as *u8, f3, 256); rv_field(rec, vl, "ctx" as *u8, f4, 256); rv_field(rec, vl, "role" as *u8, f5, 256) 494 o = rv_cat(out, o, "<div class='row'>" as *u8); o = rv_cat_esc(out, o, f1); o = rv_cat(out, o, " &mdash;[" as *u8); o = rv_cat_esc(out, o, f2); o = rv_cat(out, o, "]&rarr; " as *u8); o = rv_cat_esc(out, o, f3) 495 o = rv_cat(out, o, " <span class='mut'>" as *u8); o = rv_cat_esc(out, o, f4); o = rv_cat(out, o, " &middot; " as *u8); o = rv_cat_esc(out, o, f5); o = rv_cat(out, o, "</span></div>" as *u8) 496 } 497 } 498 } 499 j = voff + vl 500 } 501 s = s + 1 502 } 503 // deal pipeline -- LIVE from the persisted store (weighted forecast recomputed per render) 504 o = rv_cat(out, o, "<h2>Deal pipeline (live from your store)</h2>" as *u8) 505 var open_t: i64 = 0 506 var wgt: i64 = 0 507 s = 0 508 while s < ns { 509 let path: *u8 = sys_mmap(512) 510 var po: i64 = 0 511 po = rv_cat(path, po, prefix) 512 po = rv_cat(path, po, segs[s] as *u8) 513 po = rv_cat(path, po, ".docs" as *u8) 514 path[po] = 0 as u8 515 let szp: *i64 = sys_mmap(16) as *i64 516 let b: *u8 = ss_readall(path, szp) 517 let sz: i64 = szp[0] 518 var j: i64 = 0 519 while j + 9 <= sz { 520 let kl: i64 = ss_r32(b, j + 1) 521 let koff: i64 = j + 5 522 let vl: i64 = ss_r32(b, koff + kl) 523 let voff: i64 = koff + kl + 4 524 if b[j] == 1 { 525 if rv_iskind(b, koff, kl, "dea:" as *u8) == 1 { 526 let rec: *u8 = (b as i64 + voff) as *u8 527 ft[0] = 0 as u8 528 rv_field(rec, vl, "tenant" as *u8, ft, 256) 529 rv_ten_default(ft) 530 // the wall gates the SUMS too: the forecast is the caller's tenant-scoped view, not a leak 531 if rv_vis(gr, ft) == 1 { 532 rv_field(rec, vl, "name" as *u8, f1, 256) 533 rv_field(rec, vl, "account" as *u8, f2, 256) 534 rv_field(rec, vl, "amount" as *u8, f3, 256) 535 rv_field(rec, vl, "stage" as *u8, f4, 256) 536 let amt: i64 = rv_atoi(f3) 537 if rv_stage_open(f4) == 1 { open_t = open_t + amt; wgt = wgt + (amt * rv_stage_prob(f4)) / 100 } 538 o = rv_cat(out, o, "<div class='row'><b>" as *u8) 539 o = rv_cat_esc(out, o, f1) 540 o = rv_cat(out, o, "</b> <span class='mut'>" as *u8) 541 o = rv_cat_esc(out, o, f2) 542 o = rv_cat(out, o, " &middot; $" as *u8) 543 o = rv_cat_esc(out, o, f3) 544 o = rv_cat(out, o, " &middot; " as *u8) 545 o = rv_cat_esc(out, o, f4) 546 o = rv_cat(out, o, "</span></div>" as *u8) 547 } 548 } 549 } 550 j = voff + vl 551 } 552 s = s + 1 553 } 554 o = rv_cat(out, o, "<div class='row'><b>Open $" as *u8) 555 o = rv_catn(out, o, open_t) 556 o = rv_cat(out, o, " &middot; Weighted forecast $" as *u8) 557 o = rv_catn(out, o, wgt) 558 o = rv_cat(out, o, "</b></div>" as *u8) 559 // giving -- LIVE from the persisted store 560 o = rv_cat(out, o, "<h2>Giving (live from your store)</h2>" as *u8) 561 var gtot: i64 = 0 562 s = 0 563 while s < ns { 564 let path: *u8 = sys_mmap(512) 565 var po: i64 = 0 566 po = rv_cat(path, po, prefix) 567 po = rv_cat(path, po, segs[s] as *u8) 568 po = rv_cat(path, po, ".docs" as *u8) 569 path[po] = 0 as u8 570 let szp: *i64 = sys_mmap(16) as *i64 571 let b: *u8 = ss_readall(path, szp) 572 let sz: i64 = szp[0] 573 var j: i64 = 0 574 while j + 9 <= sz { 575 let kl: i64 = ss_r32(b, j + 1) 576 let koff: i64 = j + 5 577 let vl: i64 = ss_r32(b, koff + kl) 578 let voff: i64 = koff + kl + 4 579 if b[j] == 1 { 580 if rv_iskind(b, koff, kl, "giv:" as *u8) == 1 { 581 let rec: *u8 = (b as i64 + voff) as *u8 582 ft[0] = 0 as u8 583 rv_field(rec, vl, "tenant" as *u8, ft, 256) 584 rv_ten_default(ft) 585 if rv_vis(gr, ft) == 1 { 586 rv_field(rec, vl, "donor" as *u8, f1, 256) 587 rv_field(rec, vl, "fund" as *u8, f2, 256) 588 rv_field(rec, vl, "amount" as *u8, f3, 256) 589 rv_field(rec, vl, "gtype" as *u8, f4, 256) 590 o = rv_cat(out, o, "<div class='row'>" as *u8) 591 o = rv_cat_esc(out, o, f1) 592 o = rv_cat(out, o, " <span class='mut'>" as *u8) 593 o = rv_cat_esc(out, o, f2) 594 // field-level security (nx_crm_acl minrole=3): members see the gift, managers+ see the AMOUNT 595 if rv_amt_ok(gr, ft) == 1 { 596 gtot = gtot + rv_atoi(f3) 597 o = rv_cat(out, o, " &middot; $" as *u8) 598 o = rv_cat_esc(out, o, f3) 599 } else { 600 o = rv_cat(out, o, " &middot; (amount restricted)" as *u8) 601 } 602 o = rv_cat(out, o, " &middot; " as *u8) 603 o = rv_cat_esc(out, o, f4) 604 o = rv_cat(out, o, "</span></div>" as *u8) 605 } 606 } 607 } 608 j = voff + vl 609 } 610 s = s + 1 611 } 612 o = rv_cat(out, o, "<div class='row'><b>Received $" as *u8) 613 o = rv_catn(out, o, gtot) 614 o = rv_cat(out, o, "</b></div>" as *u8) 615 // share inbox -- LIVE from the persisted store (stored + shared through the graph) 616 o = rv_cat(out, o, "<h2>Share inbox (live from your store)</h2>" as *u8) 617 var shn: i64 = 0 618 s = 0 619 while s < ns { 620 let path: *u8 = sys_mmap(512) 621 var po: i64 = 0 622 po = rv_cat(path, po, prefix) 623 po = rv_cat(path, po, segs[s] as *u8) 624 po = rv_cat(path, po, ".docs" as *u8) 625 path[po] = 0 as u8 626 let szp: *i64 = sys_mmap(16) as *i64 627 let b: *u8 = ss_readall(path, szp) 628 let sz: i64 = szp[0] 629 var j: i64 = 0 630 while j + 9 <= sz { 631 let kl: i64 = ss_r32(b, j + 1) 632 let koff: i64 = j + 5 633 let vl: i64 = ss_r32(b, koff + kl) 634 let voff: i64 = koff + kl + 4 635 if b[j] == 1 { 636 if rv_iskind(b, koff, kl, "shr:" as *u8) == 1 { 637 let rec: *u8 = (b as i64 + voff) as *u8 638 ft[0] = 0 as u8 639 rv_field(rec, vl, "tenant" as *u8, ft, 256) 640 rv_ten_default(ft) 641 if rv_vis(gr, ft) == 1 { 642 rv_field(rec, vl, "itype" as *u8, f1, 256) 643 rv_field(rec, vl, "title" as *u8, f2, 256) 644 rv_field(rec, vl, "from" as *u8, f3, 256) 645 rv_field(rec, vl, "to" as *u8, f4, 256) 646 rv_field(rec, vl, "note" as *u8, f5, 256) 647 shn = shn + 1 648 o = rv_cat(out, o, "<div class='row'><b>" as *u8) 649 o = rv_cat_esc(out, o, f2) 650 o = rv_cat(out, o, "</b> <span class='mut'>" as *u8) 651 o = rv_cat_esc(out, o, f1) 652 o = rv_cat(out, o, " &middot; " as *u8) 653 o = rv_cat_esc(out, o, f3) 654 o = rv_cat(out, o, " &rarr; " as *u8) 655 o = rv_cat_esc(out, o, f4) 656 o = rv_cat(out, o, " &middot; " as *u8) 657 o = rv_cat_esc(out, o, f5) 658 o = rv_cat(out, o, "</span></div>" as *u8) 659 } 660 } 661 } 662 j = voff + vl 663 } 664 s = s + 1 665 } 666 o = rv_cat(out, o, "<div class='mut'>shares=" as *u8) 667 o = rv_catn(out, o, shn) 668 o = rv_cat(out, o, " &middot; jobs arrive scored (nx_share fit-on-receipt); consent + recommendation gates apply at the share layer.</div>" as *u8) 669 // ACCESS section (enforced mode only): the caller's memberships + the grant form. The HANDLER enforces who 670 // may grant (admin-of-tenant, or the single first bootstrap grant); rendering the form is not permission. 671 if (gr as i64) != 0 { 672 o = rv_cat(out, o, "<h2>Access</h2><div class='mut'>you are " as *u8) 673 o = rv_cat_esc(out, o, gr[0] as *u8) 674 o = rv_cat(out, o, "</div>" as *u8) 675 let gts: *i64 = gr[2] as *i64 676 let grs: *i64 = gr[3] as *i64 677 var gi: i64 = 0 678 while gi < gr[1] { 679 o = rv_cat(out, o, "<div class='row'>" as *u8) 680 o = rv_cat_esc(out, o, gts[gi] as *u8) 681 o = rv_cat(out, o, " <span class='mut'>role " as *u8) 682 o = rv_catn(out, o, grs[gi]) 683 o = rv_cat(out, o, "</span></div>" as *u8) 684 gi = gi + 1 685 } 686 o = rv_cat(out, o, "<form method='post' action='/relate/add-acl'>" as *u8) 687 o = rv_sess_field(out, o, tok) 688 o = rv_cat(out, o, "<input name='handle' placeholder='Handle'><input name='tenant' placeholder='Tenant'><input name='role' value='2'><button>Grant</button></form>" as *u8) 689 690 // MY CIRCLE -- friends + groups, backed by the SHARED nx_rebac plane (tuples in this same store). This is 691 // the operator's social example live: friend/defriend + join/leave groups, one authorization engine. 692 let me: *u8 = sys_mmap(320); rv_userref(me, gr[0] as *u8) 693 o = rv_cat(out, o, "<h2>My circle (shared authorization plane)</h2>" as *u8) 694 // friends: distinct current subjects of (user:me, friend, *) 695 let fl: *i64 = sys_mmap(8 * 64) as *i64 696 let nfr: i64 = rb_list_subjects(prefix, me, "friend" as *u8, fl, 64) 697 o = rv_cat(out, o, "<div class='mut'>Friends (" as *u8); o = rv_catn(out, o, nfr); o = rv_cat(out, o, ")</div>" as *u8) 698 var qi: i64 = 0 699 while qi < nfr { 700 o = rv_cat(out, o, "<div class='row'>" as *u8); o = rv_disp(out, o, fl[qi] as *u8) 701 o = rv_cat(out, o, " <form method='post' action='/relate/defriend' style='display:inline'>" as *u8) 702 o = rv_sess_field(out, o, tok) 703 o = rv_cat(out, o, "<input type='hidden' name='who' value='" as *u8); o = rv_disp(out, o, fl[qi] as *u8) 704 o = rv_cat(out, o, "'><button>defriend</button></form></div>" as *u8) 705 qi = qi + 1 706 } 707 o = rv_cat(out, o, "<form method='post' action='/relate/add-friend'>" as *u8) 708 o = rv_sess_field(out, o, tok) 709 o = rv_cat(out, o, "<input name='who' placeholder='handle to friend'><button>Add friend</button></form>" as *u8) 710 // groups: distinct current objects where (group, owner|member, user:me) 711 let go: *i64 = sys_mmap(8 * 64) as *i64 712 let ngo: i64 = rb_list_objects_for_sub(prefix, me, "owner" as *u8, go, 64) 713 let gm: *i64 = sys_mmap(8 * 64) as *i64 714 let ngm: i64 = rb_list_objects_for_sub(prefix, me, "member" as *u8, gm, 64) 715 o = rv_cat(out, o, "<div class='mut'>Groups (" as *u8); o = rv_catn(out, o, ngo + ngm); o = rv_cat(out, o, ")</div>" as *u8) 716 var gj: i64 = 0 717 while gj < ngo { 718 o = rv_cat(out, o, "<div class='row'>" as *u8); o = rv_disp(out, o, go[gj] as *u8); o = rv_cat(out, o, " <span class='mut'>owner</span></div>" as *u8) 719 gj = gj + 1 720 } 721 gj = 0 722 while gj < ngm { 723 o = rv_cat(out, o, "<div class='row'>" as *u8); o = rv_disp(out, o, gm[gj] as *u8) 724 o = rv_cat(out, o, " <span class='mut'>member</span> <form method='post' action='/relate/leave-group' style='display:inline'>" as *u8) 725 o = rv_sess_field(out, o, tok) 726 o = rv_cat(out, o, "<input type='hidden' name='group' value='" as *u8); o = rv_disp(out, o, gm[gj] as *u8) 727 o = rv_cat(out, o, "'><button>leave</button></form></div>" as *u8) 728 gj = gj + 1 729 } 730 o = rv_cat(out, o, "<form method='post' action='/relate/new-group'>" as *u8) 731 o = rv_sess_field(out, o, tok) 732 o = rv_cat(out, o, "<input name='group' placeholder='new group name'><button>Create group</button></form>" as *u8) 733 o = rv_cat(out, o, "<form method='post' action='/relate/join-group'>" as *u8) 734 o = rv_sess_field(out, o, tok) 735 o = rv_cat(out, o, "<input name='group' placeholder='group to join'><button>Join group</button></form>" as *u8) 736 } 737 // add-person + add-deal + share forms (POST back to this handler). Actions are /relate/-prefixed so they 738 // route correctly THROUGH the public edge (the daemon matches routes by substring, so direct loopback POSTs 739 // to the bare paths still work). Each form carries the hidden `sess` synchronizer token when authed. 740 o = rv_cat(out, o, "<h2>Add a person</h2><form method='post' action='/relate/add-entity'>" as *u8) 741 o = rv_sess_field(out, o, tok) 742 o = rv_cat(out, o, "<input name='type' value='person'><input name='name' placeholder='Full name'><input name='tenant' value='andelinwest'><button>Add</button></form>" as *u8) 743 o = rv_cat(out, o, "<h2>Add a deal</h2><form method='post' action='/relate/add-deal'>" as *u8) 744 o = rv_sess_field(out, o, tok) 745 o = rv_cat(out, o, "<input name='name' placeholder='Deal name'><input name='account' placeholder='Account'><input name='amount' placeholder='Amount'><input name='stage' value='Lead'><input name='tenant' value='andelinwest'><button>Add</button></form>" as *u8) 746 o = rv_cat(out, o, "<h2>Share an item</h2><form method='post' action='/relate/add-share'>" as *u8) 747 o = rv_sess_field(out, o, tok) 748 o = rv_cat(out, o, "<input name='itype' value='job'><input name='title' placeholder='Title'><input name='from' placeholder='From'><input name='to' placeholder='To'><input name='note' placeholder='Note'><input name='reqs' placeholder='reqs skill:lvl,...'><input name='tenant' value='andelinwest'><button>Share</button></form>" as *u8) 749 o = rv_cat(out, o, "</body></html>" as *u8) 750 return o 751} 752// legacy open-mode render (gates + any static consumer): no token, no sess fields, NO tenant filter. 753func rv_render(prefix: *u8, out: *u8, cap: i64) -> i64 { 754 let mt: *u8 = sys_mmap(8) 755 return rv_render_tok(prefix, out, cap, mt, 0 as *i64) 756} 757 758// urldecode form value for `key` from body into out (handles + and %XX) 759func rv_hexval(c: i64) -> i64 { 760 if c >= 48 { if c <= 57 { return c - 48 } } 761 if c >= 65 { if c <= 70 { return c - 55 } } 762 if c >= 97 { if c <= 102 { return c - 87 } } 763 return 0 764} 765func rv_form_get(body: *u8, blen: i64, key: *u8, out: *u8, cap: i64) -> i64 { 766 let kl: i64 = rv_slen(key) 767 var i: i64 = 0 768 while i < blen { 769 // match key= at a field boundary (i==0 or prev char is &) 770 var atk: i64 = 0 771 if i == 0 { atk = 1 } else { if body[i-1] == (38 as u8) { atk = 1 } } 772 if atk == 1 { 773 var m: i64 = 1; var j: i64 = 0 774 while j < kl { if i + j >= blen { m = 0; j = kl } else { if body[i+j] != key[j] { m = 0; j = kl } else { j = j + 1 } } } 775 if m == 1 { if i + kl < blen { if body[i+kl] == (61 as u8) { 776 var q: i64 = i + kl + 1; var t: i64 = 0 777 var go: i64 = 1 778 while go == 1 { 779 if q >= blen { go = 0 } else { 780 let c: i64 = body[q] as i64 781 if c == 38 { go = 0 } else { 782 var ch: i64 = c 783 if c == 43 { ch = 32 } 784 if c == 37 { if q + 2 < blen { ch = rv_hexval(body[q+1] as i64) * 16 + rv_hexval(body[q+2] as i64); q = q + 2 } } 785 if t < cap - 1 { out[t] = ch as u8; t = t + 1 } 786 q = q + 1 787 } 788 } 789 } 790 out[t] = 0 as u8; return t 791 } } } 792 } 793 i = i + 1 794 } 795 out[0] = 0 as u8; return 0 796} 797 798// header value extractor (bytes after `key` up to CR/LF, one leading space skipped). 0 = header absent. 799func rv_hdr_val(req: *u8, rn: i64, key: *u8, out: *u8, cap: i64) -> i64 { 800 let kl: i64 = rv_slen(key) 801 var i: i64 = 0 802 while i + kl < rn { 803 var m: i64 = 1 804 var k: i64 = 0 805 while k < kl { if req[i + k] != key[k] { m = 0; k = kl } else { k = k + 1 } } 806 if m == 1 { 807 var v: i64 = i + kl 808 if v < rn { if req[v] == (32 as u8) { v = v + 1 } } 809 var t: i64 = 0 810 var go: i64 = 1 811 while go == 1 { 812 if v >= rn { go = 0 } else { 813 let c: i64 = req[v] as i64 814 if c == 13 { go = 0 } else { if c == 10 { go = 0 } else { 815 if t < cap - 1 { out[t] = c as u8; t = t + 1 } 816 v = v + 1 817 } } 818 } 819 } 820 out[t] = 0 as u8 821 return t 822 } 823 i = i + 1 824 } 825 out[0] = 0 as u8 826 return 0 827} 828// session token for THIS request: X-Nishi-Session header (fetch clients / the bootstrap re-fetch) wins; else the 829// `sess` form field on a POST body (the zero-JS synchronizer flow -- rv_form_get urldecodes %2B/%2F/%3D back to 830// the b64 alphabet). Returns token length, 0 = none. PURE parse; the caller does the crypto. 831func rv_get_sess(req: *u8, reqlen: i64, out: *u8, cap: i64) -> i64 { 832 let hl: i64 = rv_hdr_val(req, reqlen, "X-Nishi-Session:" as *u8, out, cap) 833 if hl > 0 { return hl } 834 var bs: i64 = 0 - 1 835 var i: i64 = 0 836 while i + 3 < reqlen { 837 if req[i] == (13 as u8) { if req[i+1] == (10 as u8) { if req[i+2] == (13 as u8) { if req[i+3] == (10 as u8) { bs = i + 4; i = reqlen } } } } 838 i = i + 1 839 } 840 if bs < 0 { out[0] = 0 as u8; return 0 } 841 return rv_form_get((req as i64 + bs) as *u8, reqlen - bs, "sess" as *u8, out, cap) 842} 843// RFC9457 problem+json refusal for an unauthenticated mutation (fail-closed: NOTHING persists on this path). 844func rv_emit_401(out: *u8) -> i64 { 845 let body: *u8 = "{\"type\":\"about:blank\",\"title\":\"Unauthorized\",\"status\":401,\"detail\":\"a valid Nishi session is required; sign in at /login and retry\"}" as *u8 846 var o: i64 = 0 847 o = rv_cat(out, o, "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/problem+json\r\nConnection: close\r\nContent-Length: " as *u8) 848 o = rv_catn(out, o, rv_slen(body)) 849 o = rv_cat(out, o, "\r\n\r\n" as *u8) 850 o = rv_cat(out, o, body) 851 return o 852} 853// hex-encode n bytes of src into dst (lowercase, NUL-term); returns chars written (2n). 854func rv_hex(dst: *u8, src: *u8, n: i64) -> i64 { 855 let hx: *u8 = "0123456789abcdef" as *u8 856 var o: i64 = 0 857 var i: i64 = 0 858 while i < n { 859 let c: i64 = (src[i] as i64) & 0xff 860 dst[o] = hx[(c >> 4) & 15]; dst[o + 1] = hx[c & 15] 861 o = o + 2; i = i + 1 862 } 863 dst[o] = 0 as u8 864 return o 865} 866// uidhex -> handle from the login daemon's index (TAB-separated: field0=uidhex, field1=handle; '#' = comment). 867// returns handle length into out_h (NUL-term), 0 = not found. Pure over the buffer (gate-testable). 868func rv_idx_lookup(idx: *u8, idxlen: i64, uidhex: *u8, uxn: i64, out_h: *u8, cap: i64) -> i64 { 869 var ls: i64 = 0 870 while ls < idxlen { 871 var le: i64 = ls 872 while le < idxlen { if (idx[le] as i64) == 10 { break } le = le + 1 } 873 if le > ls { if (idx[ls] as i64) != 35 { 874 var tab: i64 = ls 875 while tab < le { if (idx[tab] as i64) == 9 { break } tab = tab + 1 } 876 if tab < le { 877 let f0len: i64 = tab - ls 878 if f0len == uxn { 879 var m: i64 = 1 880 var k: i64 = 0 881 while k < uxn { if idx[ls + k] != uidhex[k] { m = 0; k = uxn } else { k = k + 1 } } 882 if m == 1 { 883 var o: i64 = 0 884 var q: i64 = tab + 1 885 while q < le { if o < cap - 1 { out_h[o] = idx[q]; o = o + 1 } q = q + 1 } 886 out_h[o] = 0 as u8 887 return o 888 } 889 } 890 } 891 } } 892 ls = le + 1 893 } 894 out_h[0] = 0 as u8 895 return 0 896} 897// resolve a raw uid (uidn bytes) to its registered handle via the on-disk index. 0 = unmapped (fail-closed: 898// the caller then holds ZERO grants -> deny-all). idxpath is read fresh per request (small file; always current). 899func rv_resolve_handle(idxpath: *u8, uid: *u8, uidn: i64, out_h: *u8, cap: i64) -> i64 { 900 let szp: *i64 = sys_mmap(16) as *i64 901 let idx: *u8 = ss_readall(idxpath, szp) 902 if (idx as i64) == 0 { out_h[0] = 0 as u8; return 0 } 903 let uxbuf: *u8 = sys_mmap(128) 904 let uxn: i64 = rv_hex(uxbuf, uid, uidn) 905 return rv_idx_lookup(idx, szp[0], uxbuf, uxn, out_h, cap) 906} 907 908// write all n bytes to fd 909func rv_write_all(fd: i64, buf: *u8, n: i64) -> i64 { 910 var w: i64 = 0 911 while w < n { let k: i64 = sys_write(fd, (buf as i64 + w) as *u8, n - w); if k <= 0 { return 0 - 1 } w = w + k } 912 return 0 913} 914 915// read ONE full HTTP request from fd: until CRLFCRLF, plus Content-Length body bytes if present. returns total. 916func rv_read_req(fd: i64, buf: *u8, cap: i64) -> i64 { 917 var n: i64 = 0 918 var hdr_end: i64 = 0 - 1 919 var want: i64 = 0 - 1 920 var go: i64 = 1 921 while go == 1 { 922 if n >= cap - 1 { go = 0 } else { 923 let r: i64 = sys_read(fd, (buf as i64 + n) as *u8, cap - 1 - n) 924 if r <= 0 { go = 0 } else { 925 n = n + r 926 if hdr_end < 0 { 927 var i: i64 = 0 928 while i + 3 < n { 929 if buf[i] == (13 as u8) { if buf[i+1] == (10 as u8) { if buf[i+2] == (13 as u8) { if buf[i+3] == (10 as u8) { hdr_end = i + 4; i = n } } } } 930 i = i + 1 931 } 932 if hdr_end >= 0 { 933 // parse Content-Length (absent -> headers-only request) 934 var cl: i64 = 0 - 1 935 let key: *u8 = "Content-Length:" as *u8 936 let kl: i64 = 15 937 var j: i64 = 0 938 while j + kl < hdr_end { 939 var m: i64 = 1 940 var q: i64 = 0 941 while q < kl { if buf[j+q] != key[q] { m = 0; q = kl } else { q = q + 1 } } 942 if m == 1 { 943 var v: i64 = 0 944 var t: i64 = j + kl 945 while t < hdr_end { 946 let c: i64 = buf[t] as i64 947 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } 948 if c == 13 { t = hdr_end } 949 t = t + 1 950 } 951 cl = v 952 j = hdr_end 953 } 954 j = j + 1 955 } 956 if cl < 0 { want = hdr_end } else { want = hdr_end + cl } 957 } 958 } 959 if want >= 0 { if n >= want { go = 0 } } 960 } 961 } 962 } 963 return n 964} 965 966// the AUTH+ACL-AWARE interactive handler: request bytes -> response bytes (out); returns response length. 967// auth = the DAEMON's verdict bundle (this core stays crypto-free + offline-gateable): 968// null -> legacy OPEN mode (gates/loopback tools): no gate, no tenant wall, no sess fields. 969// auth[0] (tok) -> 0 = NO valid session: GET = the sessionStorage bootstrap (NO data), POST = 401. 970// auth[1] (hdl) -> the resolved registered handle; EMPTY = authed-but-unmapped -> ZERO grants (deny-all). 971// Authed requests get the caller's grants bundle built from the persisted acl records; every render + write 972// goes through the tenant wall (deny-by-default; the wall beats admin -- the nx_crm_acl law, persisted). 973func rv_handle_auth(prefix: *u8, req: *u8, reqlen: i64, out: *u8, cap: i64, auth: *i64) -> i64 { 974 // method = first token 975 var is_post: i64 = 0 976 if req[0] == (80 as u8) { if req[1] == (79 as u8) { is_post = 1 } } // "PO" 977 var tok: *u8 = 0 as *u8 978 var gr: *i64 = 0 as *i64 979 if (auth as i64) == 0 { 980 tok = sys_mmap(8) // legacy open: empty token (no sess fields), null gr (no wall) 981 } else { 982 tok = auth[0] as *u8 983 if (tok as i64) == 0 { 984 if is_post == 1 { return rv_emit_401(out) } 985 var bo: i64 = 0 986 bo = rv_cat(out, bo, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\n" as *u8) 987 bo = rv_cat(out, bo, RV_BOOTSTRAP) 988 return bo 989 } 990 gr = sys_mmap(8 * 4) as *i64 991 gr[0] = auth[1] 992 let gts: *i64 = sys_mmap(8 * 16) as *i64 993 let grs: *i64 = sys_mmap(8 * 16) as *i64 994 gr[1] = rv_acl_load(prefix, auth[1] as *u8, gts, grs) 995 gr[2] = gts as i64 996 gr[3] = grs as i64 997 } 998 // find body (after CRLFCRLF) 999 var bs: i64 = 0 - 1; var i: i64 = 0 1000 while i + 3 < reqlen { 1001 if req[i] == (13 as u8) { if req[i+1] == (10 as u8) { if req[i+2] == (13 as u8) { if req[i+3] == (10 as u8) { bs = i + 4; i = reqlen } } } } 1002 i = i + 1 1003 } 1004 if is_post == 1 { 1005 if bs >= 0 { 1006 let body: *u8 = (req as i64 + bs) as *u8 1007 let blen: i64 = reqlen - bs 1008 if rv_has(req, reqlen, "/add-acl" as *u8) == 1 { 1009 // GRANT a membership. Allowed: a role-4 ADMIN of that tenant; or the BOOTSTRAP window -- 1010 // the acl plane is EMPTY and the caller (registered, nonempty handle) grants THEMSELVES first. 1011 let ah: *u8 = sys_mmap(256); let at: *u8 = sys_mmap(128); let arr: *u8 = sys_mmap(32) 1012 rv_form_get(body, blen, "handle" as *u8, ah, 256) 1013 rv_form_get(body, blen, "tenant" as *u8, at, 128) 1014 rv_form_get(body, blen, "role" as *u8, arr, 32) 1015 var allowed: i64 = 0 1016 var grantor: *u8 = "local" as *u8 1017 if (gr as i64) == 0 { allowed = 1 } else { 1018 grantor = gr[0] as *u8 1019 if rv_role_of(gr, at) >= 4 { allowed = 1 } 1020 if allowed == 0 { 1021 if rv_acl_any(prefix) == 0 { if grantor[0] != (0 as u8) { if rv_seq(ah, grantor) == 1 { allowed = 1 } } } 1022 } 1023 } 1024 if allowed == 0 { return rv_emit_403(out) } 1025 if ah[0] != (0 as u8) { 1026 if at[0] != (0 as u8) { 1027 let wa: *i64 = ss_begin() 1028 if rv_save_acl(wa, ah, at, rv_atoi(arr), grantor) == 0 { ss_commit(prefix, wa, sys_now_us()) } 1029 // the caller's own grants may have changed -> reload the bundle before rendering 1030 if (gr as i64) != 0 { gr[1] = rv_acl_load(prefix, gr[0] as *u8, gr[2] as *i64, gr[3] as *i64) } 1031 } 1032 } 1033 } 1034 // SOCIAL: friend/defriend + groups via the SHARED nx_rebac plane. Subject = the AUTHED caller (gr[0]); 1035 // a client CANNOT act as anyone else (no spoofing). Only in enforced mode (a real handle exists). 1036 if (gr as i64) != 0 { 1037 let meh: *u8 = gr[0] as *u8 1038 if meh[0] != (0 as u8) { 1039 let me: *u8 = sys_mmap(320); rv_userref(me, meh) 1040 if rv_has(req, reqlen, "/add-friend" as *u8) == 1 { 1041 let who: *u8 = sys_mmap(128); rv_form_get(body, blen, "who" as *u8, who, 128) 1042 if rv_tok_ok(who) == 1 { 1043 let them: *u8 = sys_mmap(320); rv_userref(them, who) 1044 // mutual friendship (v1: direct on add; a request/accept flow is the next rung) 1045 rb_put(prefix, me, "friend" as *u8, them, meh, 1) 1046 rb_put(prefix, them, "friend" as *u8, me, meh, 1) 1047 } 1048 } 1049 if rv_has(req, reqlen, "/defriend" as *u8) == 1 { 1050 let who: *u8 = sys_mmap(128); rv_form_get(body, blen, "who" as *u8, who, 128) 1051 if rv_tok_ok(who) == 1 { 1052 let them: *u8 = sys_mmap(320); rv_userref(them, who) 1053 rb_put(prefix, me, "friend" as *u8, them, meh, 0) 1054 rb_put(prefix, them, "friend" as *u8, me, meh, 0) 1055 } 1056 } 1057 if rv_has(req, reqlen, "/new-group" as *u8) == 1 { 1058 let gn: *u8 = sys_mmap(128); rv_form_get(body, blen, "group" as *u8, gn, 128) 1059 if rv_tok_ok(gn) == 1 { 1060 let gref: *u8 = sys_mmap(320); rv_groupref(gref, gn) 1061 // claim only if UNCLAIMED (rb_may_grant bootstrap) -> caller becomes owner AND member 1062 if rb_may_grant(prefix, me, gref) == 1 { 1063 rb_put(prefix, gref, "owner" as *u8, me, meh, 1) 1064 rb_put(prefix, gref, "member" as *u8, me, meh, 1) 1065 } 1066 } 1067 } 1068 if rv_has(req, reqlen, "/join-group" as *u8) == 1 { 1069 let gn: *u8 = sys_mmap(128); rv_form_get(body, blen, "group" as *u8, gn, 128) 1070 if rv_tok_ok(gn) == 1 { 1071 let gref: *u8 = sys_mmap(320); rv_groupref(gref, gn) 1072 rb_put(prefix, gref, "member" as *u8, me, meh, 1) // open join (demo); group-policy gate = next rung 1073 } 1074 } 1075 if rv_has(req, reqlen, "/leave-group" as *u8) == 1 { 1076 let gn: *u8 = sys_mmap(128); rv_form_get(body, blen, "group" as *u8, gn, 128) 1077 if rv_tok_ok(gn) == 1 { 1078 let gref: *u8 = sys_mmap(320); rv_groupref(gref, gn) 1079 rb_put(prefix, gref, "member" as *u8, me, meh, 0) 1080 } 1081 } 1082 } 1083 } 1084 if rv_has(req, reqlen, "/add-entity" as *u8) == 1 { 1085 var ty: *u8 = sys_mmap(128); let nm: *u8 = sys_mmap(256); let tn: *u8 = sys_mmap(128) 1086 rv_form_get(body, blen, "type" as *u8, ty, 128) 1087 rv_form_get(body, blen, "name" as *u8, nm, 256) 1088 if rv_wcheck(gr, body, blen, tn) == 0 { return rv_emit_403(out) } 1089 if ty[0] == (0 as u8) { ty = "person" as *u8 } 1090 if nm[0] != (0 as u8) { rv_persist_entity(prefix, ty, nm, tn) } 1091 } 1092 if rv_has(req, reqlen, "/add-deal" as *u8) == 1 { 1093 let dn: *u8 = sys_mmap(256); let da: *u8 = sys_mmap(256); let dm: *u8 = sys_mmap(64); let dsg: *u8 = sys_mmap(64); let dtn: *u8 = sys_mmap(128) 1094 rv_form_get(body, blen, "name" as *u8, dn, 256) 1095 rv_form_get(body, blen, "account" as *u8, da, 256) 1096 rv_form_get(body, blen, "amount" as *u8, dm, 64) 1097 rv_form_get(body, blen, "stage" as *u8, dsg, 64) 1098 if rv_wcheck(gr, body, blen, dtn) == 0 { return rv_emit_403(out) } 1099 // validated: unknown stage / empty name never persists (deny-by-default) 1100 if dn[0] != (0 as u8) { rv_persist_deal(prefix, dn, da, rv_atoi(dm), dsg, dtn) } 1101 } 1102 if rv_has(req, reqlen, "/add-share" as *u8) == 1 { 1103 let sty: *u8 = sys_mmap(64); let stt: *u8 = sys_mmap(256); let sfr: *u8 = sys_mmap(256) 1104 let sto: *u8 = sys_mmap(256); let sno: *u8 = sys_mmap(256); let sre: *u8 = sys_mmap(512); let stn: *u8 = sys_mmap(128) 1105 rv_form_get(body, blen, "itype" as *u8, sty, 64) 1106 rv_form_get(body, blen, "title" as *u8, stt, 256) 1107 rv_form_get(body, blen, "from" as *u8, sfr, 256) 1108 rv_form_get(body, blen, "to" as *u8, sto, 256) 1109 rv_form_get(body, blen, "note" as *u8, sno, 256) 1110 rv_form_get(body, blen, "reqs" as *u8, sre, 512) 1111 if rv_wcheck(gr, body, blen, stn) == 0 { return rv_emit_403(out) } 1112 // validated: bad itype / empty title never persists 1113 if stt[0] != (0 as u8) { 1114 if rv_itype_ok(sty) == 1 { 1115 let sa: *i64 = sys_mmap(8 * 8) as *i64 1116 sa[0] = sty as i64 1117 sa[1] = stt as i64 1118 sa[2] = sfr as i64 1119 sa[3] = sto as i64 1120 sa[4] = sno as i64 1121 sa[5] = sre as i64 1122 sa[6] = stn as i64 1123 let w2: *i64 = ss_begin() 1124 if rv_save_share(w2, sa) == 0 { ss_commit(prefix, w2, sys_now_us()) } 1125 } 1126 } 1127 } 1128 } 1129 } 1130 return rv_render_tok(prefix, out, cap, tok, gr) 1131} 1132// legacy entry (offline gate + live gate + any pre-auth caller): OPEN mode -- no gate, no wall, no sess fields. 1133func rv_handle(prefix: *u8, req: *u8, reqlen: i64, out: *u8, cap: i64) -> i64 { 1134 return rv_handle_auth(prefix, req, reqlen, out, cap, 0 as *i64) 1135} 1136 1137func rv_selftest() -> i64 { 1138 p("=== NX-RELATE-SERVE SELFTEST (GET renders persisted graph; POST /add-entity persists + shows up) ===\n" as *u8) 1139 let prefix: *u8 = sys_mmap(128); var po: i64 = 0 1140 po = rv_cat(prefix, po, "/tmp/relserve_" as *u8); po = rv_catn(prefix, po, sys_now_us()); po = rv_cat(prefix, po, "_" as *u8); prefix[po] = 0 as u8 1141 1142 // seed the store: entities + 2 deals + 1 gift (weighted oracle: 50000*50 + 120000*75 = 115000) 1143 let w: *i64 = ss_begin() 1144 rv_save_entity(w, "person" as *u8, "Rose Andelin" as *u8, "andelinwest" as *u8) 1145 rv_save_entity(w, "company" as *u8, "Andelin West" as *u8, "andelinwest" as *u8) 1146 rv_save_deal(w, "Westfield Renovation" as *u8, "Westfield Group" as *u8, 50000, "Proposal" as *u8, "andelinwest" as *u8) 1147 rv_save_deal(w, "Riverside Contract" as *u8, "Riverside LLC" as *u8, 120000, "Negotiation" as *u8, "andelinwest" as *u8) 1148 rv_save_gift(w, "Rose" as *u8, "Tithing" as *u8, 500, "donation" as *u8) 1149 let sha: *i64 = sys_mmap(8 * 8) as *i64 1150 sha[0]="job" as *u8 as i64; sha[1]="Sales Director at Riverside" as *u8 as i64; sha[2]="Marcus" as *u8 as i64 1151 sha[3]="Emma" as *u8 as i64; sha[4]="warm intro from your recruiter" as *u8 as i64; sha[5]="sales:5,leadership:4,forecasting:3" as *u8 as i64 1152 sha[6]="andelinwest" as *u8 as i64 1153 rv_save_share(w, sha) 1154 if ss_commit(prefix, w, sys_now_us()) != 0 { p("seed FAILED -- fail loud\n" as *u8); return 1 } 1155 1156 let out: *u8 = sys_mmap(65536) 1157 // 1) GET renders the seeded graph 1158 let n1: i64 = rv_handle(prefix, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" as *u8, 27, out, 65536) 1159 let get_shows_rose: i64 = rv_has(out, n1, "Rose Andelin" as *u8) 1160 let get_shows_deal: i64 = rv_has(out, n1, "Westfield Renovation" as *u8) 1161 let get_shows_wgt: i64 = rv_has(out, n1, "Weighted forecast $115000" as *u8) 1162 let get_shows_gift: i64 = rv_has(out, n1, "Received $500" as *u8) 1163 // 2) POST adds First Ward (urlencoded "First+Ward") 1164 let post: *u8 = "POST /add-entity HTTP/1.1\r\nContent-Length: 41\r\n\r\ntype=org&name=First+Ward&tenant=personal" as *u8 1165 let n2: i64 = rv_handle(prefix, post, rv_slen(post), out, 65536) 1166 let ent_after: i64 = rv_count_ent(prefix) 1167 // 3) GET again shows the newly added First Ward 1168 let n3: i64 = rv_handle(prefix, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" as *u8, 27, out, 65536) 1169 let get_shows_ward: i64 = rv_has(out, n3, "First Ward" as *u8) 1170 1171 // 4) POST /add-deal (Downtown 30000 Qualified) -> forecast moves to the canonical 122500 1172 let dpost: *u8 = "POST /add-deal HTTP/1.1\r\nContent-Length: 68\r\n\r\nname=Downtown+Lease&account=Downtown+Co&amount=30000&stage=Qualified" as *u8 1173 let n4: i64 = rv_handle(prefix, dpost, rv_slen(dpost), out, 65536) 1174 let n5: i64 = rv_handle(prefix, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" as *u8, 27, out, 65536) 1175 let deal_added: i64 = rv_has(out, n5, "Downtown Lease" as *u8) 1176 let wgt_moved: i64 = rv_has(out, n5, "Weighted forecast $122500" as *u8) 1177 // 5) a bad stage NEVER persists (deny-by-default) 1178 let bpost: *u8 = "POST /add-deal HTTP/1.1\r\nContent-Length: 47\r\n\r\nname=Bad+Deal&account=X&amount=9999&stage=Vibes" as *u8 1179 let n6: i64 = rv_handle(prefix, bpost, rv_slen(bpost), out, 65536) 1180 let n7: i64 = rv_handle(prefix, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" as *u8, 27, out, 65536) 1181 let bad_absent: i64 = rv_has(out, n7, "Bad Deal" as *u8) 1182 let wgt_stable: i64 = rv_has(out, n7, "Weighted forecast $122500" as *u8) 1183 // 6) the SHARE inbox: seeded share renders; POST /add-share persists; bad itype never lands 1184 let share_seeded: i64 = rv_has(out, n7, "Sales Director at Riverside" as *u8) 1185 let spost: *u8 = "POST /add-share HTTP/1.1\r\nContent-Length: 70\r\n\r\nitype=resource&title=Negotiation+notes&from=Dana&to=Emma&note=ch3&reqs=" as *u8 1186 let n8: i64 = rv_handle(prefix, spost, rv_slen(spost), out, 65536) 1187 let share_added: i64 = rv_has(out, n8, "Negotiation notes" as *u8) 1188 let xpost: *u8 = "POST /add-share HTTP/1.1\r\nContent-Length: 55\r\n\r\nitype=gossip&title=Bad+Share&from=X&to=Y&note=n&reqs=" as *u8 1189 let n9: i64 = rv_handle(prefix, xpost, rv_slen(xpost), out, 65536) 1190 let badshare_absent: i64 = rv_has(out, n9, "Bad Share" as *u8) 1191 1192 // 7) AUTH MODE (auth bundle: [0]=tok, [1]=handle; the daemon shell does the crypto, this gates semantics): 1193 // unauthed GET = bootstrap ONLY (no stored data leaks); unauthed POST = 401 + NOTHING persists. 1194 let ub: *i64 = sys_mmap(16) as *i64 // ub[0]=0 -> no valid session 1195 let na: i64 = rv_handle_auth(prefix, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" as *u8, 27, out, 65536, ub) 1196 let gate_bootstrap: i64 = rv_has(out, na, "/login?return=/relate" as *u8) 1197 let gate_leaks: i64 = rv_has(out, na, "Rose Andelin" as *u8) 1198 let ent_pre_evil: i64 = rv_count_ent(prefix) 1199 let epost: *u8 = "POST /add-entity HTTP/1.1\r\nContent-Length: 41\r\n\r\ntype=org&name=Evil+Org&tenant=andelinwest" as *u8 1200 let nb: i64 = rv_handle_auth(prefix, epost, rv_slen(epost), out, 65536, ub) 1201 let post_401: i64 = rv_has(out, nb, "401 Unauthorized" as *u8) 1202 var evil_blocked: i64 = 0 1203 if rv_count_ent(prefix) == ent_pre_evil { evil_blocked = 1 } 1204 // 10) THE TENANT WALL (deny-by-default, persisted acl records, the nx_crm_acl law): 1205 // authed + ZERO grants -> the app frame renders (forms + sess token) but NO records leak 1206 let ftok: *u8 = "FAKETOKb64FORRENDER" as *u8 1207 let ab: *i64 = sys_mmap(16) as *i64 1208 ab[0] = ftok as i64 1209 ab[1] = "tester" as *u8 as i64 1210 let nc: i64 = rv_handle_auth(prefix, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" as *u8, 27, out, 65536, ab) 1211 let tok_embedded: i64 = rv_has(out, nc, "name='sess' value='FAKETOKb64FORRENDER'" as *u8) 1212 let wall_denies_all: i64 = 1 - rv_has(out, nc, "Rose Andelin" as *u8) 1213 // authed + no grant: WRITE refused 403 + nothing persists 1214 let dpost2: *u8 = "POST /add-deal HTTP/1.1\r\nContent-Length: 66\r\n\r\nname=Blocked+Deal&account=X&amount=5&stage=Lead&tenant=andelinwest" as *u8 1215 let nw: i64 = rv_handle_auth(prefix, dpost2, rv_slen(dpost2), out, 65536, ab) 1216 let write_403: i64 = rv_has(out, nw, "403 Forbidden" as *u8) 1217 // BOOTSTRAP: acl plane EMPTY -> a registered caller may self-grant the FIRST membership 1218 let bpost2: *u8 = "POST /relate/add-acl HTTP/1.1\r\nContent-Length: 39\r\n\r\nhandle=tester&tenant=andelinwest&role=4" as *u8 1219 let ng: i64 = rv_handle_auth(prefix, bpost2, rv_slen(bpost2), out, 65536, ab) 1220 let boot_grant_ok: i64 = rv_has(out, ng, "you are tester" as *u8) 1221 // granted andelinwest: Rose visible, but First Ward (tenant=personal) stays WALLED OFF 1222 let nc2: i64 = rv_handle_auth(prefix, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" as *u8, 27, out, 65536, ab) 1223 let authed_shows: i64 = rv_has(out, nc2, "Rose Andelin" as *u8) 1224 let ward_walled: i64 = 1 - rv_has(out, nc2, "First Ward" as *u8) 1225 let admin_sees_amt: i64 = rv_has(out, nc2, "Received $500" as *u8) 1226 // bootstrap window CLOSED: a second no-grant user cannot self-grant anymore 1227 let ab2: *i64 = sys_mmap(16) as *i64 1228 ab2[0] = ftok as i64 1229 ab2[1] = "tester2" as *u8 as i64 1230 let cpost: *u8 = "POST /relate/add-acl HTTP/1.1\r\nContent-Length: 40\r\n\r\nhandle=tester2&tenant=andelinwest&role=4" as *u8 1231 let nh: i64 = rv_handle_auth(prefix, cpost, rv_slen(cpost), out, 65536, ab2) 1232 let boot_closed: i64 = rv_has(out, nh, "403 Forbidden" as *u8) 1233 // the ADMIN grants a MEMBER (role 2); members see records but giving AMOUNTS are field-masked (minrole 3) 1234 let gpost: *u8 = "POST /relate/add-acl HTTP/1.1\r\nContent-Length: 40\r\n\r\nhandle=tester3&tenant=andelinwest&role=2" as *u8 1235 let ni: i64 = rv_handle_auth(prefix, gpost, rv_slen(gpost), out, 65536, ab) 1236 let ab3: *i64 = sys_mmap(16) as *i64 1237 ab3[0] = ftok as i64 1238 ab3[1] = "tester3" as *u8 as i64 1239 let nj: i64 = rv_handle_auth(prefix, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" as *u8, 27, out, 65536, ab3) 1240 let member_sees: i64 = rv_has(out, nj, "Rose Andelin" as *u8) 1241 let member_masked: i64 = rv_has(out, nj, "(amount restricted)" as *u8) 1242 // cross-tenant WRITE by a member of another tenant -> 403 + nothing persists (the wall beats intent) 1243 let ent_pre_cross: i64 = rv_count_ent(prefix) 1244 let xpost2: *u8 = "POST /add-entity HTTP/1.1\r\nContent-Length: 39\r\n\r\ntype=org&name=Cross+Org&tenant=personal" as *u8 1245 let nk: i64 = rv_handle_auth(prefix, xpost2, rv_slen(xpost2), out, 65536, ab3) 1246 let cross_403: i64 = rv_has(out, nk, "403 Forbidden" as *u8) 1247 var cross_blocked: i64 = 0 1248 if rv_count_ent(prefix) == ent_pre_cross { cross_blocked = 1 } 1249 // 8) rv_get_sess parse: header wins; POST-body `sess` field urldecodes back to the b64 alphabet 1250 let sb: *u8 = sys_mmap(600) 1251 let hreq: *u8 = "GET / HTTP/1.1\r\nX-Nishi-Session: AbCd+/=99\r\n\r\n" as *u8 1252 let hn: i64 = rv_get_sess(hreq, rv_slen(hreq), sb, 600) 1253 let hdr_tok_ok: i64 = rv_seq(sb, "AbCd+/=99" as *u8) 1254 let freq: *u8 = "POST /relate/add-entity HTTP/1.1\r\nContent-Length: 21\r\n\r\nsess=Zz%2Bq%2F%3D&x=1" as *u8 1255 let fn2: i64 = rv_get_sess(freq, rv_slen(freq), sb, 600) 1256 let fld_tok_ok: i64 = rv_seq(sb, "Zz+q/=" as *u8) 1257 // 9) stored-XSS liar-kill: a stored <script> renders ESCAPED (with the sess token in the page, raw = theft) 1258 let xw: *i64 = ss_begin() 1259 rv_save_entity(xw, "person" as *u8, "<script>alert(1)</script>" as *u8, "t" as *u8) 1260 ss_commit(prefix, xw, sys_now_us()) 1261 let nd: i64 = rv_handle(prefix, "GET / HTTP/1.1\r\nHost: x\r\n\r\n" as *u8, 27, out, 65536) 1262 let xss_raw: i64 = rv_has(out, nd, "<script>alert" as *u8) 1263 let xss_esc: i64 = rv_has(out, nd, "&lt;script&gt;alert" as *u8) 1264 // 11) uid->handle resolution (the daemon maps a validated session uid to its registered handle for the ACL): 1265 let hxb: *u8 = sys_mmap(16) 1266 let twob: *u8 = sys_mmap(4); twob[0] = 0xAB as u8; twob[1] = 0x0F as u8 1267 rv_hex(hxb, twob, 2) 1268 let hex_ok: i64 = rv_seq(hxb, "ab0f" as *u8) 1269 let idxbuf: *u8 = "# comment line\naabb\thandleA\ncc11\thandleB\n" as *u8 1270 let hb2: *u8 = sys_mmap(64) 1271 rv_idx_lookup(idxbuf, rv_slen(idxbuf), "cc11" as *u8, 4, hb2, 64) 1272 let idx_hit: i64 = rv_seq(hb2, "handleB" as *u8) 1273 let miss: i64 = rv_idx_lookup(idxbuf, rv_slen(idxbuf), "dead" as *u8, 4, hb2, 64) 1274 var idx_miss: i64 = 0 1275 if miss == 0 { idx_miss = 1 } 1276 1277 p(" GET1 bytes=" as *u8); pn(n1); p(" shows-Rose=" as *u8); pn(get_shows_rose) 1278 p(" deal=" as *u8); pn(get_shows_deal); p(" wgt115000=" as *u8); pn(get_shows_wgt); p(" gift500=" as *u8); pn(get_shows_gift); p("\n" as *u8) 1279 p(" after POSTs: entities=" as *u8); pn(ent_after); p(" ward=" as *u8); pn(get_shows_ward) 1280 p(" deal-added=" as *u8); pn(deal_added); p(" wgt122500=" as *u8); pn(wgt_moved) 1281 p(" bad-stage-absent=" as *u8); pn(1 - bad_absent); p(" wgt-stable=" as *u8); pn(wgt_stable); p("\n" as *u8) 1282 1283 var ok: i64 = 1 1284 if get_shows_rose != 1 { ok = 0 } 1285 if get_shows_deal != 1 { ok = 0 } 1286 if get_shows_wgt != 1 { ok = 0 } 1287 if get_shows_gift != 1 { ok = 0 } 1288 if ent_after != 3 { ok = 0 } 1289 if get_shows_ward != 1 { ok = 0 } 1290 if deal_added != 1 { ok = 0 } 1291 if wgt_moved != 1 { ok = 0 } 1292 if bad_absent != 0 { ok = 0 } 1293 if wgt_stable != 1 { ok = 0 } 1294 if share_seeded != 1 { ok = 0 } 1295 if share_added != 1 { ok = 0 } 1296 if badshare_absent != 0 { ok = 0 } 1297 if gate_bootstrap != 1 { ok = 0 } 1298 if gate_leaks != 0 { ok = 0 } 1299 if post_401 != 1 { ok = 0 } 1300 if evil_blocked != 1 { ok = 0 } 1301 if tok_embedded != 1 { ok = 0 } 1302 if authed_shows != 1 { ok = 0 } 1303 if hdr_tok_ok != 1 { ok = 0 } 1304 if fld_tok_ok != 1 { ok = 0 } 1305 if xss_raw != 0 { ok = 0 } 1306 if xss_esc != 1 { ok = 0 } 1307 // R3.5 tenant wall (the nx_crm_acl law, persisted + enforced in the serve path) 1308 if wall_denies_all != 1 { ok = 0 } 1309 if write_403 != 1 { ok = 0 } 1310 if boot_grant_ok != 1 { ok = 0 } 1311 if ward_walled != 1 { ok = 0 } 1312 if admin_sees_amt != 1 { ok = 0 } 1313 if boot_closed != 1 { ok = 0 } 1314 if member_sees != 1 { ok = 0 } 1315 if member_masked != 1 { ok = 0 } 1316 if cross_403 != 1 { ok = 0 } 1317 if cross_blocked != 1 { ok = 0 } 1318 if hex_ok != 1 { ok = 0 } 1319 if idx_hit != 1 { ok = 0 } 1320 if idx_miss != 1 { ok = 0 } 1321 p(" shares: seeded=" as *u8); pn(share_seeded); p(" added=" as *u8); pn(share_added); p(" bad-itype-absent=" as *u8); pn(1 - badshare_absent); p("\n" as *u8) 1322 p(" auth: gate-bootstrap=" as *u8); pn(gate_bootstrap); p(" no-leak=" as *u8); pn(1 - gate_leaks) 1323 p(" post-401=" as *u8); pn(post_401); p(" evil-blocked=" as *u8); pn(evil_blocked) 1324 p(" tok-embedded=" as *u8); pn(tok_embedded); p("\n" as *u8) 1325 p(" WALL: authed-no-grant-denies-all=" as *u8); pn(wall_denies_all); p(" write-403=" as *u8); pn(write_403) 1326 p(" bootstrap-self-grant=" as *u8); pn(boot_grant_ok); p(" bootstrap-CLOSES=" as *u8); pn(boot_closed); p("\n" as *u8) 1327 p(" admin-sees(rose=" as *u8); pn(authed_shows); p(",amt=" as *u8); pn(admin_sees_amt); p(") other-tenant-ward-WALLED=" as *u8); pn(ward_walled); p("\n" as *u8) 1328 p(" member-sees-records=" as *u8); pn(member_sees); p(" but-amount-field-masked=" as *u8); pn(member_masked) 1329 p(" cross-tenant-write-403=" as *u8); pn(cross_403); p(" nothing-persisted=" as *u8); pn(cross_blocked); p("\n" as *u8) 1330 p(" sess-parse: hdr=" as *u8); pn(hdr_tok_ok); p(" field-urldecoded=" as *u8); pn(fld_tok_ok) 1331 p(" xss: raw-absent=" as *u8); pn(1 - xss_raw); p(" escaped=" as *u8); pn(xss_esc); p("\n" as *u8) 1332 p("NX-RELATE-SERVE " as *u8) 1333 if ok == 1 { p("verdict=GREEN (live-from-store app + R2 AUTH + R3.5 TENANT WALL: unauthed=bootstrap/401; authed-no-grant sees NOTHING; deny-by-default wall beats admin; cross-tenant read+write walled; giving amounts field-masked below manager; bootstrap self-grant closes after first; stored data HTML-escaped)\n" as *u8); return 0 } 1334 p("verdict=RED\n" as *u8) 1335 return 1 1336}