@real-life/trust-protocol 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +187 -78
  3. package/dist/ack.d.ts +84 -0
  4. package/dist/ack.d.ts.map +1 -0
  5. package/dist/ack.js +114 -0
  6. package/dist/ack.js.map +1 -0
  7. package/dist/adapters/vti-mediator.d.ts +154 -0
  8. package/dist/adapters/vti-mediator.d.ts.map +1 -0
  9. package/dist/adapters/vti-mediator.js +621 -0
  10. package/dist/adapters/vti-mediator.js.map +1 -0
  11. package/dist/anchor.d.ts +226 -0
  12. package/dist/anchor.d.ts.map +1 -0
  13. package/dist/anchor.js +347 -0
  14. package/dist/anchor.js.map +1 -0
  15. package/dist/{holder.d.ts → carrier-holder.d.ts} +1 -1
  16. package/dist/carrier-holder.d.ts.map +1 -0
  17. package/dist/{holder.js → carrier-holder.js} +5 -4
  18. package/dist/carrier-holder.js.map +1 -0
  19. package/dist/carrier-identity.js +4 -4
  20. package/dist/carrier-identity.js.map +1 -1
  21. package/dist/carrier.d.ts.map +1 -1
  22. package/dist/carrier.js +4 -3
  23. package/dist/carrier.js.map +1 -1
  24. package/dist/ceremony.d.ts +3 -2
  25. package/dist/ceremony.d.ts.map +1 -1
  26. package/dist/ceremony.js +16 -26
  27. package/dist/ceremony.js.map +1 -1
  28. package/dist/codec.d.ts +46 -0
  29. package/dist/codec.d.ts.map +1 -0
  30. package/dist/codec.js +410 -0
  31. package/dist/codec.js.map +1 -0
  32. package/dist/continuity.d.ts +67 -0
  33. package/dist/continuity.d.ts.map +1 -0
  34. package/dist/continuity.js +110 -0
  35. package/dist/continuity.js.map +1 -0
  36. package/dist/core.d.ts +4 -0
  37. package/dist/core.d.ts.map +1 -1
  38. package/dist/core.js.map +1 -1
  39. package/dist/crypto.d.ts +23 -7
  40. package/dist/crypto.d.ts.map +1 -1
  41. package/dist/crypto.js +54 -20
  42. package/dist/crypto.js.map +1 -1
  43. package/dist/delivery.d.ts +31 -3
  44. package/dist/delivery.d.ts.map +1 -1
  45. package/dist/delivery.js +75 -28
  46. package/dist/delivery.js.map +1 -1
  47. package/dist/encounter/ceremony.d.ts +37 -0
  48. package/dist/encounter/ceremony.d.ts.map +1 -0
  49. package/dist/encounter/ceremony.js +662 -0
  50. package/dist/encounter/ceremony.js.map +1 -0
  51. package/dist/encounter.d.ts +91 -1
  52. package/dist/encounter.d.ts.map +1 -1
  53. package/dist/encounter.js +191 -12
  54. package/dist/encounter.js.map +1 -1
  55. package/dist/engine.d.ts +291 -0
  56. package/dist/engine.d.ts.map +1 -0
  57. package/dist/engine.js +1395 -0
  58. package/dist/engine.js.map +1 -0
  59. package/dist/entropy.d.ts +28 -0
  60. package/dist/entropy.d.ts.map +1 -0
  61. package/dist/entropy.js +96 -0
  62. package/dist/entropy.js.map +1 -0
  63. package/dist/events.d.ts +177 -0
  64. package/dist/events.d.ts.map +1 -0
  65. package/dist/events.js +2 -0
  66. package/dist/events.js.map +1 -0
  67. package/dist/fixtures.d.ts +30 -0
  68. package/dist/fixtures.d.ts.map +1 -0
  69. package/dist/fixtures.js +155 -0
  70. package/dist/fixtures.js.map +1 -0
  71. package/dist/forge.d.ts +47 -0
  72. package/dist/forge.d.ts.map +1 -0
  73. package/dist/forge.js +20 -0
  74. package/dist/forge.js.map +1 -0
  75. package/dist/group-star.d.ts +251 -0
  76. package/dist/group-star.d.ts.map +1 -0
  77. package/dist/group-star.js +348 -0
  78. package/dist/group-star.js.map +1 -0
  79. package/dist/holder-api.d.ts +137 -0
  80. package/dist/holder-api.d.ts.map +1 -0
  81. package/dist/holder-api.js +132 -0
  82. package/dist/holder-api.js.map +1 -0
  83. package/dist/holder-state-v1.d.ts +801 -0
  84. package/dist/holder-state-v1.d.ts.map +1 -0
  85. package/dist/holder-state-v1.js +2 -0
  86. package/dist/holder-state-v1.js.map +1 -0
  87. package/dist/identity-state.d.ts +43 -0
  88. package/dist/identity-state.d.ts.map +1 -0
  89. package/dist/identity-state.js +345 -0
  90. package/dist/identity-state.js.map +1 -0
  91. package/dist/identity.d.ts +24 -2
  92. package/dist/identity.d.ts.map +1 -1
  93. package/dist/identity.js +70 -11
  94. package/dist/identity.js.map +1 -1
  95. package/dist/index.d.ts +26 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +77 -29
  98. package/dist/index.js.map +1 -1
  99. package/dist/internal.d.ts +36 -0
  100. package/dist/internal.d.ts.map +1 -0
  101. package/dist/internal.js +22 -0
  102. package/dist/internal.js.map +1 -0
  103. package/dist/introduction/holder.d.ts +18 -0
  104. package/dist/introduction/holder.d.ts.map +1 -0
  105. package/dist/introduction/holder.js +757 -0
  106. package/dist/introduction/holder.js.map +1 -0
  107. package/dist/introduction.d.ts +101 -0
  108. package/dist/introduction.d.ts.map +1 -0
  109. package/dist/introduction.js +132 -0
  110. package/dist/introduction.js.map +1 -0
  111. package/dist/invariants.d.ts +43 -0
  112. package/dist/invariants.d.ts.map +1 -0
  113. package/dist/invariants.js +883 -0
  114. package/dist/invariants.js.map +1 -0
  115. package/dist/keys-seed.d.ts +7 -0
  116. package/dist/keys-seed.d.ts.map +1 -0
  117. package/dist/keys-seed.js +133 -0
  118. package/dist/keys-seed.js.map +1 -0
  119. package/dist/log-memory.d.ts +12 -0
  120. package/dist/log-memory.d.ts.map +1 -0
  121. package/dist/log-memory.js +101 -0
  122. package/dist/log-memory.js.map +1 -0
  123. package/dist/membership/holder.d.ts +35 -0
  124. package/dist/membership/holder.d.ts.map +1 -0
  125. package/dist/membership/holder.js +852 -0
  126. package/dist/membership/holder.js.map +1 -0
  127. package/dist/membership.d.ts +92 -0
  128. package/dist/membership.d.ts.map +1 -0
  129. package/dist/membership.js +157 -0
  130. package/dist/membership.js.map +1 -0
  131. package/dist/ports.d.ts +237 -0
  132. package/dist/ports.d.ts.map +1 -0
  133. package/dist/ports.js +2 -0
  134. package/dist/ports.js.map +1 -0
  135. package/dist/profile.d.ts +56 -0
  136. package/dist/profile.d.ts.map +1 -0
  137. package/dist/profile.js +193 -0
  138. package/dist/profile.js.map +1 -0
  139. package/dist/prompts.d.ts +14 -0
  140. package/dist/prompts.d.ts.map +1 -0
  141. package/dist/prompts.js +69 -0
  142. package/dist/prompts.js.map +1 -0
  143. package/dist/receive.d.ts +133 -0
  144. package/dist/receive.d.ts.map +1 -0
  145. package/dist/receive.js +298 -0
  146. package/dist/receive.js.map +1 -0
  147. package/dist/results.d.ts +158 -0
  148. package/dist/results.d.ts.map +1 -0
  149. package/dist/results.js +21 -0
  150. package/dist/results.js.map +1 -0
  151. package/dist/schemas.d.ts.map +1 -1
  152. package/dist/schemas.js +42 -38
  153. package/dist/schemas.js.map +1 -1
  154. package/dist/sha256.d.ts +3 -0
  155. package/dist/sha256.d.ts.map +1 -0
  156. package/dist/sha256.js +69 -0
  157. package/dist/sha256.js.map +1 -0
  158. package/dist/star.d.ts +31 -0
  159. package/dist/star.d.ts.map +1 -0
  160. package/dist/star.js +56 -0
  161. package/dist/star.js.map +1 -0
  162. package/dist/state-builder.d.ts +42 -0
  163. package/dist/state-builder.d.ts.map +1 -0
  164. package/dist/state-builder.js +100 -0
  165. package/dist/state-builder.js.map +1 -0
  166. package/dist/state.d.ts +853 -0
  167. package/dist/state.d.ts.map +1 -0
  168. package/dist/state.js +36 -0
  169. package/dist/state.js.map +1 -0
  170. package/dist/store-contract.d.ts +11 -0
  171. package/dist/store-contract.d.ts.map +1 -0
  172. package/dist/store-contract.js +189 -0
  173. package/dist/store-contract.js.map +1 -0
  174. package/dist/store-memory.d.ts +8 -0
  175. package/dist/store-memory.d.ts.map +1 -0
  176. package/dist/store-memory.js +17 -0
  177. package/dist/store-memory.js.map +1 -0
  178. package/dist/testing.d.ts +22 -0
  179. package/dist/testing.d.ts.map +1 -0
  180. package/dist/testing.js +21 -0
  181. package/dist/testing.js.map +1 -0
  182. package/dist/trust/continuity.d.ts +71 -0
  183. package/dist/trust/continuity.d.ts.map +1 -0
  184. package/dist/trust/continuity.js +1216 -0
  185. package/dist/trust/continuity.js.map +1 -0
  186. package/dist/trust/duties.d.ts +67 -0
  187. package/dist/trust/duties.d.ts.map +1 -0
  188. package/dist/trust/duties.js +179 -0
  189. package/dist/trust/duties.js.map +1 -0
  190. package/dist/trust/entries.d.ts +139 -0
  191. package/dist/trust/entries.d.ts.map +1 -0
  192. package/dist/trust/entries.js +694 -0
  193. package/dist/trust/entries.js.map +1 -0
  194. package/dist/trust/group-star.d.ts +43 -0
  195. package/dist/trust/group-star.d.ts.map +1 -0
  196. package/dist/trust/group-star.js +188 -0
  197. package/dist/trust/group-star.js.map +1 -0
  198. package/dist/trust/mapping.d.ts +56 -0
  199. package/dist/trust/mapping.d.ts.map +1 -0
  200. package/dist/trust/mapping.js +584 -0
  201. package/dist/trust/mapping.js.map +1 -0
  202. package/dist/trust/profile.d.ts +25 -0
  203. package/dist/trust/profile.d.ts.map +1 -0
  204. package/dist/trust/profile.js +249 -0
  205. package/dist/trust/profile.js.map +1 -0
  206. package/dist/trust/star.d.ts +208 -0
  207. package/dist/trust/star.d.ts.map +1 -0
  208. package/dist/trust/star.js +588 -0
  209. package/dist/trust/star.js.map +1 -0
  210. package/dist/trust/state.d.ts +83 -0
  211. package/dist/trust/state.d.ts.map +1 -0
  212. package/dist/trust/state.js +154 -0
  213. package/dist/trust/state.js.map +1 -0
  214. package/dist/util-freeze.d.ts +3 -0
  215. package/dist/util-freeze.d.ts.map +1 -0
  216. package/dist/util-freeze.js +11 -0
  217. package/dist/util-freeze.js.map +1 -0
  218. package/dist/views.d.ts +147 -0
  219. package/dist/views.d.ts.map +1 -0
  220. package/dist/views.js +268 -0
  221. package/dist/views.js.map +1 -0
  222. package/dist/visibility.d.ts +5 -5
  223. package/dist/visibility.d.ts.map +1 -1
  224. package/dist/visibility.js +22 -28
  225. package/dist/visibility.js.map +1 -1
  226. package/dist/wire.d.ts +178 -84
  227. package/dist/wire.d.ts.map +1 -1
  228. package/dist/wire.js.map +1 -1
  229. package/package.json +58 -11
  230. package/schemas/holder-state-v1.schema.json +3880 -0
  231. package/dist/holder.d.ts.map +0 -1
  232. package/dist/holder.js.map +0 -1
  233. package/dist/probe/acks.d.ts +0 -27
  234. package/dist/probe/acks.d.ts.map +0 -1
  235. package/dist/probe/acks.js +0 -152
  236. package/dist/probe/acks.js.map +0 -1
  237. package/dist/probe/continuity.d.ts +0 -20
  238. package/dist/probe/continuity.d.ts.map +0 -1
  239. package/dist/probe/continuity.js +0 -805
  240. package/dist/probe/continuity.js.map +0 -1
  241. package/dist/probe/deps.d.ts +0 -61
  242. package/dist/probe/deps.d.ts.map +0 -1
  243. package/dist/probe/deps.js +0 -54
  244. package/dist/probe/deps.js.map +0 -1
  245. package/dist/probe/encounter.d.ts +0 -26
  246. package/dist/probe/encounter.d.ts.map +0 -1
  247. package/dist/probe/encounter.js +0 -938
  248. package/dist/probe/encounter.js.map +0 -1
  249. package/dist/probe/introduce.d.ts +0 -70
  250. package/dist/probe/introduce.d.ts.map +0 -1
  251. package/dist/probe/introduce.js +0 -295
  252. package/dist/probe/introduce.js.map +0 -1
  253. package/dist/probe/membership.d.ts +0 -90
  254. package/dist/probe/membership.d.ts.map +0 -1
  255. package/dist/probe/membership.js +0 -450
  256. package/dist/probe/membership.js.map +0 -1
  257. package/dist/probe/trust.d.ts +0 -156
  258. package/dist/probe/trust.d.ts.map +0 -1
  259. package/dist/probe/trust.js +0 -1233
  260. package/dist/probe/trust.js.map +0 -1
  261. package/dist/probe.d.ts +0 -7
  262. package/dist/probe.d.ts.map +0 -1
  263. package/dist/probe.js +0 -42
  264. package/dist/probe.js.map +0 -1
