nx_wiki_vhost_table.nx source
↩ module page · 199 lines · 7713 B
1// nx_wiki_vhost_table.nx -- V-HOST-2.5: per-vhost wiki state table.
2//
3// Extends V-HOST-2 single-vhost daemon to N-vhost. Operator hosts
4// nishifamily.com + andelinwest.com + jasonewest.com on a single
5// substrate process; per-request Host header picks which site's wiki
6// state to dispatch to.
7//
8// COMPOSES (per "avoid duplicate primitives"):
9// nx_syscalls (caller-allocated structs)
10// hub/nx_admin_login_flow / nx_search_handler_flow / nx_search_inverted
11// wiki/nx_wiki_index_builder + nx_wiki_archive_router + nx_artifact_store
12//
13// COMPOSED BY:
14// wiki/nx_wiki_https_daemon (V-HOST-2.5 multi-vhost dispatch)
15//
16// V-HOST-2.5 SCOPE:
17// - NxWikiVHost struct: hostname + per-site wiki state pointers
18// - NxWikiVHostTable: array of NxWikiVHost; bounded by NX_WVT_MAX_VHOSTS
19// - nx_wiki_vhost_add: register a site at startup
20// - nx_wiki_vhost_lookup_by_host: case-insensitive Host header match
21// - Sealed fallback policy: 0=no fallback (404 on no match); idx=fallback to that vhost
22//
23// V-HOST-2.5 NON-SCOPE (queued):
24// - SNI-aware cert selection during TLS handshake (currently shared
25// multi-SAN cert across all vhosts; V-HOST-2.5.5)
26// - Per-vhost cert (requires extending nx_tls13_server_session_run
27// with cert-picker callback; V-HOST-2.5.5)
28// - Per-vhost admin auth (currently shared admin across vhosts;
29// operator may want per-vhost in V-HOST-2.5.7)
30//
31// Status: V-HOST-2.5. 2026-05-27.
32
33import "nx_syscalls.nx"
34import "hub/nx_modern_auth_flow.nx"
35import "hub/nx_search_handler_flow.nx"
36import "nx_search_inverted.nx"
37import "wiki/nx_wiki_index_builder.nx"
38import "wiki/nx_wiki_archive_router.nx"
39import "wiki/nx_artifact_store.nx"
40
41// ===== Sealed verdict surface (codes 3820-3829) =================================================
42const NX_WVT_OK: i64 = 0
43const NX_WVT_BAD_INPUT: i64 = 3820
44const NX_WVT_TABLE_FULL: i64 = 3821
45const NX_WVT_NOT_FOUND: i64 = 3822
46const NX_WVT_HOSTNAME_TOO_LONG: i64 = 3823
47
48// ===== Named constants (M7) =================================================
49const NX_WVT_MAX_VHOSTS: i64 = 16 // bounded; V+1 increases when corpus grows
50const NX_WVT_MAX_HOSTNAME_LEN: i64 = 253 // RFC 1035 per-label * total
51const NX_WVT_FALLBACK_NONE: i64 = 0 - 1
52
53// ===== Per-vhost state (caller-allocated per site) =================================================
54//
55// Each vhost holds ptrs to its OWN per-site state. The daemon allocates
56// N sets of these at startup; the table indexes them by hostname.
57
58struct NxWikiVHost {
59 hostname: *u8
60 hostname_n: i64
61 search_flow: *NxSearchFlow // per-site search context
62 search_idx: *NxInvIndex // per-site inverted index
63 doc_store: *NxWikiDocStore // per-site doc bodies
64 archive_store: *NxArchiveStore // per-site wayback archive
65 artifact_store: *NxArtifactStore // per-site pipeline state
66 admin_cfg: *NxAuthContext // V-MODAUTH-6 per-site auth context (or shared)
67}
68
69const NX_WVT_VHOST_BYTES: i64 = 72
70
71// ===== Table =================================================
72
73struct NxWikiVHostTable {
74 vhosts: *NxWikiVHost
75 vhosts_cap: i64
76 vhosts_count: i64
77 fallback_idx: i64 // NX_WVT_FALLBACK_NONE or [0..vhosts_count)
78 valid: i64
79}
80
81func nx_wiki_vhost_table_init(t: *NxWikiVHostTable) -> i64 {
82 if (t as i64) == 0 { return 0 - NX_WVT_BAD_INPUT }
83 t.vhosts = (sys_mmap(NX_WVT_MAX_VHOSTS * NX_WVT_VHOST_BYTES)) as *NxWikiVHost
84 t.vhosts_cap = NX_WVT_MAX_VHOSTS
85 t.vhosts_count = 0
86 t.fallback_idx = NX_WVT_FALLBACK_NONE
87 t.valid = 1
88 return NX_WVT_OK
89}
90
91func nx_wiki_vhost_add(t: *NxWikiVHostTable,
92 hostname: *u8, hostname_n: i64,
93 search_flow: *NxSearchFlow,
94 search_idx: *NxInvIndex,
95 doc_store: *NxWikiDocStore,
96 archive_store: *NxArchiveStore,
97 artifact_store: *NxArtifactStore,
98 admin_cfg: *NxAuthContext) -> i64 {
99 if t.valid != 1 { return 0 - NX_WVT_BAD_INPUT }
100 if t.vhosts_count >= t.vhosts_cap { return 0 - NX_WVT_TABLE_FULL }
101 if hostname_n < 1 { return 0 - NX_WVT_BAD_INPUT }
102 if hostname_n > NX_WVT_MAX_HOSTNAME_LEN { return 0 - NX_WVT_HOSTNAME_TOO_LONG }
103
104 let v: *NxWikiVHost = ((t.vhosts as i64) + t.vhosts_count * NX_WVT_VHOST_BYTES) as *NxWikiVHost
105 v.hostname = hostname
106 v.hostname_n = hostname_n
107 v.search_flow = search_flow
108 v.search_idx = search_idx
109 v.doc_store = doc_store
110 v.archive_store = archive_store
111 v.artifact_store = artifact_store
112 v.admin_cfg = admin_cfg
113
114 let idx: i64 = t.vhosts_count
115 t.vhosts_count = t.vhosts_count + 1
116 return idx
117}
118
119func nx_wiki_vhost_set_fallback(t: *NxWikiVHostTable, idx: i64) -> i64 {
120 if t.valid != 1 { return 0 - NX_WVT_BAD_INPUT }
121 if idx < 0 { t.fallback_idx = NX_WVT_FALLBACK_NONE; return NX_WVT_OK }
122 if idx >= t.vhosts_count { return 0 - NX_WVT_BAD_INPUT }
123 t.fallback_idx = idx
124 return NX_WVT_OK
125}
126
127// ===== Case-insensitive byte compare =================================================
128//
129// ASCII lowercase fold; substrate convention. Hostnames are ASCII per
130// RFC 1035 ยง2.3.1 (LDH; letters, digits, hyphen; case-insensitive).
131
132func _nx_wvt_lc(b: i64) -> i64 {
133 if b >= 0x41 { if b <= 0x5A { return b + 32 } }
134 return b
135}
136
137func _nx_wvt_eq_ci(a: *u8, a_n: i64, b: *u8, b_n: i64) -> i64 {
138 if a_n != b_n { return 0 }
139 var i: i64 = 0
140 while i < a_n {
141 let ca: i64 = _nx_wvt_lc(a[i] as i64)
142 let cb: i64 = _nx_wvt_lc(b[i] as i64)
143 if ca != cb { return 0 }
144 i = i + 1
145 }
146 return 1
147}
148
149// ===== Lookup by Host header value =================================================
150//
151// Returns vhost rowid (>= 0) on case-insensitive hostname match;
152// falls back to fallback_idx (or -1) on no match.
153//
154// host_value may include ":port" suffix per HTTP/1.1 spec; we strip
155// before comparison.
156
157func nx_wiki_vhost_lookup_by_host(t: *NxWikiVHostTable,
158 host_value: *u8, host_value_n: i64) -> i64 {
159 if t.valid != 1 { return 0 - NX_WVT_BAD_INPUT }
160 if host_value_n < 1 {
161 if t.fallback_idx >= 0 { return t.fallback_idx }
162 return 0 - NX_WVT_NOT_FOUND
163 }
164
165 // Strip ":port" suffix
166 var name_n: i64 = host_value_n
167 var i: i64 = 0
168 while i < host_value_n {
169 if host_value[i] == (0x3A as u8) { name_n = i; i = host_value_n + 1 }
170 if i <= host_value_n { if i < host_value_n { i = i + 1 } }
171 }
172
173 var v_i: i64 = 0
174 while v_i < t.vhosts_count {
175 let v: *NxWikiVHost = ((t.vhosts as i64) + v_i * NX_WVT_VHOST_BYTES) as *NxWikiVHost
176 if _nx_wvt_eq_ci(v.hostname, v.hostname_n, host_value, name_n) == 1 {
177 return v_i
178 }
179 v_i = v_i + 1
180 }
181
182 // No match -> fallback (or -1)
183 if t.fallback_idx >= 0 { return t.fallback_idx }
184 return 0 - NX_WVT_NOT_FOUND
185}
186
187// ===== Accessor: get NxWikiVHost ptr by rowid =================================================
188
189func nx_wiki_vhost_at(t: *NxWikiVHostTable, idx: i64) -> *NxWikiVHost {
190 if t.valid != 1 { return 0 as *NxWikiVHost }
191 if idx < 0 { return 0 as *NxWikiVHost }
192 if idx >= t.vhosts_count { return 0 as *NxWikiVHost }
193 return ((t.vhosts as i64) + idx * NX_WVT_VHOST_BYTES) as *NxWikiVHost
194}
195
196func nx_wiki_vhost_count(t: *NxWikiVHostTable) -> i64 {
197 if t.valid != 1 { return 0 }
198 return t.vhosts_count
199}