@real-life/trust-protocol 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +187 -78
- package/dist/ack.d.ts +84 -0
- package/dist/ack.d.ts.map +1 -0
- package/dist/ack.js +114 -0
- package/dist/ack.js.map +1 -0
- package/dist/adapters/vti-mediator.d.ts +154 -0
- package/dist/adapters/vti-mediator.d.ts.map +1 -0
- package/dist/adapters/vti-mediator.js +621 -0
- package/dist/adapters/vti-mediator.js.map +1 -0
- package/dist/anchor.d.ts +226 -0
- package/dist/anchor.d.ts.map +1 -0
- package/dist/anchor.js +347 -0
- package/dist/anchor.js.map +1 -0
- package/dist/{holder.d.ts → carrier-holder.d.ts} +1 -1
- package/dist/carrier-holder.d.ts.map +1 -0
- package/dist/{holder.js → carrier-holder.js} +5 -4
- package/dist/carrier-holder.js.map +1 -0
- package/dist/carrier-identity.js +4 -4
- package/dist/carrier-identity.js.map +1 -1
- package/dist/carrier.d.ts.map +1 -1
- package/dist/carrier.js +4 -3
- package/dist/carrier.js.map +1 -1
- package/dist/ceremony.d.ts +3 -2
- package/dist/ceremony.d.ts.map +1 -1
- package/dist/ceremony.js +16 -26
- package/dist/ceremony.js.map +1 -1
- package/dist/codec.d.ts +46 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +410 -0
- package/dist/codec.js.map +1 -0
- package/dist/continuity.d.ts +67 -0
- package/dist/continuity.d.ts.map +1 -0
- package/dist/continuity.js +110 -0
- package/dist/continuity.js.map +1 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js.map +1 -1
- package/dist/crypto.d.ts +23 -7
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +54 -20
- package/dist/crypto.js.map +1 -1
- package/dist/delivery.d.ts +31 -3
- package/dist/delivery.d.ts.map +1 -1
- package/dist/delivery.js +75 -28
- package/dist/delivery.js.map +1 -1
- package/dist/encounter/ceremony.d.ts +37 -0
- package/dist/encounter/ceremony.d.ts.map +1 -0
- package/dist/encounter/ceremony.js +662 -0
- package/dist/encounter/ceremony.js.map +1 -0
- package/dist/encounter.d.ts +91 -1
- package/dist/encounter.d.ts.map +1 -1
- package/dist/encounter.js +191 -12
- package/dist/encounter.js.map +1 -1
- package/dist/engine.d.ts +291 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1395 -0
- package/dist/engine.js.map +1 -0
- package/dist/entropy.d.ts +28 -0
- package/dist/entropy.d.ts.map +1 -0
- package/dist/entropy.js +96 -0
- package/dist/entropy.js.map +1 -0
- package/dist/events.d.ts +177 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -0
- package/dist/fixtures.d.ts +30 -0
- package/dist/fixtures.d.ts.map +1 -0
- package/dist/fixtures.js +155 -0
- package/dist/fixtures.js.map +1 -0
- package/dist/forge.d.ts +47 -0
- package/dist/forge.d.ts.map +1 -0
- package/dist/forge.js +20 -0
- package/dist/forge.js.map +1 -0
- package/dist/group-star.d.ts +251 -0
- package/dist/group-star.d.ts.map +1 -0
- package/dist/group-star.js +348 -0
- package/dist/group-star.js.map +1 -0
- package/dist/holder-api.d.ts +137 -0
- package/dist/holder-api.d.ts.map +1 -0
- package/dist/holder-api.js +132 -0
- package/dist/holder-api.js.map +1 -0
- package/dist/holder-state-v1.d.ts +801 -0
- package/dist/holder-state-v1.d.ts.map +1 -0
- package/dist/holder-state-v1.js +2 -0
- package/dist/holder-state-v1.js.map +1 -0
- package/dist/identity-state.d.ts +43 -0
- package/dist/identity-state.d.ts.map +1 -0
- package/dist/identity-state.js +345 -0
- package/dist/identity-state.js.map +1 -0
- package/dist/identity.d.ts +24 -2
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +70 -11
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +77 -29
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +36 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +22 -0
- package/dist/internal.js.map +1 -0
- package/dist/introduction/holder.d.ts +18 -0
- package/dist/introduction/holder.d.ts.map +1 -0
- package/dist/introduction/holder.js +757 -0
- package/dist/introduction/holder.js.map +1 -0
- package/dist/introduction.d.ts +101 -0
- package/dist/introduction.d.ts.map +1 -0
- package/dist/introduction.js +132 -0
- package/dist/introduction.js.map +1 -0
- package/dist/invariants.d.ts +43 -0
- package/dist/invariants.d.ts.map +1 -0
- package/dist/invariants.js +883 -0
- package/dist/invariants.js.map +1 -0
- package/dist/keys-seed.d.ts +7 -0
- package/dist/keys-seed.d.ts.map +1 -0
- package/dist/keys-seed.js +133 -0
- package/dist/keys-seed.js.map +1 -0
- package/dist/log-memory.d.ts +12 -0
- package/dist/log-memory.d.ts.map +1 -0
- package/dist/log-memory.js +101 -0
- package/dist/log-memory.js.map +1 -0
- package/dist/membership/holder.d.ts +35 -0
- package/dist/membership/holder.d.ts.map +1 -0
- package/dist/membership/holder.js +852 -0
- package/dist/membership/holder.js.map +1 -0
- package/dist/membership.d.ts +92 -0
- package/dist/membership.d.ts.map +1 -0
- package/dist/membership.js +157 -0
- package/dist/membership.js.map +1 -0
- package/dist/ports.d.ts +237 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +2 -0
- package/dist/ports.js.map +1 -0
- package/dist/profile.d.ts +56 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +193 -0
- package/dist/profile.js.map +1 -0
- package/dist/prompts.d.ts +14 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +69 -0
- package/dist/prompts.js.map +1 -0
- package/dist/receive.d.ts +133 -0
- package/dist/receive.d.ts.map +1 -0
- package/dist/receive.js +298 -0
- package/dist/receive.js.map +1 -0
- package/dist/results.d.ts +158 -0
- package/dist/results.d.ts.map +1 -0
- package/dist/results.js +21 -0
- package/dist/results.js.map +1 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +42 -38
- package/dist/schemas.js.map +1 -1
- package/dist/sha256.d.ts +3 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +69 -0
- package/dist/sha256.js.map +1 -0
- package/dist/star.d.ts +31 -0
- package/dist/star.d.ts.map +1 -0
- package/dist/star.js +56 -0
- package/dist/star.js.map +1 -0
- package/dist/state-builder.d.ts +42 -0
- package/dist/state-builder.d.ts.map +1 -0
- package/dist/state-builder.js +100 -0
- package/dist/state-builder.js.map +1 -0
- package/dist/state.d.ts +853 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +36 -0
- package/dist/state.js.map +1 -0
- package/dist/store-contract.d.ts +11 -0
- package/dist/store-contract.d.ts.map +1 -0
- package/dist/store-contract.js +189 -0
- package/dist/store-contract.js.map +1 -0
- package/dist/store-memory.d.ts +8 -0
- package/dist/store-memory.d.ts.map +1 -0
- package/dist/store-memory.js +17 -0
- package/dist/store-memory.js.map +1 -0
- package/dist/testing.d.ts +22 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +21 -0
- package/dist/testing.js.map +1 -0
- package/dist/trust/continuity.d.ts +71 -0
- package/dist/trust/continuity.d.ts.map +1 -0
- package/dist/trust/continuity.js +1216 -0
- package/dist/trust/continuity.js.map +1 -0
- package/dist/trust/duties.d.ts +67 -0
- package/dist/trust/duties.d.ts.map +1 -0
- package/dist/trust/duties.js +179 -0
- package/dist/trust/duties.js.map +1 -0
- package/dist/trust/entries.d.ts +139 -0
- package/dist/trust/entries.d.ts.map +1 -0
- package/dist/trust/entries.js +694 -0
- package/dist/trust/entries.js.map +1 -0
- package/dist/trust/group-star.d.ts +43 -0
- package/dist/trust/group-star.d.ts.map +1 -0
- package/dist/trust/group-star.js +188 -0
- package/dist/trust/group-star.js.map +1 -0
- package/dist/trust/mapping.d.ts +56 -0
- package/dist/trust/mapping.d.ts.map +1 -0
- package/dist/trust/mapping.js +584 -0
- package/dist/trust/mapping.js.map +1 -0
- package/dist/trust/profile.d.ts +25 -0
- package/dist/trust/profile.d.ts.map +1 -0
- package/dist/trust/profile.js +249 -0
- package/dist/trust/profile.js.map +1 -0
- package/dist/trust/star.d.ts +208 -0
- package/dist/trust/star.d.ts.map +1 -0
- package/dist/trust/star.js +588 -0
- package/dist/trust/star.js.map +1 -0
- package/dist/trust/state.d.ts +83 -0
- package/dist/trust/state.d.ts.map +1 -0
- package/dist/trust/state.js +154 -0
- package/dist/trust/state.js.map +1 -0
- package/dist/util-freeze.d.ts +3 -0
- package/dist/util-freeze.d.ts.map +1 -0
- package/dist/util-freeze.js +11 -0
- package/dist/util-freeze.js.map +1 -0
- package/dist/views.d.ts +147 -0
- package/dist/views.d.ts.map +1 -0
- package/dist/views.js +268 -0
- package/dist/views.js.map +1 -0
- package/dist/visibility.d.ts +5 -5
- package/dist/visibility.d.ts.map +1 -1
- package/dist/visibility.js +22 -28
- package/dist/visibility.js.map +1 -1
- package/dist/wire.d.ts +178 -84
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js.map +1 -1
- package/package.json +56 -10
- package/schemas/holder-state-v1.schema.json +3880 -0
- package/dist/holder.d.ts.map +0 -1
- package/dist/holder.js.map +0 -1
- package/dist/probe/acks.d.ts +0 -27
- package/dist/probe/acks.d.ts.map +0 -1
- package/dist/probe/acks.js +0 -152
- package/dist/probe/acks.js.map +0 -1
- package/dist/probe/continuity.d.ts +0 -20
- package/dist/probe/continuity.d.ts.map +0 -1
- package/dist/probe/continuity.js +0 -805
- package/dist/probe/continuity.js.map +0 -1
- package/dist/probe/deps.d.ts +0 -61
- package/dist/probe/deps.d.ts.map +0 -1
- package/dist/probe/deps.js +0 -54
- package/dist/probe/deps.js.map +0 -1
- package/dist/probe/encounter.d.ts +0 -26
- package/dist/probe/encounter.d.ts.map +0 -1
- package/dist/probe/encounter.js +0 -938
- package/dist/probe/encounter.js.map +0 -1
- package/dist/probe/introduce.d.ts +0 -62
- package/dist/probe/introduce.d.ts.map +0 -1
- package/dist/probe/introduce.js +0 -306
- package/dist/probe/introduce.js.map +0 -1
- package/dist/probe/membership.d.ts +0 -96
- package/dist/probe/membership.d.ts.map +0 -1
- package/dist/probe/membership.js +0 -467
- package/dist/probe/membership.js.map +0 -1
- package/dist/probe/trust.d.ts +0 -156
- package/dist/probe/trust.d.ts.map +0 -1
- package/dist/probe/trust.js +0 -1233
- package/dist/probe/trust.js.map +0 -1
- package/dist/probe.d.ts +0 -7
- package/dist/probe.d.ts.map +0 -1
- package/dist/probe.js +0 -42
- package/dist/probe.js.map +0 -1
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
// trust/star — the star on the holder (lib Stage 2, PR 4b): star@2 as the
|
|
2
|
+
// registered task star/0.2 (5.1–5.2a), reconciled per recipient (5.4),
|
|
3
|
+
// and the machinery the group star (trust/group-star.ts) shares with it:
|
|
4
|
+
// the sender's salt scope, baseline automaton and in-flight deliveries,
|
|
5
|
+
// and the receiver's assembly per (tuple, salt), on HolderState.
|
|
6
|
+
//
|
|
7
|
+
// Rule numbers: bare four-digit numbers are RLTP-TRU (trust-layer.md).
|
|
8
|
+
//
|
|
9
|
+
// What it registers (through trustModule, trust/mapping.ts):
|
|
10
|
+
// sweep trust.star resume's reconciliation: per contact and live
|
|
11
|
+
// relationship, the view against the baseline;
|
|
12
|
+
// where they differ, the class is on and the scope
|
|
13
|
+
// can issue, one production under the next salt
|
|
14
|
+
// task star/0.2 (proof absent) reception: assembly, completion,
|
|
15
|
+
// the accepted snapshot
|
|
16
|
+
// ack star/0.2 completion of a salt by the verified acks of
|
|
17
|
+
// every chunk (5660)
|
|
18
|
+
//
|
|
19
|
+
// The sender (5.4). The view (5610/5620) is the set of (member, anchor
|
|
20
|
+
// head) over the admitted entries, member = the entry id, sorted by
|
|
21
|
+
// member; every recipient gets the same view. A production takes the
|
|
22
|
+
// snapshot, assigns lastSalt + 1, builds the chunks blinded under the
|
|
23
|
+
// tuple's pair secret (KeyProvider.agree on the tuple's rkid), stamps one
|
|
24
|
+
// fresh threadId on every chunk of the salt (entropy thread.id), and
|
|
25
|
+
// commits `lastSalt`, `inflight[salt] = {view, viewDigest, digests, acked: []}` and
|
|
26
|
+
// the outbox items in one transition (5090, 5110); the outbound leaves
|
|
27
|
+
// only after that commit. Chunk retries are the outbox's, byte-identical
|
|
28
|
+
// down to the envelope and the threadId (Delivery 4.2). Completion: the
|
|
29
|
+
// verified ack of a chunk marks it acked in its inflight entry; the last
|
|
30
|
+
// one runs the baseline automaton (5670): s > H sets H = s and S = the
|
|
31
|
+
// view sent under s, s ≤ H changes nothing; the salt and every in-flight
|
|
32
|
+
// salt at or below the new H leave `inflight`. No rollback (5680).
|
|
33
|
+
//
|
|
34
|
+
// Choices this file makes where note and spec leave them open:
|
|
35
|
+
// - an available delivery contact (5690) is a resume. A divergence is
|
|
36
|
+
// delivered once per view: no new salt while an in-flight salt above
|
|
37
|
+
// H carries exactly the current view and none of its chunks has
|
|
38
|
+
// failed (the outbox retries it byte-identical). A view that changes
|
|
39
|
+
// again, or a delivery the outbox gives up (failed, its horizon or the
|
|
40
|
+
// adapter's verdict) leaves the divergence standing, and the next
|
|
41
|
+
// resume ships the current view under a fresh, higher salt (5680)
|
|
42
|
+
// - completion counts verified delivery-acks only, never an adapter's
|
|
43
|
+
// `delivered` report (ports.ts DeliveryReport, 5660)
|
|
44
|
+
// - superseded salts are not withdrawn from the outbox: their retries
|
|
45
|
+
// run to the horizon like any item (no trias state says "superseded"),
|
|
46
|
+
// and the receiver refuses them (5150) or answers a cached duplicate
|
|
47
|
+
// - needing delivery and completing are kept apart: a delivery with a
|
|
48
|
+
// failed chunk no longer counts as in flight (the next resume issues a
|
|
49
|
+
// fresh salt), but its inflight entry (view, digests, acked) stays as
|
|
50
|
+
// long as any of its outbox items exists, so a late ack still completes
|
|
51
|
+
// it (5660; the automaton then advances on s > H only, 5670). It leaves
|
|
52
|
+
// when the last of its items has left the outbox (tick's key-retention):
|
|
53
|
+
// at the tuple's next production and by tick (retainInflight)
|
|
54
|
+
// - the star goes on every live relationship of the contact (2820), the
|
|
55
|
+
// active-head tuple of each, lagging ones included (6595); a contact
|
|
56
|
+
// with no entry yet is a recipient like any other (2760: every contact)
|
|
57
|
+
// - the scope cannot issue once `exhausted` (set by the commit that
|
|
58
|
+
// issues 10¹⁸−1, 2600) and nothing is produced; state loss (5100) is
|
|
59
|
+
// not modelled: a restored state carries its lastSalt
|
|
60
|
+
// - a production whose key agreement is unavailable (the counterpart key
|
|
61
|
+
// invalid or the own key not released) is skipped silently and stays
|
|
62
|
+
// pending
|
|
63
|
+
//
|
|
64
|
+
// The receiver (5.2a). One open assembly per tuple and artifact
|
|
65
|
+
// (Tuple.starIn.assembly); its chunks are kept as {digest, threadId,
|
|
66
|
+
// body}. The checks, in order: the arrival tuple is a live active head
|
|
67
|
+
// (5360); salt above the completed high-water mark (5150) and above the
|
|
68
|
+
// lower bound (5366); not below the open assembly (5340); the MAC under
|
|
69
|
+
// the salt's key (5080); then against the assembly: a higher salt
|
|
70
|
+
// displaces it (5330); seq above the resource limit discards (5365);
|
|
71
|
+
// another threadId (5280), a repeated seq of other bytes (5260), a second
|
|
72
|
+
// last (5270), a seq beyond last (5290) reject the delivery; the same
|
|
73
|
+
// digest again is a repeat, acked, no change (5250). On completion the
|
|
74
|
+
// union must be strictly ascending (5300, 5310). A rejection (the chunk is
|
|
75
|
+
// authenticated: its MAC verified) and a resource-limit discard (5365)
|
|
76
|
+
// both commit as receive.ts TaskDiscarded, disposition `discarded` with
|
|
77
|
+
// reason `rejected` or `resource-limit`: the assembly is dropped and its
|
|
78
|
+
// salt becomes the lower bound, so no later chunk of it is acked and the
|
|
79
|
+
// delivery never completes (5366); the completed hwm stays. Checks before
|
|
80
|
+
// the MAC (salt rules) and a failed MAC are plain refusals that touch
|
|
81
|
+
// nothing, so an unauthenticated chunk can discard no assembly. Every other chunk before completion is acked on its
|
|
82
|
+
// own (Delivery: every chunk is acked); only the completing chunk
|
|
83
|
+
// changes the accepted snapshot, advances hwm (5140, 5350) and announces
|
|
84
|
+
// star.accepted, all in its ack's transaction.
|
|
85
|
+
//
|
|
86
|
+
// Choices on the receiver side:
|
|
87
|
+
// - the resource limit is a bound on seq: an open assembly holds at most
|
|
88
|
+
// ASSEMBLY_LIMIT.star = 1024 chunks (≤ 1 048 576 entries); a chunk
|
|
89
|
+
// with a higher seq discards the assembly (5365, 5366)
|
|
90
|
+
// - `accepted` keeps the whole assembled union with its salt and the
|
|
91
|
+
// matches (6160): the entry ids of the holder's other contacts one of
|
|
92
|
+
// whose held anchors (the entry's anchor head, every relationship's
|
|
93
|
+
// held head and both ends of every held lineage link, 6390) blinds to
|
|
94
|
+
// a value in it. When the holder comes to hold new anchors for a
|
|
95
|
+
// contact (the anchor mapping's acceptance: entry commit, advanced
|
|
96
|
+
// head, new links), the mapping transition re-matches every accepted
|
|
97
|
+
// star against them (starBlinds in its preparation, rematchStars in
|
|
98
|
+
// its apply). Matches only grow: held anchors are never dropped
|
|
99
|
+
import { iso, jcs } from '../core.js';
|
|
100
|
+
import { digestDoc, xRawOfMk } from '../crypto.js';
|
|
101
|
+
import { seal } from '../delivery.js';
|
|
102
|
+
import { PLAINTEXT_MAX, blindAnchor, buildStarChunks, starChunkMacOk, starChunkOk, starKeyOf, strictlyAscending } from '../star.js';
|
|
103
|
+
import { tupleByRkid } from '../receive.js';
|
|
104
|
+
import { sha256Hex } from '../sha256.js';
|
|
105
|
+
import { triasOf } from '../state.js';
|
|
106
|
+
import { ADMISSION_BOUND, activeTuple, classOnToward, entryOf, entryRelationships, findContact } from './state.js';
|
|
107
|
+
/** The registered task type that carries star@2 (Delivery §4.4, 5640). */
|
|
108
|
+
export const STAR_TYPE = 'https://real-life.org/trust-tasks/star/0.2';
|
|
109
|
+
/** The receiver's resource limit (5365): the highest seq an open assembly
|
|
110
|
+
* takes, per artifact. */
|
|
111
|
+
export const ASSEMBLY_LIMIT = Object.freeze({ star: 1024, groupStar: 64, probe: 64 });
|
|
112
|
+
const te = new TextEncoder();
|
|
113
|
+
const inc = (d) => (BigInt(d) + 1n).toString();
|
|
114
|
+
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
115
|
+
const refuse = (reason, ref) => ({ ok: false, reason, ...(ref ? { ref } : {}) });
|
|
116
|
+
const bodyOf = (chunk) => chunk.body;
|
|
117
|
+
/** Every contact's live relationships, by contact id, then relationship
|
|
118
|
+
* order; a relationship behind two entries under the one its
|
|
119
|
+
* verifications go to (2146), once. */
|
|
120
|
+
export function recipients(state) {
|
|
121
|
+
const out = [];
|
|
122
|
+
for (const contact of Object.values(state.contacts).sort((a, b) => cmp(a.id, b.id))) {
|
|
123
|
+
for (const rel of entryRelationships(state, contact)) {
|
|
124
|
+
if (rel.chainedInto !== null || entryOf(state, rel)?.id !== contact.id)
|
|
125
|
+
continue;
|
|
126
|
+
const tuple = activeTuple(rel);
|
|
127
|
+
if (tuple && !tuple.deactivated)
|
|
128
|
+
out.push({ contact, rel, tuple });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
/** 2600: the scope has issued its maximum, or would exceed the wire domain. */
|
|
134
|
+
const canIssue = (o) => !o.exhausted && BigInt(o.lastSalt) < ADMISSION_BOUND;
|
|
135
|
+
/** An in-flight salt above H carrying exactly this view, none of whose
|
|
136
|
+
* chunks has failed: the divergence is being delivered. */
|
|
137
|
+
const inFlight = (state, o, viewDigest) => Object.entries(o.inflight).some(([salt, f]) => BigInt(salt) > BigInt(o.hwm) && f.viewDigest === viewDigest && canComplete(state, f));
|
|
138
|
+
/** A uniform view, once per contacts record (the record is replaced on
|
|
139
|
+
* every change of a contact, so the cache cannot go stale). */
|
|
140
|
+
const uniformViews = new WeakMap();
|
|
141
|
+
/** The digest of a view: SHA-256 over its JCS form. Views are compared by
|
|
142
|
+
* it (the baseline's snapshotDigest, an inflight entry's viewDigest),
|
|
143
|
+
* never re-serialized per comparison. */
|
|
144
|
+
export const viewDigest = (view) => sha256Hex(jcs(view));
|
|
145
|
+
/** The view toward a recipient and its digest. */
|
|
146
|
+
function viewAt(art, state, r) {
|
|
147
|
+
if (!art.uniform) {
|
|
148
|
+
const view = art.view(state, r.contact, r.rel);
|
|
149
|
+
return { view, digest: viewDigest(view) };
|
|
150
|
+
}
|
|
151
|
+
let byKind = uniformViews.get(state.contacts);
|
|
152
|
+
if (!byKind)
|
|
153
|
+
uniformViews.set(state.contacts, (byKind = new Map()));
|
|
154
|
+
let v = byKind.get(art.kind);
|
|
155
|
+
if (!v) {
|
|
156
|
+
const view = art.view(state, r.contact, r.rel);
|
|
157
|
+
byKind.set(art.kind, (v = { view, digest: viewDigest(view) }));
|
|
158
|
+
}
|
|
159
|
+
return v;
|
|
160
|
+
}
|
|
161
|
+
/** Whether an in-flight delivery can still complete: every chunk acked,
|
|
162
|
+
* or still in the outbox and not failed (an ack refers to an outbox item). */
|
|
163
|
+
export const canComplete = (state, f) => f.digests.every((d) => f.acked.includes(d) || (Object.hasOwn(state.outbox, d) && triasOf(state.outbox[d]) !== 'failed'));
|
|
164
|
+
/** The in-flight deliveries whose completion data is still needed: any
|
|
165
|
+
* of their outbox items exists, failed ones included — an ack can still
|
|
166
|
+
* arrive late for it (Delivery §6.1 failed → delivered) and complete the
|
|
167
|
+
* salt (5660). Whether one still needs delivering is canComplete's. */
|
|
168
|
+
function retainedInflight(state, o) {
|
|
169
|
+
const out = {};
|
|
170
|
+
for (const [s, f] of Object.entries(o.inflight))
|
|
171
|
+
if (f.digests.some((d) => Object.hasOwn(state.outbox, d)))
|
|
172
|
+
out[s] = f;
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
/** Every tuple's in-flight deliveries of both artifacts pruned to those
|
|
176
|
+
* whose outbox items are not all gone; tick's retention (the same state
|
|
177
|
+
* object where nothing is pruned). */
|
|
178
|
+
export function retainInflight(state) {
|
|
179
|
+
let relationships = null;
|
|
180
|
+
for (const rel of Object.values(state.relationships)) {
|
|
181
|
+
let changed = false;
|
|
182
|
+
const tuples = rel.tuples.map((t) => {
|
|
183
|
+
const so = retainedInflight(state, t.starOut), go = retainedInflight(state, t.groupStarOut);
|
|
184
|
+
if (Object.keys(so).length === Object.keys(t.starOut.inflight).length && Object.keys(go).length === Object.keys(t.groupStarOut.inflight).length)
|
|
185
|
+
return t;
|
|
186
|
+
changed = true;
|
|
187
|
+
return { ...t, starOut: { ...t.starOut, inflight: so }, groupStarOut: { ...t.groupStarOut, inflight: go } };
|
|
188
|
+
});
|
|
189
|
+
if (changed)
|
|
190
|
+
(relationships ??= { ...state.relationships })[rel.id] = { ...rel, tuples };
|
|
191
|
+
}
|
|
192
|
+
return relationships ? { ...state, relationships } : state;
|
|
193
|
+
}
|
|
194
|
+
/** Whether the artifact is owed now on this recipient (5690–5720, 5830):
|
|
195
|
+
* the class on, the scope able to issue, the view diverging from the
|
|
196
|
+
* baseline (absent: by definition) and not already in flight. */
|
|
197
|
+
export function owed(art, state, r, digest) {
|
|
198
|
+
const o = art.out(r.tuple);
|
|
199
|
+
if (!classOnToward(r.contact, art.cls) || !canIssue(o))
|
|
200
|
+
return false;
|
|
201
|
+
if (o.snapshotDigest !== null && o.snapshotDigest === digest)
|
|
202
|
+
return false;
|
|
203
|
+
return !inFlight(state, o, digest);
|
|
204
|
+
}
|
|
205
|
+
/** The sweep's pending keys: one per owed recipient and view. */
|
|
206
|
+
export function pendingOf(art, state) {
|
|
207
|
+
const keys = [];
|
|
208
|
+
for (const r of recipients(state)) {
|
|
209
|
+
if (!classOnToward(r.contact, art.cls))
|
|
210
|
+
continue;
|
|
211
|
+
const { digest } = viewAt(art, state, r);
|
|
212
|
+
if (owed(art, state, r, digest))
|
|
213
|
+
keys.push(`${art.kind}|${r.rel.id}|${r.tuple.ownPairLabel}|${digest}`);
|
|
214
|
+
}
|
|
215
|
+
return keys;
|
|
216
|
+
}
|
|
217
|
+
/** A key agreement on the tuple's own rkid with the counterpart key, or null. */
|
|
218
|
+
export async function pairShared(ctx, t, peerKeyAgreement = t.counterpartKeyAgreement) {
|
|
219
|
+
const x = xRawOfMk(peerKeyAgreement);
|
|
220
|
+
if (!x)
|
|
221
|
+
return null;
|
|
222
|
+
const a = await ctx.keys.agree(t.rkid, x, ctx.release);
|
|
223
|
+
return a.status === 'available' ? a.shared : null;
|
|
224
|
+
}
|
|
225
|
+
/** Seal one chunk as its own document of the salt's thread (Delivery §4.4). */
|
|
226
|
+
export async function chunkDocument(ctx, type, issuer, t, contactId, threadId, chunk) {
|
|
227
|
+
const S = ctx.crypto.subtle;
|
|
228
|
+
const doc = { id: ctx.entropy.id('doc.id'), type, issuer, recipient: t.counterpartAnchor, threadId, issuedAt: iso(ctx.when), payload: chunk };
|
|
229
|
+
// 5190: within the Delivery Contract's plaintext limit, by construction
|
|
230
|
+
if (te.encode(jcs(doc)).length > PLAINTEXT_MAX)
|
|
231
|
+
throw new Error(`trust: a ${type} chunk exceeds ${PLAINTEXT_MAX} bytes`);
|
|
232
|
+
const envelope = await seal(doc, t.counterpartKeyAgreement, { ephSeed: ctx.entropy.bytes('seal.eph', 32), nonce: ctx.entropy.bytes('seal.nonce', 12) }, S);
|
|
233
|
+
return { digest: await digestDoc(doc, S), envelope, type, threadId, recipient: { contactId, rkid: t.rkid, keyAgreement: t.counterpartKeyAgreement } };
|
|
234
|
+
}
|
|
235
|
+
/** The reconciliation sweep of one artifact (5.4, 5730). */
|
|
236
|
+
export function sweepOf(art) {
|
|
237
|
+
return {
|
|
238
|
+
pending: (state) => pendingOf(art, state),
|
|
239
|
+
// contacts (views, settings), relationships (scopes), groups (the group
|
|
240
|
+
// star's view) and the outbox (whether a delivery can still complete)
|
|
241
|
+
reads: ['contacts', 'relationships', 'groups', 'outbox'],
|
|
242
|
+
async prepare(ctx) {
|
|
243
|
+
const st = ctx.snapshot;
|
|
244
|
+
const out = [];
|
|
245
|
+
for (const r of recipients(st)) {
|
|
246
|
+
if (!classOnToward(r.contact, art.cls))
|
|
247
|
+
continue;
|
|
248
|
+
const { view, digest } = viewAt(art, st, r);
|
|
249
|
+
if (!owed(art, st, r, digest))
|
|
250
|
+
continue;
|
|
251
|
+
const shared = await pairShared(ctx, r.tuple);
|
|
252
|
+
if (!shared)
|
|
253
|
+
continue;
|
|
254
|
+
const own = (await ctx.keys.anchorOf(r.tuple.ownPairLabel)).anchor;
|
|
255
|
+
const o = art.out(r.tuple);
|
|
256
|
+
const salt = inc(o.lastSalt);
|
|
257
|
+
const chunks = await art.build(ctx, { shared, own, tuple: r.tuple, salt, view });
|
|
258
|
+
if (!chunks)
|
|
259
|
+
continue;
|
|
260
|
+
// one salt = one thread: a fresh threadId on every chunk of it (Delivery §4.4)
|
|
261
|
+
const threadId = ctx.entropy.id('thread.id');
|
|
262
|
+
const outbound = [];
|
|
263
|
+
for (const chunk of chunks)
|
|
264
|
+
outbound.push(await chunkDocument(ctx, art.type, own, r.tuple, r.contact.id, threadId, chunk));
|
|
265
|
+
out.push({ relId: r.rel.id, tuple: r.tuple.ownPairLabel, contactId: r.contact.id, baseSalt: o.lastSalt, salt, view, viewDigest: digest, outbound });
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
},
|
|
269
|
+
apply(state, prepared) {
|
|
270
|
+
let cur = state;
|
|
271
|
+
const outbound = [];
|
|
272
|
+
let deferred = false;
|
|
273
|
+
for (const p of prepared.value) {
|
|
274
|
+
const rel = Object.hasOwn(cur.relationships, p.relId) ? cur.relationships[p.relId] : undefined;
|
|
275
|
+
const contact = findContact(cur, p.contactId);
|
|
276
|
+
const tuple = rel && activeTuple(rel);
|
|
277
|
+
if (!rel || !contact || !tuple || tuple.ownPairLabel !== p.tuple || rel.chainedInto !== null || tuple.deactivated || !contact.relIds.includes(rel.id) || entryOf(cur, rel)?.id !== contact.id)
|
|
278
|
+
continue;
|
|
279
|
+
const r = { contact, rel, tuple };
|
|
280
|
+
const { digest } = viewAt(art, cur, r);
|
|
281
|
+
// 5110: the snapshot, the salt and the commit are one step — a
|
|
282
|
+
// production prepared on another view or salt is not committed
|
|
283
|
+
if (art.out(tuple).lastSalt !== p.baseSalt || digest !== p.viewDigest || !owed(art, cur, r, digest)) {
|
|
284
|
+
if (owed(art, cur, r, digest))
|
|
285
|
+
deferred = true;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const o = art.out(tuple);
|
|
289
|
+
const next = {
|
|
290
|
+
...o, lastSalt: p.salt, exhausted: BigInt(p.salt) >= ADMISSION_BOUND,
|
|
291
|
+
// what no ack can reach any more leaves with the next production
|
|
292
|
+
inflight: { ...retainedInflight(cur, o), [p.salt]: { view: p.view, viewDigest: p.viewDigest, digests: p.outbound.map((d) => d.digest), acked: [] } },
|
|
293
|
+
};
|
|
294
|
+
const tuples = rel.tuples.map((t) => (t.ownPairLabel === p.tuple ? art.withOut(t, next) : t));
|
|
295
|
+
cur = { ...cur, relationships: { ...cur.relationships, [rel.id]: { ...rel, tuples } } };
|
|
296
|
+
outbound.push(...p.outbound);
|
|
297
|
+
}
|
|
298
|
+
return { state: cur, outbound, deferred };
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/** Completion of a salt by the verified acks of all its chunks (5660),
|
|
303
|
+
* then the baseline automaton (5670). */
|
|
304
|
+
export function ackOf(art) {
|
|
305
|
+
return (state, ref) => {
|
|
306
|
+
const item = state.outbox[ref];
|
|
307
|
+
const at = tupleByRkid(state, item.recipient.rkid);
|
|
308
|
+
if (!at)
|
|
309
|
+
return { state: state };
|
|
310
|
+
const o = art.out(at.tuple);
|
|
311
|
+
const hit = Object.entries(o.inflight).find(([, f]) => f.digests.includes(ref));
|
|
312
|
+
if (!hit)
|
|
313
|
+
return { state: state };
|
|
314
|
+
const [salt, f] = hit;
|
|
315
|
+
const acked = f.acked.includes(ref) ? f.acked : [...f.acked, ref];
|
|
316
|
+
let next;
|
|
317
|
+
if (f.digests.every((d) => acked.includes(d))) {
|
|
318
|
+
const advance = BigInt(salt) > BigInt(o.hwm);
|
|
319
|
+
const hwm = advance ? salt : o.hwm;
|
|
320
|
+
const inflight = {};
|
|
321
|
+
for (const [s, x] of Object.entries(o.inflight))
|
|
322
|
+
if (s !== salt && BigInt(s) > BigInt(hwm))
|
|
323
|
+
inflight[s] = x;
|
|
324
|
+
next = { ...o, hwm, snapshot: advance ? f.view : o.snapshot, snapshotDigest: advance ? f.viewDigest : o.snapshotDigest, inflight };
|
|
325
|
+
}
|
|
326
|
+
else
|
|
327
|
+
next = { ...o, inflight: { ...o.inflight, [salt]: { ...f, acked } } };
|
|
328
|
+
const tuples = at.rel.tuples.map((t) => (t.ownPairLabel === at.tuple.ownPairLabel ? art.withOut(t, next) : t));
|
|
329
|
+
return { state: { ...state, relationships: { ...state.relationships, [at.rel.id]: { ...at.rel, tuples } } } };
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* The assembly rules after the salt checks and the MAC (5250–5290, 5330,
|
|
334
|
+
* 5365): a higher salt displaces the open assembly; a seq above `limit`
|
|
335
|
+
* is the resource limit; the same digest again a repeat; another thread,
|
|
336
|
+
* a repeated seq of other bytes, a second last or a seq beyond last
|
|
337
|
+
* reject the delivery; otherwise the chunk is held, or completes the
|
|
338
|
+
* assembly with every seq up to last present.
|
|
339
|
+
*/
|
|
340
|
+
export function assemblyStep(open, salt, body, threadId, digest, limit) {
|
|
341
|
+
const base = open && open.salt === salt ? open : { salt, chunks: {} };
|
|
342
|
+
const seq = BigInt(body.seq);
|
|
343
|
+
if (seq > BigInt(limit))
|
|
344
|
+
return { kind: 'limit' };
|
|
345
|
+
const held = Object.values(base.chunks);
|
|
346
|
+
const same = Object.hasOwn(base.chunks, body.seq) ? base.chunks[body.seq] : undefined;
|
|
347
|
+
if (same && same.digest === digest)
|
|
348
|
+
return { kind: 'repeat' }; // 5250
|
|
349
|
+
if (held.some((h) => h.threadId !== threadId))
|
|
350
|
+
return { kind: 'reject', ref: 'RLTP-TRU-5280' };
|
|
351
|
+
if (same)
|
|
352
|
+
return { kind: 'reject', ref: 'RLTP-TRU-5260' };
|
|
353
|
+
const heldLast = held.find((h) => h.body.last);
|
|
354
|
+
if (body.last && heldLast)
|
|
355
|
+
return { kind: 'reject', ref: 'RLTP-TRU-5270' };
|
|
356
|
+
const last = body.last ? seq : heldLast ? BigInt(heldLast.body.seq) : null;
|
|
357
|
+
if (last !== null && (seq > last || held.some((h) => BigInt(h.body.seq) > last)))
|
|
358
|
+
return { kind: 'reject', ref: 'RLTP-TRU-5290' };
|
|
359
|
+
const chunks = { ...base.chunks, [body.seq]: { digest, threadId, body } };
|
|
360
|
+
if (last === null)
|
|
361
|
+
return { kind: 'hold', assembly: { salt, chunks } };
|
|
362
|
+
for (let i = 1n; i <= last; i++)
|
|
363
|
+
if (!Object.hasOwn(chunks, String(i)))
|
|
364
|
+
return { kind: 'hold', assembly: { salt, chunks } };
|
|
365
|
+
return { kind: 'complete', chunks, last };
|
|
366
|
+
}
|
|
367
|
+
/** The held bodies of a complete assembly, in seq order. */
|
|
368
|
+
export const assembledBodies = (chunks, last) => {
|
|
369
|
+
const out = [];
|
|
370
|
+
for (let i = 1n; i <= last; i++)
|
|
371
|
+
out.push(chunks[String(i)].body);
|
|
372
|
+
return out;
|
|
373
|
+
};
|
|
374
|
+
/** The arrival tuple, if it is the active head of a live relationship (6.4, 5360). */
|
|
375
|
+
function arrivalTuple(state, rkid) {
|
|
376
|
+
const at = tupleByRkid(state, rkid);
|
|
377
|
+
if (!at || at.tuple.deactivated || at.rel.chainedInto !== null || at.rel.activeHead !== at.tuple.ownPairLabel)
|
|
378
|
+
return null;
|
|
379
|
+
return at;
|
|
380
|
+
}
|
|
381
|
+
const maxDec = (a, b) => (BigInt(a) >= BigInt(b) ? a : b);
|
|
382
|
+
/** The task handler of one artifact (5.2a). */
|
|
383
|
+
export function taskOf(art, limit) {
|
|
384
|
+
return {
|
|
385
|
+
proof: 'absent',
|
|
386
|
+
// stage 7: the closed chunk schema (5070, 5370)
|
|
387
|
+
decode(payload) {
|
|
388
|
+
return art.chunkOk(payload) ? payload : refuse('malformed', art.malformedRef);
|
|
389
|
+
},
|
|
390
|
+
// stage 8: the salt rules, the MAC, and the assembly decision
|
|
391
|
+
async prepare(ctx, doc, arrival) {
|
|
392
|
+
if (doc.ceremony?.enactment !== undefined)
|
|
393
|
+
return refuse('validation-failed');
|
|
394
|
+
const at = arrivalTuple(ctx.snapshot, arrival.rkid);
|
|
395
|
+
if (!at)
|
|
396
|
+
return refuse('validation-failed', 'RLTP-TRU-5360');
|
|
397
|
+
const { rel, tuple: t } = at;
|
|
398
|
+
const inn = art.inn(t);
|
|
399
|
+
const body = bodyOf(doc.payload);
|
|
400
|
+
const salt = BigInt(body.salt);
|
|
401
|
+
if (salt <= BigInt(inn.hwm))
|
|
402
|
+
return refuse('validation-failed', 'RLTP-TRU-5150');
|
|
403
|
+
if (salt <= BigInt(inn.lowerBound))
|
|
404
|
+
return refuse('validation-failed', 'RLTP-TRU-5366');
|
|
405
|
+
if (inn.assembly && salt < BigInt(inn.assembly.salt))
|
|
406
|
+
return refuse('validation-failed', 'RLTP-TRU-5340');
|
|
407
|
+
const shared = await pairShared(ctx, t);
|
|
408
|
+
if (!shared)
|
|
409
|
+
return refuse('validation-failed', 'RLTP-TRU-5080');
|
|
410
|
+
const own = (await ctx.keys.anchorOf(t.ownPairLabel)).anchor;
|
|
411
|
+
if (!(await art.macOk(ctx, { shared, from: t.counterpartAnchor, own, salt: body.salt, chunk: doc.payload })))
|
|
412
|
+
return refuse('validation-failed', 'RLTP-TRU-5080');
|
|
413
|
+
const ids = { relId: rel.id, tuple: t.ownPairLabel };
|
|
414
|
+
// the chunk is authenticated: a violation rejects the whole delivery (5260–5310)
|
|
415
|
+
const reject = (ref) => ({ kind: 'discard', ...ids, salt: body.salt, reason: 'rejected', ref });
|
|
416
|
+
const step = assemblyStep(inn.assembly, body.salt, body, doc.threadId, arrival.digest, limit);
|
|
417
|
+
if (step.kind === 'limit')
|
|
418
|
+
return { kind: 'discard', ...ids, salt: body.salt, reason: 'resource-limit', ref: 'RLTP-TRU-5365' };
|
|
419
|
+
if (step.kind === 'repeat')
|
|
420
|
+
return { kind: 'repeat' };
|
|
421
|
+
if (step.kind === 'reject')
|
|
422
|
+
return reject(step.ref);
|
|
423
|
+
if (step.kind === 'hold')
|
|
424
|
+
return { kind: 'hold', ...ids, assembly: step.assembly };
|
|
425
|
+
// the completing chunk: the union in seq order, validated, evaluated
|
|
426
|
+
const union = assembledBodies(step.chunks, step.last).flatMap((b) => art.entries(b));
|
|
427
|
+
const done = await art.complete(ctx, { shared, from: t.counterpartAnchor, own, salt: body.salt, union, rel, tuple: t });
|
|
428
|
+
if (!done.ok)
|
|
429
|
+
return reject(done.ref);
|
|
430
|
+
const contactId = entryOf(ctx.snapshot, rel)?.id ?? rel.contactId;
|
|
431
|
+
return { kind: 'complete', ...ids, salt: body.salt, accepted: done.accepted, ...(done.memory ? { memory: done.memory } : {}), contactId };
|
|
432
|
+
},
|
|
433
|
+
// stage 9: the decision lands on the same snapshot
|
|
434
|
+
apply(state, prepared) {
|
|
435
|
+
const p = prepared.value;
|
|
436
|
+
if (p.kind === 'repeat')
|
|
437
|
+
return { state: state };
|
|
438
|
+
const rel = state.relationships[p.relId];
|
|
439
|
+
const t = rel.tuples.find((x) => x.ownPairLabel === p.tuple);
|
|
440
|
+
const inn = art.inn(t);
|
|
441
|
+
const put = (i, memory) => ({
|
|
442
|
+
...state,
|
|
443
|
+
relationships: {
|
|
444
|
+
...state.relationships,
|
|
445
|
+
[rel.id]: { ...rel, ...(memory ? { memory } : {}), tuples: rel.tuples.map((x) => (x.ownPairLabel === p.tuple ? art.withIn(x, i) : x)) },
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
switch (p.kind) {
|
|
449
|
+
case 'hold': return { state: put({ ...inn, assembly: p.assembly }) };
|
|
450
|
+
case 'discard':
|
|
451
|
+
// the resource limit or a rejected delivery: the assembly dropped,
|
|
452
|
+
// its salt the lower bound (5365, 5366); the completed hwm stays
|
|
453
|
+
return { discarded: true, reason: p.reason, ref: p.ref, state: put({ ...inn, assembly: null, lowerBound: maxDec(inn.lowerBound, p.salt) }) };
|
|
454
|
+
case 'complete': {
|
|
455
|
+
const events = [{ type: art.kind === 'star' ? 'star.accepted' : 'groupStar.accepted', contactId: p.contactId, relationshipId: rel.id, salt: p.salt }];
|
|
456
|
+
return { state: put({ ...inn, hwm: p.salt, assembly: null, accepted: p.accepted }, p.memory ? { ...rel.memory, ...p.memory } : undefined), events };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/** A tuple with its open assemblies dropped, as its deactivation leaves
|
|
463
|
+
* it (5360): for chaining and departure (PR 4c). */
|
|
464
|
+
export const discardAssemblies = (t) => ({
|
|
465
|
+
...t,
|
|
466
|
+
starIn: { ...t.starIn, assembly: null },
|
|
467
|
+
groupStarIn: { ...t.groupStarIn, assembly: null },
|
|
468
|
+
probeIn: { ...t.probeIn, assembly: null },
|
|
469
|
+
});
|
|
470
|
+
// ── the star ───────────────────────────────────────────────────────────
|
|
471
|
+
/** The reconciliation view of 5610/5620: (member, anchor head) of every
|
|
472
|
+
* admitted entry, member = the entry id, sorted by member. */
|
|
473
|
+
export function starView(state) {
|
|
474
|
+
return Object.values(state.contacts)
|
|
475
|
+
.filter((c) => c.status === 'admitted' && c.anchorHead !== null)
|
|
476
|
+
.map((c) => ({ member: c.id, head: c.anchorHead }))
|
|
477
|
+
.sort((a, b) => cmp(a.member, b.member));
|
|
478
|
+
}
|
|
479
|
+
/** The entry a tuple's documents come from (2145): the star's sender. */
|
|
480
|
+
const senderOf = (state, rel, t) => findContact(state, t.entryId)?.id ?? findContact(state, rel.contactId)?.id ?? rel.contactId;
|
|
481
|
+
export async function starBlinds(ctx, anchors) {
|
|
482
|
+
const out = {};
|
|
483
|
+
if (!anchors.length)
|
|
484
|
+
return out;
|
|
485
|
+
const S = ctx.crypto.subtle;
|
|
486
|
+
for (const rel of Object.values(ctx.snapshot.relationships)) {
|
|
487
|
+
for (const t of rel.tuples) {
|
|
488
|
+
const acc = t.starIn.accepted;
|
|
489
|
+
if (acc === null)
|
|
490
|
+
continue;
|
|
491
|
+
const shared = await pairShared(ctx, t);
|
|
492
|
+
if (!shared)
|
|
493
|
+
continue;
|
|
494
|
+
const own = (await ctx.keys.anchorOf(t.ownPairLabel)).anchor;
|
|
495
|
+
const k = await starKeyOf(shared, t.counterpartAnchor, own, acc.salt, S);
|
|
496
|
+
out[`${rel.id}|${t.ownPairLabel}`] = { salt: acc.salt, blinds: await Promise.all(anchors.map((a) => blindAnchor(k, a, S))) };
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return out;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* The sync half (6160, 6390): `contactId` now holds the anchors `blinds`
|
|
503
|
+
* were prepared for; every accepted star (other than the contact's own)
|
|
504
|
+
* whose stored union carries one of them lists the contact among its
|
|
505
|
+
* matches. Run in the transition that makes the holder hold them (the
|
|
506
|
+
* anchor mapping's acceptance: a new entry, an advanced head, a new link).
|
|
507
|
+
*/
|
|
508
|
+
export function rematchStars(state, contactId, blinds) {
|
|
509
|
+
let relationships = null;
|
|
510
|
+
for (const [key, b] of Object.entries(blinds)) {
|
|
511
|
+
const [relId, label] = [key.slice(0, key.indexOf('|')), key.slice(key.indexOf('|') + 1)];
|
|
512
|
+
const rel = (relationships ?? state.relationships)[relId];
|
|
513
|
+
const t = rel?.tuples.find((x) => x.ownPairLabel === label);
|
|
514
|
+
const acc = t?.starIn.accepted;
|
|
515
|
+
if (!rel || !t || !acc || acc.salt !== b.salt || acc.matches.includes(contactId) || senderOf(state, rel, t) === contactId)
|
|
516
|
+
continue;
|
|
517
|
+
const union = new Set(acc.blinded);
|
|
518
|
+
if (!b.blinds.some((x) => union.has(x)))
|
|
519
|
+
continue;
|
|
520
|
+
const matches = [...acc.matches, contactId].sort(cmp);
|
|
521
|
+
relationships ??= { ...state.relationships };
|
|
522
|
+
relationships[relId] = { ...rel, tuples: rel.tuples.map((x) => (x === t ? { ...x, starIn: { ...x.starIn, accepted: { ...acc, matches } } } : x)) };
|
|
523
|
+
}
|
|
524
|
+
return relationships ? { ...state, relationships } : state;
|
|
525
|
+
}
|
|
526
|
+
/** Every anchor the holder holds for a contact (6160, 6390): the entry's
|
|
527
|
+
* head, each relationship's held head, both ends of every held link. */
|
|
528
|
+
export function heldAnchors(state, c) {
|
|
529
|
+
const out = new Set();
|
|
530
|
+
if (c.anchorHead !== null)
|
|
531
|
+
out.add(c.anchorHead);
|
|
532
|
+
for (const r of entryRelationships(state, c)) {
|
|
533
|
+
if (r.heldCounterpartHead !== null)
|
|
534
|
+
out.add(r.heldCounterpartHead);
|
|
535
|
+
for (const l of r.links) {
|
|
536
|
+
out.add(l.prev);
|
|
537
|
+
out.add(l.next);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return [...out].sort();
|
|
541
|
+
}
|
|
542
|
+
/** The star as an artifact (5.1–5.4). */
|
|
543
|
+
export const STAR = {
|
|
544
|
+
kind: 'star',
|
|
545
|
+
type: STAR_TYPE,
|
|
546
|
+
cls: 'star',
|
|
547
|
+
malformedRef: 'RLTP-TRU-5070',
|
|
548
|
+
out: (t) => t.starOut,
|
|
549
|
+
withOut: (t, o) => ({ ...t, starOut: o }),
|
|
550
|
+
inn: (t) => t.starIn,
|
|
551
|
+
withIn: (t, i) => ({ ...t, starIn: i }),
|
|
552
|
+
view: (state) => starView(state),
|
|
553
|
+
uniform: true,
|
|
554
|
+
async build(ctx, p) {
|
|
555
|
+
const S = ctx.crypto.subtle;
|
|
556
|
+
const k = await starKeyOf(p.shared, p.own, p.tuple.counterpartAnchor, p.salt, S);
|
|
557
|
+
return (await buildStarChunks(k, p.salt, p.view.map((v) => v.head), S));
|
|
558
|
+
},
|
|
559
|
+
chunkOk: starChunkOk,
|
|
560
|
+
async macOk(ctx, p) {
|
|
561
|
+
const S = ctx.crypto.subtle;
|
|
562
|
+
return starChunkMacOk(await starKeyOf(p.shared, p.from, p.own, p.salt, S), p.chunk, S);
|
|
563
|
+
},
|
|
564
|
+
entries: (body) => body.blinded,
|
|
565
|
+
async complete(ctx, p) {
|
|
566
|
+
const union = p.union;
|
|
567
|
+
if (!strictlyAscending(union))
|
|
568
|
+
return { ok: false, ref: 'RLTP-TRU-5300' }; // 5300 order, 5310 duplicates
|
|
569
|
+
const S = ctx.crypto.subtle;
|
|
570
|
+
const k = await starKeyOf(p.shared, p.from, p.own, p.salt, S);
|
|
571
|
+
const inStar = new Set(union);
|
|
572
|
+
const sender = senderOf(ctx.snapshot, p.rel, p.tuple);
|
|
573
|
+
const matches = [];
|
|
574
|
+
for (const c of Object.values(ctx.snapshot.contacts).sort((a, b) => cmp(a.id, b.id))) {
|
|
575
|
+
if (c.id === sender)
|
|
576
|
+
continue;
|
|
577
|
+
for (const a of heldAnchors(ctx.snapshot, c)) {
|
|
578
|
+
if (inStar.has(await blindAnchor(k, a, S))) {
|
|
579
|
+
matches.push(c.id);
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// the whole union is kept: anchors learned later are tested against it (rematchStars)
|
|
585
|
+
return { ok: true, accepted: { salt: p.salt, blinded: union, matches } };
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
//# sourceMappingURL=star.js.map
|