nx_doc_envelope.nx source
↩ module page · 355 lines · 14670 B
1// nx_doc_envelope.nx -- LEGAL RUNG D2: the e-signature ENVELOPE workflow.
2//
3// module: nishi-core.legal.doc_envelope
4// capability: LEGAL_ESIGN_ENVELOPE
5//
6// The "DocuSign but better" WORKFLOW core. It orchestrates one document through
7// create -> send -> sign (per recipient, in routing order) -> complete, binding
8// the three already-GREEN legal organs into one client-facing flow:
9// D0 nx_legal_compliance -- classify the document + pick its legal regime at
10// CREATE; REFUSE to route a void instrument into the
11// e-sign flow at SEND (never-route-a-void-instrument).
12// D5 nx_doc_seal -- each signer's signature is a REAL RFC-8032 Ed25519
13// seal; SIGN verifies it; an envelope can only
14// COMPLETE when every required signer has a VERIFIED,
15// non-refused, VALID-verdict seal
16// (all-signers-or-not-complete).
17// D1 nx_doc_vault -- (gate-composed) the completed, sealed version is
18// attached to the per-tenant additive vault.
19//
20// THE s-class-exceed property, carried LIVE through the workflow (a generic
21// e-sign product lacks every one of these):
22// * a will / codicil / testamentary trust in a NON-e-wills jurisdiction can
23// NEVER be sent for e-signature -- it would produce a legally VOID instrument
24// (UETA 3(b) / ESIGN 7003(a)). The refusal is at the workflow boundary
25// (SEND), not merely at the crypto. This is the legal analog of Rule 26.
26// * routing order is enforced -- recipient N+1 cannot sign before recipient N.
27// * a forged or tampered signature can never complete an envelope.
28// * decline / void retain full history (Rule 13 additive-only).
29// * we do NOT over-refuse: a properly-executed e-will (in an e-wills state)
30// routes through the stricter regime and completes.
31//
32// Representation: caller-allocated flat i64 record arrays (substrate scale-
33// agnostic, no internal allocation), mirroring nx_doc_vault. Pure logic core ->
34// the gate is self-contained (no socket, no fork).
35//
36// Composes: nx_legal_compliance (D0 classify+regime+verdict), nx_doc_seal
37// (D5 NxSeal + verify). Distinct from nx_doc_seal because: that produces ONE
38// signature; this is the multi-recipient lifecycle + status + audit around it.
39// license_tier: ORIGINAL
40// lineage_id: nishi_doc_envelope_d2
41import "nx_syscalls.nx"
42import "nx_legal_compliance.nx"
43import "nx_doc_seal.nx"
44
45// ---- envelope status ----
46const ENV_DRAFT: i64 = 0
47const ENV_SENT: i64 = 1
48const ENV_COMPLETED: i64 = 2
49const ENV_DECLINED: i64 = 3
50const ENV_VOIDED: i64 = 4
51
52// ---- recipient roles ----
53const ROLE_SIGNER: i64 = 0
54const ROLE_CC: i64 = 1 // informational copy; never signs
55const ROLE_APPROVER: i64 = 2 // reserved: approves without a cryptographic seal
56
57// ---- recipient status ----
58const RC_PENDING: i64 = 0
59const RC_VIEWED: i64 = 1
60const RC_SIGNED: i64 = 2
61const RC_DECLINED: i64 = 3
62
63// ---- send result ----
64const ENV_SEND_OK: i64 = 0
65const ENV_SEND_REFUSED_VOID: i64 = 1 // regime REQUIRES_WET -> never route into e-sign
66const ENV_SEND_BAD_STATE: i64 = 2 // not found / not in DRAFT
67
68// ---- sign result ----
69const ENV_SIGN_OK: i64 = 0
70const ENV_SIGN_OUT_OF_ORDER: i64 = 1 // an earlier-order signer has not signed yet
71const ENV_SIGN_BAD_SEAL: i64 = 2 // seal refused / not VALID / does not verify
72const ENV_SIGN_BAD_STATE: i64 = 3 // envelope not found / not SENT
73const ENV_SIGN_NO_RCPT: i64 = 4 // recipient not on envelope, or not a signer
74
75// ---- complete result ----
76const ENV_COMPLETE_OK: i64 = 0
77const ENV_COMPLETE_NOT_READY: i64 = 1
78
79// ---- audit event kinds (UETA 12 retention trail) ----
80const EV_CREATED: i64 = 0
81const EV_SENT: i64 = 1
82const EV_VIEWED: i64 = 2
83const EV_SIGNED: i64 = 3
84const EV_COMPLETED: i64 = 4
85const EV_DECLINED: i64 = 5
86const EV_VOIDED: i64 = 6
87
88// ---- envelope record: flat_env[e*EF_STRIDE + EF_*] ----
89const EF_ENV: i64 = 0 // envelope id
90const EF_DOC: i64 = 1 // vault doc id this envelope wraps
91const EF_DTYPE: i64 = 2 // D0 document type (classified at create)
92const EF_REGIME: i64 = 3 // D0 legal regime (decided at create)
93const EF_STATUS: i64 = 4
94const EF_NSIGN: i64 = 5 // required signer count (frozen at send)
95const EF_NDONE: i64 = 6 // signers that have signed
96const EF_TS: i64 = 7
97const EF_EWILLS: i64 = 8 // jurisdiction: e-wills statute present (0/1)
98const EF_STRIDE: i64 = 9
99
100// ---- recipient record: flat_rc[r*RF_STRIDE + RF_*] ----
101const RF_ENV: i64 = 0
102const RF_RCPT: i64 = 1
103const RF_ROLE: i64 = 2
104const RF_ORDER: i64 = 3 // routing order (1,2,3,...)
105const RF_STATUS: i64 = 4
106const RF_SEALOK: i64 = 5 // 1 = a verified Ed25519 seal was recorded for this signer
107const RF_TS: i64 = 6
108const RF_STRIDE: i64 = 7
109
110// ---- audit record: flat_au[a*AF_STRIDE + AF_*] ----
111const AF_ENV: i64 = 0
112const AF_KIND: i64 = 1
113const AF_ACTOR: i64 = 2
114const AF_TS: i64 = 3
115const AF_STRIDE: i64 = 4
116
117const ENV_ORDER_NONE: i64 = 2147483647
118
119// ---- locate the envelope record index for env_id, or -1. ----
120func ne_find_env(flat_env: *i64, ne: i64, env_id: i64) -> i64 {
121 var i: i64 = 0
122 while i < ne {
123 if flat_env[i * EF_STRIDE + EF_ENV] == env_id { return i }
124 i = i + 1
125 }
126 return 0 - 1
127}
128
129// ---- locate a recipient record index for (env_id, rcpt_id), or -1. ----
130func ne_find_rcpt(flat_rc: *i64, nr: i64, env_id: i64, rcpt_id: i64) -> i64 {
131 var i: i64 = 0
132 while i < nr {
133 let b: i64 = i * RF_STRIDE
134 if flat_rc[b + RF_ENV] == env_id { if flat_rc[b + RF_RCPT] == rcpt_id { return i } }
135 i = i + 1
136 }
137 return 0 - 1
138}
139
140// ---- create an envelope: classify the doc (D0) + lock in its legal regime. ----
141// label is the human document description ("Last Will and Testament", "Services
142// Agreement"); the regime is decided here so SEND can refuse a void instrument.
143// Returns the new envelope count, or -1 on overflow.
144func nx_env_create(flat_env: *i64, ne: i64, cap: i64, env_id: i64, doc_id: i64,
145 label: *u8, e_wills_allowed: i64, ts: i64) -> i64 {
146 if ne >= cap { return 0 - 1 }
147 let dt: i64 = nx_legal_doc_classify(label)
148 let rg: i64 = nx_legal_regime(dt, e_wills_allowed)
149 let b: i64 = ne * EF_STRIDE
150 flat_env[b + EF_ENV] = env_id
151 flat_env[b + EF_DOC] = doc_id
152 flat_env[b + EF_DTYPE] = dt
153 flat_env[b + EF_REGIME] = rg
154 flat_env[b + EF_STATUS] = ENV_DRAFT
155 flat_env[b + EF_NSIGN] = 0
156 flat_env[b + EF_NDONE] = 0
157 flat_env[b + EF_TS] = ts
158 flat_env[b + EF_EWILLS] = e_wills_allowed
159 return ne + 1
160}
161
162// ---- add a recipient (role + routing order). Returns new recipient count, or -1. ----
163func nx_env_add_recipient(flat_rc: *i64, nr: i64, cap: i64, env_id: i64,
164 rcpt_id: i64, role: i64, order: i64) -> i64 {
165 if nr >= cap { return 0 - 1 }
166 let b: i64 = nr * RF_STRIDE
167 flat_rc[b + RF_ENV] = env_id
168 flat_rc[b + RF_RCPT] = rcpt_id
169 flat_rc[b + RF_ROLE] = role
170 flat_rc[b + RF_ORDER] = order
171 flat_rc[b + RF_STATUS] = RC_PENDING
172 flat_rc[b + RF_SEALOK] = 0
173 flat_rc[b + RF_TS] = 0
174 return nr + 1
175}
176
177func nx_env_status(flat_env: *i64, ne: i64, env_id: i64) -> i64 {
178 let e: i64 = ne_find_env(flat_env, ne, env_id)
179 if e < 0 { return 0 - 1 }
180 return flat_env[e * EF_STRIDE + EF_STATUS]
181}
182func nx_env_regime(flat_env: *i64, ne: i64, env_id: i64) -> i64 {
183 let e: i64 = ne_find_env(flat_env, ne, env_id)
184 if e < 0 { return 0 - 1 }
185 return flat_env[e * EF_STRIDE + EF_REGIME]
186}
187func nx_env_doctype(flat_env: *i64, ne: i64, env_id: i64) -> i64 {
188 let e: i64 = ne_find_env(flat_env, ne, env_id)
189 if e < 0 { return 0 - 1 }
190 return flat_env[e * EF_STRIDE + EF_DTYPE]
191}
192
193// ---- count SIGNER-role recipients on this envelope. ----
194func nx_env_required_signers(flat_rc: *i64, nr: i64, env_id: i64) -> i64 {
195 var n: i64 = 0
196 var i: i64 = 0
197 while i < nr {
198 let b: i64 = i * RF_STRIDE
199 if flat_rc[b + RF_ENV] == env_id { if flat_rc[b + RF_ROLE] == ROLE_SIGNER { n = n + 1 } }
200 i = i + 1
201 }
202 return n
203}
204
205// ---- count signers with a recorded verified seal. ----
206func nx_env_signed_count(flat_rc: *i64, nr: i64, env_id: i64) -> i64 {
207 var n: i64 = 0
208 var i: i64 = 0
209 while i < nr {
210 let b: i64 = i * RF_STRIDE
211 if flat_rc[b + RF_ENV] == env_id {
212 if flat_rc[b + RF_ROLE] == ROLE_SIGNER {
213 if flat_rc[b + RF_SEALOK] == 1 { n = n + 1 }
214 }
215 }
216 i = i + 1
217 }
218 return n
219}
220
221// ---- lowest routing order among SIGNERS that have not yet signed/declined. ----
222// Returns ENV_ORDER_NONE if every signer is resolved.
223func ne_next_signer_order(flat_rc: *i64, nr: i64, env_id: i64) -> i64 {
224 var best: i64 = ENV_ORDER_NONE
225 var i: i64 = 0
226 while i < nr {
227 let b: i64 = i * RF_STRIDE
228 if flat_rc[b + RF_ENV] == env_id {
229 if flat_rc[b + RF_ROLE] == ROLE_SIGNER {
230 if flat_rc[b + RF_STATUS] != RC_SIGNED {
231 if flat_rc[b + RF_STATUS] != RC_DECLINED {
232 if flat_rc[b + RF_ORDER] < best { best = flat_rc[b + RF_ORDER] }
233 }
234 }
235 }
236 }
237 i = i + 1
238 }
239 return best
240}
241
242// ---- SEND: the never-route-a-void-instrument boundary. ----
243// DRAFT -> SENT, freezing the required-signer count. If the document's regime is
244// REQUIRES_WET (an excluded testamentary instrument with no e-wills statute) the
245// envelope is REFUSED and stays DRAFT -- it can never enter the e-sign flow.
246func nx_env_send(flat_env: *i64, ne: i64, flat_rc: *i64, nr: i64, env_id: i64) -> i64 {
247 let e: i64 = ne_find_env(flat_env, ne, env_id)
248 if e < 0 { return ENV_SEND_BAD_STATE }
249 let b: i64 = e * EF_STRIDE
250 if flat_env[b + EF_STATUS] != ENV_DRAFT { return ENV_SEND_BAD_STATE }
251 if flat_env[b + EF_REGIME] == RG_REQUIRES_WET { return ENV_SEND_REFUSED_VOID }
252 flat_env[b + EF_NSIGN] = nx_env_required_signers(flat_rc, nr, env_id)
253 flat_env[b + EF_STATUS] = ENV_SENT
254 return ENV_SEND_OK
255}
256
257// ---- mark a recipient as having viewed the envelope (audit fidelity). ----
258func nx_env_mark_viewed(flat_rc: *i64, nr: i64, env_id: i64, rcpt_id: i64, ts: i64) -> i64 {
259 let r: i64 = ne_find_rcpt(flat_rc, nr, env_id, rcpt_id)
260 if r < 0 { return 0 - 1 }
261 let b: i64 = r * RF_STRIDE
262 if flat_rc[b + RF_STATUS] == RC_PENDING { flat_rc[b + RF_STATUS] = RC_VIEWED; flat_rc[b + RF_TS] = ts }
263 return 0
264}
265
266// ---- SIGN: record one signer's verified Ed25519 seal, in routing order. ----
267// The seal must be (1) non-refused (status SEAL_OK), (2) a VALID legal verdict,
268// and (3) verify against pub. Any failure -> ENV_SIGN_BAD_SEAL and the signer is
269// NOT marked signed -- so a forged/tampered/void signature can never advance the
270// envelope. Re-signing an already-signed recipient is an idempotent no-op (#10).
271func nx_env_sign(flat_env: *i64, ne: i64, flat_rc: *i64, nr: i64,
272 env_id: i64, rcpt_id: i64, seal: *NxSeal, pub: *u8) -> i64 {
273 let e: i64 = ne_find_env(flat_env, ne, env_id)
274 if e < 0 { return ENV_SIGN_BAD_STATE }
275 let eb: i64 = e * EF_STRIDE
276 if flat_env[eb + EF_STATUS] != ENV_SENT { return ENV_SIGN_BAD_STATE }
277 let r: i64 = ne_find_rcpt(flat_rc, nr, env_id, rcpt_id)
278 if r < 0 { return ENV_SIGN_NO_RCPT }
279 let rb: i64 = r * RF_STRIDE
280 if flat_rc[rb + RF_ROLE] != ROLE_SIGNER { return ENV_SIGN_NO_RCPT }
281 if flat_rc[rb + RF_STATUS] == RC_SIGNED { return ENV_SIGN_OK }
282 let nextord: i64 = ne_next_signer_order(flat_rc, nr, env_id)
283 if flat_rc[rb + RF_ORDER] > nextord { return ENV_SIGN_OUT_OF_ORDER }
284 if seal.status != SEAL_OK { return ENV_SIGN_BAD_SEAL }
285 if seal.verdict != LV_VALID { return ENV_SIGN_BAD_SEAL }
286 if nx_seal_verify(seal, pub) != SEAL_VERIFIED { return ENV_SIGN_BAD_SEAL }
287 flat_rc[rb + RF_STATUS] = RC_SIGNED
288 flat_rc[rb + RF_SEALOK] = 1
289 flat_rc[rb + RF_TS] = seal.ts
290 flat_env[eb + EF_NDONE] = flat_env[eb + EF_NDONE] + 1
291 return ENV_SIGN_OK
292}
293
294// ---- COMPLETE: only when every required signer holds a verified seal. ----
295func nx_env_try_complete(flat_env: *i64, ne: i64, flat_rc: *i64, nr: i64, env_id: i64) -> i64 {
296 let e: i64 = ne_find_env(flat_env, ne, env_id)
297 if e < 0 { return ENV_COMPLETE_NOT_READY }
298 let eb: i64 = e * EF_STRIDE
299 if flat_env[eb + EF_STATUS] != ENV_SENT { return ENV_COMPLETE_NOT_READY }
300 let need: i64 = flat_env[eb + EF_NSIGN]
301 if need <= 0 { return ENV_COMPLETE_NOT_READY }
302 let have: i64 = nx_env_signed_count(flat_rc, nr, env_id)
303 if have != need { return ENV_COMPLETE_NOT_READY }
304 flat_env[eb + EF_STATUS] = ENV_COMPLETED
305 return ENV_COMPLETE_OK
306}
307
308// ---- DECLINE: a recipient declines -> envelope terminal-DECLINED (additive). ----
309// History is retained -- no record is removed (Rule 13).
310func nx_env_decline(flat_env: *i64, ne: i64, flat_rc: *i64, nr: i64, env_id: i64, rcpt_id: i64) -> i64 {
311 let e: i64 = ne_find_env(flat_env, ne, env_id)
312 if e < 0 { return 0 - 1 }
313 let r: i64 = ne_find_rcpt(flat_rc, nr, env_id, rcpt_id)
314 if r < 0 { return 0 - 1 }
315 flat_rc[r * RF_STRIDE + RF_STATUS] = RC_DECLINED
316 flat_env[e * EF_STRIDE + EF_STATUS] = ENV_DECLINED
317 return 0
318}
319
320// ---- VOID: the sender voids the envelope -> terminal-VOIDED (additive). ----
321func nx_env_void(flat_env: *i64, ne: i64, env_id: i64) -> i64 {
322 let e: i64 = ne_find_env(flat_env, ne, env_id)
323 if e < 0 { return 0 - 1 }
324 flat_env[e * EF_STRIDE + EF_STATUS] = ENV_VOIDED
325 return 0
326}
327
328// ---- append-only audit trail (UETA 12). Returns new audit count, or -1. ----
329func nx_env_audit_add(flat_au: *i64, na: i64, cap: i64, env_id: i64, kind: i64, actor: i64, ts: i64) -> i64 {
330 if na >= cap { return 0 - 1 }
331 let b: i64 = na * AF_STRIDE
332 flat_au[b + AF_ENV] = env_id
333 flat_au[b + AF_KIND] = kind
334 flat_au[b + AF_ACTOR] = actor
335 flat_au[b + AF_TS] = ts
336 return na + 1
337}
338func nx_env_audit_count(flat_au: *i64, na: i64, env_id: i64) -> i64 {
339 var n: i64 = 0
340 var i: i64 = 0
341 while i < na {
342 if flat_au[i * AF_STRIDE + AF_ENV] == env_id { n = n + 1 }
343 i = i + 1
344 }
345 return n
346}
347
348func nx_env_status_name(s: i64) -> *u8 {
349 if s == ENV_DRAFT { return "DRAFT" }
350 if s == ENV_SENT { return "SENT" }
351 if s == ENV_COMPLETED { return "COMPLETED" }
352 if s == ENV_DECLINED { return "DECLINED" }
353 if s == ENV_VOIDED { return "VOIDED" }
354 return "?"
355}