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}