@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
@@ -1,1233 +0,0 @@
1
- // trust — der GRADUIERTE Vertrauensakt: seit dem Nachzug auf Visibility
2
- // 0.29 + Delivery 0.79 baut dieses Modul die ECHTEN Wire-Artefakte
3
- // (anchor-mapping@2 · grade-declaration@1 · star@1 mit 5.2a-Chunks),
4
- // getragen von den registrierten Task-Dokumenten (Delivery §4.4:
5
- // anchor-mapping/0.1 · grade-declaration/0.1 · star/0.1 — Payload = das
6
- // Artefakt, Dokument proof-frei, ein Träger), quittiert mit dem
7
- // deniablen delivery-ack (4.2-Klassenregel). Formen und Schlüssel sind
8
- // byte-kompatibel zu vectors/visibility.json (vectors3.test.mjs rechnet
9
- // sie unabhängig nach).
10
- //
11
- // Die Zustandsmaschinen sind die konvergierten:
12
- // · Admissionsschicht (Sektion 2): Promotion-Commit beim ERSTEN
13
- // verifizierten anchor-mapping@2 einer BEZIEHUNG (relId = die
14
- // Kette, der Idempotenzschlüssel); genau ein Eintrag je Beziehung,
15
- // self-Korrektur per höherer Revision in place; gleiche self auf
16
- // zweiter Beziehung = Weg-2-Merge (früheste Position überlebt,
17
- // Aliase); EINE totale Verifikations-Commit-Ordnung (a.seq).
18
- // · Rekonziliation (5.4): Sicht = {(member, grade)} der admittierten,
19
- // unmaskierten Mitglieder (Maske: 24h grade-wait ab Admission, fällt
20
- // mit verifizierter Declaration oder Ablauf → fail-closed count);
21
- // Baseline-Automat monoton über Ack-Completions, highWater überlebt
22
- // ⊥, kein Failure-Ereignis, nie Rollback.
23
- // · Byte-identische Redelivery (4.2): jede unquittierte Zustellung
24
- // wird als DASSELBE Dokument erneut gesendet (outbox hält die Envs);
25
- // der Empfänger re-ackt duplicate-known byte-identisch (ackStore).
26
- import { jcs, makeValidator, sameDigest } from '../core.js';
27
- import { SCHEMAS } from '../schemas.js';
28
- import * as C from './deps.js';
29
- import { buildAck, verifyAck, ACK_TYPE } from './acks.js';
30
- const te = new TextEncoder();
31
- const S = globalThis.crypto.subtle;
32
- const TT = 'https://real-life.org/trust-tasks/';
33
- const say = (p, m) => p.log.push(m);
34
- const uuid = (ent) => ent ?? globalThis.crypto.randomUUID();
35
- const V = makeValidator(SCHEMAS);
36
- const schemaOk = (file, data) => V.validate(data, SCHEMAS[file], SCHEMAS[file]).length === 0;
37
- export const GRADE_WAIT = 86_400_000; // 5.4: PT24H, in when-Millisekunden
38
- export const ADMISSION_BOUND = 10n ** 18n - 1n; // Sektion 2 — unerreichbar, geführt für die Totalität
39
- const CHUNK_MAX = 1024; // 5.2a: blinded[] je Chunk
40
- // ── Primitiven ──────────────────────────────────────────────────────────
41
- export async function hmac(keyBytes, msg) {
42
- const k = await S.importKey('raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
43
- return C.b64uOf(new Uint8Array(await S.sign('HMAC', k, te.encode(msg))));
44
- }
45
- export const hmacU = async (keyBytes, msg) => 'u' + await hmac(keyBytes, msg);
46
- export async function communityContext(p) {
47
- if (!p.selfCtx)
48
- p.selfCtx = await C.communityContext(p.rootIkm, p.communityGenesis);
49
- return p.selfCtx;
50
- }
51
- export const selfCard = async (p, whenIso) => {
52
- const s = await communityContext(p);
53
- return C.signCard(s, C.cardBody(s, { name: p.name }), whenIso);
54
- };
55
- // self-card@1 (§6.2): { body: {type, anchor, keyAgreement}, proof:
56
- // {proofValue} } — ROHE Ed25519-Signatur über JCS(body), 'z'+base58
57
- // (§2.1: bewusst KEINE DI-Suite; zwei Artefakt-Familien)
58
- export async function selfCard1(p) {
59
- const s = await communityContext(p);
60
- const body = { type: 'self-card@1', anchor: s.anchor, keyAgreement: s.keyAgreement };
61
- const sig = new Uint8Array(await S.sign({ name: 'Ed25519' }, s.ed.priv, te.encode(jcs(body))));
62
- return { body, proof: { proofValue: 'z' + C.base58(sig) } };
63
- }
64
- const verifyRawSig = async (did, body, proofValue) => {
65
- try {
66
- if (typeof proofValue !== 'string' || proofValue[0] !== 'z')
67
- return false;
68
- const raw = C.edRawOfAnchor(did);
69
- const sig = C.fromBase58(proofValue.slice(1));
70
- if (!raw || !sig || sig.length !== 64)
71
- return false;
72
- const key = await S.importKey('raw', raw, { name: 'Ed25519' }, false, ['verify']);
73
- return await S.verify({ name: 'Ed25519' }, key, sig, te.encode(jcs(body)));
74
- }
75
- catch {
76
- return false;
77
- }
78
- };
79
- const chShared = (contact) => C.ecdh(contact.channel.own.x.priv, C.xRawOfMk(contact.channel.counterpartKa));
80
- const relKey = async (contact, info) => C.hkdf(await chShared(contact), info);
81
- // ── anchor-mapping@2 — die normative Wire-Form (§6.1–6.3) ──────────────
82
- // body = { type, pair, self, to, card, revision, issuedAt } ·
83
- // proof = { mac1 (Kanal-DH, info map1) · mac2 (selfX × pairX-Adressat,
84
- // info map2) }, beide 'u'-MACs über JCS(body). Die Card reist IM Body.
85
- // der reine BAU (fehlbar, mutiert nichts) — modul-lokal: nur Aufrufer,
86
- // die den Issue-Lock halten und selbst committen (setTrust/
87
- // reissueTrust), erreichen ihn mit vorreservierter Revision. Der
88
- // explizite Revisionspfad ist NICHT mehr exportiert (Review 9, B-2:
89
- // er umging Lock, Deaktivierungs-Check und Persistenz).
90
- async function buildMappingArtifact(p, contact, counterpartAnchor, when, revision) {
91
- const s = await communityContext(p);
92
- const whenIso = C.iso(when);
93
- const card = await selfCard1(p);
94
- const body = { type: 'anchor-mapping@2', pair: contact.channel.own.anchor, self: s.anchor, to: counterpartAnchor, card, revision, issuedAt: whenIso };
95
- const msg = jcs(body);
96
- const theirKa = C.xRawOfMk(contact.channel.counterpartKa);
97
- return { body, proof: {
98
- mac1: await hmacU(await relKey(contact, 'rltp/visibility/mac/map1'), msg),
99
- mac2: await hmacU(await C.hkdf(await C.ecdh(s.x.priv, theirKa), 'rltp/visibility/mac/map2'), msg)
100
- } };
101
- }
102
- export async function makeMapping(p, counterpartAnchor, when) {
103
- const contact = p.contacts.get(counterpartAnchor);
104
- // Ausstellungs-Disziplin (Review 8/9): Peek → fehlbar bauen →
105
- // Zähler UND Body synchron committen (sentMapping = der persistierte
106
- // ausgestellte Body, §6.4: one step per scope), seriell je Kontakt;
107
- // ein deaktiviertes Tupel stellt nichts aus
108
- return withIssueLock(contact, async () => {
109
- if (contact.deactivated)
110
- throw new Error('tuple deactivated — issues nothing (§6.4)');
111
- const revision = peekRev(contact, 'mapRevOut');
112
- const m = await buildMappingArtifact(p, contact, counterpartAnchor, when, revision);
113
- if (contact.deactivated)
114
- throw new Error('tuple deactivated — issues nothing (§6.4)');
115
- contact.mapRevOut = revision;
116
- contact.sentMapping = m;
117
- return m;
118
- });
119
- }
120
- // Verifikation ist EMPFÄNGER-PRIVAT (§6.3, geschlossene Liste) — und
121
- // die Liste wird IN IHRER REIHENFOLGE ausgewertet (Review 19, B-1:
122
- // „all of the following hold, evaluated in this order"): 1 Schema ·
123
- // 2 to · 3 pair · 4 Card verifiziert unter IHREM Anker · 5
124
- // card.anchor == self · 6 k2 aus card.keyAgreement · 7 MACs.
125
- // arrivalKey = das Ankunftstupel (Empfangspfad); standalone wählt
126
- // body.pair (Schritt 3 dann trivial).
127
- export async function verifyMapping(p, m, arrivalKey) {
128
- try {
129
- if (!schemaOk('visibility-anchor-mapping.schema.json', m))
130
- return false; // 1
131
- const b = m.body;
132
- if (!C.calOK(b.issuedAt))
133
- return false; // Kalender-Validität gehört zur Parse-Ebene (Review 12, B-3)
134
- const key = arrivalKey ?? b.pair;
135
- const entry = p.contacts.get(key);
136
- if (!entry?.channel?.own || entry.deactivated)
137
- return false;
138
- if (b.to !== entry.channel.own.anchor)
139
- return false; // 2
140
- if (b.pair !== key)
141
- return false; // 3 (Cross-Tupel-Bindung, Review 9)
142
- if (!(await verifyRawSig(b.card?.body?.anchor, b.card?.body, b.card?.proof?.proofValue)))
143
- return false; // 4: unter IHREM Anker
144
- if (b.card.body.anchor !== b.self)
145
- return false; // 5
146
- const msg = jcs(b);
147
- const k2 = await C.hkdf(await C.ecdh(entry.channel.own.x.priv, C.xRawOfMk(b.card.body.keyAgreement)), 'rltp/visibility/mac/map2'); // 6: k2 aus card.keyAgreement — VOR den MACs (Review 20, B-1)
148
- if ((await hmacU(await relKey(entry, 'rltp/visibility/mac/map1'), msg)) !== m.proof.mac1)
149
- return false; // 7 (mac1)
150
- if ((await hmacU(k2, msg)) !== m.proof.mac2)
151
- return false; // 7 (mac2)
152
- return true;
153
- }
154
- catch {
155
- return false;
156
- }
157
- }
158
- // Abstreitbarkeits-Demo: der Empfänger fabriziert ein identisch
159
- // verifizierendes Mapping — ein Leak beweist Dritten nichts (Klasse V)
160
- export async function forgeMapping(forger, victimCard, pairAnchor, when) {
161
- const entry = forger.contacts.get(pairAnchor);
162
- const whenIso = C.iso(when);
163
- const body = { type: 'anchor-mapping@2', pair: pairAnchor, self: victimCard.body.anchor, to: entry.channel.own.anchor, card: victimCard, revision: '1', issuedAt: whenIso };
164
- const msg = jcs(body);
165
- const k2 = await C.hkdf(await C.ecdh(entry.channel.own.x.priv, C.xRawOfMk(victimCard.body.keyAgreement)), 'rltp/visibility/mac/map2');
166
- return { body, proof: {
167
- mac1: await hmacU(await relKey(entry, 'rltp/visibility/mac/map1'), msg),
168
- mac2: await hmacU(k2, msg)
169
- } };
170
- }
171
- // ── die Admissionsschicht (Sektion 2) ───────────────────────────────────
172
- const relIdOf = (p, contactKey) => {
173
- const c = p.contacts.get(contactKey);
174
- return c ? (c.relId ??= contactKey) : contactKey;
175
- };
176
- // Bound testbar: p.admissionBound (BigInt) überschreibt den
177
- // unerreichbaren Spec-Bound — die MECHANIK (pending, k−1, Departure)
178
- // ist dieselbe (Sektion 2, „the rule exists for totality")
179
- const admission = (p) => (p.admission ??= { seq: 0n, byRel: new Map(), admitted: 0n });
180
- const boundOf = (p) => p.admissionBound ?? ADMISSION_BOUND;
181
- // grade-wait läuft nur ab, wenn ein Admissionszeitpunkt EXISTIERT
182
- // (Review 5, MAJOR): jede Zustandsoperation verlangt eine endliche Uhr
183
- const finite = (when) => {
184
- if (!Number.isFinite(when))
185
- throw new Error('admission requires a finite clock (5.4 grade-wait)');
186
- return when;
187
- };
188
- // Scope-Preflight (Review 5, B-5): prüft OHNE zu verbrauchen — ein Akt,
189
- // der in einem erschöpften Scope ausstellen müsste, suspendiert atomar
190
- const scopeOk = (cur) => BigInt(cur ?? '0') < ADMISSION_BOUND;
191
- // EIN atomarer Merge zweier Einträge: früheste Position überlebt,
192
- // Aliase lösen auf, Grade nach commitOrder; k admitted → k−1 Slots
193
- // frei → die nächsten Pending admittieren (Set-Änderung via reconcile)
194
- function mergeEntries(p, a, survivorIn, goneIn, when) {
195
- const survivor = goneIn.pos < survivorIn.pos ? goneIn : survivorIn;
196
- const gone = survivor === goneIn ? survivorIn : goneIn;
197
- for (const rid of gone.relIds) {
198
- survivor.relIds.add(rid);
199
- a.byRel.set(rid, survivor);
200
- }
201
- for (const ck of gone.rels)
202
- survivor.rels.add(ck);
203
- // Declarations bleiben je Herkunftstupel (B-2): höchste order je Tupel
204
- for (const [ck, g] of gone.grades ?? []) {
205
- const cur = survivor.grades?.get(ck);
206
- if (!cur || g.order > cur.order)
207
- (survivor.grades ??= new Map()).set(ck, g);
208
- }
209
- // NUMERISCHES Minimum (Review 22): admittedAt ist eine Zahl — der
210
- // Default-Sort verglich Strings (9 > 10) und konnte den grade-wait
211
- // des verschmolzenen Members fälschlich verlängern
212
- survivor.admittedAt = [survivor.admittedAt, gone.admittedAt].filter((x) => x !== undefined).sort((x, y) => x - y)[0];
213
- if (survivor.status === 'admitted' && gone.status === 'admitted') {
214
- a.admitted -= 1n;
215
- admitPending(p, a, when);
216
- }
217
- else if (gone.status === 'admitted') {
218
- survivor.status = 'admitted';
219
- }
220
- return survivor;
221
- }
222
- const admitPending = (p, a, when) => {
223
- // k−1-Nachrücken in Promotion-Commit-Ordnung (Sektion 2)
224
- while (a.admitted < boundOf(p)) {
225
- const next = [...new Set(a.byRel.values())].filter((x) => x.status === 'pending').sort((x, y) => x.pos < y.pos ? -1 : 1)[0];
226
- if (!next)
227
- break;
228
- next.status = 'admitted';
229
- next.admittedAt = when;
230
- a.admitted += 1n;
231
- }
232
- };
233
- /** Departure (Sektion 2): der Abgang EINER Beziehung entfernt nur
234
- * ihren Alias — „the relationships themselves remain fully active"
235
- * (Review 16, B-3): der verschmolzene Eintrag (und sein Slot) stirbt
236
- * erst mit der LETZTEN Beziehung */
237
- export function departMember(p, relId, when) {
238
- finite(when);
239
- const a = admission(p);
240
- const e = a.byRel.get(relId);
241
- if (!e)
242
- return;
243
- // die KETTE ist EINE Beziehung (Review 35): ihr Abgang entfernt auch
244
- // die Ketten-Aliase (byRel-Schlüssel, deren Kontakt sich als DIESE
245
- // Beziehung identifiziert — z. B. der frische Tupelschlüssel nach
246
- // einer Kettung mit vorbestehendem Eintrag); Aliase ANDERER
247
- // Beziehungen (Weg-2-Merge) bleiben
248
- for (const rid of [...e.relIds]) {
249
- if (rid === relId || (p.contacts.get(rid)?.relId ?? rid) === relId) {
250
- e.relIds.delete(rid);
251
- a.byRel.delete(rid);
252
- }
253
- }
254
- e.relIds.delete(relId);
255
- a.byRel.delete(relId);
256
- // der Grade-Strang der GEGANGENEN Beziehung stirbt mit ihr (Review
257
- // 17, B-1): „across the member's LIVE relationships' active heads" —
258
- // Kontaktschlüssel dieser Beziehung verlassen die effektive
259
- // Grade-Auswertung atomar mit dem Departure
260
- for (const ck of [...e.rels]) {
261
- const c = p.contacts.get(ck);
262
- if ((c?.relId ?? ck) === relId) {
263
- e.rels.delete(ck);
264
- e.grades?.delete(ck);
265
- }
266
- }
267
- if (e.relIds.size > 0)
268
- return; // Aliase bleiben, der Member bleibt admittiert
269
- if (e.status === 'admitted') {
270
- a.admitted -= 1n;
271
- admitPending(p, a, when);
272
- }
273
- }
274
- export function promotionCommit(p, relId, self, contactKey, when) {
275
- finite(when);
276
- const a = admission(p);
277
- let e = a.byRel.get(relId);
278
- if (e) {
279
- // spätere Verifikation derselben Beziehung: in place — auch eine
280
- // self-KORREKTUR per höherer Revision ändert nur den Inhalt, nie
281
- // Position oder Status (Sektion 2). Trifft die Korrektur die self
282
- // einer ANDEREN Beziehung, ist das der Weg-2-Merge
283
- if (e.self !== self) {
284
- const twin = [...a.byRel.values()].find((x) => x !== e && x.self === self);
285
- if (twin) {
286
- e.self = self;
287
- e = mergeEntries(p, a, e, twin, when);
288
- }
289
- else
290
- e.self = self;
291
- }
292
- e.rels.add(contactKey);
293
- }
294
- else {
295
- // gleiche self auf einer ANDEREN Beziehung = der Weg-2-Merge
296
- const twin = [...a.byRel.values()].find((x) => x.self === self);
297
- if (twin) {
298
- twin.relIds.add(relId);
299
- twin.rels.add(contactKey);
300
- a.byRel.set(relId, twin);
301
- e = twin;
302
- }
303
- else {
304
- // Admission ist TOTAL: unter dem Bound admitted (der Normalfall —
305
- // die Promotion selbst ist die Admission), am Bound
306
- // deliverable-pending (triggert nichts, rückt bei Abgang nach)
307
- const status = a.admitted < boundOf(p) ? 'admitted' : 'pending';
308
- if (status === 'admitted')
309
- a.admitted += 1n;
310
- e = { id: relId, pos: (a.seq += 1n), relIds: new Set([relId]), self, rels: new Set([contactKey]), status, admittedAt: status === 'admitted' ? when : undefined, grades: new Map() }; // id = kanonische Identität (Review 36)
311
- a.byRel.set(relId, e);
312
- }
313
- }
314
- return e;
315
- }
316
- /** Kettung (B-4): der neue Kopf löst über die überlebende Beziehung auf;
317
- * hält die frische Beziehung schon einen EIGENEN Eintrag, verschmilzt
318
- * er atomar mit dem der alten (früheste Position, Aliase, k−1) */
319
- export function chainAdmission(p, survivingRelId, freshRelId, newContactKey, when) {
320
- finite(when);
321
- const a = admission(p);
322
- const oldE = a.byRel.get(survivingRelId);
323
- const freshE = a.byRel.get(freshRelId);
324
- let e = oldE;
325
- if (oldE && freshE && oldE !== freshE)
326
- e = mergeEntries(p, a, oldE, freshE, when);
327
- else if (!oldE && freshE) {
328
- e = freshE;
329
- freshE.relIds.add(survivingRelId);
330
- a.byRel.set(survivingRelId, freshE);
331
- }
332
- if (e) {
333
- e.relIds.add(freshRelId);
334
- a.byRel.set(freshRelId, e);
335
- e.rels.add(newContactKey);
336
- }
337
- return e;
338
- }
339
- // Declarations je HERKUNFTSTUPEL (Review 5, B-2): §6.4 — „a deactivated
340
- // head's declarations are dead with their tuple". order = dieselbe
341
- // totale Verifikations-Commit-Ordnung wie die Promotions.
342
- // order stammt aus dem Moment der VERIFIKATION (Review 6, B-2): eine
343
- // vor dem Mapping verifizierte Declaration trägt ihren Stempel schon
344
- // und erhält beim späteren Commit KEINEN neuen — sonst schlüge sie
345
- // nach einem Merge eine echt später verifizierte Declaration
346
- const declCommit = (p, relId, grade, contactKey, order) => {
347
- const e = admission(p).byRel.get(relId);
348
- if (e)
349
- (e.grades ??= new Map()).set(contactKey, { grade, order: order ?? (admission(p).seq += 1n) });
350
- return e;
351
- };
352
- // die effektive Grade beim LESEN über die aktiven Köpfe berechnet —
353
- // Deaktivierung tötet die Declaration ohne Migrations- oder Aufräumlogik
354
- const effectiveGrade = (p, e) => {
355
- let best = null;
356
- for (const [ck, g] of e.grades ?? []) {
357
- const c = p.contacts.get(ck);
358
- if (!c || c.deactivated)
359
- continue;
360
- if (!best || g.order > best.order)
361
- best = g;
362
- }
363
- return best;
364
- };
365
- /**
366
- * Die Rekonziliations-Sicht (5.4): {(member, grade)} der admittierten,
367
- * unmaskierten Mitglieder. Maske: keine verifizierte Declaration UND
368
- * grade-wait (24h ab Admission) offen → nicht in der Sicht; nach
369
- * Ablauf fail-closed 'count'.
370
- */
371
- export function viewOf(p, when) {
372
- const seen = new Set();
373
- const out = [];
374
- for (const e of admission(p).byRel.values()) {
375
- if (seen.has(e))
376
- continue;
377
- seen.add(e);
378
- if (e.status !== 'admitted')
379
- continue; // deliverable-pending triggert nichts (Sektion 2)
380
- const g = effectiveGrade(p, e); // nur AKTIVE Köpfe zählen (B-2)
381
- if (g)
382
- out.push({ member: e.self, grade: g.grade });
383
- // Grenzzeitpunkt: „the window elapses only strictly afterwards" —
384
- // bei exakter Gleichheit bleibt maskiert (Review 11, B-4)
385
- else if (e.admittedAt === undefined || when <= e.admittedAt + GRADE_WAIT)
386
- continue; // provisorisch maskiert (B-3)
387
- else
388
- out.push({ member: e.self, grade: 'count' });
389
- }
390
- return out.sort((x, y) => x.member < y.member ? -1 : x.member > y.member ? 1 : 0);
391
- }
392
- // ── star@1 — die normative Wire-Form (5.2/5.2a) ────────────────────────
393
- const kStar = (contact, counterpartAnchor, salt) => relKey(contact, `rltp/visibility/blind/star/${contact.channel.own.anchor}/${counterpartAnchor}/${salt}`);
394
- const kStarIn = (contact, counterpartAnchor, salt) => relKey(contact, `rltp/visibility/blind/star/${counterpartAnchor}/${contact.channel.own.anchor}/${salt}`);
395
- // reine Berechnung: die 5.2a-Chunk-Serie einer Zustellung — count zählt
396
- // ALLE Mitglieder der Sicht, blinded[] nur die blinded-graded Teilmenge
397
- async function buildStarChunks(contact, counterpartAnchor, salt, view) {
398
- const k = await kStar(contact, counterpartAnchor, salt);
399
- const blinded = [];
400
- for (const v of view)
401
- if (v.grade === 'blinded')
402
- blinded.push(await hmacU(k, v.member));
403
- blinded.sort();
404
- const count = String(view.length);
405
- const n = Math.max(1, Math.ceil(blinded.length / CHUNK_MAX));
406
- const chunks = [];
407
- for (let i = 0; i < n; i++) {
408
- const body = { type: 'star@1', salt, seq: String(i + 1), last: i === n - 1, count, blinded: blinded.slice(i * CHUNK_MAX, (i + 1) * CHUNK_MAX) };
409
- chunks.push({ body, proof: { mac: await hmacU(k, jcs(body)) } });
410
- }
411
- return chunks;
412
- }
413
- export async function buildStar(p, contact, when) {
414
- if (contact.deactivated)
415
- return null; // deaktivierte Tupel stellen nichts aus (Review 9, B-3)
416
- return withStarLock(contact, async () => {
417
- if (contact.deactivated)
418
- return null; // Re-Check nach dem Lock-Erwerb
419
- const salt = peekStarSeq(contact); // nur gepeekt (Review 25)
420
- const key = [...p.contacts.entries()].find(([, c]) => c === contact)?.[0];
421
- // dieselbe Commit-Revalidierung wie reconcile (Review 26): wanderte
422
- // die Sicht während der MAC-Awaits, wird mit der aktuellen neu
423
- // gebaut — „every delivery carries the set current at its
424
- // production commit"
425
- for (;;) {
426
- const view = viewOf(p, when);
427
- const chunks = await buildStarChunks(contact, key, salt, view);
428
- if (contact.deactivated)
429
- return null; // Re-Check nach den MAC-Awaits (Review 10) — danach kein await
430
- if (!sameView(view, viewOf(p, when)))
431
- continue;
432
- contact.starSeqNext = salt; // COMMIT synchron mit der Ausgabe
433
- return chunks[0].body;
434
- }
435
- });
436
- }
437
- // Empfängerseite: denselben Lieferschlüssel nachrechnen, EINEN Anker testen
438
- export async function starMatch(p, entry, snap, anchor) {
439
- const key = [...p.contacts.entries()].find(([, c]) => c === entry)?.[0];
440
- const k = await kStarIn(entry, key, snap.salt);
441
- return snap.blinded.includes(await hmacU(k, anchor));
442
- }
443
- export async function refreshStarInfo(p) {
444
- for (const [key, entry] of p.contacts) {
445
- if (!entry.starReceived || !entry.channel?.own)
446
- continue;
447
- const known = [];
448
- const seen = new Set();
449
- for (const [k2, e] of p.contacts) {
450
- if (k2 === key || !e.selfAnchor || seen.has(e.selfAnchor))
451
- continue;
452
- seen.add(e.selfAnchor);
453
- if (await starMatch(p, entry, entry.starReceived, e.selfAnchor))
454
- known.push(e.name);
455
- }
456
- entry.starInfo = { count: entry.starReceived.count, knownNames: known.sort() }; // count bleibt String — 18 Stellen exakt
457
- }
458
- }
459
- // ── grade-declaration@1 (5.5) ──────────────────────────────────────────
460
- const kGrade = (subjectCtx, holderKa, subjectAnchor, holderAnchor) => C.ecdh(subjectCtx.x.priv, C.xRawOfMk(holderKa)).then((sh) => C.hkdf(sh, `rltp/visibility/mac/grade/${subjectAnchor}/${holderAnchor}`));
461
- // revision IMMER vorreserviert (gepeekt) vom Aufrufer, der den
462
- // Issue-Lock hält und den Zähler synchron committet (Review 8, B-1:
463
- // kein Defaultpfad, der vor fehlbarer Arbeit verbraucht)
464
- async function buildGradeDecl(p, counterpartAnchor, grade, when, revision) {
465
- const contact = p.contacts.get(counterpartAnchor);
466
- const body = { type: 'grade-declaration@1', subject: contact.channel.own.anchor, holder: counterpartAnchor, grade, revision, issuedAt: C.iso(when) };
467
- const k = await kGrade(contact.channel.own, contact.channel.counterpartKa, body.subject, body.holder);
468
- return { body, proof: { mac: await hmacU(k, jcs(body)) } };
469
- }
470
- // ── Dokumente, Outbox, byte-identische Redelivery (Delivery §3/4.2/4.4) ─
471
- // Payload = das Artefakt selbst; das Dokument trägt KEINEN Proof (die
472
- // Artefakt-MACs sind der eine Träger). threadId frisch je Zustellung;
473
- // Chunks eines Salts teilen sie. Die outbox hält jede unquittierte
474
- // Zustellung als DENSELBEN Env — Redelivery ist byte-identisch (4.2).
475
- const outboxOf = (contact) => (contact.outbox ??= new Map());
476
- async function sendDoc(p, counterpartAnchor, slug, payload, when, threadId, meta = {}, ent = {}) {
477
- const contact = p.contacts.get(counterpartAnchor);
478
- const doc = {
479
- id: uuid(ent.id), type: TT + slug + '/0.1',
480
- issuer: contact.channel.own.anchor, recipient: counterpartAnchor,
481
- threadId, issuedAt: C.iso(when), payload,
482
- };
483
- const env = await C.seal(doc, contact.channel.counterpartKa, ent);
484
- const digest = await C.digestDoc(doc);
485
- outboxOf(contact).set(digest, { env, threadId, kind: slug, ...meta });
486
- return { to: contact, kind: slug + '/0.1', env };
487
- }
488
- // ── die Ausstellungs-Disziplin (Review 7, B-1/B-2) ─────────────────────
489
- // „Assigning a revision and persisting the issued body are one step per
490
- // scope" (§6.4): je Kontakt serialisiert EIN Issue-Lock alle
491
- // Revisions-Producer; die Revision wird nur GEPEEKT, alles Fehlbare
492
- // (Bau, Seal, Digest) läuft davor, und Zähler + Outbox + Zustand
493
- // committen in EINEM synchronen Zug am Ende — ein Fehlschlag
494
- // verbraucht NICHTS, und gradeOutLast folgt der Ausstellungsordnung.
495
- export const withIssueLock = (contact, fn) => {
496
- const run = (contact.issueLock ?? Promise.resolve()).then(fn, fn);
497
- contact.issueLock = run.catch(() => { });
498
- return run;
499
- };
500
- export const peekRev = (contact, field) => {
501
- const next = BigInt(contact[field] ?? '0') + 1n;
502
- if (next > ADMISSION_BOUND)
503
- throw new Error('scope exhausted — issues nothing further (2.1)');
504
- return String(next);
505
- };
506
- // prepare (fehlbar: Seal + Digest) / commit (synchron: Outbox-Eintrag)
507
- async function prepareDoc(p, counterpartAnchor, slug, payload, when, threadId, meta = {}, ent = {}) {
508
- const contact = p.contacts.get(counterpartAnchor);
509
- const doc = {
510
- id: uuid(ent.id), type: TT + slug + '/0.1',
511
- issuer: contact.channel.own.anchor, recipient: counterpartAnchor,
512
- threadId, issuedAt: C.iso(when), payload,
513
- };
514
- const env = await C.seal(doc, contact.channel.counterpartKa, ent);
515
- const digest = await C.digestDoc(doc);
516
- return { contact, digest, entry: { env, threadId, kind: slug, ...meta }, out: { to: contact, kind: slug + '/0.1', env } };
517
- }
518
- const commitDoc = (prep) => { outboxOf(prep.contact).set(prep.digest, prep.entry); return prep.out; };
519
- // Salt nur GEPEEKT (Review 25): strikt monoton, nie dicht (5.2) —
520
- // aber „taking the set snapshot, assigning the salt, and committing
521
- // are one step": ein Bau-Fehler verbraucht nichts, auch am letzten
522
- // Wert entsteht nie falsche Erschöpfung. Der Aufrufer committet
523
- // synchron mit der Persistenz (unter dem Star-Lock serialisiert).
524
- const peekStarSeq = (contact) => {
525
- const next = BigInt(contact.starSeqNext ?? '0') + 1n;
526
- if (next > ADMISSION_BOUND)
527
- throw new Error('scope exhausted — issues nothing further (2.1)');
528
- return String(next);
529
- };
530
- // EIN Lock je Kontakt für Produktion UND Empfang, geteilt mit dem
531
- // Continuity-Pfad (Review 4, B-1/B-2: Trust- und Continuity-Effekte
532
- // desselben Kontakts serialisieren gemeinsam; Reentry-Checks im Lock)
533
- export const withContactLock = (contact, fn) => {
534
- const run = (contact.contLock ?? Promise.resolve()).then(fn, fn);
535
- contact.contLock = run.catch(() => { });
536
- return run;
537
- };
538
- const withStarLock = withContactLock;
539
- // der Baseline-Automat (5.4, totale Tabelle): completion(salt), sobald
540
- // JEDER Chunk-Digest des Salts quittiert ist
541
- const subOf = (contact) => (contact.sub ??= { highWater: 0n, baseline: null, inFlight: new Map() });
542
- const sameView = (a, b) => a.length === b.length && a.every((x, i) => x.member === b[i].member && x.grade === b[i].grade);
543
- /**
544
- * reconcile (5.4 — die eine Regel): divergiert die Sicht von der
545
- * Baseline (⊥ = divergent, das New-Head-Korollar), und ist nicht
546
- * pausiert, wird der AKTUELLE Stand geliefert — an JEDEM weiteren
547
- * Delivery-Kontakt unter frischem, höherem Salt (5.4 wörtlich,
548
- * Review 23; Salts sind ausdrücklich nicht dicht). Es gibt keinen
549
- * Same-View-Retry; die 4.4-Byte-Identität ist eine Formvorschrift
550
- * für Chunk-Retries, keine Anordnung.
551
- */
552
- export async function reconcile(p, counterpartAnchor, when, ent = {}) {
553
- const contact = p.contacts.get(counterpartAnchor);
554
- if (!contact || contact.deactivated || !contact.trustGiven || contact.trustPaused)
555
- return null;
556
- return withStarLock(contact, async () => {
557
- // Re-Check nach dem Lock-Erwerb (Review 9 B-3 / Review 11 B-1):
558
- // auch die Pause — „while paused, the delivery duty is suppressed"
559
- if (contact.deactivated || contact.trustPaused)
560
- return null;
561
- let view = viewOf(p, when);
562
- const s = subOf(contact);
563
- if (s.baseline && sameView(s.baseline, view))
564
- return null;
565
- // überholte In-Flight-Salts (≤ highWater) sind tot: der Empfänger
566
- // MUSS sie ablehnen — purgen, nie wiederverwenden (Review 2, B-7)
567
- for (const [salt, f] of [...s.inFlight]) {
568
- if (BigInt(salt) <= s.highWater) {
569
- for (const d of f.pending.keys())
570
- outboxOf(contact).delete(d);
571
- s.inFlight.delete(salt);
572
- }
573
- }
574
- // KEIN Same-View-Retry (Review 23, B-1): 5.4 wörtlich — „a
575
- // delivery whose chunks are never all acknowledged … simply never
576
- // completes … and the next available delivery contact ships the
577
- // current view under a fresh, higher salt." Die 4.4-Byte-
578
- // Identitäts-Regel ist eine FORMVORSCHRIFT für den Fall eines
579
- // Chunk-Retries, keine Anordnung eines solchen — jeder weitere
580
- // Delivery-Kontakt prägt frisch und höher (Salts sind ausdrücklich
581
- // nicht dicht). So löst der Sender auch ein beim Empfänger
582
- // vergiftetes in-flight-Salt ab, ohne je vom Gift zu wissen; alte
583
- // Ack-Referenzen bleiben bis zur Completion-Purge sicher stehen.
584
- const salt = peekStarSeq(contact); // nur gepeekt (Review 25) — committet synchron mit Outbox + inFlight
585
- const threadId = ent.threadId ?? globalThis.crypto.randomUUID();
586
- // „taking the set snapshot, assigning the salt, and committing are
587
- // one step" (Review 9, B-4): der Bau ist fehlbar und wartet — am
588
- // Commit wird die Sicht SYNCHRON neu verglichen; wanderte sie
589
- // während des Baus (Promotion/Declaration eines ANDEREN Kontakts),
590
- // wird mit der aktuellen Sicht neu gebaut, unter demselben Salt
591
- // (nichts wurde persistiert). Jede Zustellung trägt so den Stand
592
- // ihres Production-Commits.
593
- for (;;) {
594
- const chunks = await buildStarChunks(contact, counterpartAnchor, salt, view);
595
- const preps = [];
596
- for (const chunk of chunks)
597
- preps.push(await prepareDoc(p, counterpartAnchor, 'star', chunk, when, threadId, { salt }, ent));
598
- // Re-Check (Review 9 B-3 / Review 11 B-1), danach kein await:
599
- // eine während des Baus gesetzte Pause supprimiert die Zustellung
600
- if (contact.deactivated || contact.trustPaused)
601
- return null;
602
- const now = viewOf(p, when);
603
- if (!sameView(view, now)) {
604
- view = now;
605
- if (s.baseline && sameView(s.baseline, view))
606
- return null; // konvergiert während des Baus → Schweigen
607
- continue;
608
- }
609
- // COMMIT — ein synchroner Zug: Salt + Outbox + inFlight + Sicht
610
- contact.starSeqNext = salt;
611
- const pending = new Map();
612
- const envs = [];
613
- for (const prep of preps) {
614
- const out = commitDoc(prep);
615
- pending.set(prep.digest, { env: out.env });
616
- envs.push(out.env);
617
- }
618
- s.inFlight.set(salt, { view, pending, threadId });
619
- return { to: contact, kind: 'star/0.1', envs, salt };
620
- }
621
- });
622
- }
623
- // die eigene Grade-ENTSCHEIDUNG trägt einen Ausstellungs-Ordnungsstempel
624
- // (Review 27): beim Chaining gewinnt die ZULETZT AUSGESTELLTE („MUST
625
- // re-issue its most recently issued grade"), nie die des zufällig
626
- // frischeren Kopfs
627
- export const stampGradeOut = (p, contact, grade) => {
628
- contact.gradeOutLast = grade;
629
- contact.gradeOutOrder = (p.gradeIssueSeq = (p.gradeIssueSeq ?? 0n) + 1n);
630
- };
631
- // ── der Vertrauensakt (register no. 3: EIN menschlicher Akt) ────────────
632
- // setTrust stellt aus: anchor-mapping@2 (Offenlegung) + grade-
633
- // declaration@1 'blinded' (die Wahl) — zwei registrierte Dokumente.
634
- // Der Stern folgt über reconcile (5.4: die Promotion der Gegenseite
635
- // erzeugt dort die Divergenz; hier entsteht nur die eigene Sicht).
636
- export async function setTrust(p, counterpartAnchor, when, ent = {}) {
637
- const contact = p.contacts.get(counterpartAnchor);
638
- if (!contact)
639
- return { error: 'kein Kontakt' };
640
- if (!contact.channel?.own)
641
- return { error: 'kein eigener Kanal — einseitig (◇) hast du nichts freigegeben' };
642
- // GANZER Akt unter dem Issue-Lock (Review 7, B-1/B-2): Revisionen
643
- // werden nur gepeekt, alles Fehlbare läuft davor, Zähler + Outbox +
644
- // Zustand committen synchron am Ende — ein Seal-Fehler verbraucht
645
- // NICHTS, und kein Producer schiebt sich zwischen Peek und Commit
646
- return withIssueLock(contact, async () => {
647
- // ein deaktiviertes Tupel stellt nichts aus (Review 8, B-2 —
648
- // §6.4/„a chain append is atomic with the deactivation"): geprüft
649
- // nach dem Lock-Erwerb UND synchron vor dem Commit
650
- if (contact.deactivated)
651
- return { error: 'tuple deactivated — Kopf der Beziehung ist gewandert' };
652
- if (contact.trustGiven)
653
- return { error: 'bereits geschenkt (Einweg-Tür)' };
654
- if (contact.trustPending)
655
- return { error: 'bereits unterwegs (Einweg-Tür)' }; // Latch (Review 6, B-1)
656
- if (!scopeOk(contact.mapRevOut) || !scopeOk(contact.gradeRevOut))
657
- return { error: 'scope exhausted — act suspended (2.1)' };
658
- contact.trustPending = true;
659
- try {
660
- const mapRev = peekRev(contact, 'mapRevOut');
661
- const gradeRev = peekRev(contact, 'gradeRevOut');
662
- const whenIso = C.iso(when);
663
- const mapping = await buildMappingArtifact(p, contact, counterpartAnchor, when, mapRev);
664
- const grade = await buildGradeDecl(p, counterpartAnchor, 'blinded', when, gradeRev);
665
- const prepM = await prepareDoc(p, counterpartAnchor, 'anchor-mapping', mapping, when, uuid(ent.threadId), {}, ent);
666
- const prepG = await prepareDoc(p, counterpartAnchor, 'grade-declaration', grade, when, uuid(), {}, ent);
667
- if (contact.deactivated)
668
- return { error: 'tuple deactivated — Kopf der Beziehung ist gewandert' }; // Re-Check (B-2), danach kein await
669
- // COMMIT — ein synchroner Zug (§6.4: assigning + persisting = one step)
670
- contact.mapRevOut = mapRev;
671
- contact.gradeRevOut = gradeRev;
672
- const outbound = [commitDoc(prepM), commitDoc(prepG)];
673
- contact.trustGiven = whenIso;
674
- stampGradeOut(p, contact, 'blinded'); // die zuletzt ausgestellte Entscheidung (für V2), mit Ordnungsstempel
675
- contact.sentMapping = mapping;
676
- say(p, `Vertrauen geschenkt an ${contact.name}: Anker offengelegt (anchor-mapping@2) + Grade 'blinded' erklärt`);
677
- return { to: contact, outbound, env: outbound[0].env };
678
- }
679
- finally {
680
- contact.trustPending = false;
681
- }
682
- });
683
- }
684
- /**
685
- * setGrade (5.5): die spätere Grade-Entscheidung des Subjekts — auch
686
- * 'count' (die Wahl gehört dem Kontakt; register no. 3 kollabiert sie
687
- * nur im Default-UX). Producer der Declaration, Revision monoton.
688
- */
689
- export async function setGrade(p, counterpartAnchor, grade, when, ent = {}) {
690
- const contact = p.contacts.get(counterpartAnchor);
691
- if (!contact?.channel?.own || contact.deactivated)
692
- return { error: 'kein aktiver Kanal' };
693
- return withIssueLock(contact, async () => {
694
- // Re-Check nach dem Lock-Erwerb (Review 8, B-2): eine Kettung
695
- // während der Wartezeit deaktiviert dieses Tupel — es stellt nichts
696
- // mehr aus, gradeOutLast des neuen Kopfs bleibt die Wahrheit
697
- if (contact.deactivated)
698
- return { error: 'tuple deactivated — Kopf der Beziehung ist gewandert' };
699
- const gradeRev = peekRev(contact, 'gradeRevOut'); // wirft bei Erschöpfung, verbraucht nichts (B-1)
700
- const g = await buildGradeDecl(p, counterpartAnchor, grade, when, gradeRev);
701
- const prep = await prepareDoc(p, counterpartAnchor, 'grade-declaration', g, when, uuid(), {}, ent);
702
- if (contact.deactivated)
703
- return { error: 'tuple deactivated — Kopf der Beziehung ist gewandert' }; // Re-Check (B-2), danach kein await
704
- // COMMIT — ein synchroner Zug (Review 7, B-1)
705
- contact.gradeRevOut = gradeRev;
706
- const out = commitDoc(prep);
707
- stampGradeOut(p, contact, grade);
708
- say(p, `Grade '${grade}' erklärt an ${contact.name}`);
709
- return { to: contact, outbound: [out] };
710
- });
711
- }
712
- // Pause (5.4): initial unpaused, EINZIGE Transition ist dieser Akt;
713
- // Unpause re-evaluiert die Divergenz (Producer: starRefreshAll)
714
- export function setTrustPaused(p, counterpartAnchor, paused) {
715
- const contact = p.contacts.get(counterpartAnchor);
716
- if (!contact?.trustGiven)
717
- return;
718
- contact.trustPaused = !!paused;
719
- say(p, paused ? `Kontakt-Updates an ${contact.name} pausiert (Geliefertes bleibt)` : `Kontakt-Updates an ${contact.name} wieder aktiv`);
720
- }
721
- /**
722
- * V2 (5.5): auf dem neuen Kopf stellt das Subjekt seinen zuletzt
723
- * ausgestellten Grade neu aus — Revision startet im neuen Tupel-Scope,
724
- * keine Nutzerfrage; das Mapping reist als MAY (6a.4) mit. Die Pause
725
- * supprimiert dies NICHT (5.5 kennt keine Pause-Ausnahme — sie
726
- * supprimiert nur die Stern-Delivery); die Pflicht (trustReissueDue)
727
- * stirbt erst mit der QUITTIERTEN Zustellung der Declaration.
728
- */
729
- export async function reissueTrust(p, counterpartAnchor, when, ent = {}) {
730
- const contact = p.contacts.get(counterpartAnchor);
731
- // Gate: eine AUSGESTELLTE Declaration ODER der Trust-Akt trägt die
732
- // Pflicht (Review 33, B-1) — nie nur trustGiven
733
- if (!contact?.channel?.own || contact.deactivated || (!contact.trustGiven && contact.gradeOutLast === undefined))
734
- return null;
735
- // KEINE Pause-Suppression (Review 12, B-4): 5.4 supprimiert die
736
- // DELIVERY-Pflicht (Stern); die Register-5-Neuausstellung läuft
737
- // „on the next available delivery contact" ohne Pause-Ausnahme (5.5)
738
- if (contact.reissuePending)
739
- return null; // Single-Flight
740
- contact.reissuePending = true;
741
- try {
742
- return await withIssueLock(contact, async () => {
743
- if (contact.deactivated)
744
- return null; // Re-Check nach dem Lock-Erwerb (Review 8, B-2)
745
- // unquittierte Neuausstellung → byte-identischer Retry
746
- for (const [digest, o] of outboxOf(contact))
747
- if (o.kind === 'grade-declaration' && o.v2) {
748
- return { to: contact, outbound: [{ to: contact, kind: 'grade-declaration/0.1 (Retry, byte-identisch)', env: o.env }] };
749
- }
750
- // die MUST-Grade hängt NIE am MAY-Mapping (Review 5, B-5): Grade-
751
- // Scope erschöpft → Pflicht suspendiert (2.1, trustReissueDue
752
- // bleibt); Revisionen nur gepeekt, Commit synchron (Review 7, B-1)
753
- if (!scopeOk(contact.gradeRevOut))
754
- return null;
755
- const gradeRev = peekRev(contact, 'gradeRevOut');
756
- const grade = await buildGradeDecl(p, counterpartAnchor, contact.gradeOutLast ?? 'blinded', when, gradeRev);
757
- const prepG = await prepareDoc(p, counterpartAnchor, 'grade-declaration', grade, when, uuid(), { v2: true }, ent);
758
- let prepM, mapping, mapRev;
759
- if (contact.trustGiven && scopeOk(contact.mapRevOut)) { // das MAY-Mapping gehört zum TRUST-Akt (6a.4)
760
- try {
761
- mapRev = peekRev(contact, 'mapRevOut');
762
- mapping = await buildMappingArtifact(p, contact, counterpartAnchor, when, mapRev);
763
- prepM = await prepareDoc(p, counterpartAnchor, 'anchor-mapping', mapping, when, uuid(), {}, ent);
764
- }
765
- catch {
766
- prepM = undefined; /* MAY (6a.4) — ein Mapping-Fehler suspendiert nur das Mapping, verbraucht nichts */
767
- }
768
- }
769
- if (contact.deactivated)
770
- return null; // Re-Check (B-2), danach kein await
771
- // COMMIT — ein synchroner Zug (§6.4)
772
- contact.gradeRevOut = gradeRev;
773
- stampGradeOut(p, contact, grade.body.grade); // die Neuausstellung IST eine Ausstellung (Review 27)
774
- const outbound = [commitDoc(prepG)];
775
- if (prepM) {
776
- contact.mapRevOut = mapRev;
777
- outbound.push(commitDoc(prepM));
778
- contact.sentMapping = mapping;
779
- }
780
- say(p, `Vertrauen auf dem neuen Kopf neu ausgestellt an ${contact.name} (5.5 — ohne neue Nutzerfrage)`);
781
- return { to: contact, outbound };
782
- });
783
- }
784
- finally {
785
- contact.reissuePending = false;
786
- }
787
- }
788
- /**
789
- * Der Producer-Sweep (Host-Vertrag): erst die V2-Pflichten, dann die
790
- * 5.4-Rekonziliation je Empfänger. Idempotent, per-recipient resilient,
791
- * Retries byte-identisch.
792
- */
793
- export async function starRefreshAll(p, when, ent = {}) {
794
- const outbound = [];
795
- const failures = [];
796
- for (const [anchor, contact] of p.contacts) {
797
- if (contact.deactivated)
798
- continue;
799
- try {
800
- // die V2-Pflicht läuft AUCH pausiert (Review 12, B-4) und AUCH
801
- // ohne Trust-Akt (Review 33, B-1: eine ausgestellte Declaration
802
- // genügt); nur die Stern-Delivery braucht trustGiven
803
- if (contact.trustReissueDue) {
804
- const r = await reissueTrust(p, anchor, when, ent);
805
- if (r)
806
- outbound.push(...r.outbound);
807
- }
808
- if (!contact.trustGiven || contact.trustPaused)
809
- continue;
810
- const r = await reconcile(p, anchor, when, ent);
811
- if (r)
812
- for (const env of r.envs)
813
- outbound.push({ to: contact, kind: r.kind, env });
814
- }
815
- catch (e) {
816
- failures.push({ to: contact.name ?? anchor, error: String(e?.message ?? e) });
817
- }
818
- }
819
- return Object.assign(outbound, { failures });
820
- }
821
- // ── Empfang: registrierte Dokumente, Schema-validiert, atomar geackt ────
822
- const asmOf = (contact) => contact.asm;
823
- export async function receiveTrustDoc(p, env, when, ent = {}) {
824
- const opened = await C.openEnvelope(p, env);
825
- if (opened.error)
826
- return { handled: false };
827
- const ctx = p.contexts.get(env.rkid);
828
- const fromEntry = ctx && [...p.contacts.entries()].find(([, c]) => c.channel?.own?.anchor === ctx.anchor);
829
- if (!fromEntry)
830
- return { handled: false };
831
- const [fromKey, from] = fromEntry;
832
- const doc = opened.doc;
833
- if (opened.duplicate) {
834
- // 4.2: innerhalb der Retention wird EXAKT der gespeicherte Ack
835
- // byte-identisch erneut gesendet — nie ein neu erzeugter
836
- const stored = from.ackStore?.get(opened.digest);
837
- // der Store behält sein Exemplar — der Host bekommt eine Kopie (Encounter-Review 8, B-1)
838
- return { handled: true, duplicate: true, ack: stored ? { to: from, kind: 'delivery-ack/0.1 (Re-Ack, byte-identisch)', env: structuredClone(stored) } : undefined };
839
- }
840
- // Acks laufen durch dieselbe kritische Sektion (Delivery §6.2:
841
- // „stage 9 is a critical section whose lock set is the document
842
- // digest … it re-enters at stage 4"): der Verlierer zweier
843
- // gleichzeitiger identischer Acks sieht nach der Lock-Wartezeit den
844
- // completed-effect cache und meldet duplicate-known — nie
845
- // 'ack unknown ref' (Review 13, B-3)
846
- if (doc?.type === ACK_TYPE) {
847
- // deaktivierte Tupel nehmen keine NEUEN Acks für VISIBILITY-Dokumente
848
- // an (Review 37, B-3: „not the active head … MUST be rejected") —
849
- // ein Ack auf ein ENCOUNTER-Dokument bleibt gültig (Delivery 6.1:
850
- // „a late valid acknowledgement … transitions the status to
851
- // delivered"); die Unterscheidung fällt am referenzierten
852
- // Outbox-Eintrag in receiveAckInner (Encounter-Review 2, B-5)
853
- // Stufe 5 VOR dem Payload-Gate auch für Acks (Review 33, B-2), in
854
- // ihrer Ordnung: erst das DOKUMENTPROFIL (malformed), dann recipient =
855
- // eigener Anker (Empfänger-Prinzip) — Encounter-Review 8, B-2
856
- if (!schemaOk('rltp-delivery-document.schema.json', doc))
857
- return { handled: true, error: 'malformed document' };
858
- if (doc.recipient !== from.channel.own.anchor)
859
- return { handled: true, error: 'wrong-recipient' }; // 6.2 stage 5 disposition
860
- return withContactLock(from, async () => {
861
- if ((p.deliveryCache ?? new Set()).has(opened.digest))
862
- return { handled: true, duplicate: true };
863
- return receiveAckInner(p, fromKey, from, opened);
864
- });
865
- }
866
- if (typeof doc?.type !== 'string' || !doc.type.startsWith(TT))
867
- return { handled: false, doc };
868
- const slug = doc.type.slice(TT.length);
869
- if (!['anchor-mapping/0.1', 'grade-declaration/0.1', 'star/0.1'].includes(slug))
870
- return { handled: false, doc };
871
- // Dokumentprofil (Delivery §3): Schema-validiert, proof-frei, Bindungen
872
- if (!schemaOk('rltp-delivery-document.schema.json', doc))
873
- return { handled: true, error: 'malformed document' };
874
- if (doc.proof !== undefined)
875
- return { handled: true, error: 'document proof forbidden (one carrier)' };
876
- // Stufe 5: recipient = eigener Anker (Empfänger-Prinzip). Die
877
- // issuer-Bindung ist eine TYP-Konsistenzregel (Stufe 8) und läuft je
878
- // Zweig NACH dem Payload-Schema (Review 32)
879
- if (doc.recipient !== from.channel.own.anchor)
880
- return { handled: true, error: 'wrong-recipient' }; // 6.2 stage 5 disposition
881
- // Kalender-Validität des DOKUMENTS (Review 12, B-3): „a verifier
882
- // MUST reject a date its RFC 3339 parser refuses" — das Dokument-
883
- // Schema lief bereits; die PAYLOAD-Kalenderprüfung sitzt je Typ NACH
884
- // dem Payload-Schema (Review 31: Stufe 7 vor Stufe 8)
885
- if (!C.calOK(doc.issuedAt))
886
- return { handled: true, error: 'calendar-invalid issuedAt' };
887
- // Ack erst NACH der Payload-Validierung bauen (M-3: keine Krypto
888
- // für formwidrige Fracht), aber VOR jeder Zustandsmutation
889
- // (poisoning rule: ein Ack-Bau-Fehler konsumiert nichts); Effekt +
890
- // Cache + ackStore committen in EINEM synchronen Zug (4.2/§6.2)
891
- const mkAck = () => buildAck(p, from, opened.digest, doc.threadId, when).then((a) => a.env);
892
- const commit = (result, ackEnv) => {
893
- C.effectDone(p, opened.digest);
894
- (from.ackStore ??= new Map()).set(opened.digest, ackEnv);
895
- return { ...result, ack: { to: from, kind: 'delivery-ack/0.1', env: ackEnv } };
896
- };
897
- const r0 = await (async () => {
898
- switch (slug) {
899
- case 'anchor-mapping/0.1': {
900
- return withContactLock(from, async () => {
901
- // Reentry (B-1/B-2): Kopf noch aktiv? Duplikat inzwischen? Revision noch frisch?
902
- if (from.deactivated)
903
- return { handled: true, error: 'tuple deactivated' };
904
- if ((p.deliveryCache ?? new Set()).has(opened.digest)) {
905
- const stored = from.ackStore?.get(opened.digest);
906
- return { handled: true, duplicate: true, ack: stored ? { to: from, kind: 'delivery-ack/0.1 (Re-Ack, byte-identisch)', env: stored } : undefined };
907
- }
908
- const m = doc.payload;
909
- // Stufe 7 zuerst: Payload-Schema → failed(malformed) (Review 30)
910
- if (!schemaOk('visibility-anchor-mapping.schema.json', m))
911
- return { handled: true, error: 'malformed mapping' };
912
- if (doc.issuer !== fromKey)
913
- return { handled: true, error: 'outer binding (issuer, Stufe 8)' };
914
- if (!C.calOK(m.body.issuedAt))
915
- return { handled: true, error: 'calendar-invalid issuedAt' }; // Stufe 8, nach dem Schema (Review 31)
916
- if (doc.ceremony?.enactment !== undefined)
917
- return { handled: true, error: 'ceremony.enactment kann nicht nachrechnen (validation-failed)' }; // Stufe 8 — NACH dem Payload-Schema (Review 30)
918
- // §6.3 in Spec-REIHENFOLGE (Review 19, B-1), Tupel = Ankunftskanal
919
- // (Review 9, B-1: Cross-Tupel-Bindung = Schritt 3 der Liste)
920
- if (!(await verifyMapping(p, m, fromKey))) {
921
- say(p, `Mapping von ${from.name} verifiziert NICHT — verworfen`);
922
- return { handled: true, error: 'mapping' };
923
- }
924
- // 6.4, die GENERISCHE Revisionsregel (Review 11, B-2): höher
925
- // ersetzt · gleich + JCS-identischer Body = idempotenter Repeat
926
- // (No-op-Effekt, geackt — resends bleiben harmlos) · gleich +
927
- // anderer Body = Äquivokation (reject, Zustand bleibt) ·
928
- // niedriger = reject
929
- if (BigInt(m.body.revision) === BigInt(from.mapRevIn ?? '0')) {
930
- if (from.mapping && jcs(m.body) === jcs(from.mapping.body)) {
931
- const ackEnv = await mkAck();
932
- if (from.deactivated)
933
- return { handled: true, error: 'tuple deactivated' };
934
- return commit({ handled: true, idempotent: true }, ackEnv);
935
- }
936
- return { handled: true, error: 'equivocation (gleiche Revision, anderer Body)' };
937
- }
938
- if (BigInt(m.body.revision) < BigInt(from.mapRevIn ?? '0'))
939
- return { handled: true, error: 'mapping revision (niedriger)' };
940
- const ackEnv = await mkAck(); // fehlbar VOR jeder Mutation
941
- // synchroner Re-Check nach dem LETZTEN await (Review 5, B-1): eine
942
- // Kettung unter dem Lock eines ANDEREN Kontakts kann dieses Tupel
943
- // während verify/mkAck deaktiviert haben — danach kein await mehr
944
- if (from.deactivated)
945
- return { handled: true, error: 'tuple deactivated' };
946
- if (BigInt(m.body.revision) <= BigInt(from.mapRevIn ?? '0'))
947
- return { handled: true, error: 'mapping revision (nicht strikt größer)' };
948
- from.mapRevIn = m.body.revision;
949
- from.selfAnchor = m.body.self;
950
- from.mapping = m;
951
- from.trustReceived = doc.issuedAt;
952
- const e = promotionCommit(p, relIdOf(p, fromKey), m.body.self, fromKey, when);
953
- e.admittedAt ??= when; // grade-wait ankert an der Admission (5.4)
954
- // eine VOR dem Mapping verifizierte Declaration steht und wirkt
955
- // bei der Admission sofort (Sektion 2 / 5.4)
956
- if (from.gradeIn && !e.grades?.has(fromKey))
957
- declCommit(p, relIdOf(p, fromKey), from.gradeIn, fromKey, from.gradeInOrder);
958
- say(p, `${from.name} vertraut dir: stabiler Anker geprüft übernommen (nur für dich beweisend)`);
959
- // der Producer-Sweep läuft NACH der Lock-Sektion (Reentranz!)
960
- return commit({ handled: true, disclosed: m.body.self, fromName: from.name, __sweep: true }, ackEnv);
961
- });
962
- }
963
- case 'grade-declaration/0.1': {
964
- return withContactLock(from, async () => {
965
- if (from.deactivated)
966
- return { handled: true, error: 'tuple deactivated' };
967
- if ((p.deliveryCache ?? new Set()).has(opened.digest)) {
968
- const stored = from.ackStore?.get(opened.digest);
969
- return { handled: true, duplicate: true, ack: stored ? { to: from, kind: 'delivery-ack/0.1 (Re-Ack, byte-identisch)', env: stored } : undefined };
970
- }
971
- const g = doc.payload;
972
- if (!schemaOk('visibility-grade-declaration.schema.json', g))
973
- return { handled: true, error: 'malformed grade' };
974
- if (doc.issuer !== fromKey)
975
- return { handled: true, error: 'outer binding (issuer, Stufe 8)' };
976
- if (!C.calOK(g.body.issuedAt))
977
- return { handled: true, error: 'calendar-invalid issuedAt' }; // Stufe 8, nach dem Schema (Review 31)
978
- if (doc.ceremony?.enactment !== undefined)
979
- return { handled: true, error: 'ceremony.enactment kann nicht nachrechnen (validation-failed)' }; // Stufe 8 — NACH dem Payload-Schema (Review 30)
980
- if (g.body.subject !== fromKey || g.body.holder !== from.channel.own.anchor)
981
- return { handled: true, error: 'grade binding' };
982
- const k = await kGrade(from.channel.own, from.channel.counterpartKa, g.body.subject, g.body.holder);
983
- if ((await hmacU(k, jcs(g.body))) !== g.proof.mac)
984
- return { handled: true, error: 'grade mac' };
985
- // 6.4, die GENERISCHE Revisionsregel (Review 11, B-2):
986
- // dreiteilig — siehe Mapping-Pfad
987
- if (BigInt(g.body.revision) === BigInt(from.gradeRevIn ?? '0')) {
988
- if (from.gradeBodyIn && jcs(g.body) === from.gradeBodyIn) {
989
- const ackEnv = await mkAck();
990
- if (from.deactivated)
991
- return { handled: true, error: 'tuple deactivated' };
992
- return commit({ handled: true, idempotent: true }, ackEnv);
993
- }
994
- return { handled: true, error: 'equivocation (gleiche Revision, anderer Body)' };
995
- }
996
- if (BigInt(g.body.revision) < BigInt(from.gradeRevIn ?? '0'))
997
- return { handled: true, error: 'grade revision (niedriger)' };
998
- const ackEnv = await mkAck(); // fehlbar VOR jeder Mutation
999
- if (from.deactivated)
1000
- return { handled: true, error: 'tuple deactivated' }; // Re-Check (B-1), danach kein await
1001
- if (BigInt(g.body.revision) <= BigInt(from.gradeRevIn ?? '0'))
1002
- return { handled: true, error: 'grade revision (nicht strikt größer)' };
1003
- from.gradeRevIn = g.body.revision;
1004
- from.gradeBodyIn = jcs(g.body); // für den Idempotenz-Vergleich (Review 11, B-2)
1005
- from.gradeIn = g.body.grade;
1006
- from.gradeInOrder = (admission(p).seq += 1n); // Verifikations-Commit-Ordnung JETZT (B-2)
1007
- if (from.selfAnchor)
1008
- declCommit(p, relIdOf(p, fromKey), g.body.grade, fromKey, from.gradeInOrder);
1009
- say(p, `${from.name} erklärt Grade '${g.body.grade}'`);
1010
- return commit({ handled: true, grade: g.body.grade, __sweep: true }, ackEnv);
1011
- });
1012
- }
1013
- case 'star/0.1': {
1014
- // Stage-9-Lockmenge in kind: konkurrierende Chunks derselben
1015
- // Assembly serialisieren über den Kontakt-Lock (§4.4)
1016
- return withStarLock(from, async () => {
1017
- if (from.deactivated)
1018
- return { handled: true, error: 'tuple deactivated' }; // §6.4 — auch der Star-Pfad (Review 5, B-1)
1019
- // Stage-4-Reentry nach Lock-Wartezeit (§6.2): ein inzwischen
1020
- // verarbeitetes Duplikat erhält den gespeicherten Ack byte-identisch
1021
- if ((p.deliveryCache ?? new Set()).has(opened.digest)) {
1022
- const stored = from.ackStore?.get(opened.digest);
1023
- return { handled: true, duplicate: true, ack: stored ? { to: from, kind: 'delivery-ack/0.1 (Re-Ack, byte-identisch)', env: stored } : undefined };
1024
- }
1025
- const chunk = doc.payload;
1026
- if (!schemaOk('visibility-star.schema.json', chunk))
1027
- return { handled: true, error: 'malformed star' };
1028
- if (doc.issuer !== fromKey)
1029
- return { handled: true, error: 'outer binding (issuer, Stufe 8)' };
1030
- if (doc.ceremony?.enactment !== undefined)
1031
- return { handled: true, error: 'ceremony.enactment kann nicht nachrechnen (validation-failed)' }; // Stufe 8 — NACH dem Payload-Schema (Review 30)
1032
- const b = chunk.body;
1033
- // Empfänger-Monotonie (5.2): nicht strikt größer als der
1034
- // completed High-Water → verworfen
1035
- if (BigInt(b.salt) <= BigInt(from.starSaltIn ?? '0'))
1036
- return { handled: true, error: 'star replay (nicht strikt größer)' };
1037
- // VERGIFTETER Salt (Review 21): nach einem Assembly-Konflikt
1038
- // wird KEIN weiterer Chunk dieser Lieferung quittiert — sonst
1039
- // kombinieren früh geackte, verlorene Pufferwirkungen mit
1040
- // späteren Acks zu einer falschen Completion („the delivery
1041
- // simply cannot complete until every seq is held")
1042
- if (from.starPoison?.has(b.salt))
1043
- return { handled: true, error: 'star salt vergiftet (assembly conflict)' };
1044
- const k = await kStarIn(from, fromKey, b.salt);
1045
- if ((await hmacU(k, jcs(b))) !== chunk.proof.mac)
1046
- return { handled: true, error: 'star mac' };
1047
- // Re-Check nach den MAC-Awaits (Review 6, B-1): ab hier wird bis
1048
- // zum jeweils nächsten await NUR NOCH LOKAL entschieden — die
1049
- // Assembly ist Tupel-Zustand und darf nach einer Deaktivierung
1050
- // keinen Chunk mehr aufnehmen („dies, atomically, with its
1051
- // tuple's deactivation")
1052
- if (from.deactivated)
1053
- return { handled: true, error: 'tuple deactivated' };
1054
- // 5.2a-Entscheidungen auf LOCALS (Review 6, B-1): höherer Salt
1055
- // verdrängt, niedrigerer wird verworfen, threadId-Dissens
1056
- // verwirft, Lücken puffern — GESCHRIEBEN wird erst nach Ack-Bau
1057
- // und letztem Re-Check, in einem synchronen Zug
1058
- const cur = asmOf(from);
1059
- const displaced = cur && BigInt(b.salt) > BigInt(cur.salt);
1060
- if (cur && !displaced && BigInt(b.salt) < BigInt(cur.salt))
1061
- return { handled: true, error: 'below open assembly' };
1062
- const a = (!cur || displaced) ? { salt: b.salt, threadId: doc.threadId, count: b.count, chunks: new Map(), lastSeq: undefined } : cur;
1063
- if (doc.threadId !== a.threadId || b.count !== a.count) {
1064
- from.asm = undefined;
1065
- (from.starPoison ??= new Set()).add(b.salt);
1066
- return { handled: true, error: 'assembly conflict (threadId/count)' };
1067
- }
1068
- const seq = b.seq;
1069
- if (a.chunks.has(seq)) {
1070
- from.asm = undefined;
1071
- (from.starPoison ??= new Set()).add(b.salt);
1072
- return { handled: true, error: 'assembly conflict (seq)' };
1073
- }
1074
- if (b.last && a.lastSeq !== undefined) {
1075
- from.asm = undefined;
1076
- (from.starPoison ??= new Set()).add(b.salt);
1077
- return { handled: true, error: 'assembly conflict (two last)' };
1078
- }
1079
- const candLast = b.last ? seq : a.lastSeq;
1080
- if (candLast !== undefined && BigInt(seq) > BigInt(candLast)) {
1081
- from.asm = undefined;
1082
- (from.starPoison ??= new Set()).add(b.salt);
1083
- return { handled: true, error: 'seq beyond last' };
1084
- }
1085
- // auch BEREITS GEPUFFERTE Chunks oberhalb von last verwerfen die
1086
- // Assembly (Review 16, B-1): „a seq greater than the held last
1087
- // chunk's … reject" gilt unabhängig von der Ankunftsreihenfolge
1088
- if (candLast !== undefined)
1089
- for (const s of a.chunks.keys())
1090
- if (BigInt(s) > BigInt(candLast)) {
1091
- from.asm = undefined;
1092
- (from.starPoison ??= new Set()).add(b.salt);
1093
- return { handled: true, error: 'seq beyond last (gepuffert)' };
1094
- }
1095
- let ackEnv;
1096
- const complete0 = candLast !== undefined && (() => { for (let i = 1n; i <= BigInt(candLast); i++)
1097
- if (String(i) !== seq && !a.chunks.has(String(i)))
1098
- return false; return true; })();
1099
- if (!complete0) {
1100
- ackEnv = await mkAck();
1101
- if (from.deactivated)
1102
- return { handled: true, error: 'tuple deactivated' }; // Re-Check (B-1), danach kein await
1103
- from.asm = a; // Übernahme JETZT — synchron mit dem Puffern
1104
- if (b.last)
1105
- a.lastSeq = seq;
1106
- a.chunks.set(seq, b);
1107
- return commit({ handled: true, partial: true }, ackEnv);
1108
- }
1109
- // der KOMPLETTIERENDE Chunk: Union aus Puffer + aktuellem Chunk
1110
- // OHNE Mutation berechnen und validieren, dann Ack + Re-Check,
1111
- // dann Übernahme + completed-salt + Cache + Ack in EINEM
1112
- // SYNCHRONEN Zug (§4.4); die UI-Auffrischung läuft NACH dem Commit
1113
- const union = [];
1114
- for (let i = 1n; i <= BigInt(candLast); i++)
1115
- union.push(...(String(i) === seq ? b : a.chunks.get(String(i))).blinded);
1116
- const sorted = [...union].every((x, i, arr) => i === 0 || arr[i - 1] < x);
1117
- if (!sorted || BigInt(union.length) > BigInt(a.count)) {
1118
- from.asm = undefined;
1119
- (from.starPoison ??= new Set()).add(a.salt);
1120
- return { handled: true, error: 'assembly invalid (order/|union|>count)' };
1121
- }
1122
- ackEnv = await mkAck(); // JETZT ist alles validiert (M-2), vor der Übernahme
1123
- if (from.deactivated)
1124
- return { handled: true, error: 'tuple deactivated' }; // Re-Check (B-1), danach kein await
1125
- from.asm = undefined;
1126
- from.starSaltIn = a.salt;
1127
- from.starReceived = { salt: a.salt, count: a.count, blinded: union };
1128
- if (from.starPoison)
1129
- for (const ps of from.starPoison)
1130
- if (BigInt(ps) <= BigInt(a.salt))
1131
- from.starPoison.delete(ps); // Gift unterhalb des High-Water ist gegenstandslos
1132
- const out = commit({ handled: true, star: true }, ackEnv);
1133
- await refreshStarInfo(p);
1134
- say(p, `Kontakt-Update von ${from.name}: ${a.count} Kontakte (geblendet: ${union.length})`);
1135
- return out;
1136
- });
1137
- }
1138
- }
1139
- return { handled: false, doc };
1140
- })();
1141
- // Effekt fertig, Lock frei — jetzt der Producer-Sweep (5.4), dessen
1142
- // reconcile die Empfänger-Locks selbst nimmt
1143
- if (r0?.__sweep) {
1144
- delete r0.__sweep;
1145
- const outbound = await starRefreshAll(p, when, ent);
1146
- r0.outbound = [...outbound];
1147
- if (outbound.failures.length)
1148
- r0.outboundError = outbound.failures.map((f) => `${f.to}: ${f.error}`).join(' · ');
1149
- }
1150
- return r0;
1151
- }
1152
- // completion (5.4-Automat): jeder Chunk-Ack streicht seinen Digest;
1153
- // der letzte lässt den Automaten laufen
1154
- async function receiveAckInner(p, fromKey, from, opened) {
1155
- // der referenzierte Outbox-Eintrag zuerst — seine KLASSE wählt die
1156
- // Proof-Form des Acks (4.2/4.4: Encounter-Payloads tragen übertragbare
1157
- // Signaturen → signierter Ack; Visibility-Payloads sind DV → MAC).
1158
- // Digest-Gleichheit über die dekodierten Multihash-Bytes (Encounter
1159
- // 2.3) — ein `z`-gerenderter ref trifft den `u`-Outbox-Schlüssel
1160
- // Stufe 7 zuerst: das Payload-Schema (malformed) — VOR jeder Klassen-
1161
- // oder Konsistenzregel der Stufe 8 (Encounter-Review 16, B-1)
1162
- if (!schemaOk('payload-delivery-ack.schema.json', opened.doc?.payload))
1163
- return { handled: true, error: 'malformed' };
1164
- const ref0 = opened.doc.payload.ref;
1165
- let refKey, o;
1166
- for (const [k, e] of outboxOf(from))
1167
- if (sameDigest(k, ref0)) {
1168
- refKey = k;
1169
- o = e;
1170
- break;
1171
- }
1172
- if (!o)
1173
- return { handled: true, error: 'ack unknown ref' }; // Stufe 8: ref trifft kein gesendetes Dokument — keine Klasse wählbar
1174
- const ENCOUNTER_KIND = o.kind === 'encounter-bundle' || o.kind === 'encounter-credential-delivery';
1175
- const v = await verifyAck(p, from, opened.doc, ENCOUNTER_KIND ? 'signature' : 'dv');
1176
- if (!v)
1177
- return { handled: true, error: 'ack invalid' };
1178
- if (o.threadId !== v.threadId)
1179
- return { handled: true, error: 'ack unknown ref' };
1180
- if (from.deactivated && !ENCOUNTER_KIND)
1181
- return { handled: true, error: 'tuple deactivated' }; // nach den Awaits (Review 37, B-3), danach kein await
1182
- // ZWEI Pflicht-Ebenen (Review 19, B-3): der Delivery-Ack erledigt den
1183
- // TRANSPORT — die Visibility-Pflicht eines choice/report-Mappings
1184
- // („resent … until answered by the counterpart's aligned mapping")
1185
- // lebt weiter: der Eintrag bleibt für byte-identische Resends, bis
1186
- // die Alignment-Antwort ihn retired. Die ALIGNMENT-Pflicht selbst
1187
- // stirbt mit dem Ack (completed delivery, §6a.3).
1188
- if (o.kind === 'continuity-mapping' && o.duty !== 'align' && !o.retired)
1189
- o.acked = true;
1190
- else
1191
- outboxOf(from).delete(refKey);
1192
- C.effectDone(p, opened.digest);
1193
- if (o.kind === 'star') {
1194
- const s = subOf(from);
1195
- const f = s.inFlight.get(o.salt);
1196
- if (f) {
1197
- f.pending.delete(refKey);
1198
- if (f.pending.size === 0) {
1199
- s.inFlight.delete(o.salt);
1200
- if (BigInt(o.salt) > s.highWater) {
1201
- s.highWater = BigInt(o.salt);
1202
- s.baseline = f.view;
1203
- }
1204
- // Completion räumt alle niedrigeren In-Flights (tot beim Empfänger)
1205
- for (const [salt2, f2] of [...s.inFlight])
1206
- if (BigInt(salt2) < s.highWater) {
1207
- for (const d of f2.pending.keys())
1208
- outboxOf(from).delete(d);
1209
- s.inFlight.delete(salt2);
1210
- }
1211
- return { handled: true, completed: o.salt, advanced: true };
1212
- }
1213
- return { handled: true, partialAck: true };
1214
- }
1215
- }
1216
- if (o.v2)
1217
- delete from.trustReissueDue; // die V2-Pflicht stirbt mit der QUITTIERTEN Zustellung
1218
- if (o.duty === 'align')
1219
- from.contAligned = true; // 6a.3: Alignment-Pflicht stirbt mit der Completion
1220
- // Encounter (Delivery 6.1): „delivered" ist der Sender-Status NUR auf
1221
- // den gültigen Ack — der Host liest diese Marken, nie die Ankunft
1222
- if (o.kind === 'encounter-bundle')
1223
- from.bundleAcked = true;
1224
- if (o.kind === 'encounter-credential-delivery')
1225
- from.counterAcked = true;
1226
- // the host correlates the ack with ITS active encounter (Encounter-Review 11, B-3):
1227
- // the acked document's digest, thread and the counterpart anchor travel along
1228
- return { handled: true, acked: o.kind, ref: refKey, threadId: o.threadId, peerAnchor: fromKey };
1229
- }
1230
- export async function receiveAck(p, env) {
1231
- return receiveTrustDoc(p, env, 0);
1232
- }
1233
- //# sourceMappingURL=trust.js.map