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