@real-life/trust-protocol 0.3.2 → 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 +56 -10
  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 -62
  250. package/dist/probe/introduce.d.ts.map +0 -1
  251. package/dist/probe/introduce.js +0 -306
  252. package/dist/probe/introduce.js.map +0 -1
  253. package/dist/probe/membership.d.ts +0 -96
  254. package/dist/probe/membership.d.ts.map +0 -1
  255. package/dist/probe/membership.js +0 -467
  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
@@ -1,805 +0,0 @@
1
- // continuity.mjs — rltp-visibility §6a im Simulator: continuity-probe@1 +
2
- // continuity-mapping@1. „Kontinuität wird NACH der Zeremonie aufgelöst,
3
- // nie in ihr" (Encounter 4.4): jedes Enactment UND jede Vorstellung prägt
4
- // ein frisches Tupel; ob es ein Wiedersehen war, klärt die Probe über den
5
- // frischen Kanal — automatisch, ohne Dialog, ohne Namen.
6
- //
7
- // Die Probe ist das dritte Mitglied EINER Familie (Stern, Mapping, Probe:
8
- // geblendete Werte unter Beziehungs-Schlüsseln): die EIGENEN bisherigen
9
- // pair-Anker reisen als HMAC unter k_p (DH des NEUEN Kanals), gepolstert
10
- // auf Vielfache von 256, sortiert, sequenziert. Nur wer eine der
11
- // Beziehungen wirklich hält, kann matchen; ein leerer Bestand sendet
12
- // trotzdem (sonst leakte die Abwesenheit die Kardinalität „null").
13
- //
14
- // Die Maschine (6a.4): EIN Wähler (Record-Seite = lexikographisch
15
- // kleinerer neuer pair-Anker), EINE endgültige Wahl (eingefroren,
16
- // Abweichung = Äquivokation), EIN Auslöser (Record-Seite kettet atomar
17
- // mit ihrer Wahl; die Gegenseite NUR auf deren verifiziertes Mapping —
18
- // ihr eigenes ist ein Match-Report). Kette: neues Tupel wird Kopf, das
19
- // alte deaktiviert; Evidenz akkumuliert AUF DER BEZIEHUNG (✓ bleibt ✓),
20
- // für Dritte unsichtbar. Wer die Probe nicht beantworten kann, IST
21
- // protokollisch eine neue Beziehung (Identity 9.3).
22
- import { jcs, makeValidator } from '../core.js';
23
- import { SCHEMAS } from '../schemas.js';
24
- import * as C from './deps.js';
25
- import { hmac, hmacU, reissueTrust, chainAdmission, withContactLock, withIssueLock } from './trust.js';
26
- import { buildAck, ACK_TYPE } from './acks.js';
27
- const say = (p, m) => p.log.push(m);
28
- const CHUNK = 256;
29
- const V = makeValidator(SCHEMAS);
30
- const schemaOk = (file, data) => V.validate(data, SCHEMAS[file], SCHEMAS[file]).length === 0;
31
- // ── Schlüssel (Spec-Infostrings, richtungsgebunden) ─────────────────────
32
- // k_p = HKDF(ECDH(newPairX_sender, newPairX_recipient),
33
- // 'rltp/visibility/blind/probe/' + senderNewPair + '/' + recipientNewPair)
34
- const kProbe = async (xPriv, theirKa, senderAnchor, recipientAnchor) => C.hkdf(await C.ecdh(xPriv, C.xRawOfMk(theirKa)), 'rltp/visibility/blind/probe/' + senderAnchor + '/' + recipientAnchor);
35
- const kMac = async (xPriv, theirKa, info) => C.hkdf(await C.ecdh(xPriv, C.xRawOfMk(theirKa)), info);
36
- // ── Schnappschuss-Helfer (Prior-Candidate-Set, Sicht des Halters) ───────
37
- // wird von introduce.mjs BEI TUPEL-ERZEUGUNG aufgerufen: aktive Heads,
38
- // das frische Tupel selbst ausgeschlossen; deaktivierte Glieder nie.
39
- export const snapshotPriors = (p) => ({
40
- own: [...p.contacts.values()].filter((c) => !c.deactivated && c.channel?.own).map((c) => c.channel.own.anchor),
41
- counterpart: [...p.contacts.entries()].filter(([, c]) => !c.deactivated).map(([k]) => k),
42
- });
43
- // ── continuity-probe@1: bauen ───────────────────────────────────────────
44
- // Sequenzvergabe SYNCHRON (BigInt-String — 18 Stellen exakt), Bau je
45
- // Kontakt serialisiert: zwei parallele buildProbe-Aufrufe teilen nie
46
- // eine Sequenz, und der Wire-Wert ist der reservierte (Review 2, B-1)
47
- const withContLock = withContactLock; // EIN Lock je Kontakt, geteilt mit trust (Review 4, B-1)
48
- export async function buildProbe(p, newKey, when, ent = {}) {
49
- const t = p.contacts.get(newKey);
50
- if (!t?.channel?.own || t.deactivated)
51
- return null; // deaktivierte Tupel stellen nichts aus (Review 8, B-2)
52
- return withContLock(t, async () => {
53
- if (t.deactivated)
54
- return null; // Re-Check nach dem Lock-Erwerb
55
- // Sequenz nur GEPEEKT (Review 25): „assigning … and committing are
56
- // one step" — ein Bau-Fehler verbraucht nichts, auch am letzten
57
- // Wert entsteht nie falsche Erschöpfung; committet wird synchron
58
- // mit der Outbox (persisted atomically before send)
59
- const nextProbe = BigInt(t.probeSeqOut ?? '0') + 1n;
60
- if (nextProbe > 10n ** 18n - 1n)
61
- throw new Error('scope exhausted — issues nothing further (2.1)');
62
- const probeSeq = String(nextProbe);
63
- const kp = await kProbe(t.channel.own.x.priv, t.channel.counterpartKa, t.channel.own.anchor, newKey);
64
- const entries = [];
65
- // §6a: prior ist „nie das frische Tupel" — Hygiene am NUTZUNGSORT:
66
- // ein Host, der das Set nach der Tupel-Anlage (neu) schnappschießt,
67
- // darf keine Selbst-Kette provozieren können (Selbst-Match →
68
- // chainTuple(key, key) deaktivierte den eigenen Kopf)
69
- for (const a of t.priorCands?.own ?? [])
70
- if (a !== t.channel.own.anchor)
71
- entries.push(await hmacU(kp, a));
72
- // Polstern auf das nächste Vielfache von 256 (leerer Bestand → 1 Voll-Polster-Chunk);
73
- // KOLLISION → Neusampling (Review 6, MAJOR: „the padding value is
74
- // resampled") — ein injizierter ent.pad wird höchstens EINMAL
75
- // verwendet, Duplikate fallen auf frische Zufallswerte zurück
76
- const total = Math.max(CHUNK, Math.ceil(entries.length / CHUNK) * CHUNK);
77
- const used = new Set(entries);
78
- while (entries.length < total) {
79
- let v = 'u' + C.b64uOf(ent.pad ?? C.rand(32));
80
- while (used.has(v))
81
- v = 'u' + C.b64uOf(C.rand(32));
82
- used.add(v);
83
- entries.push(v);
84
- }
85
- entries.sort();
86
- // Bau + Seal + Digest sind FEHLBAR und warten — vorbereiten, dann
87
- // Re-Check, dann Outbox-Schreibungen synchron (Review 9, B-3: ein
88
- // während der Awaits deaktiviertes Tupel stellt nichts aus)
89
- const preps = [];
90
- const threadId = ent.threadId ?? globalThis.crypto.randomUUID();
91
- for (let i = 0; i * CHUNK < entries.length; i++) {
92
- const body = { type: 'continuity-probe@1', probe: probeSeq, seq: String(i + 1), last: (i + 1) * CHUNK >= entries.length, blinded: entries.slice(i * CHUNK, (i + 1) * CHUNK) };
93
- const artifact = { body, proof: { mac: await hmacU(kp, jcs(body)) } };
94
- const doc = { id: globalThis.crypto.randomUUID(), type: 'https://real-life.org/trust-tasks/continuity-probe/0.1', issuer: t.channel.own.anchor, recipient: newKey, threadId, issuedAt: C.iso(when), payload: artifact };
95
- const env = await C.seal(doc, t.channel.counterpartKa, ent);
96
- const digest = await C.digestDoc(doc);
97
- preps.push({ digest, env });
98
- }
99
- if (t.deactivated)
100
- return null; // Re-Check (B-3), danach kein await
101
- // COMMIT — Sequenz + Outbox in EINEM synchronen Zug (Review 25):
102
- // byte-identische Redelivery-Zuordnung fürs Ack-Matching (4.2;
103
- // erneut SENDEN tut ein Retry nie, §6a)
104
- t.probeSeqOut = probeSeq;
105
- const outbound = [];
106
- for (const { digest, env } of preps) {
107
- ;
108
- (t.outbox ??= new Map()).set(digest, { env, threadId, kind: 'continuity-probe' });
109
- outbound.push({ to: t, kind: 'continuity-probe/0.1 (geblendet, gepolstert, versiegelt)', env });
110
- }
111
- say(p, `Kontinuitäts-Probe an ${t.name}: ${t.priorCands?.own.length ?? 0} eigene Beziehungs-Anker, geblendet + gepolstert (${entries.length})`);
112
- return outbound;
113
- });
114
- }
115
- // ── continuity-mapping@1: bauen ─────────────────────────────────────────
116
- // prior = EIGENER alter pair-Anker · next = eigener neuer · to = neuer der
117
- // Gegenseite; mac1 unter dem ALTEN Beziehungs-Schlüssel, mac2 unter dem
118
- // NEUEN — nur derselbe Schlüssel-Inhaber kann beide (für die Adressatin
119
- // fälschbar: Klasse V, abstreitbar).
120
- // PREPARE (fehlbar: MACs, Seal, Digest — mutiert nichts) und COMMIT
121
- // (synchron: Zähler + Outbox) getrennt (Review 12, B-2): die Record-
122
- // Wahl kettet ATOMAR mit der Ausgabe ihres Mappings — beide Schritte
123
- // liegen dann in EINEM synchronen Zug. Alle Aufrufer laufen unter dem
124
- // Kontakt-Lock des Tupels (Handler + Flush), der die Revisionsvergabe
125
- // serialisiert; buildMapping hält zusätzlich den Issue-Lock.
126
- async function prepareContMapping(p, newKey, oldKey, when, ent = {}, duty) {
127
- const t = p.contacts.get(newKey), old = p.contacts.get(oldKey);
128
- if (t.deactivated)
129
- throw new Error('tuple deactivated — issues nothing (§6.4)');
130
- // 6.4/6a.4: Revision je (next, to) monoton — nur gepeekt, ein
131
- // Fehlschlag verbraucht nichts (§6.4: one step per scope)
132
- const nextRev = BigInt(t.contRevOut ?? '0') + 1n;
133
- if (nextRev > 10n ** 18n - 1n)
134
- throw new Error('scope exhausted — issues nothing further (2.1)');
135
- const rev = String(nextRev);
136
- const body = { type: 'continuity-mapping@1', prior: old.channel.own.anchor, next: t.channel.own.anchor, to: newKey, revision: rev, issuedAt: C.iso(when) };
137
- const msg = jcs(body);
138
- const artifact = { body, proof: {
139
- mac1: await hmacU(await kMac(old.channel.own.x.priv, old.channel.counterpartKa, 'rltp/visibility/mac/cont1'), msg),
140
- mac2: await hmacU(await kMac(t.channel.own.x.priv, t.channel.counterpartKa, 'rltp/visibility/mac/cont2'), msg)
141
- } };
142
- const doc = { id: globalThis.crypto.randomUUID(), type: 'https://real-life.org/trust-tasks/continuity-mapping/0.1', issuer: t.channel.own.anchor, recipient: newKey, threadId: ent.threadId ?? globalThis.crypto.randomUUID(), issuedAt: C.iso(when), payload: artifact };
143
- const env = await C.seal(doc, t.channel.counterpartKa, ent);
144
- const digest = await C.digestDoc(doc);
145
- return { t, rev, env, digest, threadId: doc.threadId, duty, prior: old.channel.own.anchor };
146
- }
147
- const commitContMapping = (prep) => {
148
- prep.t.contRevOut = prep.rev;
149
- (prep.t.outbox ??= new Map()).set(prep.digest, { env: prep.env, threadId: prep.threadId, kind: 'continuity-mapping', duty: prep.duty, prior: prep.prior });
150
- return { to: prep.t, kind: 'continuity-mapping/0.1 (mac1 alt · mac2 neu, versiegelt)', env: prep.env };
151
- };
152
- async function buildMapping(p, newKey, oldKey, when, ent = {}, duty) {
153
- const t = p.contacts.get(newKey);
154
- return withIssueLock(t, async () => {
155
- const prep = await prepareContMapping(p, newKey, oldKey, when, ent, duty);
156
- if (t.deactivated)
157
- throw new Error('tuple deactivated — issues nothing (§6.4)'); // Re-Check (B-2), danach kein await
158
- return commitContMapping(prep);
159
- });
160
- }
161
- // ── die Kette: neues Tupel wird Kopf, das alte deaktiviert ──────────────
162
- // Visibility 0.29 §6.4: PER-TUPEL-ZUSTAND WANDERT NICHT — Grade,
163
- // Stern-Salts, Baseline und empfangene Sterne sterben mit dem alten
164
- // Kopf (der neue beginnt fail-closed; das Fenster schließen 5.5s
165
- // Neuausstellung und das New-Head-Korollar von 5.4, nie eine
166
- // Migration). KETTEN-Ebene wandert: Kontakt-Gedächtnis, Evidenz
167
- // (✓ akkumuliert), das WISSEN um den offengelegten Anker (die
168
- // Admissionsschicht hängt an der Beziehung), die eigene
169
- // Vertrauens-ENTSCHEIDUNG (trustGiven) und die Pause.
170
- function chainTuple(p, newKey, oldKey, when) {
171
- const head = p.contacts.get(newKey), old = p.contacts.get(oldKey);
172
- if (!head || !old || head === old || old.deactivated)
173
- return false; // idempotent; NIE Selbst-Kette (§6a)
174
- old.deactivated = true;
175
- old.chainedInto = newKey;
176
- head.chain = [...(old.chain ?? []), oldKey];
177
- // Zustands-Vorrang entlang der Kette (Encounter 4.2, „evidence accumulates
178
- // on the relationship through its chain"): ✓ > Zeremonie-Richtung (→/←)
179
- // > Vorstellung (⇄). Eine einseitige Zeremonie ist stärker als eine
180
- // Vorstellung — der Kopf verliert seine Richtung nicht an ein ⇄
181
- if (old.state === '✓' || head.state === '✓') {
182
- head.state = '✓';
183
- head.provenance = 'ceremony';
184
- }
185
- else if (head.state === '→' || head.state === '←') { /* die Richtung des Kopfes bleibt */ }
186
- else if (old.state === '→' || old.state === '←') {
187
- head.state = old.state;
188
- head.provenance = 'ceremony';
189
- }
190
- else if (old.state === '⇄' || head.state === '⇄')
191
- head.state = '⇄';
192
- head.since = [old.since, head.since].filter(Boolean).sort()[0];
193
- head.name = old.name; // lokales Kontakt-Gedächtnis
194
- head.selfAnchor ??= old.selfAnchor; // Ketten-WISSEN: wer das ist (das Mapping selbst stirbt mit dem Tupel, §6.4)
195
- const oldRel = old.relId ?? oldKey;
196
- // Admission (B-4): der neue Kopf löst über die überlebende Beziehung
197
- // auf; ein schon existierender Eintrag der frischen Beziehung
198
- // verschmilzt atomar (früheste Position, Aliase, k−1). Die
199
- // IDENTITÄT ist die des TATSÄCHLICH überlebenden Eintrags (Review
200
- // 36: „the merged relationship's identity is the surviving
201
- // entry's") — existiert nur der frische, bleibt DESSEN Identität;
202
- // existiert keiner, die des jüngsten Kopfs (Review 4, B-4)
203
- const survivor = chainAdmission(p, oldRel, newKey, newKey, when);
204
- // Ketten-Identität = die des Survivors NUR, wenn sie zu DIESER Kette
205
- // gehört (Review 36 + 37, B-1): die kanonische Identität eines
206
- // Weg-2-Merges gehört einer ANDEREN, weiterhin aktiven Beziehung —
207
- // Admission-Identität und Beziehungsstrang bleiben unterscheidbar,
208
- // der Abgang dieser Kette darf die andere nie mitreißen
209
- const sid = survivor?.id;
210
- const sidInChain = sid !== undefined && (sid === oldRel || sid === newKey || (p.contacts.get(sid)?.relId ?? null) === oldRel);
211
- head.relId = sidInChain ? sid : (survivor ? oldRel : newKey);
212
- // die GANZE Kette identifiziert sich als DIESE Beziehung — auch der
213
- // alte Kopf (Review 36: der Abgang räumt sämtliche Ketten-Aliase)
214
- old.relId = head.relId;
215
- head.trustGiven ??= old.trustGiven; // die Entscheidung gehört der Beziehung (V2)
216
- // 5.5: „MUST re-issue its most recently ISSUED grade" — der
217
- // Ordnungsstempel entscheidet, nie der zufällig frischere Kopf
218
- // (Review 27: setGrade auf dem alten Kopf NACH dem Trust-Akt des
219
- // frischen muss die Kettung überleben)
220
- if (old.gradeOutLast !== undefined && (head.gradeOutLast === undefined || (old.gradeOutOrder ?? 0n) > (head.gradeOutOrder ?? 0n))) {
221
- head.gradeOutLast = old.gradeOutLast;
222
- head.gradeOutOrder = old.gradeOutOrder;
223
- }
224
- // Pause ist chain-level und ÜBERLEBT die Kettung (Review 37, B-2):
225
- // der Zustand der Beziehung, auf die gekettet wird, gewinnt gegen
226
- // Vor-Kettungs-Akte des frischen Tupels
227
- head.trustPaused = old.trustPaused ?? head.trustPaused;
228
- head.sharedGroups = [...new Set([...(old.sharedGroups ?? []), ...(head.sharedGroups ?? [])])];
229
- // NICHT migriert (per Tupel, stirbt mit dem alten Kopf):
230
- // trustReceived, starReceived, starInfo, starSaltIn, sentMapping,
231
- // sub (Baseline/inFlight), starSeqNext. Die 5.5-Neuausstellung ist
232
- // die stehende Pflicht des neuen Kopfs; ihr Producer ist
233
- // trust.reissueTrust via starRefreshAll.
234
- // die Pflicht entsteht, wenn eine DECLARATION ausgestellt war (5.5:
235
- // „a subject holding an issued declaration … MUST re-issue") — auch
236
- // ohne Trust-Akt (eigenständiges setGrade); Review 33, B-1
237
- if (head.trustGiven || head.gradeOutLast !== undefined)
238
- head.trustReissueDue = true;
239
- say(p, `Kontinuität: ${head.name} wiedererkannt — neues Tupel an die Beziehung gekettet (${head.state}, ${head.chain.length + 1} Enactments)`);
240
- return true;
241
- }
242
- // V2-Hook (5.5): direkt nach der Kettung wird die Neuausstellung
243
- // versucht; ein Bau-Fehler lässt die Pflicht (trustReissueDue) stehen —
244
- // ihr Producer ist starRefreshAll, nie eine Wiederzustellung.
245
- async function v2Reissue(p, newKey, when, ent, outbound) {
246
- const t = p.contacts.get(newKey);
247
- if (!t?.trustReissueDue)
248
- return undefined;
249
- try {
250
- const r = await reissueTrust(p, newKey, when, ent);
251
- if (r)
252
- outbound.push(...r.outbound);
253
- return undefined;
254
- }
255
- catch (e) {
256
- return String(e?.message ?? e);
257
- }
258
- }
259
- // ── Empfangs-Dispatch ───────────────────────────────────────────────────
260
- const iAmRecord = (t, newKey) => t.channel.own.anchor < newKey; // lexikographisch kleinerer NEUER Anker
261
- const tupleOf = (p, rkid) => {
262
- const ctx = p.contexts.get(rkid);
263
- if (!ctx)
264
- return null;
265
- const e = [...p.contacts.entries()].find(([, c]) => c.channel?.own?.anchor === ctx.anchor);
266
- return e ? { key: e[0], t: e[1], ctx } : null;
267
- };
268
- const TT_PROBE = 'https://real-life.org/trust-tasks/continuity-probe/0.1';
269
- const TT_MAP = 'https://real-life.org/trust-tasks/continuity-mapping/0.1';
270
- export async function receiveContinuity(p, env, when, ent = {}) {
271
- const opened = await C.openEnvelope(p, env); // Stufen 1–4, Cache-Lesung
272
- if (opened.error)
273
- return { handled: false };
274
- const hit = tupleOf(p, env.rkid);
275
- if (!hit)
276
- return { handled: false };
277
- if (opened.duplicate) {
278
- // 4.2: byte-identischer Re-Ack innerhalb der Retention
279
- const stored = hit.t.ackStore?.get(opened.digest);
280
- return { handled: true, duplicate: true, ack: stored ? { to: hit.t, kind: 'delivery-ack/0.1 (Re-Ack, byte-identisch)', env: stored } : undefined };
281
- }
282
- const doc = opened.doc;
283
- if (doc?.type !== TT_PROBE && doc?.type !== TT_MAP)
284
- return { handled: false, doc };
285
- // Dokumentprofil (Delivery §3) + Artefakt-Schema (Stufen 5/7)
286
- if (!schemaOk('rltp-delivery-document.schema.json', doc) || doc.proof !== undefined)
287
- return { handled: true, error: 'malformed document' };
288
- // Stufe 5: recipient; die issuer-Bindung (Stufe 8) läuft NACH den
289
- // Payload-Schemata (Review 32)
290
- if (doc.recipient !== hit.t.channel.own.anchor)
291
- return { handled: true, error: 'outer binding (recipient)' };
292
- // Kalender-Validität des DOKUMENTS (Review 12, B-3); die PAYLOAD-
293
- // Kalenderprüfung sitzt NACH dem Artefakt-Schema (Review 31)
294
- if (!C.calOK(doc.issuedAt))
295
- return { handled: true, error: 'calendar-invalid issuedAt' };
296
- if (doc.type === TT_PROBE && !schemaOk('visibility-continuity-probe.schema.json', doc.payload))
297
- return { handled: true, error: 'malformed probe' };
298
- if (doc.type === TT_MAP && !schemaOk('visibility-continuity-mapping.schema.json', doc.payload))
299
- return { handled: true, error: 'malformed mapping' };
300
- if (doc.type === TT_MAP && !C.calOK(doc.payload?.body?.issuedAt))
301
- return { handled: true, error: 'calendar-invalid issuedAt' }; // Stufe 8, nach dem Schema (Review 31)
302
- if (doc.issuer !== hit.key)
303
- return { handled: true, error: 'outer binding (issuer, Stufe 8)' };
304
- // Profil-Regel (Review 29/30): Stufe 8 NACH dem Payload-Schema —
305
- // ein präsentes ceremony.enactment kann bei diesen Payloads nie
306
- // nachrechnen (kein umschlossenes Bindungsmaterial) →
307
- // failed(validation-failed); andere Framework-Member passieren (§4.11.1)
308
- if (doc.ceremony?.enactment !== undefined)
309
- return { handled: true, error: 'ceremony.enactment kann nicht nachrechnen (validation-failed)' };
310
- // GESAMTER Empfang unter dem Kontakt-Lock (B-1: kein Rollback durch
311
- // parallele Handler); der Ack entsteht IN den Handlern nach der
312
- // vollen Validierung (M-2), Commit hier atomar mit Cache+Store
313
- return withContLock(hit.t, async () => {
314
- // §6.4: ein deaktiviertes Tupel nimmt nichts mehr an (Review 5, B-1)
315
- if (hit.t.deactivated)
316
- return { handled: true, error: 'tuple deactivated' };
317
- // Stage-4-Reentry nach Lock-Wartezeit: ein inzwischen verarbeitetes
318
- // Duplikat erhält den gespeicherten Ack byte-identisch
319
- if ((p.deliveryCache ?? new Set()).has(opened.digest)) {
320
- const stored = hit.t.ackStore?.get(opened.digest);
321
- return { handled: true, duplicate: true, ack: stored ? { to: hit.t, kind: 'delivery-ack/0.1 (Re-Ack, byte-identisch)', env: stored } : undefined };
322
- }
323
- const mkAck = () => buildAck(p, hit.t, opened.digest, doc.threadId, when).then((a) => a.env);
324
- // Effekt + completed-effect cache + gespeicherter Ack committen in
325
- // EINEM synchronen Zug IM Handler (Review 6, B-5: „inside the
326
- // effect's transaction") — die Outbound-PRODUKTION (buildMapping,
327
- // V2) läuft danach; ihr Scheitern hinterlässt eine Pflicht, nie
328
- // einen halben Empfangs-Commit
329
- const commitAck = (ackEnv) => {
330
- C.effectDone(p, opened.digest);
331
- (hit.t.ackStore ??= new Map()).set(opened.digest, ackEnv);
332
- return { to: hit.t, kind: 'delivery-ack/0.1', env: ackEnv };
333
- };
334
- return doc.type === TT_PROBE
335
- ? handleProbe(p, hit, doc.payload, doc.threadId, when, ent, mkAck, commitAck)
336
- : handleMapping(p, hit, doc.payload, when, ent, mkAck, commitAck);
337
- });
338
- }
339
- async function handleProbe(p, { key, t }, artifact, threadId, when, ent, mkAck, commitAck) {
340
- // Schema lief im Dispatch; hier die wertgebundenen Reste (M-3)
341
- const doc = artifact.body;
342
- if (!C.intStr(doc.probe) || !C.intStr(doc.seq) // NaN in probeSeqIn wäre Replay-Zustandskorruption (Review 9, P-B1)
343
- || doc.blinded.length !== 256) // die Chunkform ist FEST: genau 256 Einträge (Review 10, P-B1-Rest)
344
- return { handled: true, error: 'malformed probe' };
345
- // Senderseitiger k_p, aus Empfängersicht nachgerechnet (Richtung!)
346
- const kp = await kProbe(t.channel.own.x.priv, t.channel.counterpartKa, key, t.channel.own.anchor);
347
- if ((await hmacU(kp, jcs(doc))) !== artifact.proof.mac)
348
- return { handled: true, error: 'probe mac' };
349
- if (BigInt(doc.probe) <= BigInt(t.probeSeqIn ?? '0'))
350
- return { handled: true, error: 'probe replay (nicht streng größer)' };
351
- // VERGIFTETE Probe (Review 21): nach einem Assembly-Konflikt wird
352
- // KEIN weiterer Chunk dieser Lieferung quittiert — sonst
353
- // kombinieren früh geackte, verlorene Pufferwirkungen mit späteren
354
- // Acks zu einer falschen Completion (§6a.2 übernimmt die
355
- // 5.2a-Konfliktregeln in kind)
356
- if (t.probePoison?.has(doc.probe))
357
- return { handled: true, error: 'probe vergiftet (assembly conflict)' };
358
- // Re-Check nach den MAC-Awaits (Review 6, B-1): die Assembly ist
359
- // Tupel-Zustand und nimmt nach einer Deaktivierung nichts mehr auf
360
- if (t.deactivated)
361
- return { handled: true, error: 'tuple deactivated' };
362
- // Chunk-Sammlung pro (Tupel, probe): LÜCKEN PUFFERN (Ankunftsreihen-
363
- // folge irrelevant, 5.2a in kind), nur eine STRIKT HÖHERE probe
364
- // verdrängt die offene, eine niedrigere wird verworfen; threadId-
365
- // Dissens verwirft; seq-Konflikt (andere Bytes) verwirft (byte-
366
- // identische Wiederholung fängt duplicate-known auf Envelope-Ebene).
367
- // Entscheidungen auf LOCALS (Review 6, B-1) — GESCHRIEBEN wird erst
368
- // nach Ack-Bau und letztem Re-Check, synchron mit dem Commit
369
- const cur = t.probeAsm;
370
- const displaced = cur && BigInt(doc.probe) > BigInt(cur.probe);
371
- if (cur && !displaced && BigInt(doc.probe) < BigInt(cur.probe))
372
- return { handled: true, error: 'below open probe assembly' };
373
- const a = (!cur || displaced) ? { probe: doc.probe, threadId, chunks: new Map(), lastSeq: undefined } : cur;
374
- if (threadId !== a.threadId) {
375
- t.probeAsm = null;
376
- (t.probePoison ??= new Set()).add(doc.probe);
377
- return { handled: true, error: 'probe assembly conflict (threadId)' };
378
- }
379
- const seq = BigInt(doc.seq);
380
- if (a.chunks.has(doc.seq)) {
381
- t.probeAsm = null;
382
- (t.probePoison ??= new Set()).add(doc.probe);
383
- return { handled: true, error: 'probe assembly conflict (seq)' };
384
- }
385
- if (doc.last && a.lastSeq !== undefined) {
386
- t.probeAsm = null;
387
- (t.probePoison ??= new Set()).add(doc.probe);
388
- return { handled: true, error: 'probe assembly conflict (two last)' };
389
- }
390
- const candLast = doc.last ? doc.seq : a.lastSeq;
391
- if (candLast !== undefined && seq > BigInt(candLast)) {
392
- t.probeAsm = null;
393
- (t.probePoison ??= new Set()).add(doc.probe);
394
- return { handled: true, error: 'seq beyond last' };
395
- }
396
- // auch BEREITS GEPUFFERTE Chunks oberhalb von last verwerfen die
397
- // Assembly (Review 16, B-1 — 5.2a in kind, Ankunftsreihenfolge egal)
398
- if (candLast !== undefined)
399
- for (const s of a.chunks.keys())
400
- if (BigInt(s) > BigInt(candLast)) {
401
- t.probeAsm = null;
402
- (t.probePoison ??= new Set()).add(doc.probe);
403
- return { handled: true, error: 'seq beyond last (gepuffert)' };
404
- }
405
- // Komplettierung VOR dem Puffern vorhersagen (Review 5, B-4 — die
406
- // Star-Struktur gespiegelt): jeder NICHT-komplettierende Chunk (auch
407
- // ein früh eintreffender last:true) wird nach den Per-Chunk-Gates
408
- // quittiert und gepuffert; NUR der KOMPLETTIERENDE wartet auf die
409
- // Union-Validierung (M-2) — Ankunftsreihenfolge ist irrelevant (5.2a)
410
- const complete = candLast !== undefined && (() => {
411
- for (let i = 1n; i <= BigInt(candLast); i++)
412
- if (String(i) !== doc.seq && !a.chunks.has(String(i)))
413
- return false;
414
- return true;
415
- })();
416
- if (!complete) {
417
- const __ackEnv = await mkAck();
418
- if (t.deactivated)
419
- return { handled: true, error: 'tuple deactivated' }; // Re-Check (B-1), danach kein await
420
- t.probeAsm = a; // Übernahme JETZT — synchron mit Puffern + Commit (B-5)
421
- if (doc.last)
422
- a.lastSeq = doc.seq;
423
- a.chunks.set(doc.seq, doc.blinded);
424
- return { handled: true, partial: true, ack: commitAck(__ackEnv) };
425
- }
426
- // Union aus Puffer + aktuellem Chunk OHNE Mutation; globale
427
- // Sortierung + Duplikatfreiheit (§6a.2)
428
- const union = [];
429
- for (let i = 1n; i <= BigInt(candLast); i++)
430
- union.push(...(String(i) === doc.seq ? doc.blinded : a.chunks.get(String(i))));
431
- if (!union.every((x, i, arr) => i === 0 || arr[i - 1] < x)) {
432
- t.probeAsm = null;
433
- (t.probePoison ??= new Set()).add(doc.probe);
434
- return { handled: true, error: 'probe assembly invalid (order/duplicates)' };
435
- }
436
- // Schnitt gegen die GEGENSEITEN-Anker des eigenen Prior-Candidate-Sets
437
- // — reine Rechnung, VOR dem Ack-Bau, damit zwischen Re-Check und
438
- // Übernahme kein await mehr liegt (Review 5, B-1)
439
- const matches = [];
440
- for (const cp of t.priorCands?.counterpart ?? []) {
441
- if (cp === key)
442
- continue; // §6a: das frische Tupel selbst ist NIE ein prior (Selbst-Ketten-Schutz)
443
- if (union.includes(await hmacU(kp, cp)))
444
- matches.push(cp);
445
- }
446
- const __ackEnv = await mkAck(); // JETZT ist alles validiert (M-2)
447
- if (t.deactivated)
448
- return { handled: true, error: 'tuple deactivated' }; // Re-Check (B-1), danach kein await bis zum Commit
449
- // Übernahme + Cache + Ack in EINEM synchronen Zug (B-5); die
450
- // Kettenwahl mutiert ebenfalls synchron — erst DANACH die fehlbare
451
- // Outbound-Produktion (ihr Scheitern hinterlässt eine Pflicht)
452
- t.probeAsm = null;
453
- if (BigInt(doc.probe) > BigInt(t.probeSeqIn ?? '0'))
454
- t.probeSeqIn = doc.probe; // monoton (B-1)
455
- if (t.probePoison)
456
- for (const ps of t.probePoison)
457
- if (BigInt(ps) <= BigInt(t.probeSeqIn))
458
- t.probePoison.delete(ps); // Gift unterhalb des High-Water ist gegenstandslos
459
- const ack = commitAck(__ackEnv);
460
- if (!matches.length) {
461
- say(p, `Probe von ${t.name}: kein Match — ehrlich eine neue Beziehung`);
462
- return { handled: true, matches: 0, ack };
463
- }
464
- const outbound = [];
465
- let chained = false;
466
- let outboundError;
467
- if (iAmRecord(t, key)) {
468
- // Record-Seite: einmal wählen, endgültig — „the record side chains
469
- // atomically with issuing its choice" (Review 12, B-2): erst das
470
- // Wahl-Mapping FEHLBAR vorbereiten, dann Wahl + Kette + Ausgabe in
471
- // EINEM synchronen Zug; scheitert die Vorbereitung, wird NICHT
472
- // gekettet und die volle Wahl-Pflicht bleibt stehen (Flush holt sie
473
- // atomar nach)
474
- if (!t.contChosen) {
475
- // Wahl frei — hier: der erste LEBENDE, KETTBARE Match (Review 19,
476
- // B-4: ein von einem konkurrierenden Tupel verbrauchter erster
477
- // Match blockiert die weiteren gültigen nicht; reichste Historie
478
- // wäre App-Sache). Kette ZUERST (Review 13, B-1): „chains
479
- // atomically with issuing its choice" heißt beides oder keins.
480
- for (const choice of matches) {
481
- if (p.contacts.get(choice)?.deactivated)
482
- continue; // toter prior → nächster Match
483
- let prep;
484
- try {
485
- prep = await prepareContMapping(p, key, choice, when, ent);
486
- }
487
- catch (err) {
488
- // die Pflicht trägt ALLE Matches (Review 20, B-3): wird der
489
- // erste vor dem Flush unkettbar, versucht der Flush die
490
- // übrigen lebenden Kandidaten
491
- t.contMappingDue = { prior: choice, kind: 'choice', alts: matches.filter((x) => x !== choice) };
492
- outboundError = String(err?.message ?? err);
493
- break;
494
- }
495
- if (t.deactivated || t.contChosen)
496
- break;
497
- chained = chainTuple(p, key, choice, when);
498
- if (!chained)
499
- continue; // Rennen verloren → nächster lebender Match
500
- t.contChosen = choice;
501
- if (t.contMappingDue?.kind === 'choice')
502
- delete t.contMappingDue; // die alte Bau-Pflicht ist ERFÜLLT (Review 14, B-2)
503
- outbound.push(commitContMapping(prep));
504
- const v2Err = await v2Reissue(p, key, when, ent, outbound);
505
- if (v2Err)
506
- outboundError = outboundError ? outboundError + ' · ' + v2Err : v2Err;
507
- break;
508
- }
509
- }
510
- }
511
- else {
512
- // Nicht-Record: Match-Report (löst NIE eine Kette aus). Ist die
513
- // Record-Wahl auf diesem Tupel schon verifiziert, ist die Report-
514
- // Pflicht GESTORBEN (6a.3) — eine späte Probe der Record-Seite erzeugt
515
- // keinen Report mehr: „a match report flushed after the choice is a
516
- // stale counter-claim that MUST NOT be sent". Ohne diese Schranke
517
- // entstand ein nie retirebarer Eintrag, den jeder Flush byte-identisch
518
- // erneut sandte (Gesamtsimulator, 06.09.2026: Endlos-Retry nach der
519
- // gegenseitigen Verifikation)
520
- if (!t.contReported && !t.contRecordPrior) {
521
- t.contReported = matches[0];
522
- try {
523
- outbound.push(await buildMapping(p, key, matches[0], when, ent));
524
- }
525
- catch (err) {
526
- t.contMappingDue = { prior: matches[0], kind: 'report' };
527
- outboundError = String(err?.message ?? err);
528
- }
529
- }
530
- }
531
- return { handled: true, matches: matches.length, chained, name: chained ? t.name : undefined, outbound, outboundError, ack };
532
- }
533
- async function handleMapping(p, { key, t }, artifact, when, ent, mkAck, commitAck) {
534
- if (!C.shaped(artifact ?? {}, { body: 'object', proof: 'object' })
535
- || !C.shaped(artifact.body, { prior: 'string', next: 'string', to: 'string' })
536
- || typeof artifact.proof.mac1 !== 'string' || typeof artifact.proof.mac2 !== 'string')
537
- return { handled: true, error: 'malformed mapping' };
538
- const body = artifact.body;
539
- const { mac1, mac2 } = artifact.proof;
540
- const fail = (why) => { say(p, `Kontinuitäts-Mapping verworfen: ${why}`); return { handled: true, error: why }; };
541
- if (body.to !== t.channel.own.anchor)
542
- return fail('to ≠ eigener neuer Anker');
543
- if (body.next !== key)
544
- return fail('next ≠ neuer Anker der Gegenseite');
545
- // prior MUSS im Schnappschuss liegen (nie das frische Tupel, nie tiefere Kettenglieder)
546
- if (body.prior === key)
547
- return fail('prior = frisches Tupel (§6a)'); // Selbst-Ketten-Schutz, auch bei vergiftetem Schnappschuss
548
- if (!(t.priorCands?.counterpart ?? []).includes(body.prior))
549
- return fail('prior nicht im Prior-Candidate-Set');
550
- const old = p.contacts.get(body.prior);
551
- if (!old?.channel?.own)
552
- return fail('alte Beziehung ohne eigenen Kontext');
553
- const msg = jcs(body);
554
- if ((await hmacU(await kMac(old.channel.own.x.priv, old.channel.counterpartKa, 'rltp/visibility/mac/cont1'), msg)) !== mac1)
555
- return fail('mac1 (alter Beziehungs-Schlüssel)');
556
- if ((await hmacU(await kMac(t.channel.own.x.priv, t.channel.counterpartKa, 'rltp/visibility/mac/cont2'), msg)) !== mac2)
557
- return fail('mac2 (neuer Beziehungs-Schlüssel)');
558
- // 6.4, die GENERISCHE Revisionsregel im (next, to)-Scope (Review 11,
559
- // B-2): höher ersetzt · gleich + JCS-identischer Body = idempotenter
560
- // Repeat (No-op, geackt) · gleich + anderer Body = Äquivokation ·
561
- // niedriger = reject. Der Revisionsstand rückt erst NACH allen
562
- // Ablehnungsgründen vor (Record-Freeze gilt whatever its revision)
563
- if (BigInt(body.revision) === BigInt(t.contRevIn ?? '0')) {
564
- if (t.contBodyIn && msg === t.contBodyIn) {
565
- const __ackEnv = await mkAck();
566
- if (t.deactivated)
567
- return fail('tuple deactivated');
568
- return { handled: true, idempotent: true, ack: commitAck(__ackEnv) };
569
- }
570
- return fail('Äquivokation — gleiche Revision, anderer Body');
571
- }
572
- if (BigInt(body.revision) < BigInt(t.contRevIn ?? '0'))
573
- return fail('mapping revision (niedriger)');
574
- const senderIsRecord = key < t.channel.own.anchor;
575
- // Record-Freeze VOR Ack und Revisionsstand (B-3): ein verworfenes
576
- // Dokument konsumiert weder Revision noch Krypto
577
- if (senderIsRecord && t.contRecordPrior && t.contRecordPrior !== body.prior)
578
- return fail('Äquivokation — Record-Wahl ist eingefroren');
579
- // §6a.4/6.4: „On verification of a record-side mapping, chain" —
580
- // wurde der genannte alte Kopf von einem KONKURRIERENDEN Tupel
581
- // gekettet, ist die Kette hier nicht möglich: ablehnen statt
582
- // Freeze + Ack + Alignment ohne Kette (Review 14, B-3). ABER: „a
583
- // head deactivated by THIS VERY chaining remains a valid prior for
584
- // this tuple" (Review 15) — eine höhere Revision derselben
585
- // eingefrorenen Wahl wird per 6.4 normal verarbeitet
586
- const priorTaken = () => old.deactivated && old.chainedInto !== key;
587
- if (senderIsRecord && priorTaken())
588
- return fail('prior von konkurrierendem Tupel gekettet (§6.4) — nicht kettbar');
589
- const __ackEnv = await mkAck(); // alle Gates bestanden — VOR der Mutation
590
- if (t.deactivated)
591
- return fail('tuple deactivated'); // Re-Check (B-1), danach kein await bis zum Commit
592
- if (senderIsRecord && priorTaken())
593
- return fail('prior von konkurrierendem Tupel gekettet (§6.4) — nicht kettbar'); // Re-Check (B-3)
594
- // Effekt (Revision, Freeze, Kette) + Cache + Ack in EINEM synchronen
595
- // Zug (Review 6, B-5/B-1); die fehlbare Outbound-Produktion läuft
596
- // DANACH — ihr Scheitern hinterlässt die Pflicht (contMappingDue),
597
- // nie einen halben Commit
598
- t.contRevIn = body.revision;
599
- t.contBodyIn = msg; // für den Idempotenz-Vergleich (Review 11, B-2)
600
- const outbound = [];
601
- let chainedNow = false; // das ECHTE Kettenergebnis (Review 14, B-3), nie eine Ableitung
602
- const done = (extra = {}) => ({ handled: true, chained: chainedNow, name: t.name, outbound, ack, ...extra });
603
- let ack;
604
- if (senderIsRecord) {
605
- t.contRecordPrior = body.prior;
606
- // nach den Re-Checks garantiert kettbar ODER schon in GENAU dieses
607
- // Tupel gekettet (Re-Verifikation derselben Wahl, Review 15)
608
- chainedNow = chainTuple(p, key, body.prior, when) || old.chainedInto === key;
609
- // „the non-record side's match-report duty … dies on verifying the
610
- // record side's mapping on that tuple" (Review 11, B-3): der alte
611
- // Match-Report wird retired — nie mehr gesendet (stale
612
- // counter-claim MUST NOT be sent), sein Ack-Ref bleibt erhalten
613
- for (const o of (t.outbox ?? new Map()).values())
614
- if (o.kind === 'continuity-mapping' && o.duty !== 'align')
615
- o.retired = true;
616
- if (t.contMappingDue?.kind === 'report')
617
- delete t.contMappingDue;
618
- ack = commitAck(__ackEnv);
619
- if (!t.contAligned) {
620
- // ein noch UNQUITTIERTES Alignment wird byte-identisch erneut
621
- // gesendet (Review 16, B-2: „is resent — same revision,
622
- // idempotent by 6.4") — eine höhere Record-Revision derselben
623
- // Wahl prägt NIE eine zweite Alignment-Revision für dieselbe
624
- // offene Pflicht
625
- const pending = [...(t.outbox ?? new Map()).values()].find((o) => o.kind === 'continuity-mapping' && o.duty === 'align' && !o.retired);
626
- if (pending)
627
- outbound.push({ to: t, kind: 'continuity-mapping/0.1 (Retry, byte-identisch)', env: pending.env });
628
- else {
629
- // Produktion registriert in der Outbox; die Pflicht stirbt erst
630
- // mit dem QUITTIERTEN Dokument (Ack-Pfad setzt contAligned)
631
- try {
632
- outbound.push(await buildMapping(p, key, body.prior, when, ent, 'align'));
633
- if (t.contMappingDue?.prior === body.prior)
634
- delete t.contMappingDue;
635
- }
636
- catch (err) {
637
- t.contMappingDue = { prior: body.prior, kind: 'align' };
638
- return done({ outboundError: String(err?.message ?? err) });
639
- }
640
- }
641
- }
642
- const v2Err = await v2Reissue(p, key, when, ent, outbound);
643
- if (v2Err)
644
- return done({ outboundError: v2Err });
645
- }
646
- else {
647
- // Match-Report der Nicht-Record-Seite: darf die Wahl INFORMIEREN,
648
- // kettet nie selbst. Die Record-Wahl darauf kettet ATOMAR mit der
649
- // Ausgabe ihres Mappings (Review 12, B-2): erst vorbereiten, dann
650
- // Wahl + Kette + Ausgabe synchron; Fehlschlag → volle Wahl-Pflicht
651
- if (iAmRecord(t, key) && !t.contChosen) {
652
- ack = commitAck(__ackEnv); // der EMPFANGS-Effekt (Revision) committet unabhängig von der Wahl
653
- let prep;
654
- try {
655
- prep = await prepareContMapping(p, key, body.prior, when, ent);
656
- }
657
- catch (err) {
658
- t.contMappingDue = { prior: body.prior, kind: 'choice' };
659
- return done({ outboundError: String(err?.message ?? err) });
660
- }
661
- if (t.deactivated || t.contChosen)
662
- return done();
663
- // Kette ZUERST (Review 13, B-1): scheitert sie, keine Wahl, keine Ausgabe
664
- chainedNow = chainTuple(p, key, body.prior, when);
665
- if (chainedNow) {
666
- t.contChosen = body.prior;
667
- if (t.contMappingDue?.kind === 'choice')
668
- delete t.contMappingDue; // die alte Bau-Pflicht ist ERFÜLLT (Review 14, B-2)
669
- outbound.push(commitContMapping(prep));
670
- const v2Err = await v2Reissue(p, key, when, ent, outbound);
671
- if (v2Err)
672
- return done({ outboundError: v2Err });
673
- }
674
- }
675
- else {
676
- // NUR das Gegen-Mapping AUF DER GEWÄHLTEN Beziehung ist die
677
- // ALIGNMENT-Antwort (Review 14, B-1): body.prior = der eigene
678
- // alte Anker des Senders = unser Kontaktschlüssel der gewählten
679
- // alten Beziehung (t.contChosen). Ein Match-Report für eine
680
- // ANDERE gemeinsame Altbeziehung beantwortet die Wahl NICHT —
681
- // erst die echte Antwort beendet die Resend-Pflicht (retiren,
682
- // Ack-Ref bleibt; Review 13, B-2)
683
- if (t.contChosen && body.prior === t.contChosen)
684
- for (const o of (t.outbox ?? new Map()).values())
685
- if (o.kind === 'continuity-mapping')
686
- o.retired = true;
687
- ack = commitAck(__ackEnv);
688
- }
689
- }
690
- return done();
691
- }
692
- /**
693
- * The host's producing call for a mapping the receiver still owes
694
- * (review 6, B-3): a transient seal failure during probe/mapping
695
- * handling persists the obligation in contMappingDue; this call — not a
696
- * redelivery, which the freshness rule forbids — builds and returns it.
697
- */
698
- export async function flushContinuity(p, newKey, when, ent = {}) {
699
- const t = p.contacts.get(newKey);
700
- if (!t || t.deactivated)
701
- return { outbound: [] }; // Per-Tupel-Pflichten sterben mit dem Tupel (Review 8, B-2)
702
- // Retry-ENTSCHEIDUNG und Pflicht-Bau laufen unter dem Kontakt-Lock
703
- // (Review 12, B-1): der Empfang (der Reports retired) hält denselben
704
- // Lock — kein Fenster, in dem ein gestarteter Flush eine gerade
705
- // gestorbene Report-Pflicht doch noch ausstellt („a match report
706
- // flushed after the choice … MUST NOT be sent"). Nur der frische
707
- // Probe-Neubau läuft danach (buildProbe nimmt denselben Lock selbst).
708
- const r = await withContactLock(t, async () => {
709
- if (t.deactivated)
710
- return { outbound: [] };
711
- // unquittierte MAPPINGS: byte-identischer Retry (4.2), retired nie
712
- // (gestorbene Match-Report-Pflicht, Review 11, B-3). PROBEN nie
713
- // byte-identisch — §6a: „a resend is always a fresh probe sequence
714
- // with freshly sampled padding" (Review 5, B-3): stale Einträge
715
- // werden RETIRED (Ack-Ref bleibt, Review 6, B-3), EINE frische
716
- // Probe ersetzt sie (Bau nach der Lock-Sektion)
717
- // ein Report nach verifizierter Record-Wahl ist tot (6a.3) — auch wenn
718
- // er der Wahl zuvorkam, aber erst danach in die Outbox gelangte
719
- if (t.contRecordPrior)
720
- for (const o of (t.outbox ?? new Map()).values())
721
- if (o.kind === 'continuity-mapping' && o.duty !== 'align')
722
- o.retired = true;
723
- const retries = [...(t.outbox ?? new Map()).values()]
724
- .filter((o) => o.kind === 'continuity-mapping' && !o.retired)
725
- .map((o) => ({ to: t, kind: o.kind + '/0.1 (Retry, byte-identisch)', env: o.env }));
726
- const staleProbes = [...(t.outbox ?? new Map()).values()].filter((o) => o.kind === 'continuity-probe' && !o.retired);
727
- let probeDue = false;
728
- if (staleProbes.length) {
729
- for (const o of staleProbes)
730
- o.retired = true;
731
- probeDue = true;
732
- }
733
- if (!t.contMappingDue)
734
- return { outbound: retries, probeDue };
735
- // Single-Flight (Review 7, B-3): zwei parallele Flushes liefern nie
736
- // zwei Mappings für EINE Pflicht
737
- if (t.contFlushPending)
738
- return { outbound: retries, pending: true, probeDue };
739
- t.contFlushPending = true;
740
- try {
741
- const due = t.contMappingDue;
742
- // eine Choice-Pflicht nach bereits GEFALLENER Wahl ist tot
743
- // (Review 14, B-2): der Direktpfad hat sie erfüllt oder ersetzt —
744
- // sie darf NIE in den generischen Bau fallen (der eine frische
745
- // Revision prägte statt der Same-Revision-Wiederholung)
746
- if (due.kind === 'choice' && t.contChosen) {
747
- if (t.contMappingDue === due)
748
- delete t.contMappingDue;
749
- return { outbound: retries, probeDue };
750
- }
751
- if (due.kind === 'choice' && !t.contChosen) {
752
- // die nachgeholte RECORD-WAHL kettet atomar mit der Ausgabe
753
- // (Review 12, B-2) und ITERIERT alle bekannten Matches
754
- // (Review 20, B-3): erster lebender, kettbarer Kandidat
755
- // gewinnt; sind alle tot, stirbt die Pflicht ehrlich
756
- // ungekettet (spätere Probes wählen neu). Ein Bau-Fehler
757
- // wirft → der äußere catch hält die Pflicht am Leben.
758
- const cands = [...new Set([due.prior, ...(due.alts ?? [])])];
759
- for (const cand of cands) {
760
- if (p.contacts.get(cand)?.deactivated)
761
- continue;
762
- const prep = await prepareContMapping(p, newKey, cand, when, ent);
763
- if (t.deactivated)
764
- return { outbound: retries, probeDue };
765
- if (t.contChosen)
766
- break;
767
- if (!chainTuple(p, newKey, cand, when))
768
- continue; // Rennen verloren → nächster Kandidat
769
- t.contChosen = cand;
770
- retries.push(commitContMapping(prep));
771
- break;
772
- }
773
- if (t.contMappingDue === due)
774
- delete t.contMappingDue;
775
- return { outbound: retries, probeDue };
776
- }
777
- const mapping = await buildMapping(p, newKey, due.prior, when, ent, due.kind);
778
- // der Flush stellt GENAU den Zustand her, den der gelungene
779
- // Direktpfad hinterlassen hätte — kind-bewusst. Und er löscht NUR
780
- // die Pflicht, die er selbst gebaut hat: ist während des await
781
- // eine NEUERE an ihre Stelle getreten, bleibt sie stehen (Review
782
- // 8, B-3); aligned setzt erst der ACK — hier stirbt die BAU-Pflicht
783
- if (t.contMappingDue === due)
784
- delete t.contMappingDue;
785
- return { outbound: [...retries, mapping], probeDue };
786
- }
787
- catch (err) {
788
- return { outbound: retries, probeDue, outboundError: String(err?.message ?? err) };
789
- }
790
- finally {
791
- t.contFlushPending = false;
792
- }
793
- });
794
- if (r.probeDue) {
795
- try {
796
- r.outbound.push(...(await buildProbe(p, newKey, when, { ...ent, pad: undefined, threadId: undefined }) ?? []));
797
- }
798
- catch (err) {
799
- r.outboundError = r.outboundError ? r.outboundError + ' · ' + String(err?.message ?? err) : String(err?.message ?? err);
800
- }
801
- }
802
- delete r.probeDue;
803
- return r;
804
- }
805
- //# sourceMappingURL=continuity.js.map