@@ -0,0 +1,1216 @@
1
+ // trust/continuity — continuity on the holder (lib Stage 2, PR 4c; Trust
2
+ // §6a, 6.4): the probe (6a.2), the continuity mapping (6a.4) and the
3
+ // chaining it triggers, with the standing duties of 6a.3. Graduated from
4
+ // probe/continuity.ts (buildProbe, receiveContinuity, handleProbe,
5
+ // handleMapping, flushContinuity, chainTuple) and probe/trust.ts
6
+ // (chainAdmission) onto HolderState and the engine; probe/continuity.ts
7
+ // stays as it is for the simulators until PR 7. The pure wire forms are
8
+ // the root module's (continuity.ts).
9
+ //
10
+ // Rule numbers: bare four-digit numbers are RLTP-TRU (trust-layer.md).
11
+ //
12
+ // Spec follow-up 14 (Trust 0.32): 6900 has the record side chain
13
+ // "atomically with issuing its choice", and 6910 lets it choose on the
14
+ // first match. A probe hit is no evidence of the prior relationship: it
15
+ // proves knowledge of the prior's counterpart anchor (a photographed card,
16
+ // a mediator who saw it in the clear) and of the fresh tuple's k_p, never
17
+ // possession of the prior relationship key, which only mac1 proves (6970).
18
+ // Chaining on a hit lets anyone who knows a pair anchor take over that
19
+ // relationship. This library therefore chains only on a verified mapping
20
+ // of the counterpart, mac1 under the prior key: the record side issues its
21
+ // choice on a hit and holds it as a candidate (the standing mapping duty,
22
+ // 6840); it chains when the counterpart's report or alignment naming the
23
+ // chosen prior verifies (6970). The non-record side already chains only on
24
+ // the record side's verified mapping (6900). 6900/6910 need the evidence
25
+ // wording against 6970.
26
+ //
27
+ // Spec follow-up 17 (Trust 0.32, where 2300 freezes the prior-candidate
28
+ // set): candidate substitution. Two fresh tuples toward one counterpart
29
+ // opened before either chained (a double scan under a transport outage)
30
+ // both name the prior P; the later one, F2, also names F1. Where F2's
31
+ // traffic arrives first, F2 chains onto P, and F1's only candidate is
32
+ // consumed: the record side makes no choice, the non-record side resends
33
+ // its report forever, two entries stay. So: when a chaining deactivates P
34
+ // as the prior of a chain whose new head is X, every other fresh tuple
35
+ // not yet chained whose prior-candidate set names P gets X in P's place
36
+ // (same position; substitutePrior, run by chainTuple on its result), and
37
+ // what its flow held about P is re-targeted to X as a fresh issuance under
38
+ // the next revision, through the mapping duty: the record side's choice
39
+ // (`chosen`, an owed `due`) is owed again naming X, X now the chain's head
40
+ // and chainable; the non-record side's report or alignment likewise; the
41
+ // frozen record prior (6290) follows to X's counterpart anchor. mac1 then
42
+ // runs under X's relationship key (6970), which both holders hold, X being
43
+ // a verified chained tuple; both sides apply the rule when they themselves
44
+ // chain X, so the substituted candidate verifies on both. Transitive: a
45
+ // chaining onto X later substitutes again. Only a tuple verified chained
46
+ // onto P takes its place (chainTuple runs only on a verified mapping,
47
+ // follow-up 14); a third party's fresh tuple never enters another flow's
48
+ // candidates, and a probe hit proves nothing as before. A probe built
49
+ // before the counterpart substituted still blinds P's anchor: X keeps, in
50
+ // this tuple's set, the anchors it replaced in this flow (`replaces`,
51
+ // transitively: Y in X's place stands for X and P), and answers a probe
52
+ // for its own counterpart anchor and for those (standsFor). This is a
53
+ // deviation from 6800, which matches the frozen set only (A19), limited to
54
+ // anchors substituted in the tuple's own flow: a tuple opened after a
55
+ // chaining completed has no history and matches its frozen candidates
56
+ // only, so an attacker who knows an old pair anchor learns no current one
57
+ // (stack review round 6: Codex I). A mapping
58
+ // naming P that arrives after the substitution is refused (6960); its
59
+ // sender re-targets on its own chaining. P's key is not kept for the
60
+ // substituted flow: nothing verifies or issues under it there any more.
61
+ // What it registers (through trustModule, trust/mapping.ts):
62
+ // tasks continuity-probe/0.1 (proof absent): the assembly per tuple
63
+ // (6790, the 5.2a rules with `probe` as the salt), the highest
64
+ // completed probe (6690, 6700), the matches against the own
65
+ // prior-candidate set (6800) and what they trigger: the record
66
+ // side's choice, issued but not chained (spec follow-up 14), or
67
+ // the non-record side's match report
68
+ // continuity-mapping/0.1 (proof absent): steps 1–6 of 6940 (to,
69
+ // next, prior in the frozen prior-candidate set, both MACs, the
70
+ // revision rule with the record freeze, 6270, 6290); a record
71
+ // side's mapping chains (6900) and raises the alignment (6850,
72
+ // 6930); the non-record side's report or alignment informs the
73
+ // record side's choice (6910) and, verified under the prior key,
74
+ // chains it where it names the chosen relationship (follow-up 14)
75
+ // duty continuity/1: step `probe` sends the pending probe (6830),
76
+ // step `mapping` produces an owed choice, report or alignment, or
77
+ // resends the last issuance under its revision (6840)
78
+ // acks a completed probe ends the probe duty; the alignment's
79
+ // delivery ends the alignment duty (6850)
80
+ // failures a failed probe chunk re-opens the probe duty (a fresh probe,
81
+ // 6710, 6830); a failed mapping not yet answered re-opens the
82
+ // mapping duty (6840)
83
+ //
84
+ // What it exports for tests and the encounter (PR 7): openTuple, the
85
+ // fresh tuple with its prior-candidate set frozen (2300) and its pending
86
+ // probe (6820, 6830), behind the testing module's non-human command
87
+ // continuity.newTuple.
88
+ //
89
+ // Choices this file makes where note and spec leave them open:
90
+ // - the prior-candidate set is the active-head tuples of every live
91
+ // relationship at tuple creation, in the order their heads were
92
+ // created (entryOrder.heads), the fresh tuple excluded (2300, 6740)
93
+ // - a probe is pending from the tuple's creation (6820) and goes out on
94
+ // the next resume (the next available delivery contact). The probe
95
+ // duty stands until a probe completes: after each send it is due again
96
+ // after the holder's retry backoff (DutyCtx.backoff, growing with its
97
+ // attempt) and then sends a fresh probe, next sequence, fresh padding
98
+ // (6710, 6830). The outbox never reproduces a probe (ownRetries); a
99
+ // failed chunk of the current probe (report, give-up) makes the duty
100
+ // due as soon as its backoff allows, max(when, lastAt +
101
+ // backoff(attempt)), never earlier; a superseded probe's failure
102
+ // touches nothing
103
+ // - the substitution (follow-up 17) applies to every fresh tuple not yet
104
+ // chained, its flow open or not: a flow that has matched nothing yet
105
+ // can still be reached by a late probe or mapping naming the head.
106
+ // `chosen` is final once set except here, where the chosen prior was
107
+ // consumed by another chaining and the choice is owed again on its head
108
+ // - the record side chooses the first match, in prior-candidate order,
109
+ // whose tuple is still the active head of a live relationship, and
110
+ // chains on nothing yet; the non-record side reports the first such
111
+ // match. A match report that reaches a record side without a choice is
112
+ // chosen from where its prior is still chainable (6910), and chained
113
+ // with that issuance: the report is the evidence
114
+ // - a choice, a report or an alignment whose preparation fails (a key
115
+ // not available) leaves `continuityOut.due` and a mapping duty; the
116
+ // receive still commits (its ack, the assembly, the revision)
117
+ // - every issuance opens the standing mapping duty; while the issuance
118
+ // stands it resends after the backoff (6840): the stored artifact (body
119
+ // and proof) re-sealed in a new document, same revision, a repeat at
120
+ // the receiver (6270), but only when no document of it is still with
121
+ // the outbox (whose retries are byte-identical, Delivery 4.2); a
122
+ // failure of this issuance makes it due as soon as its backoff allows. The record side's choice stands until
123
+ // the counterpart's mapping on the chosen relationship arrives
124
+ // under a higher revision than it held (`aligned`; a repeat answers
125
+ // nothing, 6270, and the report a choice was made from is no answer),
126
+ // the match report until the record side's mapping is verified (6850,
127
+ // 6860: it is never produced after), the alignment until its delivery
128
+ // completes (6850), counted by this issuance's documents only (`sent`
129
+ // is per issuance, `align` marks the alignment). The non-record side
130
+ // always aligns with a new issuance under the next revision, even where
131
+ // its report already named the chosen relationship (6930). Residue: a
132
+ // report the record side never saw, arriving first after the choice
133
+ // and naming it, is wire-identical to an alignment without a report
134
+ // before it and counts as the answer
135
+ // The outbox's retries of an issued report run to their horizon like
136
+ // superseded star salts; the record side rejects a lower revision
137
+ // - tick keeps the address key of a deactivated prior tuple while a live
138
+ // fresh tuple whose flow is open names it among its prior candidates
139
+ // (continuityKeys, the engine's keepKeys): mac1 needs it (6970)
140
+ // - a prior that a competing chaining took (deactivated, its chain's
141
+ // head another tuple) is refused at the record side's mapping as
142
+ // validation-failed with ref 6900 where the flow still names it (an
143
+ // unchained one had it substituted, follow-up 17, and refuses with
144
+ // 6960); a chaining replayed is one append (6230): a record-side
145
+ // mapping on an already chained tuple chains nothing again
146
+ // - chaining keeps the chain's relationship id (the chain continues);
147
+ // the fresh relationship stays as a husk without tuples, chainedInto
148
+ // the chain, its contactId a chain alias (2090). The chain takes the
149
+ // fresh relationship's place in entryOrder.heads (2180). Chain state
150
+ // carries (2280): links and bindings as the union (6610), a binding
151
+ // conflict kept as two bindings for one anchor, which the binding rule
152
+ // sees: the two entries behind the chain never merge, no merge duty
153
+ // opens, and no mapping touching that anchor verifies on the chain
154
+ // (6118); the chaining itself happens (6900), the earliest anchor as the earlier of the two
155
+ // by generation of the own register lineage (2285), contact memory
156
+ // (the chain's wins key by key, the group-star pairs merged per group
157
+ // with the appended head's winning: chainMemory), the relationship's anchor head (the appended
158
+ // tuple's verified self, else the chain's), firstVerified the earlier;
159
+ // provenance the chain's, except that an enactment chained onto an
160
+ // introduced chain sets it to ceremony, at the chain's time and without
161
+ // the introducer, never downward (8790, 8800: chainProvenance). Tuple
162
+ // state does not migrate (6250): the prior tuple is deactivated with
163
+ // its open assemblies discarded
164
+ // (5360), the fresh tuple keeps its own scopes, so its star baseline
165
+ // is fresh unless it completed a delivery before (5810, 5820)
166
+ // - the entries at chaining: where both relationships hold an entry,
167
+ // the appended tuple keeps its entry and the chain stands behind both
168
+ // (2145) until a merge duty merges them; every entry behind the chain
169
+ // settles its anchor head on what the chain holds (6560), so the
170
+ // appended tuple's entry, which may hold none yet, takes the chain's; where only one holds one,
171
+ // that one takes the other contact as an alias, the settings merged
172
+ // by 2160; where neither does, the chain's contact takes the fresh one
173
+ // as an alias (the chain continues; 2180 decides merges, and this is
174
+ // an append)
175
+ // - after a chaining whose entry has the anchor mapping class on, a
176
+ // class-on duty owes the mapping on the new head (6980) where nothing
177
+ // was issued on it, or where the chain and the tuple were issued under
178
+ // different community anchors (6985); its lineage starts at the
179
+ // chain's earliest anchor (2285). The duty respects lagging (6595):
180
+ // a lagging relationship gets it when it ceases to lag (2850)
181
+ // - at a chaining, the class duties owed on the chain and on the
182
+ // appended relationship (class-on, re-issue, profile) move to the
183
+ // chain under the entry that takes its verifications (2146); several
184
+ // re-issues become one, the most urgent awaited head kept; a class off
185
+ // toward that entry drops its duties (trust/duties.ts moveDuties, the
186
+ // follow rule)
187
+ // - 6465 (the profile on the new head after a chaining): the chaining
188
+ // opens the profile's class-on duty on the chain (trust/duties.ts
189
+ // afterChain), which trust/profile.ts delivers
190
+ // - the class duties of both relationships move to the chain and the
191
+ // follow rule runs over the chaining (trust/duties.ts)
192
+ //
193
+ // Open point for Trust 0.32 (found by the invariants generator, seed 5 and
194
+ // 50; reproduced in test/trust-continuity.test.mjs "one-sided chaining"):
195
+ // concurrent re-encounters of one pair race for one prior and leave the
196
+ // two holders with different chains. Exact sequence, two fresh pairs x
197
+ // and y of A and C, created before either chained, C the record side of
198
+ // x, A of y (isRecordSide per fresh pair):
199
+ // 1. the probes cross; C, record side of x, matches A's probe on x and
200
+ // chooses its prior (the original tuple, the first match still an
201
+ // active head, 6910), issuing its choice without chaining (follow-up
202
+ // 14); A, record side of y, does the same for y. The two choices
203
+ // cross: A chains x on verifying C's (6900, 6220), C chains y on A's
204
+ // 2. the reports and alignments follow: A's alignment on y reaches C,
205
+ // the record side of y, naming the prior it chose; its prior is in
206
+ // C's frozen prior-candidate set of y (6960 passes on the snapshot),
207
+ // but C's original tuple is no longer an active head: x took it.
208
+ // Nothing in 6.4 or 6a.4 says onto what C appends; this library
209
+ // refuses it (validation-failed, ref 6900), symmetrically at A for x.
210
+ // Since follow-up 17 the refusal comes earlier, ref 6960: C's chaining
211
+ // of y put y in the original's place in x's candidates (and A's of x
212
+ // in y's), and each record side's re-choice on its own head meets a
213
+ // counterpart already chained onto the original. The race stays
214
+ // 3. end state: A's chain is original→x, C's is original→y; C keeps x,
215
+ // A keeps y, as relationships of their own. x's standing mapping duty
216
+ // never ends (6840): 6990's "chained to the record-chosen relationship,
217
+ // or honestly unchained pending transport" is false, nothing is
218
+ // pending. The net still puts C's r-A-x into its entry A (2145)
219
+ // 4. C rotates before anything went out on x; its anchor mapping on x
220
+ // carries the lineage from x's own earliest anchor (2285, 6420: per
221
+ // relationship), the new generation, so none. A holds C's earlier
222
+ // generation on its chain, whose head is x: 6170 (an unlinked change
223
+ // of self) refuses it on every re-issue. 6985 does not apply at C: C
224
+ // never chained. (Had C issued on x before rotating, x's earliest
225
+ // anchor would be the earlier generation and the segment would reach
226
+ // A; the divergent chains and the endless duty of step 3 remain.)
227
+ // Each local decision follows a rule as written (6900, 6910, 6960); the
228
+ // refusal of step 2 is this library's conservative reading, which 6900
229
+ // does not state; the end state violates the progress rules 6870 and
230
+ // 6990. What 0.32 needs to decide, either:
231
+ // (a) order competing chainings: one deterministic winner among fresh
232
+ // pairs that choose the same prior (say the lower record anchor),
233
+ // the loser chained after the winner on both sides, so 6960's
234
+ // snapshot verification has a defined append target; then 6990
235
+ // holds, and 6985 covers the loser's head; or
236
+ // (b) widen the lineage: 6420/2285 take the earliest generation issued
237
+ // on any relationship of the entry (2145), not of the relationship
238
+ // — C's r-A-x and r-A are one entry, so the segment from generation
239
+ // 1 would reach A — and 6260/6990 name the unchained end state.
240
+ // C's earliestAnchor on r-A-x is right by 2285 as written (nothing
241
+ // earlier was issued on that relationship); it is the per-relationship
242
+ // scope, not the value, that leaves A without the link.
243
+ import { iso, jcs, fromB64u } from '../core.js';
244
+ import { digestBytes, digestDoc } from '../crypto.js';
245
+ import { withLabel, withPairNonce } from '../identity-state.js';
246
+ import { seal } from '../delivery.js';
247
+ import { assembleContinuityMapping, blindPrior, buildProbeChunks, continuityMacsOk, continuityMappingOk, isRecordSide, probeChunkMacOk, probeChunkOk, probeKeyOf, PROBE_CHUNK, } from '../continuity.js';
248
+ import { strictlyAscending } from '../star.js';
249
+ import { tupleByRkid } from '../receive.js';
250
+ import { triasOf } from '../state.js';
251
+ import { emptyTuple } from '../state-builder.js';
252
+ import { ADMISSION_BOUND, activeTuple, classOnToward, entryOf, findContact, liveRel, tupleByLabel, unionBindings } from './state.js';
253
+ import { assembledBodies, assemblyStep, canComplete, chunkDocument, discardAssemblies, pairShared } from './star.js';
254
+ import { mergeDutiesAfter, mergeInto, settleHead } from './entries.js';
255
+ import { afterChain, classOnDuty, followClasses, moveDuties } from './duties.js';
256
+ import { isKnownDuty } from '../codec.js';
257
+ /** The registered task types of 6a (Delivery §4.4, 6810). */
258
+ export const CONTINUITY_PROBE_TYPE = 'https://real-life.org/trust-tasks/continuity-probe/0.1';
259
+ export const CONTINUITY_MAPPING_TYPE = 'https://real-life.org/trust-tasks/continuity-mapping/0.1';
260
+ /** The testing module's command (./testing). */
261
+ export const NEW_TUPLE = 'continuity.newTuple';
262
+ const inc = (d) => (BigInt(d) + 1n).toString();
263
+ const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
264
+ const refuse = (reason, ref) => ({ ok: false, reason, ...(ref ? { ref } : {}) });
265
+ const probeDutyId = (label) => `continuity:probe:${label}`;
266
+ const mappingDutyId = (label) => `continuity:mapping:${label}`;
267
+ // ── reads ──────────────────────────────────────────────────────────────
268
+ /** The tuple `label` if it is the active head of a live relationship. */
269
+ function liveHead(state, label) {
270
+ const at = tupleByLabel(state, label);
271
+ if (!at || !liveRel(at.rel) || at.rel.activeHead !== label)
272
+ return null;
273
+ return at;
274
+ }
275
+ /** The arrival tuple, if it is the active head of a live relationship (6190, 6240). */
276
+ function arrivalTuple(state, rkid) {
277
+ const at = tupleByRkid(state, rkid);
278
+ if (!at || !liveRel(at.rel) || at.rel.activeHead !== at.tuple.ownPairLabel)
279
+ return null;
280
+ return at;
281
+ }
282
+ /** A prior the fresh tuple `label` can still be chained onto: a tuple of
283
+ * another relationship, its live active head. */
284
+ const chainable = (state, label, prior) => {
285
+ const p = liveHead(state, prior);
286
+ const t = tupleByLabel(state, label);
287
+ return p !== null && t !== null && p.rel.id !== t.rel.id;
288
+ };
289
+ /** The tuple `label` with `fn` applied, wherever it lives. */
290
+ function withTuple(state, label, fn) {
291
+ const at = tupleByLabel(state, label);
292
+ if (!at)
293
+ throw new Error(`continuity: no tuple ${label}`);
294
+ const tuples = at.rel.tuples.map((t) => (t.ownPairLabel === label ? fn(t) : t));
295
+ return { ...state, relationships: { ...state.relationships, [at.rel.id]: { ...at.rel, tuples } } };
296
+ }
297
+ /** A probe is owed until one completed (6830). */
298
+ const probeOwed = (_state, t) => !t.probeOut.done;
299
+ /** The fresh tuple's continuity flow is still open: no answered
300
+ * issuance, and something chosen, reported, owed or not yet probed. */
301
+ const flowOpen = (t) => !t.continuityOut.aligned && (t.continuityOut.chosen !== null || t.continuityIn.recordPrior !== null || t.continuityOut.reported !== null ||
302
+ t.continuityOut.due !== null || !t.probeOut.done || t.probeIn.hwm === '0');
303
+ /** The own rkids of deactivated prior tuples that a live fresh tuple with
304
+ * an open flow still names (2300, 6200): their keys verify mac1 (6970)
305
+ * and issue it, so tick keeps them (engine keepKeys). */
306
+ export function continuityKeys(state) {
307
+ const out = new Set();
308
+ let byLabel = null; // every tuple by its own label, once per call
309
+ for (const rel of Object.values(state.relationships)) {
310
+ if (!liveRel(rel))
311
+ continue;
312
+ const t = activeTuple(rel);
313
+ if (!t.priorCandidates.length || !flowOpen(t))
314
+ continue;
315
+ if (!byLabel) {
316
+ byLabel = new Map();
317
+ for (const r of Object.values(state.relationships))
318
+ for (const x of r.tuples)
319
+ byLabel.set(x.ownPairLabel, x);
320
+ }
321
+ for (const pc of t.priorCandidates) {
322
+ const p = byLabel.get(pc.ownPairLabel);
323
+ if (p && p.deactivated)
324
+ out.add(p.rkid);
325
+ }
326
+ }
327
+ return [...out];
328
+ }
329
+ /** The own pair label of the prior-candidate entry whose counterpart anchor is `anchor` (6960). */
330
+ const priorOf = (t, anchor) => t.priorCandidates.find((pc) => pc.counterpartAnchor === anchor && pc.counterpartAnchor !== t.counterpartAnchor && pc.ownPairLabel !== t.ownPairLabel);
331
+ /** The counterpart anchors a prior candidate answers a probe for (6800):
332
+ * its own and those it replaced in this tuple's own flow (follow-up 17,
333
+ * `replaces`): a probe built before the counterpart's substitution still
334
+ * blinds the replaced prior's anchor. Never the chain's other deactivated
335
+ * tuples: a tuple opened after a chaining matches its frozen candidates
336
+ * only, so an old pair anchor discloses no current one (Codex I). */
337
+ const standsFor = (pc) => [pc.counterpartAnchor, ...(pc.replaces ?? [])];
338
+ /** Whether the last issuance still stands (6840, 6850, 6860), and the
339
+ * record side's view of it. */
340
+ function issuanceStands(t, record) {
341
+ const o = t.continuityOut;
342
+ if (o.body === null)
343
+ return false;
344
+ if (record)
345
+ return !o.aligned;
346
+ if (t.continuityIn.recordPrior === null)
347
+ return true; // the match report, until the choice is verified
348
+ const chosen = priorOf(t, t.continuityIn.recordPrior)?.ownPairLabel;
349
+ return o.align && o.reported === chosen && !o.aligned; // the alignment, until its delivery completes
350
+ }
351
+ /** The record side of a tuple, read from its last issuance or its prior state. */
352
+ const recordOf = (t) => {
353
+ if (t.continuityOut.chosen !== null)
354
+ return true;
355
+ if (t.continuityOut.reported !== null || t.continuityIn.recordPrior !== null)
356
+ return false;
357
+ return null;
358
+ };
359
+ /**
360
+ * A fresh tuple toward a counterpart as its own relationship and contact
361
+ * (Encounter §4.4: fresh-always), its prior-candidate set frozen now
362
+ * (2300), its own rkid live, its probe pending (6820, 6830).
363
+ */
364
+ export function openTuple(state, i, rkid, when) {
365
+ if (Object.hasOwn(state.relationships, i.relId))
366
+ throw new Error(`${NEW_TUPLE}: relationship ${i.relId} exists`);
367
+ if (Object.hasOwn(state.contacts, i.contactId) || Object.values(state.contacts).some((c) => c.aliases.includes(i.contactId)))
368
+ throw new Error(`${NEW_TUPLE}: contact ${i.contactId} exists`);
369
+ if (tupleByLabel(state, i.ownPairLabel))
370
+ throw new Error(`${NEW_TUPLE}: label ${i.ownPairLabel} is in use`);
371
+ const heads = state.entryOrder.heads;
372
+ const priorCandidates = Object.values(state.relationships)
373
+ .filter((r) => liveRel(r))
374
+ .sort((a, b) => (heads.indexOf(a.id) - heads.indexOf(b.id)) || cmp(a.id, b.id))
375
+ .map((r) => activeTuple(r))
376
+ .map((t) => ({ ownPairLabel: t.ownPairLabel, counterpartAnchor: t.counterpartAnchor }));
377
+ const tuple = emptyTuple({
378
+ ownPairLabel: i.ownPairLabel, counterpartAnchor: i.counterpartAnchor, counterpartKeyAgreement: i.counterpartKeyAgreement, rkid,
379
+ entryId: i.contactId, priorCandidates,
380
+ });
381
+ const rel = {
382
+ id: i.relId, contactId: i.contactId, tuples: [tuple], activeHead: i.ownPairLabel, earliestAnchor: null, links: [], bindings: {},
383
+ heldCounterpartHead: null, memory: {}, chainedInto: null, firstVerified: null,
384
+ provenance: { kind: i.provenance?.kind ?? 'ceremony', ...(i.provenance?.via !== undefined ? { via: i.provenance.via } : {}), ...(i.provenance?.viaPin !== undefined ? { viaPin: i.provenance.viaPin } : {}), at: when },
385
+ };
386
+ const contact = {
387
+ id: i.contactId, pos: null, status: 'none', aliases: [], relIds: [i.relId], anchorHead: null,
388
+ settings: { star: 'default', groupStar: 'default', anchorMapping: 'default', sealedGroupStar: 'default', profile: 'default' }, groupSettings: {},
389
+ };
390
+ const duty = { kind: 'continuity', v: 1, id: probeDutyId(i.ownPairLabel), relId: i.relId, tuple: i.ownPairLabel, step: 'probe' };
391
+ return {
392
+ state: {
393
+ ...state,
394
+ register: i.nonce === undefined ? withLabel(state.register, i.ownPairLabel) : withPairNonce(state.register, i.ownPairLabel, i.nonce),
395
+ addresses: { ...state.addresses, keys: { ...state.addresses.keys, [rkid]: { label: i.ownPairLabel, lastShown: when } } },
396
+ contacts: { ...state.contacts, [i.contactId]: contact },
397
+ relationships: { ...state.relationships, [i.relId]: rel },
398
+ entryOrder: { ...state.entryOrder, heads: [...state.entryOrder.heads, i.relId] },
399
+ },
400
+ events: [{ type: 'contact.added', contactId: i.contactId }],
401
+ duties: [duty],
402
+ };
403
+ }
404
+ // ── the chaining (6220–6250, 2145, 2280, 2285, 6610) ───────────────────
405
+ /** The generation order of own community anchors along the register's
406
+ * lineage (2285): the index of the element leaving the anchor, or the
407
+ * lineage's length for its end; with no lineage, every anchor is one. */
408
+ function generationOf(state, anchor) {
409
+ const lin = state.register.lineage;
410
+ if (!lin.length)
411
+ return 0;
412
+ const at = lin.findIndex((r) => r.body?.prev === anchor);
413
+ if (at >= 0)
414
+ return at;
415
+ return lin[lin.length - 1].body?.next === anchor ? lin.length : Number.MAX_SAFE_INTEGER;
416
+ }
417
+ const earlierAnchor = (state, a, b) => a === null ? b : b === null ? a : generationOf(state, b) < generationOf(state, a) ? b : a;
418
+ /** The anchors the holder issued its anchor mapping under on these tuples and relationship. */
419
+ const issuedUnder = (r) => [r.earliestAnchor, ...r.tuples.map((t) => t.mappingOut.underAnchor)].filter((a) => a !== null);
420
+ const mapOf = (x) => (x !== null && typeof x === 'object' && !Array.isArray(x) ? x : {});
421
+ /** Contact memory at a chaining (2280): the chain's wins key by key
422
+ * (introducedName, vouchedBy with its vouchedPin: what the earlier
423
+ * relationship established),
424
+ * except the class-V group-star pairs (5580), merged per group with the
425
+ * appended relationship's winning, since it is the active head whose
426
+ * stars are the newer (5770). */
427
+ function chainMemory(r0, rn) {
428
+ const m = { ...rn, ...r0 };
429
+ if (Object.hasOwn(r0, 'groupMembers') || Object.hasOwn(rn, 'groupMembers'))
430
+ m.groupMembers = { ...mapOf(r0.groupMembers), ...mapOf(rn.groupMembers) };
431
+ return m;
432
+ }
433
+ /** The chain's provenance (8790, 8800): an introduced chain becomes
434
+ * `ceremony` when the appended relationship came by an enactment, at the
435
+ * chain's time and without the introducer (via and viaPin name the
436
+ * introduction; the voucher mark and the introduced name are memory and
437
+ * stay); never downward: a ceremony chain stays ceremony, an introduced
438
+ * one appended by an introduction keeps its own introducer. */
439
+ const chainProvenance = (p0, pn) => p0.kind === 'introduced' && pn.kind === 'ceremony' ? { kind: 'ceremony', at: p0.at } : p0;
440
+ /**
441
+ * The chain append (6220): the fresh tuple `label` becomes the active head
442
+ * of the chain whose active head `prior` is, atomically with that prior's
443
+ * deactivation; chain state carries, tuple state does not (6250). Returns
444
+ * `already` where this very append is done (6230), `taken` where the prior
445
+ * is no longer chainable.
446
+ */
447
+ export function chainTuple(state, label, prior, bound) {
448
+ const fresh = tupleByLabel(state, label);
449
+ const pr = tupleByLabel(state, prior);
450
+ if (!fresh || !pr)
451
+ return 'taken';
452
+ if (fresh.rel.id === pr.rel.id)
453
+ return fresh.rel.activeHead === label && pr.tuple.deactivated ? 'already' : 'taken';
454
+ // a strand absorbs no second one (2220): the fresh tuple's relationship is just this tuple
455
+ if (fresh.rel.tuples.length !== 1 || !liveRel(fresh.rel) || fresh.rel.activeHead !== label)
456
+ return 'taken';
457
+ if (!liveRel(pr.rel) || pr.rel.activeHead !== prior)
458
+ return 'taken';
459
+ const R0 = pr.rel, Rn = fresh.rel;
460
+ const t = fresh.tuple;
461
+ const Eo = entryOf(state, R0), En = entryOf(state, Rn);
462
+ const links = [...R0.links];
463
+ for (const l of Rn.links)
464
+ if (!links.some((x) => x.prev === l.prev && x.next === l.next))
465
+ links.push(l);
466
+ const firstVerified = R0.firstVerified === null ? Rn.firstVerified : Rn.firstVerified === null ? R0.firstVerified
467
+ : BigInt(Rn.firstVerified) < BigInt(R0.firstVerified) ? Rn.firstVerified : R0.firstVerified;
468
+ const chain = {
469
+ ...R0,
470
+ tuples: [...R0.tuples.map((x) => (x.ownPairLabel === prior ? discardAssemblies({ ...x, deactivated: true }) : x)), t],
471
+ activeHead: label,
472
+ earliestAnchor: earlierAnchor(state, R0.earliestAnchor, Rn.earliestAnchor),
473
+ links,
474
+ bindings: unionBindings(R0.bindings, Rn.bindings),
475
+ heldCounterpartHead: Rn.heldCounterpartHead ?? R0.heldCounterpartHead,
476
+ memory: chainMemory(R0.memory, Rn.memory),
477
+ firstVerified,
478
+ provenance: chainProvenance(R0.provenance, Rn.provenance),
479
+ };
480
+ const husk = {
481
+ ...Rn, tuples: [], chainedInto: R0.id, links: [], bindings: {}, earliestAnchor: null, heldCounterpartHead: null, memory: {},
482
+ };
483
+ // the chain takes the fresh relationship's place in the creation order of active heads (2180)
484
+ const heads = state.entryOrder.heads.filter((id) => id !== R0.id).map((id) => (id === Rn.id ? R0.id : id));
485
+ // the fresh relationship's continuity duties move to the chain; the
486
+ // class and re-issue duties of both are relocated below, once the entry
487
+ // that takes the chain's verifications is known
488
+ const duties = {};
489
+ for (const [id, d] of Object.entries(state.duties)) {
490
+ const relId = d.relId;
491
+ if (relId !== Rn.id || d.kind === 'merge') {
492
+ duties[id] = d;
493
+ continue;
494
+ }
495
+ if (d.kind === 'continuity' && isKnownDuty(d))
496
+ duties[id] = { ...d, relId: R0.id };
497
+ else
498
+ duties[id] = d; // class duties move below (moveDuties); unknown kinds stay as they are
499
+ }
500
+ const rotations = {};
501
+ for (const [id, r] of Object.entries(state.rotations)) {
502
+ if (!Object.hasOwn(r.awaitHead, Rn.id)) {
503
+ rotations[id] = r;
504
+ continue;
505
+ }
506
+ // the 6596 decision of a running rotation moves to the chain like the
507
+ // re-issue duties (moveDuties): one re-issue owed, due at once where
508
+ // either was (null wins), else the chain's own wait
509
+ const { [Rn.id]: fresh, ...awaitHead } = r.awaitHead;
510
+ const own = Object.hasOwn(awaitHead, R0.id) ? awaitHead[R0.id] : undefined;
511
+ awaitHead[R0.id] = own === undefined ? fresh : own === null || fresh === null ? null : own;
512
+ rotations[id] = { ...r, awaitHead };
513
+ }
514
+ let next = {
515
+ ...state, relationships: { ...state.relationships, [R0.id]: chain, [Rn.id]: husk }, duties, rotations,
516
+ entryOrder: { ...state.entryOrder, heads },
517
+ };
518
+ // the entries (2145, 2140)
519
+ const swap = (c) => ({ ...c, relIds: [...new Set(c.relIds.map((id) => (id === Rn.id ? R0.id : id)))] });
520
+ let entryId;
521
+ const out = [];
522
+ const renames = [];
523
+ if (!Eo || !En || Eo.id === En.id) {
524
+ const e = (Eo ?? En);
525
+ entryId = e.id;
526
+ next = { ...next, contacts: { ...next.contacts, [e.id]: swap(next.contacts[e.id]) } };
527
+ }
528
+ else if (Eo.pos !== null && En.pos !== null) {
529
+ // 2145: the chain stands behind both entries; the appended tuple's takes its verifications
530
+ entryId = En.id;
531
+ next = { ...next, contacts: { ...next.contacts, [En.id]: swap(next.contacts[En.id]) } };
532
+ }
533
+ else {
534
+ // one entry or none: the other contact folds into it as an alias (2160 settings)
535
+ const survivor = Eo.pos === null && En.pos !== null ? En : Eo;
536
+ const gone = survivor === Eo ? En : Eo;
537
+ next = { ...next, contacts: { ...next.contacts, [En.id]: swap(next.contacts[En.id]) } };
538
+ const m = mergeInto(next, next.contacts[survivor.id], survivor.id, [next.contacts[gone.id]], bound);
539
+ next = m.state;
540
+ out.push(...(m.duties ?? []));
541
+ renames.push(...(m.renames ?? []));
542
+ entryId = m.survivor;
543
+ }
544
+ next = withTuple(next, label, (x) => ({ ...x, entryId }));
545
+ // 6560: the entries behind the chain settle their anchor heads on what
546
+ // the chain holds now — the appended tuple's entry included, which (2145)
547
+ // takes its verifications and may hold none of its own yet
548
+ for (const id of new Set([entryId, Eo?.id, En?.id].filter((x) => x !== undefined))) {
549
+ const c = findContact(next, id);
550
+ if (!c)
551
+ continue;
552
+ const settled = settleHead(next, c, chain.heldCounterpartHead);
553
+ if (settled !== c)
554
+ next = { ...next, contacts: { ...next.contacts, [c.id]: settled } };
555
+ }
556
+ next = moveDuties(next, [R0.id, Rn.id], R0.id, entryId);
557
+ // 6980, 6985: the anchor mapping on the new head, from the chain's earliest anchor (2285)
558
+ const entry = next.contacts[entryId];
559
+ if (classOnToward(entry, 'anchorMapping')) {
560
+ const anchors = new Set([...issuedUnder(R0), ...issuedUnder(Rn)]);
561
+ const differ = issuedUnder(R0).length > 0 && issuedUnder(Rn).length > 0 && anchors.size > 1;
562
+ if (t.mappingOut.body === null || differ)
563
+ out.push(...classOnDuty(next, entryId, R0.id, 'anchorMapping', out));
564
+ }
565
+ // 6465: the profile on the new head (trust/duties.ts)
566
+ out.push(...afterChain(next, R0.id, out));
567
+ // 2145: a merge the two entries admit runs after this commit (2230)
568
+ const md = mergeDutiesAfter(state, next, R0.id, { net: false });
569
+ out.push(...md.duties);
570
+ // the follow rule over the whole chaining: the drop of a class off toward
571
+ // the entry now, a class on for the chain that was not before (2850)
572
+ const f = followClasses(state, md.state, out);
573
+ // spec follow-up 17 (2300): the new head takes the prior's place in every other unchained flow
574
+ return { state: substitutePrior(f.state, prior, label), events: [{ type: 'contact.chained', contactId: entryId, from: Rn.id, into: R0.id }], duties: f.duties, renames };
575
+ }
576
+ /**
577
+ * Candidate substitution (spec follow-up 17, 2300): `prior` was just
578
+ * deactivated as the prior of a chaining whose new head is `head`, a tuple
579
+ * verified chained onto it (mac1 under the prior's key, 6970). Every other
580
+ * fresh tuple not yet chained whose prior-candidate set names `prior` gets
581
+ * `head` in its place (same position, a duplicate dropped), and what its
582
+ * flow held about `prior` is re-targeted to `head`, a fresh issuance under
583
+ * the next revision through the mapping duty: the record side's choice is
584
+ * owed again naming `head` (chainable now), the non-record side's report or
585
+ * alignment likewise, the frozen record prior (6290) follows to `head`'s
586
+ * counterpart anchor. The head's entry records in `replaces` the anchors
587
+ * it stands for in this flow (standsFor). Transitive by construction: a chaining onto `head`
588
+ * later substitutes again. Pure: the state only, the duties written into it.
589
+ */
590
+ export function substitutePrior(state, prior, head) {
591
+ const h = tupleByLabel(state, head);
592
+ const p = tupleByLabel(state, prior);
593
+ if (!h || !p)
594
+ return state;
595
+ const sub = (l) => (l === prior ? head : l);
596
+ let next = state;
597
+ for (const rel of Object.values(state.relationships)) {
598
+ if (!liveRel(rel) || rel.tuples.length !== 1)
599
+ continue; // a strand absorbs no second one (2220)
600
+ const t = rel.tuples[0];
601
+ if (t.ownPairLabel === head || !t.priorCandidates.some((pc) => pc.ownPairLabel === prior))
602
+ continue;
603
+ // the head in the prior's place, standing for whatever the prior stood for in this flow and for
604
+ // the prior, oldest first (transitive); a head already in the set keeps the first position
605
+ const pc0 = t.priorCandidates.find((pc) => pc.ownPairLabel === prior);
606
+ const into = { ownPairLabel: head, counterpartAnchor: h.tuple.counterpartAnchor };
607
+ const replaces = new Set();
608
+ for (const pc of t.priorCandidates)
609
+ if (pc.ownPairLabel === prior || pc.ownPairLabel === head)
610
+ for (const a of [...(pc.replaces ?? []), pc.counterpartAnchor])
611
+ replaces.add(a);
612
+ replaces.delete(into.counterpartAnchor);
613
+ if (replaces.size)
614
+ into.replaces = [...replaces];
615
+ const priorCandidates = [];
616
+ for (const pc of t.priorCandidates) {
617
+ const x = pc.ownPairLabel === prior || pc.ownPairLabel === head ? into : pc;
618
+ if (!priorCandidates.some((y) => y.ownPairLabel === x.ownPairLabel))
619
+ priorCandidates.push(x);
620
+ }
621
+ const o = t.continuityOut;
622
+ const rp = t.continuityIn.recordPrior;
623
+ let due = o.due === null ? null
624
+ : o.due.kind === 'choice' ? { kind: 'choice', prior: sub(o.due.prior), alts: [...new Set(o.due.alts.map(sub))].filter((x) => x !== sub(o.due.prior)) }
625
+ : { kind: o.due.kind, prior: sub(o.due.prior) };
626
+ // the record side re-chooses: its choice on the prior is owed again, on the head
627
+ let chosen = o.chosen;
628
+ if (chosen === prior) {
629
+ chosen = null;
630
+ due = { kind: 'choice', prior: head, alts: [] };
631
+ }
632
+ // the non-record side re-reports, or re-aligns, naming the head
633
+ if (o.reported === prior && due === null)
634
+ due = { kind: rp !== null ? 'align' : 'report', prior: head };
635
+ const recordPrior = rp === pc0.counterpartAnchor ? into.counterpartAnchor : rp;
636
+ next = withTuple(next, t.ownPairLabel, (x) => ({
637
+ ...x, priorCandidates, continuityIn: { ...x.continuityIn, recordPrior }, continuityOut: { ...x.continuityOut, chosen, due },
638
+ }));
639
+ if (due !== null && (chosen !== o.chosen || due.kind !== o.due?.kind || due.prior !== o.due.prior))
640
+ next = owe(next, t.ownPairLabel, rel.id);
641
+ }
642
+ return next;
643
+ }
644
+ /** Seal a continuity-mapping/0.1 document on tuple `t`. */
645
+ async function mappingDocument(ctx, t, contactId, own, m) {
646
+ const S = ctx.crypto.subtle;
647
+ const threadId = ctx.entropy.id('thread.id');
648
+ const doc = { id: ctx.entropy.id('doc.id'), type: CONTINUITY_MAPPING_TYPE, issuer: own, recipient: t.counterpartAnchor, threadId, issuedAt: iso(ctx.when), payload: m };
649
+ const envelope = await seal(doc, t.counterpartKeyAgreement, { ephSeed: ctx.entropy.bytes('seal.eph', 32), nonce: ctx.entropy.bytes('seal.nonce', 12) }, S);
650
+ return { digest: await digestDoc(doc, S), envelope, type: CONTINUITY_MAPPING_TYPE, threadId, recipient: { contactId, rkid: t.rkid, keyAgreement: t.counterpartKeyAgreement } };
651
+ }
652
+ /** Prepare the next revision of the tuple's continuity mapping naming `prior` (6300). */
653
+ async function prepareIssue(ctx, label, prior) {
654
+ const st = ctx.snapshot;
655
+ const at = tupleByLabel(st, label);
656
+ const pr = tupleByLabel(st, prior);
657
+ if (!at || !pr)
658
+ return null;
659
+ const t = at.tuple;
660
+ const rev = inc(t.continuityOut.rev);
661
+ if (BigInt(rev) > ADMISSION_BOUND)
662
+ return null; // the scope suspends (2.1, 6870)
663
+ const shNew = await pairShared(ctx, t);
664
+ const shPrior = await pairShared(ctx, pr.tuple);
665
+ if (!shNew || !shPrior)
666
+ return null;
667
+ const own = (await ctx.keys.anchorOf(t.ownPairLabel)).anchor;
668
+ const body = {
669
+ type: 'continuity-mapping@1', prior: (await ctx.keys.anchorOf(pr.tuple.ownPairLabel)).anchor, next: own, to: t.counterpartAnchor,
670
+ revision: rev, issuedAt: iso(ctx.when),
671
+ };
672
+ const m = await assembleContinuityMapping(body, shPrior, shNew, ctx.crypto.subtle);
673
+ const contactId = entryOf(st, at.rel)?.id ?? at.rel.contactId;
674
+ return { label, prior, baseRev: t.continuityOut.rev, rev, body, proof: m.proof, outbound: await mappingDocument(ctx, t, contactId, own, m) };
675
+ }
676
+ /** Commit an issuance on the state it lands on: revision and body in one
677
+ * step (6300), the recipient's current entry; null where the scope moved. */
678
+ function commitIssue(state, p, set) {
679
+ const at = tupleByLabel(state, p.label);
680
+ if (!at || at.tuple.continuityOut.rev !== p.baseRev)
681
+ return null;
682
+ const next = withTuple(state, p.label, (t) => ({
683
+ ...t,
684
+ continuityOut: {
685
+ ...t.continuityOut, rev: p.rev, body: p.body, proof: p.proof, sent: [p.outbound.digest], due: null,
686
+ align: t.continuityIn.recordPrior !== null, ...set(t.continuityOut),
687
+ },
688
+ }));
689
+ const after = tupleByLabel(next, p.label);
690
+ const contactId = entryOf(next, after.rel)?.id ?? p.outbound.recipient.contactId;
691
+ return { state: next, outbound: { ...p.outbound, recipient: { ...p.outbound.recipient, contactId } } };
692
+ }
693
+ /** The standing mapping duty of the tuple (6840, 6850), in the state:
694
+ * opened (or restarted) at an issuance; its first run schedules the
695
+ * resend after the backoff. */
696
+ const openMapping = (state, label, relId, when) => ({ ...state, duties: { ...state.duties, [mappingDutyId(label)]: { kind: 'continuity', v: 1, id: mappingDutyId(label), relId, tuple: label, step: 'mapping', attempt: 1, lastAt: when } } });
697
+ /** The earliest time the standing duty may send again: the backoff after its last send. */
698
+ const nextAt = (d, backoff) => Math.min((d.lastAt ?? 0) + Math.max(0, Math.ceil(backoff(Math.max(1, d.attempt ?? 1)))), Number.MAX_SAFE_INTEGER);
699
+ /** A failure (6830/6840) makes the standing duty due as soon as its
700
+ * backoff allows, never earlier: max(when, lastAt + backoff(attempt)). */
701
+ const dueOnFailure = (state, d, when, backoff) => {
702
+ const cur = Object.hasOwn(state.duties, d.id) ? state.duties[d.id] : d;
703
+ const dueAt = cur.lastAt === undefined ? when : Math.max(when, nextAt(cur, backoff));
704
+ return cur.dueAt === dueAt ? state : { ...state, duties: { ...state.duties, [d.id]: { ...cur, dueAt } } };
705
+ };
706
+ const dropDuty = (state, id) => {
707
+ if (!Object.hasOwn(state.duties, id))
708
+ return state;
709
+ const { [id]: _, ...duties } = state.duties;
710
+ return { ...state, duties };
711
+ };
712
+ /**
713
+ * The record side's choice (6900, 6910): the first prepared prior still
714
+ * chainable is issued; where none was prepared but some match is still
715
+ * chainable, the choice is owed (due). It chains with the issuance only on
716
+ * `evidence`: a verified mapping of the counterpart under the prior
717
+ * relationship's key (mac1, 6970), its match report naming that prior. A
718
+ * probe hit alone proves only that the counterpart knows the prior's
719
+ * anchor and shares the fresh tuple's key, never that it holds the prior
720
+ * relationship (spec follow-up 14, header): on a hit the choice is issued
721
+ * and stands as a candidate until that mapping arrives (mappingTask).
722
+ */
723
+ function choose(state, label, matches, issues, bound, when, evidence) {
724
+ for (const p of issues) {
725
+ if (!chainable(state, label, p.prior))
726
+ continue;
727
+ const c = evidence ? chainTuple(state, label, p.prior, bound) : null;
728
+ if (typeof c === 'string')
729
+ continue;
730
+ const i = commitIssue(c ? c.state : state, p, () => ({ chosen: p.prior }));
731
+ if (!i)
732
+ continue;
733
+ const after = tupleByLabel(i.state, label);
734
+ const opened = openMapping(i.state, label, after.rel.id, when);
735
+ return c ? { state: opened, outbound: [i.outbound], events: c.events, duties: c.duties, renames: c.renames } : { state: opened, outbound: [i.outbound] };
736
+ }
737
+ const viable = matches.filter((m) => chainable(state, label, m));
738
+ // every candidate gone: an owed choice dies, honestly unchained (6990)
739
+ if (!viable.length)
740
+ return { state: withTuple(state, label, (t) => (t.continuityOut.due?.kind === 'choice' ? { ...t, continuityOut: { ...t.continuityOut, due: null } } : t)) };
741
+ const at = tupleByLabel(state, label);
742
+ const s = withTuple(state, label, (t) => ({ ...t, continuityOut: { ...t.continuityOut, due: { kind: 'choice', prior: viable[0], alts: viable.slice(1) } } }));
743
+ return { state: owe(s, label, at.rel.id) };
744
+ }
745
+ /** A production owed: the mapping duty, due at once. */
746
+ const owe = (state, label, relId) => {
747
+ const id = mappingDutyId(label);
748
+ const fresh = { kind: 'continuity', v: 1, id, relId, tuple: label, step: 'mapping' };
749
+ const { dueAt: _, ...cur } = (Object.hasOwn(state.duties, id) ? state.duties[id] : fresh);
750
+ return { ...state, duties: { ...state.duties, [id]: cur } };
751
+ };
752
+ /** The non-record side's match report or alignment (6930): issued, or owed. */
753
+ function issueOrOwe(state, label, prior, issue, kind, when) {
754
+ const at = tupleByLabel(state, label);
755
+ const i = issue && issue.prior === prior ? commitIssue(state, issue, () => ({ reported: prior, aligned: false })) : null;
756
+ if (i)
757
+ return { state: openMapping(i.state, label, at.rel.id, when), outbound: [i.outbound] };
758
+ const s = withTuple(state, label, (t) => ({ ...t, continuityOut: { ...t.continuityOut, due: { kind, prior } } }));
759
+ return { state: owe(s, label, at.rel.id) };
760
+ }
761
+ const merge = (a, b) => ({
762
+ state: b.state,
763
+ outbound: [...(a.outbound ?? []), ...(b.outbound ?? [])],
764
+ events: [...(a.events ?? []), ...(b.events ?? [])],
765
+ duties: [...(a.duties ?? []), ...(b.duties ?? [])],
766
+ renames: [...(a.renames ?? []), ...(b.renames ?? [])],
767
+ });
768
+ async function prepareProbe(ctx, label) {
769
+ const st = ctx.snapshot;
770
+ const at = liveHead(st, label);
771
+ if (!at || !probeOwed(st, at.tuple))
772
+ return null;
773
+ const t = at.tuple;
774
+ const seq = inc(t.probeOut.seq);
775
+ if (BigInt(seq) > ADMISSION_BOUND)
776
+ return null; // 6680, the scope suspends
777
+ const shared = await pairShared(ctx, t);
778
+ if (!shared)
779
+ return null;
780
+ const S = ctx.crypto.subtle;
781
+ const own = (await ctx.keys.anchorOf(t.ownPairLabel)).anchor;
782
+ // 6730, 6740: the own pair anchors of the prior-candidate set, never the fresh tuple's
783
+ const priors = [];
784
+ for (const pc of t.priorCandidates)
785
+ if (pc.ownPairLabel !== t.ownPairLabel)
786
+ priors.push((await ctx.keys.anchorOf(pc.ownPairLabel)).anchor);
787
+ const k = await probeKeyOf(shared, own, t.counterpartAnchor, S);
788
+ const chunks = await buildProbeChunks(k, seq, priors.filter((a) => a !== own), () => ctx.entropy.bytes('pad', 32), S);
789
+ // one probe = one thread (Delivery §4.4)
790
+ const threadId = ctx.entropy.id('thread.id');
791
+ const contactId = entryOf(st, at.rel)?.id ?? at.rel.contactId;
792
+ const outbound = [];
793
+ for (const c of chunks)
794
+ outbound.push(await chunkDocument(ctx, CONTINUITY_PROBE_TYPE, own, t, contactId, threadId, c));
795
+ return { label, baseSeq: t.probeOut.seq, seq, outbound };
796
+ }
797
+ async function prepareMappingWork(ctx, label) {
798
+ const st = ctx.snapshot;
799
+ const at = liveHead(st, label);
800
+ if (!at)
801
+ return null;
802
+ const t = at.tuple;
803
+ const o = t.continuityOut;
804
+ if (o.due?.kind === 'choice') {
805
+ const issues = [];
806
+ for (const p of [o.due.prior, ...o.due.alts]) {
807
+ if (!chainable(st, label, p))
808
+ continue;
809
+ const i = await prepareIssue(ctx, label, p);
810
+ if (i)
811
+ issues.push(i);
812
+ }
813
+ return { kind: 'choice', baseRev: o.rev, issues };
814
+ }
815
+ if (o.due)
816
+ return { kind: o.due.kind, baseRev: o.rev, issue: await prepareIssue(ctx, label, o.due.prior) };
817
+ if (o.body === null || o.proof === null)
818
+ return null;
819
+ // 6840: the same artifact under the same revision, in a new document
820
+ const own = (await ctx.keys.anchorOf(t.ownPairLabel)).anchor;
821
+ const m = { body: o.body, proof: o.proof };
822
+ return { kind: 'resend', baseRev: o.rev, outbound: await mappingDocument(ctx, t, entryOf(st, at.rel)?.id ?? at.rel.contactId, own, m) };
823
+ }
824
+ function continuityDuty(bound) {
825
+ return {
826
+ v: 1,
827
+ prepare: (ctx, duty) => {
828
+ const d = duty;
829
+ return d.step === 'probe' ? prepareProbe(ctx, d.tuple) : prepareMappingWork(ctx, d.tuple);
830
+ },
831
+ apply(state, duty, ctx, when) {
832
+ const d = duty;
833
+ const at = liveHead(state, d.tuple);
834
+ if (!at)
835
+ return { state: state }; // per-tuple duties die with the tuple (6250)
836
+ const t = at.tuple;
837
+ const p = ctx.prepared.value;
838
+ const attempt = d.attempt ?? 0;
839
+ // the standing duty stays, due again after the backoff (6830, 6840)
840
+ // sent now (`sent`), or only rescheduled from the last send
841
+ const again = (n, sent = true) => {
842
+ const { dueAt: _, ...rest } = d;
843
+ const lastAt = sent ? when : (d.lastAt ?? when);
844
+ const next = { ...rest, relId: at.rel.id, attempt: n, lastAt };
845
+ return { ...next, dueAt: Math.max(when, nextAt(next, ctx.backoff)) };
846
+ };
847
+ if (d.step === 'probe') {
848
+ if (!probeOwed(state, t))
849
+ return { state: state };
850
+ const q = p;
851
+ if (q === null)
852
+ return { ok: false, code: BigInt(t.probeOut.seq) >= ADMISSION_BOUND ? 'scope-exhausted' : 'no-own-channel' };
853
+ if (t.probeOut.seq !== q.baseSeq)
854
+ return 'not-due';
855
+ // 6680: the sequence and the outbox in one commit, before the send;
856
+ // every send is a fresh probe (6710), never the outbox's reproduction
857
+ const inflight = {};
858
+ for (const [s, f] of Object.entries(t.probeOut.inflight))
859
+ if (f.digests.some((x) => Object.hasOwn(state.outbox, x)))
860
+ inflight[s] = f;
861
+ inflight[q.seq] = { digests: q.outbound.map((o) => o.digest), acked: [] };
862
+ return {
863
+ state: withTuple(state, d.tuple, (x) => ({ ...x, probeOut: { ...x.probeOut, seq: q.seq, inflight } })),
864
+ outbound: q.outbound, duties: [again(attempt + 1)],
865
+ };
866
+ }
867
+ const w = p;
868
+ const o = t.continuityOut;
869
+ if (w === null)
870
+ return o.due || issuanceStands(t, recordOf(t) ?? false) ? { ok: false, code: 'no-own-channel' } : { state: state };
871
+ if (o.rev !== w.baseRev)
872
+ return 'not-due';
873
+ if (w.kind === 'choice') {
874
+ if (o.chosen !== null || o.due?.kind !== 'choice')
875
+ return { state: withTuple(state, d.tuple, (x) => ({ ...x, continuityOut: { ...x.continuityOut, due: null } })) };
876
+ // prepared before a substitution re-targeted it (follow-up 17): prepared again
877
+ const owed = [o.due.prior, ...o.due.alts];
878
+ if (w.issues.length && !w.issues.some((i) => owed.includes(i.prior)))
879
+ return 'not-due';
880
+ const r = choose(state, d.tuple, [o.due.prior, ...o.due.alts], w.issues, bound, when, false);
881
+ if (r.outbound?.length)
882
+ return { ...r, duties: [...(r.duties ?? []), again(1)] };
883
+ // still owed (no key for any candidate): the duty stays; else every candidate is gone
884
+ return tupleByLabel(r.state, d.tuple).tuple.continuityOut.due ? { ok: false, code: 'no-own-channel' } : { state: r.state };
885
+ }
886
+ if (w.kind === 'report' || w.kind === 'align') {
887
+ if (o.due?.kind !== w.kind)
888
+ return { state: state };
889
+ // 6860: a match report after the record side's choice is never sent
890
+ if (w.kind === 'report' && t.continuityIn.recordPrior !== null)
891
+ return { state: withTuple(state, d.tuple, (x) => ({ ...x, continuityOut: { ...x.continuityOut, due: null } })) };
892
+ if (!w.issue)
893
+ return { ok: false, code: 'no-own-channel' };
894
+ // prepared before a substitution re-targeted it (follow-up 17): prepared again
895
+ if (w.issue.prior !== o.due.prior)
896
+ return 'not-due';
897
+ const i = commitIssue(state, w.issue, () => ({ reported: w.issue.prior, aligned: false }));
898
+ return i ? { state: i.state, outbound: [i.outbound], duties: [again(1)] } : 'not-due';
899
+ }
900
+ // the resend (6840): while the issuance stands, after the backoff,
901
+ // unless a document of it is still with the outbox
902
+ const record = recordOf(t);
903
+ if (record === null || !issuanceStands(t, record))
904
+ return { state: state };
905
+ if (when < nextAt(d, ctx.backoff) || o.sent.some((x) => pending(state, x)))
906
+ return { state: state, duties: [again(Math.max(1, attempt), false)] };
907
+ const resend = w.outbound;
908
+ return {
909
+ state: withTuple(state, d.tuple, (x) => ({ ...x, continuityOut: { ...x.continuityOut, sent: [...x.continuityOut.sent, resend.digest] } })),
910
+ outbound: [resend], duties: [again(attempt + 1)],
911
+ };
912
+ },
913
+ };
914
+ }
915
+ const delivered = (state, digest) => Object.hasOwn(state.outbox, digest) && triasOf(state.outbox[digest]) === 'delivered';
916
+ /** Still with the outbox: neither delivered nor failed. */
917
+ const pending = (state, digest) => Object.hasOwn(state.outbox, digest) && triasOf(state.outbox[digest]) !== 'delivered' && triasOf(state.outbox[digest]) !== 'failed';
918
+ // ── acks and failures ──────────────────────────────────────────────────
919
+ /** A probe's chunks all acked: the probe completed and the duty ends (6830). */
920
+ const probeAck = (state, ref) => {
921
+ const at = tupleByRkid(state, state.outbox[ref].recipient.rkid);
922
+ if (!at)
923
+ return { state: state };
924
+ const o = at.tuple.probeOut;
925
+ const hit = Object.entries(o.inflight).find(([, f]) => f.digests.includes(ref));
926
+ if (!hit)
927
+ return { state: state };
928
+ const [seq, f] = hit;
929
+ const acked = f.acked.includes(ref) ? f.acked : [...f.acked, ref];
930
+ const done = f.digests.every((x) => acked.includes(x));
931
+ const next = withTuple(state, at.tuple.ownPairLabel, (t) => ({
932
+ ...t, probeOut: done ? { ...t.probeOut, inflight: {}, done: true } : { ...t.probeOut, inflight: { ...t.probeOut.inflight, [seq]: { ...f, acked } } },
933
+ }));
934
+ return { state: done ? dropDuty(next, probeDutyId(at.tuple.ownPairLabel)) : next };
935
+ };
936
+ /** A failed probe chunk: the fresh probe is due at once (6710, 6830). */
937
+ const probeFailed = (state, ref, when, { backoff }) => {
938
+ const at = tupleByRkid(state, state.outbox[ref].recipient.rkid);
939
+ if (!at || !liveHead(state, at.tuple.ownPairLabel) || !probeOwed(state, at.tuple))
940
+ return { state: state };
941
+ // only the current probe's failure counts; a superseded one's touches nothing
942
+ if (!(at.tuple.probeOut.inflight[at.tuple.probeOut.seq]?.digests ?? []).includes(ref))
943
+ return { state: state };
944
+ const label = at.tuple.ownPairLabel;
945
+ return { state: dueOnFailure(state, { kind: 'continuity', v: 1, id: probeDutyId(label), relId: at.rel.id, tuple: label, step: 'probe' }, when, backoff) };
946
+ };
947
+ /** The alignment delivered: its duty ends (6850). */
948
+ const mappingAck = (state, ref) => {
949
+ const at = tupleByRkid(state, state.outbox[ref].recipient.rkid);
950
+ if (!at || !at.tuple.continuityOut.sent.includes(ref))
951
+ return { state: state };
952
+ const t = at.tuple;
953
+ const rp = t.continuityIn.recordPrior;
954
+ // only the alignment's own documents (this issuance's `sent`) complete it
955
+ if (rp === null || !t.continuityOut.align || t.continuityOut.reported !== priorOf(t, rp)?.ownPairLabel)
956
+ return { state: state };
957
+ const next = withTuple(state, t.ownPairLabel, (x) => ({ ...x, continuityOut: { ...x.continuityOut, aligned: true } }));
958
+ return { state: t.continuityOut.due ? next : dropDuty(next, mappingDutyId(t.ownPairLabel)) };
959
+ };
960
+ /** A failed mapping that still stands: the resend is due at once (6840). */
961
+ const mappingFailed = (state, ref, when, { backoff }) => {
962
+ const at = tupleByRkid(state, state.outbox[ref].recipient.rkid);
963
+ if (!at || !liveHead(state, at.tuple.ownPairLabel))
964
+ return { state: state };
965
+ const t = at.tuple;
966
+ const record = recordOf(t);
967
+ if (!t.continuityOut.sent.includes(ref) || record === null || !issuanceStands(t, record))
968
+ return { state: state };
969
+ if (t.continuityOut.sent.some((x) => pending(state, x)))
970
+ return { state: state };
971
+ const label = t.ownPairLabel;
972
+ return { state: dueOnFailure(state, { kind: 'continuity', v: 1, id: mappingDutyId(label), relId: at.rel.id, tuple: label, step: 'mapping', attempt: 1 }, when, backoff) };
973
+ };
974
+ const maxDec = (a, b) => (BigInt(a) >= BigInt(b) ? a : b);
975
+ function probeTask(bound, limit) {
976
+ return {
977
+ proof: 'absent',
978
+ // stage 7: the closed chunk schema, exactly 256 entries (6670)
979
+ decode: (payload) => (probeChunkOk(payload) ? payload : refuse('malformed', 'RLTP-TRU-6670')),
980
+ async prepare(ctx, doc, arrival) {
981
+ if (doc.ceremony?.enactment !== undefined)
982
+ return refuse('validation-failed');
983
+ const st = ctx.snapshot;
984
+ const at = arrivalTuple(st, arrival.rkid);
985
+ if (!at)
986
+ return refuse('validation-failed', 'RLTP-TRU-6240');
987
+ const t = at.tuple;
988
+ const body = doc.payload.body;
989
+ const probe = BigInt(body.probe);
990
+ if (probe <= BigInt(t.probeIn.hwm))
991
+ return refuse('validation-failed', 'RLTP-TRU-6700');
992
+ if (probe <= BigInt(t.probeIn.lowerBound))
993
+ return refuse('validation-failed', 'RLTP-TRU-5366');
994
+ if (t.probeIn.assembly && probe < BigInt(t.probeIn.assembly.salt))
995
+ return refuse('validation-failed', 'RLTP-TRU-5340');
996
+ const shared = await pairShared(ctx, t);
997
+ if (!shared)
998
+ return refuse('validation-failed', 'RLTP-TRU-6670');
999
+ const S = ctx.crypto.subtle;
1000
+ const own = (await ctx.keys.anchorOf(t.ownPairLabel)).anchor;
1001
+ // k_p of the sender's direction (6720)
1002
+ const k = await probeKeyOf(shared, t.counterpartAnchor, own, S);
1003
+ if (!(await probeChunkMacOk(k, doc.payload, S)))
1004
+ return refuse('validation-failed', 'RLTP-TRU-6670');
1005
+ const label = t.ownPairLabel;
1006
+ const step = assemblyStep(t.probeIn.assembly, body.probe, body, doc.threadId, arrival.digest, limit);
1007
+ if (step.kind === 'limit')
1008
+ return { kind: 'discard', label, probe: body.probe, reason: 'resource-limit', ref: 'RLTP-TRU-5365' };
1009
+ if (step.kind === 'repeat')
1010
+ return { kind: 'repeat' };
1011
+ if (step.kind === 'reject')
1012
+ return { kind: 'discard', label, probe: body.probe, reason: 'rejected', ref: step.ref };
1013
+ if (step.kind === 'hold')
1014
+ return { kind: 'hold', label, assembly: step.assembly };
1015
+ const union = assembledBodies(step.chunks, step.last).flatMap((b) => b.blinded);
1016
+ if (union.length !== Number(step.last) * PROBE_CHUNK || !strictlyAscending(union))
1017
+ return { kind: 'discard', label, probe: body.probe, reason: 'rejected', ref: 'RLTP-TRU-6760' };
1018
+ // 6800: the counterpart anchors of the own prior-candidate set, never the fresh tuple's
1019
+ const inUnion = new Set(union);
1020
+ const matches = [];
1021
+ for (const pc of t.priorCandidates) {
1022
+ if (pc.ownPairLabel === label || pc.counterpartAnchor === t.counterpartAnchor)
1023
+ continue;
1024
+ for (const a of standsFor(pc)) {
1025
+ if (inUnion.has(await blindPrior(k, a, S))) {
1026
+ matches.push(pc.ownPairLabel);
1027
+ break;
1028
+ }
1029
+ }
1030
+ }
1031
+ const record = isRecordSide(own, t.counterpartAnchor);
1032
+ const issues = [];
1033
+ const o = t.continuityOut;
1034
+ if (matches.length && record && o.chosen === null) {
1035
+ for (const m of matches) {
1036
+ if (!chainable(st, label, m))
1037
+ continue;
1038
+ const i = await prepareIssue(ctx, label, m);
1039
+ if (i)
1040
+ issues.push(i);
1041
+ }
1042
+ }
1043
+ else if (matches.length && !record && o.reported === null && t.continuityIn.recordPrior === null) {
1044
+ const m = matches.find((x) => chainable(st, label, x)) ?? matches[0];
1045
+ const i = await prepareIssue(ctx, label, m);
1046
+ if (i)
1047
+ issues.push(i);
1048
+ }
1049
+ return { kind: 'complete', label, probe: body.probe, matches, record, issues };
1050
+ },
1051
+ apply(state, prepared, _doc, arrival) {
1052
+ const p = prepared.value;
1053
+ if (p.kind === 'repeat')
1054
+ return { state: state };
1055
+ if (p.kind === 'hold')
1056
+ return { state: withTuple(state, p.label, (t) => ({ ...t, probeIn: { ...t.probeIn, assembly: p.assembly } })) };
1057
+ if (p.kind === 'discard') {
1058
+ // 5365, 5366: the assembly dropped, its probe the lower bound; the completed one stays
1059
+ return { discarded: true, reason: p.reason, ref: p.ref, state: withTuple(state, p.label, (t) => ({ ...t, probeIn: { ...t.probeIn, assembly: null, lowerBound: maxDec(t.probeIn.lowerBound, p.probe) } })) };
1060
+ }
1061
+ // 6690: the highest completed probe, in the completing chunk's commit
1062
+ let next = withTuple(state, p.label, (t) => ({ ...t, probeIn: { ...t.probeIn, hwm: p.probe, assembly: null } }));
1063
+ if (!p.matches.length)
1064
+ return { state: next }; // 6260: honestly a new relationship
1065
+ const t = tupleByLabel(next, p.label).tuple;
1066
+ if (p.record) {
1067
+ if (t.continuityOut.chosen !== null)
1068
+ return { state: next };
1069
+ return choose(next, p.label, p.matches, p.issues, bound, arrival.when, false);
1070
+ }
1071
+ if (t.continuityOut.reported !== null || t.continuityIn.recordPrior !== null)
1072
+ return { state: next };
1073
+ const m = p.issues[0]?.prior ?? p.matches.find((x) => chainable(next, p.label, x)) ?? p.matches[0];
1074
+ return issueOrOwe(next, p.label, m, p.issues[0] ?? null, 'report', arrival.when);
1075
+ },
1076
+ };
1077
+ }
1078
+ function mappingTask(bound) {
1079
+ return {
1080
+ proof: 'absent',
1081
+ // step 1 (6940): the schema and a calendar-valid issuedAt
1082
+ decode: (payload) => (continuityMappingOk(payload) ? payload : refuse('malformed', 'RLTP-TRU-6940')),
1083
+ async prepare(ctx, doc, arrival) {
1084
+ if (doc.ceremony?.enactment !== undefined)
1085
+ return refuse('validation-failed');
1086
+ const st = ctx.snapshot;
1087
+ const at = arrivalTuple(st, arrival.rkid);
1088
+ if (!at)
1089
+ return refuse('validation-failed', 'RLTP-TRU-6240');
1090
+ const t = at.tuple;
1091
+ const b = doc.payload.body;
1092
+ const own = (await ctx.keys.anchorOf(t.ownPairLabel)).anchor;
1093
+ // steps 2 and 3 (6950): the tuple it arrived on
1094
+ if (b.to !== own || b.next !== t.counterpartAnchor)
1095
+ return refuse('validation-failed', 'RLTP-TRU-6950');
1096
+ // step 4 (6960): prior in the frozen prior-candidate set (6200)
1097
+ const pc = priorOf(t, b.prior);
1098
+ const pr = pc && tupleByLabel(st, pc.ownPairLabel);
1099
+ if (!pc || !pr)
1100
+ return refuse('validation-failed', 'RLTP-TRU-6960');
1101
+ // step 5 (6970): both secrets non-zero, mac1 under the prior relationship's, mac2 under the new
1102
+ const shNew = await pairShared(ctx, t);
1103
+ const shPrior = await pairShared(ctx, pr.tuple);
1104
+ if (!shNew || !shPrior || !(await continuityMacsOk(doc.payload, shPrior, shNew, ctx.crypto.subtle)))
1105
+ return refuse('validation-failed', 'RLTP-TRU-6970');
1106
+ const senderRecord = isRecordSide(b.next, own);
1107
+ const ownPrior = (await ctx.keys.anchorOf(pc.ownPairLabel)).anchor;
1108
+ const o = t.continuityOut;
1109
+ let issue = null;
1110
+ // the alignment (6930): a new issuance under the next revision, even
1111
+ // where an earlier report already named this relationship
1112
+ if (senderRecord && !(o.align && o.reported === pc.ownPairLabel))
1113
+ issue = await prepareIssue(ctx, t.ownPairLabel, pc.ownPairLabel);
1114
+ if (!senderRecord && o.chosen === null && chainable(st, t.ownPairLabel, pc.ownPairLabel))
1115
+ issue = await prepareIssue(ctx, t.ownPairLabel, pc.ownPairLabel);
1116
+ return { label: t.ownPairLabel, prior: pc.ownPairLabel, senderRecord, ownPrior, issue };
1117
+ },
1118
+ // step 6 (6970): the revision rule with the record freeze, and what follows
1119
+ apply(state, prepared, doc, arrival) {
1120
+ const at = arrivalTuple(state, arrival.rkid);
1121
+ if (!at)
1122
+ return refuse('validation-failed', 'RLTP-TRU-6240');
1123
+ const p = prepared.value;
1124
+ const t = at.tuple;
1125
+ const b = doc.payload.body;
1126
+ const rev = BigInt(b.revision);
1127
+ const cur = BigInt(t.continuityIn.rev);
1128
+ if (rev === cur) {
1129
+ // a repeat is ignored (6270); another body under it an equivocation
1130
+ // a repeat changes nothing, not even an open duty: it answers no choice
1131
+ if (t.continuityIn.body === null || jcs(b) !== jcs(t.continuityIn.body))
1132
+ return refuse('validation-failed', 'RLTP-TRU-6270');
1133
+ return { state: state };
1134
+ }
1135
+ if (rev < cur)
1136
+ return refuse('validation-failed', 'RLTP-TRU-6270');
1137
+ if (p.senderRecord && t.continuityIn.recordPrior !== null && t.continuityIn.recordPrior !== b.prior)
1138
+ return refuse('validation-failed', 'RLTP-TRU-6290');
1139
+ let next = withTuple(state, p.label, (x) => ({
1140
+ ...x, continuityIn: { rev: b.revision, body: b, recordPrior: p.senderRecord ? b.prior : x.continuityIn.recordPrior },
1141
+ }));
1142
+ if (p.senderRecord) {
1143
+ // 6900: the non-record side chains on verifying the record side's mapping
1144
+ const c = chainTuple(next, p.label, p.prior, bound);
1145
+ if (c === 'taken')
1146
+ return refuse('validation-failed', 'RLTP-TRU-6900');
1147
+ let out = { state: next };
1148
+ if (c !== 'already')
1149
+ out = { state: c.state, events: c.events, duties: c.duties, renames: c.renames };
1150
+ next = out.state;
1151
+ // 6850: the report duty ends; the alignment arises in its place (6930)
1152
+ const x = tupleByLabel(next, p.label).tuple;
1153
+ if (x.continuityOut.due?.kind === 'report')
1154
+ next = withTuple(next, p.label, (y) => ({ ...y, continuityOut: { ...y.continuityOut, due: null } }));
1155
+ // a higher revision of the same choice, already aligned to: nothing new
1156
+ if (x.continuityOut.align && x.continuityOut.reported === p.prior)
1157
+ return { ...out, state: next };
1158
+ return merge(out, issueOrOwe(next, p.label, p.prior, p.issue, 'align', arrival.when));
1159
+ }
1160
+ // the non-record side's report or alignment, verified with mac1 under the
1161
+ // prior relationship's key (6970): the evidence the record side chains on
1162
+ const x = tupleByLabel(next, p.label).tuple;
1163
+ if (x.continuityOut.chosen === null)
1164
+ return choose(next, p.label, [p.prior], p.issue ? [p.issue] : [], bound, arrival.when, true);
1165
+ if (x.continuityOut.chosen !== p.prior)
1166
+ return { state: next };
1167
+ // the choice made on a hit: chained now (6900), once
1168
+ const c = chainTuple(next, p.label, p.prior, bound);
1169
+ if (c === 'taken')
1170
+ return refuse('validation-failed', 'RLTP-TRU-6900');
1171
+ const out = c === 'already' ? { state: next } : { state: c.state, events: c.events, duties: c.duties, renames: c.renames };
1172
+ // the counterpart's mapping on the chosen relationship, verified under a
1173
+ // higher revision than any it held, answers the choice (6840); the
1174
+ // report a choice was made from is not its answer
1175
+ return { ...out, state: answered(out.state, p.label) };
1176
+ },
1177
+ };
1178
+ }
1179
+ /** The record side's choice answered: its standing duty ends (6840). */
1180
+ const answered = (state, label) => dropDuty(withTuple(state, label, (y) => ({ ...y, continuityOut: { ...y.continuityOut, aligned: true } })), mappingDutyId(label));
1181
+ // ── the module's parts ─────────────────────────────────────────────────
1182
+ /** What trustModule registers for continuity. */
1183
+ export function continuityParts(bound, limit) {
1184
+ return {
1185
+ ownRetries: [CONTINUITY_PROBE_TYPE],
1186
+ keepKeys: continuityKeys,
1187
+ tasks: { [CONTINUITY_PROBE_TYPE]: probeTask(bound, limit), [CONTINUITY_MAPPING_TYPE]: mappingTask(bound) },
1188
+ duties: { continuity: continuityDuty(bound) },
1189
+ acks: { [CONTINUITY_PROBE_TYPE]: probeAck, [CONTINUITY_MAPPING_TYPE]: mappingAck },
1190
+ failures: { [CONTINUITY_PROBE_TYPE]: probeFailed, [CONTINUITY_MAPPING_TYPE]: mappingFailed },
1191
+ };
1192
+ }
1193
+ /**
1194
+ * The testing module (./testing): its non-human command continuity.newTuple
1195
+ * opens a fresh tuple the way the encounter will (PR 7), with the rkid
1196
+ * the KeyProvider derives for its label.
1197
+ */
1198
+ export const continuityTestModule = Object.freeze({
1199
+ name: 'continuity-test',
1200
+ commands: Object.freeze({
1201
+ [NEW_TUPLE]: {
1202
+ human: false,
1203
+ async prepare(ctx, input) {
1204
+ // Identity §6.1: the label digests the 32 nonce bytes
1205
+ if (input.nonce !== undefined) {
1206
+ const n = fromB64u(input.nonce);
1207
+ if (n.length !== 32 || 'pair/' + await digestBytes(n, ctx.crypto.subtle) !== input.ownPairLabel)
1208
+ throw new TypeError(`${NEW_TUPLE}: the label is not pair/<multihash(nonce)> of a 32-byte nonce`);
1209
+ }
1210
+ return (await ctx.keys.anchorOf(input.ownPairLabel)).keyAgreement;
1211
+ },
1212
+ apply: (state, prepared, input, when) => openTuple(state, input, prepared.value, when),
1213
+ },
1214
+ }),
1215
+ });
1216
+ //# sourceMappingURL=continuity.js.map