nx_doc_vault.nx source
↩ module page · 177 lines · 7012 B
1// nx_doc_vault.nx -- LEGAL RUNG D1: per-tenant document vault (retention core).
2//
3// module: nishi-core.legal.doc_vault
4// capability: LEGAL_DOC_VAULT
5//
6// The storage + audit home for D5 seals, encoding the registry's retention
7// requirements as invariants BY CONSTRUCTION:
8// ADDITIVE-ONLY (Rule 13) -- a new version never overwrites or
9// removes an old one; soft-delete only
10// demotes is_current. History is sacred.
11// SINGLE AUTHORITATIVE COPY -- exactly one is_current=1 per doc
12// (UETA Section 16) = the authoritative copy.
13// VERSIONED + TAMPER-EVIDENT -- each version links prev_hash ==
14// (UETA Section 12) prior version's content_hash, so any
15// alteration of a non-final version
16// breaks the chain (the I/O layer adds
17// recompute-SHA256-vs-stored for the
18// final version).
19// PER-TENANT ISOLATION -- vault index lives at the tenant's
20// (MRPC 1.6 confidentiality) private prefix <base><tid>.docvault;
21// tenant-id validated [A-Za-z0-9_-] only
22// (no '.'/'/' -> no cross-tenant traversal),
23// the proven nx_vizsla_tenant discipline.
24//
25// Representation: a flat i64 record array (caller-allocated; substrate scale-
26// agnostic, no internal allocation) -- record r, field f at flat[r*VF_STRIDE+f].
27// content_hash is an i64 in this logic core; the I/O layer stores the full
28// SHA-256 digest and the chain comparison is identical.
29//
30// Composes: nx_syscalls (tenant-id validation). Distinct from nx_vizsla_tenant
31// because: that shards CONTACTS/relate data; this shards VERSIONED DOCUMENTS
32// with a tamper-evident chain. license_tier: ORIGINAL
33// lineage_id: nishi_doc_vault_d1
34import "nx_syscalls.nx"
35
36// ---- status codes ----
37const VAULT_OK: i64 = 0
38const VAULT_CHAIN_BROKEN: i64 = 1
39const VAULT_NOT_FOUND: i64 = 2
40const VAULT_FULL: i64 = 3
41
42// ---- record field layout (flat[r*VF_STRIDE + VF_*]) ----
43const VF_DOC: i64 = 0 // document id (logical doc, e.g. a name hash)
44const VF_VER: i64 = 1 // version number (1,2,3...)
45const VF_HASH: i64 = 2 // content hash (SHA-256 in the I/O layer)
46const VF_SIZE: i64 = 3 // content size in bytes
47const VF_CUR: i64 = 4 // 1 = current authoritative copy, 0 = superseded/retired
48const VF_TS: i64 = 5 // timestamp
49const VF_PREV: i64 = 6 // prev version's content hash (0 for v1) -- the chain link
50const VF_SEAL: i64 = 7 // 1 = a D5 e-sign seal is attached to this version
51const VF_STRIDE: i64 = 8
52
53// ---- add a new version of doc_id (additive: demote prior current, never remove). ----
54// Returns the new record count, or -VAULT_FULL.
55func nx_vault_add(flat: *i64, count: i64, cap: i64, doc_id: i64, chash: i64, size: i64, ts: i64) -> i64 {
56 if count >= cap { return 0 - VAULT_FULL }
57 var prev_ver: i64 = 0
58 var prev_h: i64 = 0
59 var i: i64 = 0
60 while i < count {
61 let b: i64 = i * VF_STRIDE
62 if flat[b + VF_DOC] == doc_id {
63 if flat[b + VF_CUR] == 1 {
64 flat[b + VF_CUR] = 0 // demote the prior authoritative copy (kept, not removed)
65 prev_ver = flat[b + VF_VER]
66 prev_h = flat[b + VF_HASH]
67 }
68 }
69 i = i + 1
70 }
71 let nb: i64 = count * VF_STRIDE
72 flat[nb + VF_DOC] = doc_id
73 flat[nb + VF_VER] = prev_ver + 1
74 flat[nb + VF_HASH] = chash
75 flat[nb + VF_SIZE] = size
76 flat[nb + VF_CUR] = 1
77 flat[nb + VF_TS] = ts
78 flat[nb + VF_PREV] = prev_h
79 flat[nb + VF_SEAL] = 0
80 return count + 1
81}
82
83// ---- index of the current (authoritative) version of doc_id, or -1. ----
84func nx_vault_current_idx(flat: *i64, count: i64, doc_id: i64) -> i64 {
85 var i: i64 = 0
86 while i < count {
87 let b: i64 = i * VF_STRIDE
88 if flat[b + VF_DOC] == doc_id { if flat[b + VF_CUR] == 1 { return i } }
89 i = i + 1
90 }
91 return 0 - 1
92}
93
94// ---- how many versions of doc_id exist (history depth -- additive proof). ----
95func nx_vault_version_count(flat: *i64, count: i64, doc_id: i64) -> i64 {
96 var n: i64 = 0
97 var i: i64 = 0
98 while i < count {
99 if flat[i * VF_STRIDE + VF_DOC] == doc_id { n = n + 1 }
100 i = i + 1
101 }
102 return n
103}
104
105// ---- how many CURRENT versions of doc_id (must be exactly 1 -- single-authoritative). ----
106func nx_vault_current_count(flat: *i64, count: i64, doc_id: i64) -> i64 {
107 var n: i64 = 0
108 var i: i64 = 0
109 while i < count {
110 let b: i64 = i * VF_STRIDE
111 if flat[b + VF_DOC] == doc_id { if flat[b + VF_CUR] == 1 { n = n + 1 } }
112 i = i + 1
113 }
114 return n
115}
116
117// ---- verify the tamper-evident chain for doc_id (each prev_hash links). ----
118func nx_vault_verify_chain(flat: *i64, count: i64, doc_id: i64) -> i64 {
119 var expect_ver: i64 = 1
120 var prev_h: i64 = 0
121 var go: i64 = 1
122 while go == 1 {
123 var idx: i64 = 0 - 1
124 var i: i64 = 0
125 while i < count {
126 let b: i64 = i * VF_STRIDE
127 if flat[b + VF_DOC] == doc_id { if flat[b + VF_VER] == expect_ver { idx = i } }
128 i = i + 1
129 }
130 if idx < 0 {
131 go = 0
132 } else {
133 let b: i64 = idx * VF_STRIDE
134 if flat[b + VF_PREV] != prev_h { return VAULT_CHAIN_BROKEN }
135 prev_h = flat[b + VF_HASH]
136 expect_ver = expect_ver + 1
137 }
138 }
139 if expect_ver == 1 { return VAULT_NOT_FOUND }
140 return VAULT_OK
141}
142
143// ---- soft-delete: demote the current version (additive -- history retained). ----
144func nx_vault_soft_delete(flat: *i64, count: i64, doc_id: i64) -> i64 {
145 let idx: i64 = nx_vault_current_idx(flat, count, doc_id)
146 if idx < 0 { return VAULT_NOT_FOUND }
147 flat[idx * VF_STRIDE + VF_CUR] = 0
148 return VAULT_OK
149}
150
151// ---- attach a D5 seal to the current version of doc_id. ----
152func nx_vault_attach_seal(flat: *i64, count: i64, doc_id: i64) -> i64 {
153 let idx: i64 = nx_vault_current_idx(flat, count, doc_id)
154 if idx < 0 { return VAULT_NOT_FOUND }
155 flat[idx * VF_STRIDE + VF_SEAL] = 1
156 return VAULT_OK
157}
158
159// ---- tenant-id validation: nonempty, [A-Za-z0-9_-] only (no '.' or '/'). ----
160// The nx_vizsla_tenant discipline: a tenant can never path-traverse into
161// another tenant's vault.
162func nx_vault_valid_tid(s: *u8) -> i64 {
163 if s[0] == (0 as u8) { return 0 }
164 var i: i64 = 0
165 while s[i] != (0 as u8) {
166 let c: i64 = s[i] as i64
167 var ok: i64 = 0
168 if c >= 48 { if c <= 57 { ok = 1 } }
169 if c >= 65 { if c <= 90 { ok = 1 } }
170 if c >= 97 { if c <= 122 { ok = 1 } }
171 if c == 45 { ok = 1 }
172 if c == 95 { ok = 1 }
173 if ok == 0 { return 0 }
174 i = i + 1
175 }
176 return 1
177}