code wiki / (root) / nx_doc_envelope.nx

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}