@real-life/trust-protocol 0.1.1 → 0.3.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.
- package/README.md +18 -11
- package/dist/carrier-identity.d.ts +9 -0
- package/dist/carrier-identity.d.ts.map +1 -1
- package/dist/carrier-identity.js +32 -2
- package/dist/carrier-identity.js.map +1 -1
- package/dist/ceremony.d.ts +3 -0
- package/dist/ceremony.d.ts.map +1 -0
- package/dist/ceremony.js +29 -0
- package/dist/ceremony.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/probe/acks.d.ts +27 -0
- package/dist/probe/acks.d.ts.map +1 -0
- package/dist/probe/acks.js +152 -0
- package/dist/probe/acks.js.map +1 -0
- package/dist/probe/continuity.d.ts +3 -82
- package/dist/probe/continuity.d.ts.map +1 -1
- package/dist/probe/continuity.js +636 -147
- package/dist/probe/continuity.js.map +1 -1
- package/dist/probe/encounter.d.ts +26 -0
- package/dist/probe/encounter.d.ts.map +1 -0
- package/dist/probe/encounter.js +938 -0
- package/dist/probe/encounter.js.map +1 -0
- package/dist/probe/trust.d.ts +114 -74
- package/dist/probe/trust.d.ts.map +1 -1
- package/dist/probe/trust.js +1120 -232
- package/dist/probe/trust.js.map +1 -1
- package/dist/probe.d.ts +0 -2
- package/dist/probe.d.ts.map +1 -1
- package/dist/probe.js +8 -7
- package/dist/probe.js.map +1 -1
- package/dist/schemas.js +22 -22
- package/dist/schemas.js.map +1 -1
- package/dist/visibility.d.ts +6 -0
- package/dist/visibility.d.ts.map +1 -0
- package/dist/visibility.js +31 -0
- package/dist/visibility.js.map +1 -0
- package/dist/wire.d.ts +28 -26
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js.map +1 -1
- package/package.json +5 -3
package/dist/probe/trust.js
CHANGED
|
@@ -1,40 +1,48 @@
|
|
|
1
|
-
// trust
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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).
|
|
4
10
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// Ehrlicher Rest: 1-Bit-Orakel über gehaltene Anker + die Zählung.
|
|
23
|
-
import { jcs } from '../core.js';
|
|
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';
|
|
24
28
|
import * as C from './deps.js';
|
|
29
|
+
import { buildAck, verifyAck, ACK_TYPE } from './acks.js';
|
|
25
30
|
const te = new TextEncoder();
|
|
26
31
|
const S = globalThis.crypto.subtle;
|
|
27
32
|
const TT = 'https://real-life.org/trust-tasks/';
|
|
28
33
|
const say = (p, m) => p.log.push(m);
|
|
29
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
|
|
30
40
|
// ── Primitiven ──────────────────────────────────────────────────────────
|
|
31
41
|
export async function hmac(keyBytes, msg) {
|
|
32
42
|
const k = await S.importKey('raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
33
43
|
return C.b64uOf(new Uint8Array(await S.sign('HMAC', k, te.encode(msg))));
|
|
34
44
|
}
|
|
35
|
-
|
|
36
|
-
// gewöhnlicher Gruppen-Kontext über die Genesis der persönlichen Community —
|
|
37
|
-
// nie mehr die zero-input-Recovery-Strings
|
|
45
|
+
export const hmacU = async (keyBytes, msg) => 'u' + await hmac(keyBytes, msg);
|
|
38
46
|
export async function communityContext(p) {
|
|
39
47
|
if (!p.selfCtx)
|
|
40
48
|
p.selfCtx = await C.communityContext(p.rootIkm, p.communityGenesis);
|
|
@@ -44,122 +52,394 @@ export const selfCard = async (p, whenIso) => {
|
|
|
44
52
|
const s = await communityContext(p);
|
|
45
53
|
return C.signCard(s, C.cardBody(s, { name: p.name }), whenIso);
|
|
46
54
|
};
|
|
47
|
-
//
|
|
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
|
+
};
|
|
48
79
|
const chShared = (contact) => C.ecdh(contact.channel.own.x.priv, C.xRawOfMk(contact.channel.counterpartKa));
|
|
49
80
|
const relKey = async (contact, info) => C.hkdf(await chShared(contact), info);
|
|
50
|
-
// ── anchor-mapping@2
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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) {
|
|
57
91
|
const s = await communityContext(p);
|
|
58
92
|
const whenIso = C.iso(when);
|
|
59
|
-
const card = await
|
|
60
|
-
const body =
|
|
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 };
|
|
61
95
|
const msg = jcs(body);
|
|
62
96
|
const theirKa = C.xRawOfMk(contact.channel.counterpartKa);
|
|
63
|
-
return { body,
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
});
|
|
66
119
|
}
|
|
67
|
-
// Verifikation ist EMPFÄNGER-PRIVAT
|
|
68
|
-
//
|
|
69
|
-
|
|
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) {
|
|
70
128
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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)
|
|
76
137
|
return false;
|
|
77
138
|
if (b.to !== entry.channel.own.anchor)
|
|
78
|
-
return false;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
82
146
|
const msg = jcs(b);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if ((await
|
|
87
|
-
return false;
|
|
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)
|
|
88
152
|
return true;
|
|
89
153
|
}
|
|
90
154
|
catch {
|
|
91
155
|
return false;
|
|
92
156
|
}
|
|
93
157
|
}
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
// er hält — es verifiziert identisch. Genau darum überzeugt ein
|
|
97
|
-
// geleaktes Mapping niemanden.
|
|
158
|
+
// Abstreitbarkeits-Demo: der Empfänger fabriziert ein identisch
|
|
159
|
+
// verifizierendes Mapping — ein Leak beweist Dritten nichts (Klasse V)
|
|
98
160
|
export async function forgeMapping(forger, victimCard, pairAnchor, when) {
|
|
99
161
|
const entry = forger.contacts.get(pairAnchor);
|
|
100
162
|
const whenIso = C.iso(when);
|
|
101
|
-
const body =
|
|
163
|
+
const body = { type: 'anchor-mapping@2', pair: pairAnchor, self: victimCard.body.anchor, to: entry.channel.own.anchor, card: victimCard, revision: '1', issuedAt: whenIso };
|
|
102
164
|
const msg = jcs(body);
|
|
103
|
-
const k2 = await C.hkdf(await C.ecdh(entry.channel.own.x.priv, C.xRawOfMk(victimCard.keyAgreement)), 'rltp/
|
|
104
|
-
return { body,
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
} };
|
|
107
170
|
}
|
|
108
|
-
// ──
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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;
|
|
116
187
|
};
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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;
|
|
123
231
|
}
|
|
124
232
|
};
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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;
|
|
132
351
|
};
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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))
|
|
141
376
|
continue;
|
|
142
|
-
|
|
143
|
-
|
|
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' });
|
|
144
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));
|
|
145
403
|
blinded.sort();
|
|
146
|
-
|
|
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;
|
|
147
412
|
}
|
|
148
|
-
export function buildStar(p, contact) {
|
|
413
|
+
export async function buildStar(p, contact, when) {
|
|
414
|
+
if (contact.deactivated)
|
|
415
|
+
return null; // deaktivierte Tupel stellen nichts aus (Review 9, B-3)
|
|
149
416
|
return withStarLock(contact, async () => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
154
435
|
});
|
|
155
436
|
}
|
|
156
437
|
// Empfängerseite: denselben Lieferschlüssel nachrechnen, EINEN Anker testen
|
|
157
438
|
export async function starMatch(p, entry, snap, anchor) {
|
|
158
|
-
const
|
|
159
|
-
|
|
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));
|
|
160
442
|
}
|
|
161
|
-
// Cache für die synchrone UI: pro Kontakt { count, knownNames } aus dem
|
|
162
|
-
// empfangenen Schnappschuss gegen die EIGENEN gehaltenen Self-Anker.
|
|
163
443
|
export async function refreshStarInfo(p) {
|
|
164
444
|
for (const [key, entry] of p.contacts) {
|
|
165
445
|
if (!entry.starReceived || !entry.channel?.own)
|
|
@@ -173,60 +453,264 @@ export async function refreshStarInfo(p) {
|
|
|
173
453
|
if (await starMatch(p, entry, entry.starReceived, e.selfAnchor))
|
|
174
454
|
known.push(e.name);
|
|
175
455
|
}
|
|
176
|
-
entry.starInfo = { count: entry.starReceived.count, knownNames: known.sort() };
|
|
456
|
+
entry.starInfo = { count: entry.starReceived.count, knownNames: known.sort() }; // count bleibt String — 18 Stellen exakt
|
|
177
457
|
}
|
|
178
458
|
}
|
|
179
|
-
// ──
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
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).
|
|
186
636
|
export async function setTrust(p, counterpartAnchor, when, ent = {}) {
|
|
187
637
|
const contact = p.contacts.get(counterpartAnchor);
|
|
188
638
|
if (!contact)
|
|
189
639
|
return { error: 'kein Kontakt' };
|
|
190
640
|
if (!contact.channel?.own)
|
|
191
641
|
return { error: 'kein eigener Kanal — einseitig (◇) hast du nichts freigegeben' };
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
};
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
+
});
|
|
229
711
|
}
|
|
712
|
+
// Pause (5.4): initial unpaused, EINZIGE Transition ist dieser Akt;
|
|
713
|
+
// Unpause re-evaluiert die Divergenz (Producer: starRefreshAll)
|
|
230
714
|
export function setTrustPaused(p, counterpartAnchor, paused) {
|
|
231
715
|
const contact = p.contacts.get(counterpartAnchor);
|
|
232
716
|
if (!contact?.trustGiven)
|
|
@@ -234,39 +718,99 @@ export function setTrustPaused(p, counterpartAnchor, paused) {
|
|
|
234
718
|
contact.trustPaused = !!paused;
|
|
235
719
|
say(p, paused ? `Kontakt-Updates an ${contact.name} pausiert (Geliefertes bleibt)` : `Kontakt-Updates an ${contact.name} wieder aktiv`);
|
|
236
720
|
}
|
|
237
|
-
// Bestand geändert (neuer Self-Anker gehalten) → Stern überall auffrischen,
|
|
238
|
-
// wo ich vertraue und nicht pausiert habe. Ohne das frieren Sterne in der
|
|
239
|
-
// Toggle-Reihenfolge ein (Antons Erstlauf-Befund im Graph-Simulator).
|
|
240
721
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
|
|
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.
|
|
248
792
|
*/
|
|
249
793
|
export async function starRefreshAll(p, when, ent = {}) {
|
|
250
794
|
const outbound = [];
|
|
251
795
|
const failures = [];
|
|
252
796
|
for (const [anchor, contact] of p.contacts) {
|
|
253
|
-
if (
|
|
797
|
+
if (contact.deactivated)
|
|
254
798
|
continue;
|
|
255
799
|
try {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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 });
|
|
270
814
|
}
|
|
271
815
|
catch (e) {
|
|
272
816
|
failures.push({ to: contact.name ?? anchor, error: String(e?.message ?? e) });
|
|
@@ -274,72 +818,416 @@ export async function starRefreshAll(p, when, ent = {}) {
|
|
|
274
818
|
}
|
|
275
819
|
return Object.assign(outbound, { failures });
|
|
276
820
|
}
|
|
277
|
-
// ──
|
|
821
|
+
// ── Empfang: registrierte Dokumente, Schema-validiert, atomar geackt ────
|
|
822
|
+
const asmOf = (contact) => contact.asm;
|
|
278
823
|
export async function receiveTrustDoc(p, env, when, ent = {}) {
|
|
279
|
-
const opened = await C.openEnvelope(p, env);
|
|
280
|
-
if (opened.duplicate)
|
|
281
|
-
return { handled: true, duplicate: true };
|
|
824
|
+
const opened = await C.openEnvelope(p, env);
|
|
282
825
|
if (opened.error)
|
|
283
826
|
return { handled: false };
|
|
284
|
-
const r = await receiveTrustDocInner(p, env, opened.doc, when, ent);
|
|
285
|
-
if (r?.handled && !r.error)
|
|
286
|
-
C.effectDone(p, opened.digest);
|
|
287
|
-
return r;
|
|
288
|
-
}
|
|
289
|
-
async function receiveTrustDocInner(p, env, doc, when, ent = {}) {
|
|
290
827
|
const ctx = p.contexts.get(env.rkid);
|
|
291
|
-
|
|
292
|
-
return { handled: false };
|
|
293
|
-
if (typeof doc?.type !== 'string' || !doc.type.startsWith(TT + 'trust-'))
|
|
294
|
-
return { handled: false, doc };
|
|
295
|
-
// form BEFORE fields (M-3): issuer, proof and payload as used below
|
|
296
|
-
// form BEFORE fields (M-3, verschärft in Runde 3): jeder Typ verlangt
|
|
297
|
-
// SEIN Payload — star@probe ohne star ist formwidrig, nicht ein Throw
|
|
298
|
-
const kind = doc.type.slice((TT + 'trust-').length);
|
|
299
|
-
const starOk = C.shaped(doc.payload ?? {}, { star: 'object' }) && C.shaped(doc.payload.star, { salt: 'string', count: 'number', blinded: 'array' })
|
|
300
|
-
&& C.intStr(doc.payload.star.salt) && Number.isInteger(doc.payload.star.count) && doc.payload.star.count >= 0
|
|
301
|
-
&& doc.payload.star.count === doc.payload.star.blinded.length
|
|
302
|
-
&& doc.payload.star.blinded.every((b) => typeof b === 'string');
|
|
303
|
-
const mappingOk = C.shaped(doc.payload ?? {}, { mapping: 'object' }) && C.shaped(doc.payload.mapping, { mac1: 'string', mac2: 'string', card: 'object' });
|
|
304
|
-
if (!C.shaped(doc, { issuer: 'string', proof: 'object', payload: 'object' })
|
|
305
|
-
|| (kind === 'disclosure@probe' && !(mappingOk && starOk))
|
|
306
|
-
|| (kind === 'star@probe' && !starOk))
|
|
307
|
-
return { handled: true, error: 'malformed' };
|
|
308
|
-
const fromEntry = [...p.contacts.entries()].find(([, c]) => c.channel?.own?.anchor === ctx.anchor);
|
|
828
|
+
const fromEntry = ctx && [...p.contacts.entries()].find(([, c]) => c.channel?.own?.anchor === ctx.anchor);
|
|
309
829
|
if (!fromEntry)
|
|
310
|
-
return { handled:
|
|
830
|
+
return { handled: false };
|
|
311
831
|
const [fromKey, from] = fromEntry;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
});
|
|
320
1012
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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;
|
|
335
1171
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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 };
|
|
341
1214
|
}
|
|
342
1215
|
}
|
|
343
|
-
|
|
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);
|
|
344
1232
|
}
|
|
345
1233
|
//# sourceMappingURL=trust.js.map
|