nx_nxsite_spec.nx source
↩ module page · 413 lines · 16406 B
1// nx_nxsite_spec.nx -- V-PROV-1: sealed NxSiteDeploymentSpec struct +
2// sealed-enum kinds + per-field constants.
3//
4// Per NISHI_SITE_PROVISIONING_CHARTER §3: ONE sealed declarative spec
5// drives all three frontends (CLI / UI / AI) -> one provisioner backend.
6// This module defines the data model; parser (nx_nxsite_parse) reads
7// .nxsite files; validator (nx_nxsite_validate) checks integrity;
8// applier (nx_site_provisioner) executes.
9//
10// COMPOSES (per "avoid duplicate primitives"):
11// nx_syscalls.sys_mmap (struct allocation)
12// Reuses NxVHostTable + nx_pages_config conventions where applicable
13//
14// COMPOSED BY:
15// hub/nx_nxsite_parse.nx (line-walker fills this struct from .nxsite text)
16// hub/nx_nxsite_validate.nx (checks sealed-enum ranges + cross-section consistency)
17// hub/nx_site_provisioner.nx (V-PROV-2; applies sealed spec to live substrate)
18//
19// V-PROV-1 SCOPE:
20// - 9 sealed sections (per charter §3.2): site, dns, tls, content,
21// auth, routes, rate_limit, telemetry, deploy
22// - Sealed enum kinds for cert_kind, content_format, route_handler
23// - String pool storage (caller-allocated; M4 buf-own)
24// - Per-section sub-arrays (DNS records, routes, rate_limit buckets)
25//
26// Status: V-PROV-1. 2026-05-27.
27
28import "nx_syscalls.nx"
29
30// ===== Sealed verdict surface (codes 3500-3519) =================================================
31const NX_NSPEC_OK: i64 = 0
32const NX_NSPEC_BAD_INPUT: i64 = 3500
33const NX_NSPEC_POOL_FULL: i64 = 3501
34const NX_NSPEC_TOO_MANY_RECORDS: i64 = 3502
35const NX_NSPEC_TOO_MANY_ROUTES: i64 = 3503
36const NX_NSPEC_TOO_MANY_BUCKETS: i64 = 3504
37const NX_NSPEC_VALUE_TOO_LONG: i64 = 3505
38const NX_NSPEC_BAD_ENUM: i64 = 3506
39
40// ===== Named constants (M7) =================================================
41const NX_NSPEC_STRING_POOL_CAP: i64 = 65536 // 64 KB packed string pool
42const NX_NSPEC_MAX_DNS_RECORDS: i64 = 64
43const NX_NSPEC_MAX_ROUTES: i64 = 32
44const NX_NSPEC_MAX_RATE_BUCKETS: i64 = 16
45const NX_NSPEC_MAX_FIELD_LEN: i64 = 1024 // per-value cap
46const NX_NSPEC_MAX_NAME_LEN: i64 = 255 // hostname per RFC 1035
47const NX_NSPEC_SPEC_VERSION: *u8 = "1.0.0" as *u8
48const NX_NSPEC_SPEC_VERSION_N: i64 = 5
49
50// ===== Sealed enum: TLS cert kind =================================================
51const NX_NSPEC_CERT_KIND_NONE: i64 = 0
52const NX_NSPEC_CERT_KIND_SOVEREIGN_SELF: i64 = 1 // sovereign self-signed
53const NX_NSPEC_CERT_KIND_SOVEREIGN_MINI_CA: i64 = 2 // sovereign operator-CA-issued (V+2)
54const NX_NSPEC_CERT_KIND_N: i64 = 3
55
56func nx_nspec_cert_kind_name(k: i64) -> *u8 {
57 if k == NX_NSPEC_CERT_KIND_NONE { return "NONE" as *u8 }
58 if k == NX_NSPEC_CERT_KIND_SOVEREIGN_SELF { return "sovereign_self_signed" as *u8 }
59 if k == NX_NSPEC_CERT_KIND_SOVEREIGN_MINI_CA { return "sovereign_mini_ca" as *u8 }
60 return "UNKNOWN" as *u8
61}
62
63// ===== Sealed enum: content format =================================================
64const NX_NSPEC_CONTENT_NONE: i64 = 0
65const NX_NSPEC_CONTENT_NISHI_PAGE_V1: i64 = 1
66const NX_NSPEC_CONTENT_STATIC_HTML: i64 = 2
67const NX_NSPEC_CONTENT_MIRROR_LAYER: i64 = 3
68const NX_NSPEC_CONTENT_N: i64 = 4
69
70func nx_nspec_content_format_name(f: i64) -> *u8 {
71 if f == NX_NSPEC_CONTENT_NONE { return "NONE" as *u8 }
72 if f == NX_NSPEC_CONTENT_NISHI_PAGE_V1 { return "nishi_page_v1" as *u8 }
73 if f == NX_NSPEC_CONTENT_STATIC_HTML { return "static_html" as *u8 }
74 if f == NX_NSPEC_CONTENT_MIRROR_LAYER { return "mirror_layer" as *u8 }
75 return "UNKNOWN" as *u8
76}
77
78// ===== Sealed enum: route handler kind =================================================
79const NX_NSPEC_ROUTE_NONE: i64 = 0
80const NX_NSPEC_ROUTE_STATIC: i64 = 1
81const NX_NSPEC_ROUTE_WIKI_DAEMON: i64 = 2
82const NX_NSPEC_ROUTE_SEARCH_HANDLER: i64 = 3
83const NX_NSPEC_ROUTE_ADMIN_PANEL: i64 = 4
84const NX_NSPEC_ROUTE_SITE_API: i64 = 5
85const NX_NSPEC_ROUTE_404: i64 = 6
86const NX_NSPEC_ROUTE_N: i64 = 7
87
88func nx_nspec_route_handler_name(h: i64) -> *u8 {
89 if h == NX_NSPEC_ROUTE_NONE { return "NONE" as *u8 }
90 if h == NX_NSPEC_ROUTE_STATIC { return "static" as *u8 }
91 if h == NX_NSPEC_ROUTE_WIKI_DAEMON { return "wiki_daemon" as *u8 }
92 if h == NX_NSPEC_ROUTE_SEARCH_HANDLER { return "search_handler" as *u8 }
93 if h == NX_NSPEC_ROUTE_ADMIN_PANEL { return "admin_panel" as *u8 }
94 if h == NX_NSPEC_ROUTE_SITE_API { return "site_api_handler" as *u8 }
95 if h == NX_NSPEC_ROUTE_404 { return "404" as *u8 }
96 return "UNKNOWN" as *u8
97}
98
99// ===== Sealed enum: DNS record type =================================================
100const NX_NSPEC_DNS_NONE: i64 = 0
101const NX_NSPEC_DNS_A: i64 = 1
102const NX_NSPEC_DNS_AAAA: i64 = 2
103const NX_NSPEC_DNS_MX: i64 = 3
104const NX_NSPEC_DNS_TXT: i64 = 4
105const NX_NSPEC_DNS_CNAME: i64 = 5
106const NX_NSPEC_DNS_NS: i64 = 6
107const NX_NSPEC_DNS_N: i64 = 7
108
109func nx_nspec_dns_kind_name(k: i64) -> *u8 {
110 if k == NX_NSPEC_DNS_A { return "a" as *u8 }
111 if k == NX_NSPEC_DNS_AAAA { return "aaaa" as *u8 }
112 if k == NX_NSPEC_DNS_MX { return "mx" as *u8 }
113 if k == NX_NSPEC_DNS_TXT { return "txt" as *u8 }
114 if k == NX_NSPEC_DNS_CNAME { return "cname" as *u8 }
115 if k == NX_NSPEC_DNS_NS { return "ns" as *u8 }
116 return "UNKNOWN" as *u8
117}
118
119// ===== Sub-structs for per-section repeating items =================================================
120
121// DNS record: kind + name_off/len + value_off/len + extra_off/len (for MX priority, etc.)
122struct NxNspecDnsRecord {
123 kind: i64 // sealed NX_NSPEC_DNS_*
124 name_off: i64 // into string pool
125 name_len: i64
126 value_off: i64
127 value_len: i64
128 extra_off: i64 // MX priority / TXT etc.; 0 if unused
129 extra_len: i64
130}
131
132const NX_NSPEC_DNS_RECORD_BYTES: i64 = 56
133
134// Route: path prefix + handler kind
135struct NxNspecRoute {
136 prefix_off: i64 // into string pool
137 prefix_len: i64
138 handler: i64 // sealed NX_NSPEC_ROUTE_*
139}
140
141const NX_NSPEC_ROUTE_BYTES: i64 = 24
142
143// Rate limit bucket: prefix + quota + per_seconds + scope (global / per_ip)
144struct NxNspecRateBucket {
145 prefix_off: i64
146 prefix_len: i64
147 quota: i64 // requests
148 per_seconds: i64 // window
149 scope: i64 // 0 = global; 1 = per_ip
150}
151
152const NX_NSPEC_RATE_BUCKET_BYTES: i64 = 40
153
154// ===== Top-level NxSiteDeploymentSpec =================================================
155//
156// String pool stores all variable-length strings (names, paths, values).
157// Per-field offsets/lengths reference into pool.
158
159struct NxSiteDeploymentSpec {
160 // String pool (M4 caller-allocated)
161 pool: *u8
162 pool_cap: i64
163 pool_used: i64
164
165 // [site]
166 site_name_off: i64
167 site_name_len: i64
168 site_canonical_off: i64
169 site_canonical_len: i64
170 site_description_off: i64
171 site_description_len: i64
172 site_license_off: i64
173 site_license_len: i64
174 site_spec_version_off: i64
175 site_spec_version_len: i64
176
177 // [dns]
178 dns_records: *NxNspecDnsRecord
179 dns_records_cap: i64
180 dns_records_count: i64
181 dns_ns_primary_off: i64
182 dns_ns_primary_len: i64
183 dns_ns_secondary_off: i64
184 dns_ns_secondary_len: i64
185
186 // [tls]
187 tls_cert_kind: i64 // sealed enum
188 tls_signing_key_off: i64
189 tls_signing_key_len: i64
190 tls_cert_validity_days: i64
191 tls_san_off: i64 // comma-separated SANs
192 tls_san_len: i64
193
194 // [content]
195 content_root_off: i64
196 content_root_len: i64
197 content_format: i64 // sealed enum
198 content_index_off: i64
199 content_index_len: i64
200 content_language_off: i64
201 content_language_len: i64
202
203 // [auth]
204 auth_enabled: i64 // 0|1
205 auth_admin_user_off: i64
206 auth_admin_user_len: i64
207 auth_hash_file_off: i64
208 auth_hash_file_len: i64
209 auth_session_max_age: i64 // seconds
210
211 // [routes]
212 routes: *NxNspecRoute
213 routes_cap: i64
214 routes_count: i64
215
216 // [rate_limit]
217 rate_buckets: *NxNspecRateBucket
218 rate_buckets_cap: i64
219 rate_buckets_count: i64
220
221 // [telemetry]
222 tele_enabled: i64 // 0|1
223 tele_retention_days: i64
224 tele_namespace_off: i64
225 tele_namespace_len: i64
226 tele_redact_regex_off: i64
227 tele_redact_regex_len: i64
228
229 // [deploy]
230 deploy_blue_green: i64 // 0|1
231 deploy_signed_by_off: i64
232 deploy_signed_by_len: i64
233 deploy_prev_manifest_off: i64
234 deploy_prev_manifest_len: i64
235
236 valid: i64
237}
238
239// ===== Init =================================================
240
241func nx_nxsite_spec_init(s: *NxSiteDeploymentSpec,
242 pool: *u8, pool_cap: i64) -> i64 {
243 if (s as i64) == 0 { return 0 - NX_NSPEC_BAD_INPUT }
244 if (pool as i64) == 0 { return 0 - NX_NSPEC_BAD_INPUT }
245 if pool_cap < 4096 { return 0 - NX_NSPEC_BAD_INPUT }
246 if pool_cap > NX_NSPEC_STRING_POOL_CAP { return 0 - NX_NSPEC_POOL_FULL }
247
248 s.pool = pool
249 s.pool_cap = pool_cap
250 s.pool_used = 0
251
252 // Zero all offsets/lengths/enum values
253 s.site_name_off = 0; s.site_name_len = 0
254 s.site_canonical_off = 0; s.site_canonical_len = 0
255 s.site_description_off = 0; s.site_description_len = 0
256 s.site_license_off = 0; s.site_license_len = 0
257 s.site_spec_version_off = 0; s.site_spec_version_len = 0
258
259 s.dns_records = (sys_mmap(NX_NSPEC_MAX_DNS_RECORDS * NX_NSPEC_DNS_RECORD_BYTES)) as *NxNspecDnsRecord
260 s.dns_records_cap = NX_NSPEC_MAX_DNS_RECORDS
261 s.dns_records_count = 0
262 s.dns_ns_primary_off = 0; s.dns_ns_primary_len = 0
263 s.dns_ns_secondary_off = 0; s.dns_ns_secondary_len = 0
264
265 s.tls_cert_kind = NX_NSPEC_CERT_KIND_NONE
266 s.tls_signing_key_off = 0; s.tls_signing_key_len = 0
267 s.tls_cert_validity_days = 0
268 s.tls_san_off = 0; s.tls_san_len = 0
269
270 s.content_root_off = 0; s.content_root_len = 0
271 s.content_format = NX_NSPEC_CONTENT_NONE
272 s.content_index_off = 0; s.content_index_len = 0
273 s.content_language_off = 0; s.content_language_len = 0
274
275 s.auth_enabled = 0
276 s.auth_admin_user_off = 0; s.auth_admin_user_len = 0
277 s.auth_hash_file_off = 0; s.auth_hash_file_len = 0
278 s.auth_session_max_age = 0
279
280 s.routes = (sys_mmap(NX_NSPEC_MAX_ROUTES * NX_NSPEC_ROUTE_BYTES)) as *NxNspecRoute
281 s.routes_cap = NX_NSPEC_MAX_ROUTES
282 s.routes_count = 0
283
284 s.rate_buckets = (sys_mmap(NX_NSPEC_MAX_RATE_BUCKETS * NX_NSPEC_RATE_BUCKET_BYTES)) as *NxNspecRateBucket
285 s.rate_buckets_cap = NX_NSPEC_MAX_RATE_BUCKETS
286 s.rate_buckets_count = 0
287
288 s.tele_enabled = 0
289 s.tele_retention_days = 0
290 s.tele_namespace_off = 0; s.tele_namespace_len = 0
291 s.tele_redact_regex_off = 0; s.tele_redact_regex_len = 0
292
293 s.deploy_blue_green = 0
294 s.deploy_signed_by_off = 0; s.deploy_signed_by_len = 0
295 s.deploy_prev_manifest_off = 0; s.deploy_prev_manifest_len = 0
296
297 s.valid = 1
298 return NX_NSPEC_OK
299}
300
301// ===== Internal: append string to pool; returns offset =================================================
302
303func nx_nspec_pool_append(s: *NxSiteDeploymentSpec,
304 src: *u8, src_n: i64) -> i64 {
305 if s.valid != 1 { return 0 - NX_NSPEC_BAD_INPUT }
306 if src_n < 0 { return 0 - NX_NSPEC_BAD_INPUT }
307 if src_n > NX_NSPEC_MAX_FIELD_LEN { return 0 - NX_NSPEC_VALUE_TOO_LONG }
308 if s.pool_used + src_n > s.pool_cap { return 0 - NX_NSPEC_POOL_FULL }
309 let off: i64 = s.pool_used
310 var i: i64 = 0
311 while i < src_n { s.pool[off + i] = src[i]; i = i + 1 }
312 s.pool_used = s.pool_used + src_n
313 return off
314}
315
316// ===== Sub-record accessors =================================================
317
318func nx_nspec_dns_record_at(s: *NxSiteDeploymentSpec, i: i64) -> *NxNspecDnsRecord {
319 if s.valid != 1 { return 0 as *NxNspecDnsRecord }
320 if i < 0 { return 0 as *NxNspecDnsRecord }
321 if i >= s.dns_records_count { return 0 as *NxNspecDnsRecord }
322 return ((s.dns_records as i64) + i * NX_NSPEC_DNS_RECORD_BYTES) as *NxNspecDnsRecord
323}
324
325func nx_nspec_route_at(s: *NxSiteDeploymentSpec, i: i64) -> *NxNspecRoute {
326 if s.valid != 1 { return 0 as *NxNspecRoute }
327 if i < 0 { return 0 as *NxNspecRoute }
328 if i >= s.routes_count { return 0 as *NxNspecRoute }
329 return ((s.routes as i64) + i * NX_NSPEC_ROUTE_BYTES) as *NxNspecRoute
330}
331
332func nx_nspec_rate_bucket_at(s: *NxSiteDeploymentSpec, i: i64) -> *NxNspecRateBucket {
333 if s.valid != 1 { return 0 as *NxNspecRateBucket }
334 if i < 0 { return 0 as *NxNspecRateBucket }
335 if i >= s.rate_buckets_count { return 0 as *NxNspecRateBucket }
336 return ((s.rate_buckets as i64) + i * NX_NSPEC_RATE_BUCKET_BYTES) as *NxNspecRateBucket
337}
338
339// ===== Adders for sub-records (called by parser) =================================================
340
341func nx_nspec_dns_add(s: *NxSiteDeploymentSpec,
342 kind: i64,
343 name_off: i64, name_len: i64,
344 value_off: i64, value_len: i64,
345 extra_off: i64, extra_len: i64) -> i64 {
346 if s.valid != 1 { return 0 - NX_NSPEC_BAD_INPUT }
347 if s.dns_records_count >= s.dns_records_cap { return 0 - NX_NSPEC_TOO_MANY_RECORDS }
348 if kind < 0 { return 0 - NX_NSPEC_BAD_ENUM }
349 if kind >= NX_NSPEC_DNS_N { return 0 - NX_NSPEC_BAD_ENUM }
350 let r: *NxNspecDnsRecord = ((s.dns_records as i64) + s.dns_records_count * NX_NSPEC_DNS_RECORD_BYTES) as *NxNspecDnsRecord
351 r.kind = kind
352 r.name_off = name_off; r.name_len = name_len
353 r.value_off = value_off; r.value_len = value_len
354 r.extra_off = extra_off; r.extra_len = extra_len
355 s.dns_records_count = s.dns_records_count + 1
356 return s.dns_records_count - 1
357}
358
359func nx_nspec_route_add(s: *NxSiteDeploymentSpec,
360 prefix_off: i64, prefix_len: i64,
361 handler: i64) -> i64 {
362 if s.valid != 1 { return 0 - NX_NSPEC_BAD_INPUT }
363 if s.routes_count >= s.routes_cap { return 0 - NX_NSPEC_TOO_MANY_ROUTES }
364 if handler < 0 { return 0 - NX_NSPEC_BAD_ENUM }
365 if handler >= NX_NSPEC_ROUTE_N { return 0 - NX_NSPEC_BAD_ENUM }
366 let r: *NxNspecRoute = ((s.routes as i64) + s.routes_count * NX_NSPEC_ROUTE_BYTES) as *NxNspecRoute
367 r.prefix_off = prefix_off; r.prefix_len = prefix_len
368 r.handler = handler
369 s.routes_count = s.routes_count + 1
370 return s.routes_count - 1
371}
372
373func nx_nspec_rate_add(s: *NxSiteDeploymentSpec,
374 prefix_off: i64, prefix_len: i64,
375 quota: i64, per_seconds: i64, scope: i64) -> i64 {
376 if s.valid != 1 { return 0 - NX_NSPEC_BAD_INPUT }
377 if s.rate_buckets_count >= s.rate_buckets_cap { return 0 - NX_NSPEC_TOO_MANY_BUCKETS }
378 if quota < 1 { return 0 - NX_NSPEC_BAD_INPUT }
379 if per_seconds < 1 { return 0 - NX_NSPEC_BAD_INPUT }
380 if scope < 0 { return 0 - NX_NSPEC_BAD_ENUM }
381 if scope > 1 { return 0 - NX_NSPEC_BAD_ENUM }
382 let r: *NxNspecRateBucket = ((s.rate_buckets as i64) + s.rate_buckets_count * NX_NSPEC_RATE_BUCKET_BYTES) as *NxNspecRateBucket
383 r.prefix_off = prefix_off; r.prefix_len = prefix_len
384 r.quota = quota
385 r.per_seconds = per_seconds
386 r.scope = scope
387 s.rate_buckets_count = s.rate_buckets_count + 1
388 return s.rate_buckets_count - 1
389}
390
391// ===== Accessors (frontend-friendly) =================================================
392
393func nx_nspec_site_name(s: *NxSiteDeploymentSpec, out_ptr: *i64, out_n: *i64) -> i64 {
394 if s.valid != 1 { return 0 - NX_NSPEC_BAD_INPUT }
395 out_ptr[0] = (s.pool as i64) + s.site_name_off
396 out_n[0] = s.site_name_len
397 return NX_NSPEC_OK
398}
399
400func nx_nspec_dns_records_count(s: *NxSiteDeploymentSpec) -> i64 {
401 if s.valid != 1 { return 0 }
402 return s.dns_records_count
403}
404
405func nx_nspec_routes_count(s: *NxSiteDeploymentSpec) -> i64 {
406 if s.valid != 1 { return 0 }
407 return s.routes_count
408}
409
410func nx_nspec_rate_buckets_count(s: *NxSiteDeploymentSpec) -> i64 {
411 if s.valid != 1 { return 0 }
412 return s.rate_buckets_count
413}