code wiki / _hdl_build / nx_connect_accounts.nx

nx_connect_accounts.nx source

↩ module page · 336 lines · 14506 B

1// nx_connect_accounts.nx -- CONNECT accounts + PERSISTENCE (the "demo state resets on restart" gap, 2// the gateway from demo to product). PURE CORE (no main). Persists the SAME cs_world the daemon serves 3// (composes nx_connect_serve) to a per-account binary snapshot file dir/<account_id>.cst, so each 4// account's chat + events + poll SURVIVE a daemon restart AND are ISOLATED by construction (the 5// account_id is in the path -- account A can never read account B's file). Snapshot-on-mutation + 6// load-on-session; additive (rule 13): a save is a full snapshot, the prior file is replaced tmp+rename 7// so a crash never leaves a torn state. account_id = a stable i64 (a session-derived pseudonym, or a 8// handle hash for the demo). license_tier: ORIGINAL 9import "nx_connect_serve.nx" 10const ACC_MAGIC_4096: i64 = 4096 11const ACC_MAGIC_16777619: i64 = 16777619 12const ACC_MAGIC_1000003: i64 = 1000003 13 14const ACC_MAGIC0: i64 = 0x4e // 'N' 15const ACC_MAGIC1: i64 = 0x58 // 'X' 16const ACC_MAGIC2: i64 = 0x43 // 'C' 17const ACC_MAGIC3: i64 = 0x41 // 'A' 18 19// little-endian i64 write/read 20func acc_w64(p: *u8, off: i64, v: i64) -> i64 { 21 var x: i64 = v 22 var i: i64 = 0 23 while i < 8 { p[off+i] = (x & 0xff) as u8; x = x / 256; i = i + 1 } 24 return off + 8 25} 26func acc_r64(p: *u8, off: i64) -> i64 { 27 var v: i64 = 0 28 var i: i64 = 7 29 while i >= 0 { v = v * 256 + (p[off+i] as i64 & 0xff); i = i - 1 } 30 return v 31} 32// account_id -> path: dir + "/" + decimal(id) + ".cst" (id in the path = isolation by construction) 33func acc_path(dir: *u8, account_id: i64, out: *u8) -> i64 { 34 var o: i64 = 0 35 var i: i64 = 0 36 while dir[i] != (0 as u8) { out[o] = dir[i]; o = o + 1; i = i + 1 } 37 out[o] = 47 as u8; o = o + 1 // '/' 38 let t: *u8 = sys_mmap(28) 39 var m: i64 = account_id 40 var k: i64 = 0 41 if m <= 0 { out[o] = 48 as u8; o = o + 1 } else { 42 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 43 var j: i64 = 0 44 while j < k { out[o] = t[k-1-j]; o = o + 1; j = j + 1 } 45 } 46 let ext: *u8 = ".cst\x00" as *u8 47 var e: i64 = 0 48 while ext[e] != (0 as u8) { out[o] = ext[e]; o = o + 1; e = e + 1 } 49 out[o] = 0 as u8 50 return o 51} 52 53// SERIALIZE the ctx world -> buf. returns byte length. 54func acc_serialize(ctx: *i64, account_id: i64, buf: *u8) -> i64 { 55 let spk: *i64 = ctx[CS_SPK] as *i64 56 let txt: *u8 = ctx[CS_TXTA] as *u8 57 let corr: *u8 = ctx[CS_CORR] as *u8 58 let nmsg: i64 = ctx[CS_NMSG] 59 var o: i64 = 0 60 buf[0] = ACC_MAGIC0 as u8; buf[1] = ACC_MAGIC1 as u8; buf[2] = ACC_MAGIC2 as u8; buf[3] = ACC_MAGIC3 as u8 61 o = 4 62 o = acc_w64(buf, o, account_id) 63 o = acc_w64(buf, o, nmsg) 64 var i: i64 = 0 65 while i < nmsg { 66 o = acc_w64(buf, o, spk[i]) 67 var k: i64 = 0 68 while k < CS_TXT { buf[o+k] = txt[i*CS_TXT+k]; k = k + 1 } 69 o = o + CS_TXT 70 k = 0 71 while k < CS_TXT { buf[o+k] = corr[i*CS_TXT+k]; k = k + 1 } 72 o = o + CS_TXT 73 i = i + 1 74 } 75 o = acc_w64(buf, o, ctx[CS_EV1_GOING]) 76 o = acc_w64(buf, o, ctx[CS_EV2_ADULTS]) 77 o = acc_w64(buf, o, ctx[CS_EV2_MINORS]) 78 o = acc_w64(buf, o, ctx[8]) // poll vote 79 // 2026-07-23 APPENDED (rule 19 additive): reconnection + community state. Strictly at the END, and the 80 // reader treats every field below as optional, so a snapshot written before this change still loads. 81 o = acc_w64(buf, o, ctx[CS_RC_CONSENT]) 82 o = acc_w64(buf, o, ctx[CS_RC_MISSION]) 83 o = acc_w64(buf, o, ctx[CS_RC_AREA]) 84 o = acc_w64(buf, o, ctx[CS_RC_FROM]) 85 o = acc_w64(buf, o, ctx[CS_RC_TO]) 86 o = acc_w64(buf, o, ctx[CS_RC_DONE]) 87 o = acc_w64(buf, o, ctx[CS_WARD]) 88 o = acc_w64(buf, o, ctx[CS_FAITH_CONSENT]) 89 o = acc_w64(buf, o, ctx[CS_LANE_ME]) 90 o = acc_w64(buf, o, ctx[CS_LANE_THEM]) 91 o = acc_w64(buf, o, ctx[CS_BLOCKED]) 92 // content-blind ciphertext log (count + fixed-width entries). The server persists opaque ciphertext. 93 o = acc_w64(buf, o, ctx[CS_NCT]) 94 let ctl: *u8 = ctx[CS_CT] as *u8 95 let nct: i64 = ctx[CS_NCT] 96 var ci: i64 = 0 97 while ci < nct { 98 var k: i64 = 0 99 while k < CS_CTHEX { buf[o+k] = ctl[ci*CS_CTHEX+k]; k = k + 1 } 100 o = o + CS_CTHEX 101 ci = ci + 1 102 } 103 // 2026-07-25 APPENDED (rule 19 additive): the per-account contact list. Strictly at the END; an older 104 // snapshot without it simply stops before this and cs_world_reset's default (0 contacts) stands. 105 o = acc_w64(buf, o, ctx[CS_NCONTACTS]) 106 let contacts: *u8 = ctx[CS_CONTACTS] as *u8 107 let ncc: i64 = ctx[CS_NCONTACTS] 108 var cci: i64 = 0 109 while cci < ncc { 110 var k2: i64 = 0 111 while k2 < CS_CONTACT_LEN { buf[o+k2] = contacts[cci*CS_CONTACT_LEN+k2]; k2 = k2 + 1 } 112 o = o + CS_CONTACT_LEN 113 cci = cci + 1 114 } 115 return o 116} 117// DESERIALIZE buf (n bytes) into ctx. returns 1 ok, 0 bad-magic/short. 118func acc_deserialize(ctx: *i64, buf: *u8, n: i64) -> i64 { 119 if n < 28 { return 0 } 120 if (buf[0] as i64 & 0xff) != ACC_MAGIC0 { return 0 } 121 if (buf[1] as i64 & 0xff) != ACC_MAGIC1 { return 0 } 122 if (buf[2] as i64 & 0xff) != ACC_MAGIC2 { return 0 } 123 if (buf[3] as i64 & 0xff) != ACC_MAGIC3 { return 0 } 124 let spk: *i64 = ctx[CS_SPK] as *i64 125 let txt: *u8 = ctx[CS_TXTA] as *u8 126 let corr: *u8 = ctx[CS_CORR] as *u8 127 var o: i64 = 4 128 o = o + 8 // skip account_id 129 let nmsg: i64 = acc_r64(buf, o); o = o + 8 130 var cap_msg: i64 = nmsg 131 if cap_msg > CS_MSGCAP { cap_msg = CS_MSGCAP } 132 var i: i64 = 0 133 while i < cap_msg { 134 spk[i] = acc_r64(buf, o); o = o + 8 135 var k: i64 = 0 136 while k < CS_TXT { txt[i*CS_TXT+k] = buf[o+k]; k = k + 1 } 137 o = o + CS_TXT 138 k = 0 139 while k < CS_TXT { corr[i*CS_TXT+k] = buf[o+k]; k = k + 1 } 140 o = o + CS_TXT 141 i = i + 1 142 } 143 ctx[CS_NMSG] = cap_msg 144 ctx[CS_EV1_GOING] = acc_r64(buf, o); o = o + 8 145 ctx[CS_EV2_ADULTS] = acc_r64(buf, o); o = o + 8 146 ctx[CS_EV2_MINORS] = acc_r64(buf, o); o = o + 8 147 ctx[8] = acc_r64(buf, o); o = o + 8 148 // OPTIONAL TAIL: each field is read only if the snapshot actually carries it. A pre-2026-07-23 file 149 // simply stops here and the caller's cs_world_reset defaults stand -- consent therefore defaults to 150 // OFF on an old snapshot, which is the fail-SAFE direction for a privacy flag. 151 if o + 8 <= n { ctx[CS_RC_CONSENT] = acc_r64(buf, o); o = o + 8 } 152 if o + 8 <= n { ctx[CS_RC_MISSION] = acc_r64(buf, o); o = o + 8 } 153 if o + 8 <= n { ctx[CS_RC_AREA] = acc_r64(buf, o); o = o + 8 } 154 if o + 8 <= n { ctx[CS_RC_FROM] = acc_r64(buf, o); o = o + 8 } 155 if o + 8 <= n { ctx[CS_RC_TO] = acc_r64(buf, o); o = o + 8 } 156 if o + 8 <= n { ctx[CS_RC_DONE] = acc_r64(buf, o); o = o + 8 } 157 if o + 8 <= n { ctx[CS_WARD] = acc_r64(buf, o); o = o + 8 } 158 if o + 8 <= n { ctx[CS_FAITH_CONSENT] = acc_r64(buf, o); o = o + 8 } 159 if o + 8 <= n { ctx[CS_LANE_ME] = acc_r64(buf, o); o = o + 8 } 160 if o + 8 <= n { ctx[CS_LANE_THEM] = acc_r64(buf, o); o = o + 8 } 161 if o + 8 <= n { ctx[CS_BLOCKED] = acc_r64(buf, o); o = o + 8 } 162 if o + 8 <= n { 163 ctx[CS_NCT] = acc_r64(buf, o); o = o + 8 164 var nct: i64 = ctx[CS_NCT] 165 if nct > CS_MSGCAP { nct = CS_MSGCAP; ctx[CS_NCT] = CS_MSGCAP } 166 let ctl: *u8 = ctx[CS_CT] as *u8 167 var ci: i64 = 0 168 while ci < nct { 169 if o + CS_CTHEX <= n { 170 var k: i64 = 0 171 while k < CS_CTHEX { ctl[ci*CS_CTHEX+k] = buf[o+k]; k = k + 1 } 172 o = o + CS_CTHEX 173 } 174 ci = ci + 1 175 } 176 } 177 // OPTIONAL TAIL: the contact list (additive; a pre-2026-07-25 snapshot stops before this). 178 if o + 8 <= n { 179 ctx[CS_NCONTACTS] = acc_r64(buf, o); o = o + 8 180 var ncc: i64 = ctx[CS_NCONTACTS] 181 if ncc > CS_CONTACT_MAX { ncc = CS_CONTACT_MAX; ctx[CS_NCONTACTS] = CS_CONTACT_MAX } 182 if ncc < 0 { ncc = 0; ctx[CS_NCONTACTS] = 0 } 183 let contacts: *u8 = ctx[CS_CONTACTS] as *u8 184 var cci: i64 = 0 185 while cci < ncc { 186 if o + CS_CONTACT_LEN <= n { 187 var k2: i64 = 0 188 while k2 < CS_CONTACT_LEN { contacts[cci*CS_CONTACT_LEN+k2] = buf[o+k2]; k2 = k2 + 1 } 189 o = o + CS_CONTACT_LEN 190 } 191 cci = cci + 1 192 } 193 } 194 return 1 195} 196// SAVE ctx -> dir/<account_id>.cst (tmp+rename atomic; the prior snapshot is never torn). 197func acc_save(ctx: *i64, account_id: i64, dir: *u8) -> i64 { 198 let buf: *u8 = sys_mmap(CS_MSGCAP*(CS_TXT*2+8) + CS_MSGCAP*CS_CTHEX + CS_CONTACT_MAX*CS_CONTACT_LEN + ACC_MAGIC_4096) 199 let n: i64 = acc_serialize(ctx, account_id, buf) 200 let path: *u8 = sys_mmap(512) 201 acc_path(dir, account_id, path) 202 let tmp: *u8 = sys_mmap(512) 203 var o: i64 = 0 204 while path[o] != (0 as u8) { tmp[o] = path[o]; o = o + 1 } 205 tmp[o] = 46 as u8; tmp[o+1] = 116 as u8; tmp[o+2] = 109 as u8; tmp[o+3] = 112 as u8; tmp[o+4] = 0 as u8 // ".tmp" 206 let fd: i64 = sys_openat_wr(tmp, 0x1a4) 207 if fd < 0 { return 0-1 } 208 sys_write(fd, buf, n) 209 sys_close(fd) 210 if sys_renameat(tmp, path) < 0 { return 0-2 } 211 return n 212} 213// LOAD dir/<account_id>.cst -> ctx. returns 1 loaded, 0 absent (caller keeps the fresh world). 214func acc_load(ctx: *i64, account_id: i64, dir: *u8) -> i64 { 215 let path: *u8 = sys_mmap(512) 216 acc_path(dir, account_id, path) 217 let fd: i64 = sys_openat_rd(path) 218 if fd < 0 { return 0 } 219 // buffer must fit the WHOLE serialised world (msgs + ciphertext log + contacts) or a full snapshot would 220 // silently truncate on read -- fixed 2026-07-25 (also closes a latent ciphertext-log truncation). 221 let buf: *u8 = sys_mmap(CS_MSGCAP*(CS_TXT*2+8) + CS_MSGCAP*CS_CTHEX + CS_CONTACT_MAX*CS_CONTACT_LEN + ACC_MAGIC_4096) 222 var total: i64 = 0 223 let cap: i64 = CS_MSGCAP*(CS_TXT*2+8) + CS_MSGCAP*CS_CTHEX + CS_CONTACT_MAX*CS_CONTACT_LEN + ACC_MAGIC_4096 224 while total < cap { 225 let r: i64 = sys_read(fd, (buf as i64 + total) as *u8, cap - total) 226 if r <= 0 { break } 227 total = total + r 228 } 229 sys_close(fd) 230 return acc_deserialize(ctx, buf, total) 231} 232// derive a stable account_id from a handle string (fnv-1a-ish; a session pseudonym would replace this) 233func acc_id_from_handle(h: *u8) -> i64 { 234 var hash: i64 = 0x811c9dc5 235 var i: i64 = 0 236 while h[i] != (0 as u8) { 237 hash = hash ^ (h[i] as i64 & 0xff) 238 hash = (hash * ACC_MAGIC_16777619) & 0x7fffffffffffffff 239 i = i + 1 240 } 241 if hash <= 0 { hash = 1 } 242 return hash 243} 244// fnv over a byte range (session id extracted from a header, not NUL-terminated) 245func acc_id_from_range(p: *u8, n: i64) -> i64 { 246 var hash: i64 = 0x811c9dc5 247 var i: i64 = 0 248 while i < n { 249 hash = hash ^ (p[i] as i64 & 0xff) 250 hash = (hash * ACC_MAGIC_16777619) & 0x7fffffffffffffff 251 i = i + 1 252 } 253 if hash <= 0 { hash = 1 } 254 return hash 255} 256 257// ---- R2: anonymous SESSION layer -- each visitor gets a cookie -> their own isolated persistent world. 258// extract the value of "nxc_sess=<value>" from the request's Cookie header into out (NUL-terminated); 259// returns the value length, 0 if no such cookie. Scans the raw request bytes for "nxc_sess=". 260func acc_cookie_get(req: *u8, reqlen: i64, out: *u8, cap: i64) -> i64 { 261 let key: *u8 = "nxc_sess=" as *u8 262 let kl: i64 = 9 263 var i: i64 = 0 264 while i + kl <= reqlen { 265 var m: i64 = 1 266 var j: i64 = 0 267 while j < kl { if req[i+j] != key[j] { m = 0; j = kl } else { j = j + 1 } } 268 if m == 1 { 269 var q: i64 = i + kl 270 var t: i64 = 0 271 var go: i64 = 1 272 while go == 1 { 273 if q >= reqlen { go = 0 } else { 274 let c: i64 = req[q] as i64 & 0xff 275 // cookie value ends at ; , whitespace, CR or LF 276 if c == 59 { go = 0 } else { if c == 32 { go = 0 } else { if c == 13 { go = 0 } else { if c == 10 { go = 0 } else { 277 if t < cap - 1 { out[t] = c as u8; t = t + 1 } 278 q = q + 1 279 } } } } 280 } 281 } 282 out[t] = 0 as u8 283 return t 284 } 285 i = i + 1 286 } 287 out[0] = 0 as u8 288 return 0 289} 290// mint a fresh session id into out (NUL-terminated) from time+counter (unique per new visitor; the 291// caller passes a monotonically-increasing salt so two mints in the same ms still differ). returns len. 292func acc_session_mint(salt: i64, out: *u8) -> i64 { 293 var v: i64 = sys_now_realtime_ms() 294 v = (v * ACC_MAGIC_1000003 + salt) & 0x7fffffffffffffff 295 if v <= 0 { v = salt + 1 } 296 let t: *u8 = sys_mmap(28) 297 var m: i64 = v 298 var k: i64 = 0 299 if m == 0 { t[0] = 48 as u8; k = 1 } 300 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 301 var o: i64 = 0 302 while o < k { out[o] = t[k-1-o]; o = o + 1 } 303 out[o] = 0 as u8 304 return o 305} 306// INSERT "Set-Cookie: nxc_sess=<sess>; Path=/; HttpOnly; SameSite=Lax\r\n" right after the HTTP status 307// line of resp (resp_len bytes) -> out (cap). returns new length. Used by the daemon only when it minted 308// a NEW session (existing sessions keep their cookie). Path-anchored so it covers /connect and its POSTs. 309func acc_inject_cookie(resp: *u8, resp_len: i64, sess: *u8, sess_len: i64, out: *u8, cap: i64) -> i64 { 310 // find end of the status line (first \r\n) 311 var eol: i64 = 0 - 1 312 var i: i64 = 0 313 while i + 1 < resp_len { 314 if resp[i] == (13 as u8) { if resp[i+1] == (10 as u8) { eol = i + 2; i = resp_len } } 315 i = i + 1 316 } 317 if eol < 0 { // no headers found -> passthrough 318 var k: i64 = 0 319 while k < resp_len { if k < cap { out[k] = resp[k] } k = k + 1 } 320 return resp_len 321 } 322 var o: i64 = 0 323 var a: i64 = 0 324 while a < eol { if o < cap { out[o] = resp[a] } o = o + 1; a = a + 1 } 325 let pre: *u8 = "Set-Cookie: nxc_sess=" as *u8 326 var pi: i64 = 0 327 while pre[pi] != (0 as u8) { if o < cap { out[o] = pre[pi] } o = o + 1; pi = pi + 1 } 328 var si: i64 = 0 329 while si < sess_len { if o < cap { out[o] = sess[si] } o = o + 1; si = si + 1 } 330 let post: *u8 = "; Path=/; HttpOnly; SameSite=Lax\r\n" as *u8 331 var qi: i64 = 0 332 while post[qi] != (0 as u8) { if o < cap { out[o] = post[qi] } o = o + 1; qi = qi + 1 } 333 var b: i64 = eol 334 while b < resp_len { if o < cap { out[o] = resp[b] } o = o + 1; b = b + 1 } 335 return o 336}