@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,757 @@
|
|
|
1
|
+
// introduction/holder — the Introduction Act on the holder (lib Stage 2,
|
|
2
|
+
// PR 7a; Trust Layer 0.31 §8 on Delivery 0.81 §4.4). The pure forms are
|
|
3
|
+
// the root module's (introduction.ts). Trust 0.31 §8 relays the act
|
|
4
|
+
// through the mediator; no rendezvous drop is needed or built.
|
|
5
|
+
//
|
|
6
|
+
// The three roles, each its own lifecycle (8320), in
|
|
7
|
+
// state.encounter.introductions under `<role>:<act>`:
|
|
8
|
+
// requester holds an offer (below), decides on an `introduction`
|
|
9
|
+
// prompt, sends introduction-request/0.1 on its pinned tuple
|
|
10
|
+
// toward the mediator (8270), with a fresh pair label and card;
|
|
11
|
+
// sent → acked (the ack) → completed (an accepted reply, 8.2);
|
|
12
|
+
// sent or acked → failed at the act's expiry (8340), the
|
|
13
|
+
// requester waiting for the ack and the reply alike until
|
|
14
|
+
// then: users are mostly offline, a mediator not online
|
|
15
|
+
// within ack-delay + act-skew would fail every introduction
|
|
16
|
+
// (A20, spec follow-up 18: 8690's ack timeout is not applied);
|
|
17
|
+
// the discard at the absolute lifetime as before (8310)
|
|
18
|
+
// mediator received (checks 1–3 of 8410–8430, the ack duty bound),
|
|
19
|
+
// forwarded (the target resolved, the byte-identical payload
|
|
20
|
+
// on its tuple, 8440), completed once the ack is produced, the
|
|
21
|
+
// reply forwarded and both vouchers sent (8360)
|
|
22
|
+
// target received (a forward verified, 8050) → an `introduction`
|
|
23
|
+
// prompt; consented (its reply sent) or refused (local, silent)
|
|
24
|
+
// The new relationship: the requester opens its fresh tuple toward the
|
|
25
|
+
// target's card on an accepted reply, the target toward the requester's
|
|
26
|
+
// card at consent (Trust openTuple, provenance `introduced` via the
|
|
27
|
+
// mediator's contact, 8790); the continuity probe follows on it. A
|
|
28
|
+
// verified voucher marks the relationship's memory `vouchedBy`. Both name
|
|
29
|
+
// the mediator with the own pair label toward it the act pinned
|
|
30
|
+
// (provenance.viaPin, memory.vouchedPin), so a split of the mediator's
|
|
31
|
+
// entry moves them with that relationship (2080).
|
|
32
|
+
//
|
|
33
|
+
// What it registers (introductionModule):
|
|
34
|
+
// human acts introduction.introduce the mediator's act (below)
|
|
35
|
+
// decision introduction: on an offer, accept sends the request; on a
|
|
36
|
+
// forward, accept consents (the reply), decline refuses
|
|
37
|
+
// tasks introduction-request/0.1, -forward/0.1, -reply/0.1
|
|
38
|
+
// (signature class: signed acks), -ack/0.1, -voucher/0.1 (DV
|
|
39
|
+
// class: MAC acks), registry-declaration/0.1 (proof
|
|
40
|
+
// required) and x-introduction-offer/0.1 (this library's
|
|
41
|
+
// form, proof required); every one on the tuple binding,
|
|
42
|
+
// the active head of a live relationship (receive.ts, 6240),
|
|
43
|
+
// except the reply, ack and voucher: `pinned`, on the tuple
|
|
44
|
+
// the act pinned, deactivated or not (8270)
|
|
45
|
+
// duty intro-ack/1: the mediator's one ack at t_received + ack-delay
|
|
46
|
+
// retain time-driven transitions (8140: the first whole second after
|
|
47
|
+
// the bound) and the discard at the absolute lifetime (8310)
|
|
48
|
+
// keepKeys the own rkids of every pinned tuple of a held act: a
|
|
49
|
+
// chaining mid-act deactivates it, tick keeps its key (8270,
|
|
50
|
+
// 8290)
|
|
51
|
+
//
|
|
52
|
+
// Choices this file makes where spec and note leave them open:
|
|
53
|
+
// - who starts: §8 has the requester mint the act and name the target by
|
|
54
|
+
// the mediator's designator (8.3), but says nothing of how the
|
|
55
|
+
// requester learns that designator. This library has the mediator's
|
|
56
|
+
// act `introduce(requester, target, {label?})` (the human who knows
|
|
57
|
+
// both) send the requester an
|
|
58
|
+
// experimental, signed x-introduction-offer/0.1 with the designator and
|
|
59
|
+
// at most a label the mediator's human typed in that act — never a
|
|
60
|
+
// name read from the target's profile (audience: the mediator alone,
|
|
61
|
+
// 6b) or card, and no provenance of the mediator↔target relationship —
|
|
62
|
+
// with the mediator's registry declaration of ack-delay (Delivery §4.4:
|
|
63
|
+
// a requester never sends without holding it). The requester's human
|
|
64
|
+
// accepts on an `introduction` prompt, which sends the request. Gaps
|
|
65
|
+
// for Trust 0.32: the designator's way to the requester (§8.3), and
|
|
66
|
+
// the residue that the offer, signed, is transferable: the requester
|
|
67
|
+
// can show a third party that the mediator offered it this designator
|
|
68
|
+
// and label
|
|
69
|
+
// - the designator: 32 fresh bytes, u-base64url, minted once per target
|
|
70
|
+
// contact and kept (stable per (mediator, target), 8610); the target
|
|
71
|
+
// verifies "designates itself" by the channel the forward arrives on,
|
|
72
|
+
// the mediator's pinned tuple toward it — it cannot read the handle
|
|
73
|
+
// - ack-delay: a library constant the mediator declares, PT30S (in
|
|
74
|
+
// [PT1S, PT1H]), revision 1, identical to every counterpart
|
|
75
|
+
// - acks of the documents: the request, forward and reply carry
|
|
76
|
+
// transferable signatures and earn signed delivery-acks (Delivery 4.4's
|
|
77
|
+
// class rule); the ack and voucher artifacts are DV and earn MAC acks
|
|
78
|
+
// - the forward opens a fresh thread toward the target; the reply comes
|
|
79
|
+
// on it and is forwarded on the request's thread; the ack and the
|
|
80
|
+
// requester's voucher travel on the request's thread, the target's on
|
|
81
|
+
// the forward's
|
|
82
|
+
// - an artifact outside its window is refused at stage 8 from its own
|
|
83
|
+
// bytes (8110), the offer by its document's issuedAt; a refusal by a role's lifecycle (a late reply of a
|
|
84
|
+
// failed act, 8350; a reply after the cutoff, 8180) commits only the
|
|
85
|
+
// document's acknowledgement: the act is unchanged
|
|
86
|
+
// - the requester's profile in the request is its display name (the own
|
|
87
|
+
// profile's, at most 64 characters, 8450); the cards carry no name
|
|
88
|
+
// - the name of an introduced contact: at the target the request's
|
|
89
|
+
// displayName, at the requester the offer's label; kept in the
|
|
90
|
+
// relationship's memory as `introducedName`, which the views read until
|
|
91
|
+
// a profile arrives (self-declared, Encounter 6120)
|
|
92
|
+
import { iso, jcs } from '../core.js';
|
|
93
|
+
import { diSignWith, diVerify, digestBytes, digestDoc, xRawOfMk } from '../crypto.js';
|
|
94
|
+
import { seal } from '../delivery.js';
|
|
95
|
+
import { cardBody } from '../encounter.js';
|
|
96
|
+
import { ACT_EXPIRY, ACT_SKEW, DECLARATION_TYPE, INTRODUCTION_TYPES, MAC_ACK_INFO, MAC_VOUCHER_INFO, MEDIATOR_ROLE, actIdOf, designatorOf, designatorOk, introDigest, actFiresAt, introCardShapeOk, introMacKey, introSchemaOk, introMacArtifact, introMacOk, mediatorConstants, replyBound, replyCutoffHolds, requestCutoffHolds, requestFormOk, signIntroArtifact, actWindowHolds, } from '../introduction.js';
|
|
97
|
+
import { b64uOf } from '../core.js';
|
|
98
|
+
import { tupleByRkid } from '../receive.js';
|
|
99
|
+
import { openTuple } from '../trust/continuity.js';
|
|
100
|
+
import { activeTuple, entryOf, findContact, liveRel, tupleByLabel } from '../trust/state.js';
|
|
101
|
+
import { idsOf } from '../encounter/ceremony.js';
|
|
102
|
+
import { withPairNonce } from '../identity-state.js';
|
|
103
|
+
import { tsec } from '../core.js';
|
|
104
|
+
const TT = 'https://real-life.org/trust-tasks/';
|
|
105
|
+
export const OFFER_TYPE = TT + 'x-introduction-offer/0.1';
|
|
106
|
+
export { DECLARATION_TYPE, INTRODUCTION_TYPES };
|
|
107
|
+
/** The mediator's declared ack-delay (Delivery §4.4, Trust 8.4). */
|
|
108
|
+
export const ACK_DELAY = 'PT30S';
|
|
109
|
+
export const ACK_DELAY_MS = 30_000;
|
|
110
|
+
export const INTRODUCTION_COMMANDS = Object.freeze({ introduce: 'introduction.introduce' });
|
|
111
|
+
const refuse = (reason, ref) => ({ ok: false, reason, ...(ref ? { ref } : {}) });
|
|
112
|
+
const no = (code, ref) => ({ ok: false, code, ...(ref ? { ref } : {}) });
|
|
113
|
+
const isRefusal = (x) => x?.ok === false;
|
|
114
|
+
const keyOf = (role, act) => `${role}:${act}`;
|
|
115
|
+
const offerPromptId = (digest) => 'intro-offer:' + digest;
|
|
116
|
+
const consentPromptId = (act) => 'intro:' + act;
|
|
117
|
+
const draft = (s) => ({ digest: s.digest, envelope: s.envelope, type: s.type, threadId: s.threadId, recipient: s.recipient });
|
|
118
|
+
/** A document from an own pair label's anchor on a tuple, sealed to its counterpart. */
|
|
119
|
+
async function onTuple(ctx, label, type, payload, threadId, sign = false) {
|
|
120
|
+
const at = tupleByLabel(ctx.snapshot, label);
|
|
121
|
+
if (!at)
|
|
122
|
+
throw new Error(`introduction: no tuple ${label}`);
|
|
123
|
+
const own = await ctx.keys.anchorOf(label);
|
|
124
|
+
const issuedAt = iso(ctx.when);
|
|
125
|
+
let doc = { id: ctx.entropy.id('doc.id'), type, issuer: own.anchor, recipient: at.tuple.counterpartAnchor, threadId, issuedAt, payload };
|
|
126
|
+
if (sign)
|
|
127
|
+
doc = await diSignWith(own.anchor, (b) => ctx.keys.sign(label, b), doc, issuedAt, ctx.crypto.subtle);
|
|
128
|
+
const S = ctx.crypto.subtle;
|
|
129
|
+
return {
|
|
130
|
+
digest: await digestDoc(doc, S),
|
|
131
|
+
envelope: await seal(doc, at.tuple.counterpartKeyAgreement, { ephSeed: ctx.entropy.bytes('seal.eph', 32), nonce: ctx.entropy.bytes('seal.nonce', 12) }, S),
|
|
132
|
+
threadId, type,
|
|
133
|
+
recipient: { contactId: entryOf(ctx.snapshot, at.rel)?.id ?? at.rel.contactId, rkid: at.tuple.rkid, keyAgreement: at.tuple.counterpartKeyAgreement },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** The MAC key of the pinned tuple `label` (8630, 8730). */
|
|
137
|
+
async function pinKey(ctx, label, info) {
|
|
138
|
+
const at = tupleByLabel(ctx.snapshot, label);
|
|
139
|
+
const raw = at ? xRawOfMk(at.tuple.counterpartKeyAgreement) : null;
|
|
140
|
+
if (!at || !raw)
|
|
141
|
+
return null;
|
|
142
|
+
const a = await ctx.keys.agree(at.tuple.rkid, raw, ctx.release);
|
|
143
|
+
return a.status === 'available' ? introMacKey(a.shared, info, ctx.crypto.subtle) : null;
|
|
144
|
+
}
|
|
145
|
+
/** The live active tuple toward a contact (8020: a pair relationship). */
|
|
146
|
+
function channelTo(state, contactId) {
|
|
147
|
+
const c = findContact(state, contactId);
|
|
148
|
+
if (!c)
|
|
149
|
+
return null;
|
|
150
|
+
const ids = new Set([c.id, ...c.aliases]);
|
|
151
|
+
for (const id of c.relIds) {
|
|
152
|
+
const r = state.relationships[id];
|
|
153
|
+
const t = r && liveRel(r) ? activeTuple(r) : undefined;
|
|
154
|
+
if (t && !t.deactivated && ids.has(t.entryId))
|
|
155
|
+
return { contact: c, rel: r, tuple: t };
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
/** The arrival tuple of a document, with the entry it feeds. */
|
|
160
|
+
function arrival(state, rkid) {
|
|
161
|
+
const at = tupleByRkid(state, rkid);
|
|
162
|
+
return at ? { label: at.tuple.ownPairLabel, contactId: entryOf(state, at.rel)?.id ?? at.rel.contactId, tuple: at.tuple, rel: at.rel } : null;
|
|
163
|
+
}
|
|
164
|
+
const withAct = (state, a) => ({ ...state, encounter: { ...state.encounter, introductions: { ...state.encounter.introductions, [a.key]: a } } });
|
|
165
|
+
const held = (state, role, act) => state.encounter.introductions[keyOf(role, act)];
|
|
166
|
+
/** The requester's status at `when` (8340, 8140): sent or acked, it fails
|
|
167
|
+
* at the act's expiry, the first whole second after it, whether or not
|
|
168
|
+
* tick ran; no ack timeout (A20, spec follow-up 18, against 8690). */
|
|
169
|
+
function requesterStatus(a, when) {
|
|
170
|
+
if ((a.status === 'sent' || a.status === 'acked') && when >= actFiresAt(a.at + ACT_EXPIRY))
|
|
171
|
+
return 'failed';
|
|
172
|
+
return a.status;
|
|
173
|
+
}
|
|
174
|
+
const lifetimeOver = (a, when) => when >= actFiresAt(a.issuedAt + ACT_EXPIRY + ACT_SKEW);
|
|
175
|
+
/** The mediator's completion (8360): ack produced, reply forwarded, vouchers sent. */
|
|
176
|
+
const completedIfDone = (a) => a.role === 'mediator' && a.ack !== null && a.replyForwarded && a.vouchersSent ? { ...a, status: 'completed' } : a;
|
|
177
|
+
/** The own profile name for the request's displayName (≤ 64, 8450). */
|
|
178
|
+
const displayNameOf = (state) => {
|
|
179
|
+
const n = state.profile.body?.name;
|
|
180
|
+
return typeof n === 'string' && n.length ? [...n].slice(0, 64).join('') : undefined;
|
|
181
|
+
};
|
|
182
|
+
/** A fresh pair label and its displayed card, no challenge, no name (the
|
|
183
|
+
* act's fresh anchor, 8010; an rltp-card/0.25 under Encounter's DI rules). */
|
|
184
|
+
async function freshCard(ctx) {
|
|
185
|
+
const n = ctx.entropy.bytes('founding.nonce', 32);
|
|
186
|
+
const label = 'pair/' + await digestBytes(n, ctx.crypto.subtle);
|
|
187
|
+
const me = await ctx.keys.anchorOf(label);
|
|
188
|
+
const at = iso(ctx.when);
|
|
189
|
+
const card = await diSignWith(me.anchor, (b) => ctx.keys.sign(label, b), cardBody(me), at, ctx.crypto.subtle);
|
|
190
|
+
return { label, nonce: b64uOf(n), rkid: me.keyAgreement, anchor: me.anchor, card };
|
|
191
|
+
}
|
|
192
|
+
/** The fresh relationship of an act toward the counterpart's card, introduced via the mediator (8790). */
|
|
193
|
+
function openIntroduced(state, label, nonce, rkid, card, viaPin, name, when) {
|
|
194
|
+
const ids = idsOf(label);
|
|
195
|
+
const opened = openTuple(state, {
|
|
196
|
+
relId: ids.relId, contactId: ids.contactId, ownPairLabel: label, counterpartAnchor: card.anchor, counterpartKeyAgreement: card.keyAgreement,
|
|
197
|
+
provenance: { kind: 'introduced', via: arrivalContact(state, viaPin), viaPin }, ...(nonce !== undefined ? { nonce } : {}),
|
|
198
|
+
}, rkid, when);
|
|
199
|
+
if (name === undefined)
|
|
200
|
+
return opened;
|
|
201
|
+
const rel = opened.state.relationships[ids.relId];
|
|
202
|
+
return { ...opened, state: { ...opened.state, relationships: { ...opened.state.relationships, [ids.relId]: { ...rel, memory: { ...rel.memory, introducedName: name } } } } };
|
|
203
|
+
}
|
|
204
|
+
const introduceDef = {
|
|
205
|
+
human: true,
|
|
206
|
+
async prepare(ctx, input) {
|
|
207
|
+
const s = ctx.snapshot;
|
|
208
|
+
const r = channelTo(s, input?.requesterId), t = channelTo(s, input?.targetId);
|
|
209
|
+
if (!r || !t)
|
|
210
|
+
return no('no-own-channel', 'RLTP-TRU-8020');
|
|
211
|
+
if (r.contact.id === t.contact.id)
|
|
212
|
+
return no('unknown-contact');
|
|
213
|
+
const held = Object.entries(s.encounter.designators).find(([, c]) => c === t.contact.id)?.[0];
|
|
214
|
+
const designator = held ?? designatorOf(ctx.entropy.bytes('intro.target', 32));
|
|
215
|
+
const label = input.label;
|
|
216
|
+
if (label !== undefined && (typeof label !== 'string' || !label.length || [...label].length > 64))
|
|
217
|
+
throw new TypeError('introduction.introduce: label is 1 to 64 characters');
|
|
218
|
+
const declaration = await onTuple(ctx, r.tuple.ownPairLabel, DECLARATION_TYPE, { declaration: { role: MEDIATOR_ROLE, revision: '1', constants: { 'ack-delay': ACK_DELAY } } }, ctx.entropy.id('thread.id'), true);
|
|
219
|
+
const offer = await onTuple(ctx, r.tuple.ownPairLabel, OFFER_TYPE, { target: designator, ...(label !== undefined ? { label } : {}) }, ctx.entropy.id('thread.id'), true);
|
|
220
|
+
return { designator, minted: held === undefined, offer, declaration };
|
|
221
|
+
},
|
|
222
|
+
apply(state, prepared, input) {
|
|
223
|
+
const p = prepared.value;
|
|
224
|
+
if (isRefusal(p))
|
|
225
|
+
return p;
|
|
226
|
+
const designators = p.minted ? { ...state.encounter.designators, [p.designator]: findContact(state, input.targetId).id } : state.encounter.designators;
|
|
227
|
+
return { state: { ...state, encounter: { ...state.encounter, designators } }, outbound: [draft(p.declaration), draft(p.offer)] };
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
// ── the registry declaration (Delivery §4.4) ───────────────────────────
|
|
231
|
+
const declarationTask = {
|
|
232
|
+
proof: 'required',
|
|
233
|
+
decode: (p) => (introSchemaOk('payload-registry-declaration.schema.json', p) ? p : refuse('malformed')),
|
|
234
|
+
async prepare(ctx, doc) {
|
|
235
|
+
if (!(await diVerify(doc, doc.issuer, ctx.crypto.subtle)))
|
|
236
|
+
return refuse('validation-failed');
|
|
237
|
+
const d = doc.payload.declaration;
|
|
238
|
+
// the registering specification closes names and domains: only the mediator role is known here
|
|
239
|
+
if (d.role !== MEDIATOR_ROLE || mediatorConstants(d.constants) === null)
|
|
240
|
+
return refuse('validation-failed');
|
|
241
|
+
return null;
|
|
242
|
+
},
|
|
243
|
+
// the generic revision rule: higher wins, equal and identical is
|
|
244
|
+
// idempotent, equal and different or lower is refused
|
|
245
|
+
apply(state, _p, doc, arr) {
|
|
246
|
+
const d = doc.payload.declaration;
|
|
247
|
+
const k = `${doc.issuer} ${d.role}`;
|
|
248
|
+
const h = state.encounter.declarations[k];
|
|
249
|
+
if (h) {
|
|
250
|
+
const cmp = BigInt(d.revision) - BigInt(h.revision);
|
|
251
|
+
if (cmp < 0n)
|
|
252
|
+
return refuse('validation-failed');
|
|
253
|
+
if (cmp === 0n)
|
|
254
|
+
return jcs(h.constants) === jcs(d.constants) ? { state: state } : refuse('validation-failed');
|
|
255
|
+
}
|
|
256
|
+
return { state: { ...state, encounter: { ...state.encounter, declarations: { ...state.encounter.declarations, [k]: { issuer: doc.issuer, role: d.role, revision: d.revision, constants: d.constants, at: arr.when } } } } };
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
// ── the offer at the requester (this library's form) ───────────────────
|
|
260
|
+
const offerTask = {
|
|
261
|
+
proof: 'required',
|
|
262
|
+
decode(p) {
|
|
263
|
+
const x = p;
|
|
264
|
+
if (x === null || typeof x !== 'object' || !designatorOk(x.target) ||
|
|
265
|
+
(x.label !== undefined && (typeof x.label !== 'string' || !x.label.length || [...x.label].length > 64)) || Object.keys(x).some((k) => !['target', 'label'].includes(k)))
|
|
266
|
+
return refuse('malformed');
|
|
267
|
+
return x;
|
|
268
|
+
},
|
|
269
|
+
async prepare(ctx, doc) {
|
|
270
|
+
// the act window, like the request's and the forward's (8110): a replay
|
|
271
|
+
// after the effect cache let the offer go opens no prompt again
|
|
272
|
+
if (!actWindowHolds(doc.issuedAt, ctx.when))
|
|
273
|
+
return refuse('validation-failed', 'RLTP-TRU-8110');
|
|
274
|
+
return (await diVerify(doc, doc.issuer, ctx.crypto.subtle)) ? null : refuse('validation-failed');
|
|
275
|
+
},
|
|
276
|
+
apply(state, _p, doc, arr) {
|
|
277
|
+
const a = arrival(state, arr.rkid);
|
|
278
|
+
const o = {
|
|
279
|
+
id: offerPromptId(arr.digest), mediatorContactId: a.contactId, pin: a.label, target: doc.payload.target,
|
|
280
|
+
...(doc.payload.label !== undefined ? { label: doc.payload.label } : {}), at: arr.when,
|
|
281
|
+
};
|
|
282
|
+
const prompt = { id: o.id, kind: 'introduction', contactId: a.contactId, openedAt: arr.when, expiresAt: arr.when + ACT_EXPIRY };
|
|
283
|
+
return {
|
|
284
|
+
state: { ...state, encounter: { ...state.encounter, offers: { ...state.encounter.offers, [o.id]: o } }, prompts: { ...state.prompts, [prompt.id]: prompt } },
|
|
285
|
+
events: [{ type: 'prompt.opened', promptId: prompt.id, kind: 'introduction' }],
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
async function prepareRequest(ctx, o) {
|
|
290
|
+
const pin = tupleByLabel(ctx.snapshot, o.pin);
|
|
291
|
+
if (!pin || pin.tuple.deactivated)
|
|
292
|
+
return no('no-own-channel', 'RLTP-TRU-8270');
|
|
293
|
+
// Delivery §4.4: never a request to a mediator whose declaration is not held
|
|
294
|
+
const dec = ctx.snapshot.encounter.declarations[`${pin.tuple.counterpartAnchor} ${MEDIATOR_ROLE}`];
|
|
295
|
+
const c = dec ? mediatorConstants(dec.constants) : null;
|
|
296
|
+
if (!c)
|
|
297
|
+
return no('no-declaration');
|
|
298
|
+
const fresh = await freshCard(ctx);
|
|
299
|
+
const act = actIdOf(ctx.entropy.bytes('intro.act', 32));
|
|
300
|
+
const name = displayNameOf(ctx.snapshot);
|
|
301
|
+
const body = {
|
|
302
|
+
type: 'introduction-request@1', act, issuedAt: iso(ctx.when), target: o.target,
|
|
303
|
+
cardDigest: await introDigest(fresh.card, ctx.crypto.subtle), profile: name !== undefined ? { displayName: name } : {},
|
|
304
|
+
};
|
|
305
|
+
const request = await signIntroArtifact(body, (b) => ctx.keys.sign(fresh.label, b));
|
|
306
|
+
const doc = await onTuple(ctx, o.pin, INTRODUCTION_TYPES.request, { introduction: request, card: fresh.card }, ctx.entropy.id('thread.id'));
|
|
307
|
+
return { act, fresh, request, doc, ackDelayMs: c.ackDelayMs };
|
|
308
|
+
}
|
|
309
|
+
function applyRequest(state, o, p, when) {
|
|
310
|
+
if (held(state, 'requester', p.act))
|
|
311
|
+
throw new Error('introduction: the act id is not fresh (8080)');
|
|
312
|
+
if (state.register.labels.includes(p.fresh.label))
|
|
313
|
+
throw new Error('introduction: the pair label is not fresh');
|
|
314
|
+
const a = {
|
|
315
|
+
key: keyOf('requester', p.act), act: p.act, role: 'requester', status: 'sent', issuedAt: tsec(when), request: p.request,
|
|
316
|
+
requestCard: p.fresh.card, reply: null, replyCard: null, pins: { mediator: o.pin }, threads: { request: p.doc.threadId },
|
|
317
|
+
at: when, ackDelayMs: p.ackDelayMs, ownLabel: p.fresh.label, ack: null, replyForwarded: false, vouchersSent: false,
|
|
318
|
+
pendingVoucher: null, voucher: null, vouched: false, promptId: null, ...(o.label !== undefined ? { peerName: o.label } : {}),
|
|
319
|
+
};
|
|
320
|
+
const offers = { ...state.encounter.offers };
|
|
321
|
+
delete offers[o.id];
|
|
322
|
+
const next = {
|
|
323
|
+
...state,
|
|
324
|
+
// the act's fresh label in the register before its card is shown (Identity 6.1)
|
|
325
|
+
register: withPairNonce(state.register, p.fresh.label, p.fresh.nonce),
|
|
326
|
+
addresses: { ...state.addresses, keys: { ...state.addresses.keys, [p.fresh.rkid]: { label: p.fresh.label, lastShown: when } } },
|
|
327
|
+
encounter: { ...state.encounter, offers },
|
|
328
|
+
};
|
|
329
|
+
return { state: withAct(next, a), outbound: [draft(p.doc)] };
|
|
330
|
+
}
|
|
331
|
+
const requestTask = {
|
|
332
|
+
proof: 'absent',
|
|
333
|
+
decode: (p) => (introSchemaOk('visibility-payload-introduction-request.schema.json', p) ? p : refuse('malformed')),
|
|
334
|
+
async prepare(ctx, doc, arr) {
|
|
335
|
+
const s = ctx.snapshot;
|
|
336
|
+
const a = arrival(s, arr.rkid);
|
|
337
|
+
const body = doc.payload.introduction.body;
|
|
338
|
+
// step 1: an active channel (the pin set at arrival)
|
|
339
|
+
if (!liveRel(a.rel) || a.tuple.deactivated || a.rel.activeHead !== a.label)
|
|
340
|
+
return refuse('validation-failed', 'RLTP-TRU-8410');
|
|
341
|
+
// step 2: form — schema (decode), digest, signature, the window, the ack cutoff
|
|
342
|
+
if (!introCardShapeOk(doc.payload.card) || !(await diVerify(doc.payload.card, doc.payload.card.anchor, ctx.crypto.subtle)))
|
|
343
|
+
return refuse('validation-failed', 'RLTP-TRU-8420');
|
|
344
|
+
if (!(await requestFormOk(doc.payload, ctx.crypto.subtle)))
|
|
345
|
+
return refuse('validation-failed', 'RLTP-TRU-8420');
|
|
346
|
+
if (!actWindowHolds(body.issuedAt, ctx.when) || !requestCutoffHolds(body.issuedAt, ctx.when, ACK_DELAY_MS))
|
|
347
|
+
return refuse('validation-failed', 'RLTP-TRU-8160');
|
|
348
|
+
const h = held(s, 'mediator', body.act);
|
|
349
|
+
if (h) {
|
|
350
|
+
if (jcs(h.request.body) !== jcs(doc.payload.introduction.body))
|
|
351
|
+
return refuse('validation-failed', 'RLTP-TRU-8240');
|
|
352
|
+
// a byte-identical repeat: the one ack value, once produced (8220, 8230)
|
|
353
|
+
const ack = h.ack ? await onTuple(ctx, a.label, INTRODUCTION_TYPES.ack, { introduction: h.ack }, h.threads.request) : null;
|
|
354
|
+
return { dup: 'same', forward: null, target: null, ack };
|
|
355
|
+
}
|
|
356
|
+
// step 4: resolution, for forwarding only and silently (8440)
|
|
357
|
+
const targetId = s.encounter.designators[body.target];
|
|
358
|
+
const t = targetId !== undefined ? channelTo(s, targetId) : null;
|
|
359
|
+
const forward = t && t.tuple.ownPairLabel !== a.label
|
|
360
|
+
? await onTuple(ctx, t.tuple.ownPairLabel, INTRODUCTION_TYPES.forward, doc.payload, ctx.entropy.id('thread.id'))
|
|
361
|
+
: null;
|
|
362
|
+
return { dup: 'none', forward, target: forward ? t.tuple.ownPairLabel : null, ack: null };
|
|
363
|
+
},
|
|
364
|
+
// step 3: received, the ack duty bound before and independent of resolution (8430)
|
|
365
|
+
apply(state, prepared, doc, arr) {
|
|
366
|
+
const p = prepared.value;
|
|
367
|
+
if (p.dup === 'same')
|
|
368
|
+
return { state: state, outbound: p.ack ? [draft(p.ack)] : [] };
|
|
369
|
+
const a = arrival(state, arr.rkid);
|
|
370
|
+
const body = doc.payload.introduction.body;
|
|
371
|
+
const act = {
|
|
372
|
+
key: keyOf('mediator', body.act), act: body.act, role: 'mediator', status: p.forward ? 'forwarded' : 'received',
|
|
373
|
+
issuedAt: Date.parse(body.issuedAt), request: doc.payload.introduction, requestCard: doc.payload.card,
|
|
374
|
+
reply: null, replyCard: null, pins: { requester: a.label, ...(p.target ? { target: p.target } : {}) },
|
|
375
|
+
threads: { request: doc.threadId, ...(p.forward ? { forward: p.forward.threadId } : {}) }, at: arr.when, ackDelayMs: ACK_DELAY_MS,
|
|
376
|
+
ownLabel: null, ack: null, replyForwarded: false, vouchersSent: false, pendingVoucher: null, voucher: null, vouched: false, promptId: null,
|
|
377
|
+
};
|
|
378
|
+
const duty = { kind: 'intro-ack', v: 1, id: 'intro-ack:' + body.act, dueAt: arr.when + ACK_DELAY_MS, act: body.act };
|
|
379
|
+
return { state: withAct(state, act), duties: [duty], outbound: p.forward ? [draft(p.forward)] : [] };
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
/** The mediator's one ack, at t_received + ack-delay (8640), identical for
|
|
383
|
+
* every target (8650); none after the absolute lifetime (8280). */
|
|
384
|
+
const ackDuty = {
|
|
385
|
+
v: 1,
|
|
386
|
+
async prepare(ctx, duty) {
|
|
387
|
+
const a = held(ctx.snapshot, 'mediator', duty.act);
|
|
388
|
+
if (!a || a.ack || lifetimeOver(a, ctx.when) || !a.pins.requester)
|
|
389
|
+
return null;
|
|
390
|
+
const key = await pinKey(ctx, a.pins.requester, MAC_ACK_INFO);
|
|
391
|
+
if (!key)
|
|
392
|
+
return null;
|
|
393
|
+
const ack = await introMacArtifact({ type: 'introduction-ack@1', act: a.act }, key, ctx.crypto.subtle);
|
|
394
|
+
return { sealed: await onTuple(ctx, a.pins.requester, INTRODUCTION_TYPES.ack, { introduction: ack }, a.threads.request), ack: ack };
|
|
395
|
+
},
|
|
396
|
+
apply(state, duty, ctx, when) {
|
|
397
|
+
const a = held(state, 'mediator', duty.act);
|
|
398
|
+
const p = ctx.prepared.value;
|
|
399
|
+
// past the lifetime, or the pinned key gone: the duty ends without production (8280)
|
|
400
|
+
if (!a || a.ack || lifetimeOver(a, when) || p === null)
|
|
401
|
+
return { state: state };
|
|
402
|
+
// the ack value as produced, retained for re-delivery (8200, 8230)
|
|
403
|
+
return { state: withAct(state, completedIfDone({ ...a, ack: p.ack })), outbound: [draft(p.sealed)] };
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
// ── the target: the forward (8050) and its consent ─────────────────────
|
|
407
|
+
const forwardTask = {
|
|
408
|
+
proof: 'absent',
|
|
409
|
+
decode: (p) => (introSchemaOk('visibility-payload-introduction-forward.schema.json', p) ? p : refuse('malformed')),
|
|
410
|
+
async prepare(ctx, doc) {
|
|
411
|
+
const body = doc.payload.introduction.body;
|
|
412
|
+
if (!actWindowHolds(body.issuedAt, ctx.when))
|
|
413
|
+
return refuse('validation-failed', 'RLTP-TRU-8110');
|
|
414
|
+
const h = held(ctx.snapshot, 'target', body.act);
|
|
415
|
+
if (h)
|
|
416
|
+
return jcs(h.request.body) === jcs(doc.payload.introduction.body) ? { known: true } : refuse('validation-failed', 'RLTP-TRU-8260');
|
|
417
|
+
// the request signature under the card's anchor, cardDigest, the card (8050)
|
|
418
|
+
if (!(await requestFormOk(doc.payload, ctx.crypto.subtle)))
|
|
419
|
+
return refuse('validation-failed', 'RLTP-TRU-8050');
|
|
420
|
+
if (!introCardShapeOk(doc.payload.card) || !(await diVerify(doc.payload.card, doc.payload.card.anchor, ctx.crypto.subtle)))
|
|
421
|
+
return refuse('validation-failed', 'RLTP-TRU-8050');
|
|
422
|
+
return { known: false };
|
|
423
|
+
},
|
|
424
|
+
// received: the pin at the forward's arrival (8270); the human decides
|
|
425
|
+
// on its own device (8810); "designates itself" is the channel it came on
|
|
426
|
+
apply(state, prepared, doc, arr) {
|
|
427
|
+
if (prepared.value.known)
|
|
428
|
+
return { state: state };
|
|
429
|
+
const a = arrival(state, arr.rkid);
|
|
430
|
+
const body = doc.payload.introduction.body;
|
|
431
|
+
const promptId = consentPromptId(body.act);
|
|
432
|
+
const act = {
|
|
433
|
+
key: keyOf('target', body.act), act: body.act, role: 'target', status: 'received', issuedAt: Date.parse(body.issuedAt),
|
|
434
|
+
request: doc.payload.introduction, requestCard: doc.payload.card, reply: null, replyCard: null,
|
|
435
|
+
pins: { mediator: a.label }, threads: { request: '', forward: doc.threadId }, at: arr.when, ackDelayMs: 0, ownLabel: null, ack: null,
|
|
436
|
+
replyForwarded: false, vouchersSent: false, pendingVoucher: null, voucher: null, vouched: false, promptId,
|
|
437
|
+
};
|
|
438
|
+
const prompt = { id: promptId, kind: 'introduction', contactId: a.contactId, openedAt: arr.when, expiresAt: actFiresAt(act.issuedAt + ACT_EXPIRY + ACT_SKEW) };
|
|
439
|
+
return {
|
|
440
|
+
state: { ...withAct(state, act), prompts: { ...state.prompts, [promptId]: prompt } },
|
|
441
|
+
events: [{ type: 'prompt.opened', promptId, kind: 'introduction' }],
|
|
442
|
+
};
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
async function prepareReply(ctx, a) {
|
|
446
|
+
if (!a.pins.mediator || !tupleByLabel(ctx.snapshot, a.pins.mediator))
|
|
447
|
+
return no('no-own-channel', 'RLTP-TRU-8270');
|
|
448
|
+
const fresh = await freshCard(ctx);
|
|
449
|
+
const S = ctx.crypto.subtle;
|
|
450
|
+
const request = a.request;
|
|
451
|
+
const body = {
|
|
452
|
+
type: 'introduction-reply@1', act: a.act, requestDigest: await introDigest(request.body, S),
|
|
453
|
+
requesterPair: a.requestCard.anchor, cardDigest: await introDigest(fresh.card, S),
|
|
454
|
+
};
|
|
455
|
+
const reply = await signIntroArtifact(body, (b) => ctx.keys.sign(fresh.label, b));
|
|
456
|
+
const doc = await onTuple(ctx, a.pins.mediator, INTRODUCTION_TYPES.reply, { introduction: reply, card: fresh.card }, a.threads.forward);
|
|
457
|
+
return { fresh, reply, doc };
|
|
458
|
+
}
|
|
459
|
+
/** The target consents: its reply out, its fresh tuple toward the requester's card (8380). */
|
|
460
|
+
function applyConsent(state, a, p, when) {
|
|
461
|
+
if (a.status !== 'received')
|
|
462
|
+
return no('unknown-act');
|
|
463
|
+
if (lifetimeOver(a, when))
|
|
464
|
+
return no('prompt-expired', 'RLTP-TRU-8280');
|
|
465
|
+
const name = a.request.body.profile.displayName;
|
|
466
|
+
const opened = openIntroduced(state, p.fresh.label, p.fresh.nonce, p.fresh.rkid, a.requestCard, a.pins.mediator, name, when);
|
|
467
|
+
const next = { ...a, status: 'consented', reply: p.reply, replyCard: p.fresh.card, ownLabel: p.fresh.label };
|
|
468
|
+
return { state: withAct(opened.state, next), outbound: [draft(p.doc)], events: opened.events, duties: opened.duties };
|
|
469
|
+
}
|
|
470
|
+
const arrivalContact = (state, label) => {
|
|
471
|
+
const at = tupleByLabel(state, label);
|
|
472
|
+
return at ? (entryOf(state, at.rel)?.id ?? at.rel.contactId) : '';
|
|
473
|
+
};
|
|
474
|
+
/** introduction: on an offer, accept sends the request; on a forward,
|
|
475
|
+
* accept consents and decline refuses, locally and silently (8380). */
|
|
476
|
+
const introductionDecision = {
|
|
477
|
+
async prepare(ctx, prompt, choice) {
|
|
478
|
+
if (choice !== 'accept')
|
|
479
|
+
return null;
|
|
480
|
+
const o = ctx.snapshot.encounter.offers[prompt.id];
|
|
481
|
+
if (o)
|
|
482
|
+
return prepareRequest(ctx, o);
|
|
483
|
+
const a = Object.values(ctx.snapshot.encounter.introductions).find((x) => x.promptId === prompt.id && x.role === 'target');
|
|
484
|
+
return a ? prepareReply(ctx, a) : no('unknown-act');
|
|
485
|
+
},
|
|
486
|
+
apply(state, prompt, choice, when, prepared) {
|
|
487
|
+
const o = state.encounter.offers[prompt.id];
|
|
488
|
+
const a = Object.values(state.encounter.introductions).find((x) => x.promptId === prompt.id && x.role === 'target');
|
|
489
|
+
if (choice === 'decline') {
|
|
490
|
+
if (o) {
|
|
491
|
+
const offers = { ...state.encounter.offers };
|
|
492
|
+
delete offers[o.id];
|
|
493
|
+
return { state: { ...state, encounter: { ...state.encounter, offers } } };
|
|
494
|
+
}
|
|
495
|
+
return a ? { state: withAct(state, { ...a, status: 'refused' }) } : { state: state };
|
|
496
|
+
}
|
|
497
|
+
const p = prepared.value;
|
|
498
|
+
if (p === null || isRefusal(p))
|
|
499
|
+
return p ?? no('unknown-act');
|
|
500
|
+
if (o)
|
|
501
|
+
return applyRequest(state, o, p, when);
|
|
502
|
+
return a ? applyConsent(state, a, p, when) : no('unknown-act');
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
const replyChecks = async (ctx, a, payload) => {
|
|
506
|
+
const r = payload.introduction.body;
|
|
507
|
+
if (!(await replyBound(payload, ctx.crypto.subtle)))
|
|
508
|
+
return a.role === 'mediator' ? 'RLTP-TRU-8500' : 'RLTP-TRU-8560';
|
|
509
|
+
if (r.requestDigest !== await introDigest(a.request.body, ctx.crypto.subtle))
|
|
510
|
+
return a.role === 'mediator' ? 'RLTP-TRU-8510' : 'RLTP-TRU-8570';
|
|
511
|
+
if (r.requesterPair !== a.requestCard.anchor)
|
|
512
|
+
return a.role === 'mediator' ? 'RLTP-TRU-8520' : 'RLTP-TRU-8580';
|
|
513
|
+
if (!introCardShapeOk(payload.card) || !(await diVerify(payload.card, payload.card.anchor, ctx.crypto.subtle)))
|
|
514
|
+
return a.role === 'mediator' ? 'RLTP-TRU-8530' : 'RLTP-TRU-8590';
|
|
515
|
+
return null;
|
|
516
|
+
};
|
|
517
|
+
const replyTask = {
|
|
518
|
+
proof: 'absent',
|
|
519
|
+
// on the tuple the act pinned, even one a chaining deactivated since (8270)
|
|
520
|
+
pinned: true,
|
|
521
|
+
decode: (p) => (introSchemaOk('visibility-payload-introduction-reply.schema.json', p) ? p : refuse('malformed')),
|
|
522
|
+
async prepare(ctx, doc, arr) {
|
|
523
|
+
const s = ctx.snapshot;
|
|
524
|
+
const r = doc.payload.introduction.body;
|
|
525
|
+
const at = arrival(s, arr.rkid);
|
|
526
|
+
const m = held(s, 'mediator', r.act);
|
|
527
|
+
if (m && m.pins.target === at.label) {
|
|
528
|
+
// step 2: on the pinned mediator↔target tuple; step 3: forwarded, window and cutoff
|
|
529
|
+
if (m.status !== 'forwarded' && m.status !== 'completed')
|
|
530
|
+
return refuse('validation-failed', 'RLTP-TRU-8490');
|
|
531
|
+
// a held reply first: a differing one is rejected at any time (8250)
|
|
532
|
+
if (m.reply) {
|
|
533
|
+
if (jcs(m.reply.body) !== jcs(doc.payload.introduction.body))
|
|
534
|
+
return refuse('validation-failed', 'RLTP-TRU-8250');
|
|
535
|
+
return { repeat: true, toRequester: null, vouchers: [] };
|
|
536
|
+
}
|
|
537
|
+
const issuedAt = m.request.body.issuedAt;
|
|
538
|
+
if (!actWindowHolds(issuedAt, ctx.when) || !replyCutoffHolds(issuedAt, ctx.when))
|
|
539
|
+
return { repeat: true, toRequester: null, vouchers: [] }; // 8180: dropped silently
|
|
540
|
+
const bad = await replyChecks(ctx, m, doc.payload);
|
|
541
|
+
if (bad)
|
|
542
|
+
return refuse('validation-failed', bad);
|
|
543
|
+
// the requester's relationship departed while the act ran: the reply is
|
|
544
|
+
// taken (an authentic document on the pinned target tuple), the relay
|
|
545
|
+
// ends as failed; nothing goes to a departed relationship
|
|
546
|
+
if (!tupleByLabel(s, m.pins.requester))
|
|
547
|
+
return { repeat: false, toRequester: null, vouchers: [], orphaned: true };
|
|
548
|
+
// forward unchanged (8060) and both vouchers (8740)
|
|
549
|
+
const toRequester = await onTuple(ctx, m.pins.requester, INTRODUCTION_TYPES.reply, doc.payload, m.threads.request);
|
|
550
|
+
const vouchers = [];
|
|
551
|
+
const vb = { type: 'introduction-voucher@1', act: r.act, requesterPair: m.requestCard.anchor, targetPair: doc.payload.card.anchor };
|
|
552
|
+
for (const [side, thread] of [[m.pins.requester, m.threads.request], [m.pins.target, m.threads.forward]]) {
|
|
553
|
+
const key = await pinKey(ctx, side, MAC_VOUCHER_INFO);
|
|
554
|
+
if (!key)
|
|
555
|
+
return refuse('validation-failed');
|
|
556
|
+
vouchers.push(await onTuple(ctx, side, INTRODUCTION_TYPES.voucher, { voucher: await introMacArtifact(vb, key, ctx.crypto.subtle) }, thread));
|
|
557
|
+
}
|
|
558
|
+
return { repeat: false, toRequester, vouchers };
|
|
559
|
+
}
|
|
560
|
+
const q = held(s, 'requester', r.act);
|
|
561
|
+
if (!q || q.pins.mediator !== at.label)
|
|
562
|
+
return refuse('validation-failed', 'RLTP-TRU-8550');
|
|
563
|
+
if (!actWindowHolds(q.request.body.issuedAt, ctx.when))
|
|
564
|
+
return refuse('validation-failed', 'RLTP-TRU-8550');
|
|
565
|
+
if (q.reply)
|
|
566
|
+
return jcs(q.reply.body) === jcs(doc.payload.introduction.body) ? { ignore: true } : refuse('validation-failed', 'RLTP-TRU-8260');
|
|
567
|
+
// a late reply of a failed act is ignored (8350)
|
|
568
|
+
if (requesterStatus(q, ctx.when) === 'failed')
|
|
569
|
+
return { ignore: true };
|
|
570
|
+
const bad = await replyChecks(ctx, q, doc.payload);
|
|
571
|
+
return bad ? refuse('validation-failed', bad) : { ignore: false };
|
|
572
|
+
},
|
|
573
|
+
apply(state, prepared, doc, arr) {
|
|
574
|
+
const r = doc.payload.introduction.body;
|
|
575
|
+
const p = prepared.value;
|
|
576
|
+
if ('vouchers' in p) {
|
|
577
|
+
const m = held(state, 'mediator', r.act);
|
|
578
|
+
if (p.repeat)
|
|
579
|
+
return { state: state };
|
|
580
|
+
if (p.orphaned)
|
|
581
|
+
return { state: withAct(state, { ...m, status: 'failed', reply: doc.payload.introduction, replyCard: doc.payload.card }) };
|
|
582
|
+
const next = completedIfDone({ ...m, reply: doc.payload.introduction, replyCard: doc.payload.card, replyForwarded: true, vouchersSent: true });
|
|
583
|
+
return { state: withAct(state, next), outbound: [draft(p.toRequester), ...p.vouchers.map(draft)] };
|
|
584
|
+
}
|
|
585
|
+
if (p.ignore)
|
|
586
|
+
return { state: state };
|
|
587
|
+
const q = held(state, 'requester', r.act);
|
|
588
|
+
const opened = openIntroduced(state, q.ownLabel, state.register.pairNonces[q.ownLabel], rkidOf(state, q.ownLabel), doc.payload.card, q.pins.mediator, q.peerName, arr.when);
|
|
589
|
+
let next = { ...q, status: 'completed', reply: doc.payload.introduction, replyCard: doc.payload.card };
|
|
590
|
+
// the pending voucher, re-evaluated (8600, 8770)
|
|
591
|
+
let st = opened.state;
|
|
592
|
+
if (q.pendingVoucher) {
|
|
593
|
+
// matched it is accepted and retained; unmatched it vouches for nothing and goes
|
|
594
|
+
next = voucherMatches(next, q.pendingVoucher)
|
|
595
|
+
? { ...next, pendingVoucher: null, voucher: q.pendingVoucher, vouched: true }
|
|
596
|
+
: { ...next, pendingVoucher: null };
|
|
597
|
+
if (next.vouched)
|
|
598
|
+
st = markVouched(st, next);
|
|
599
|
+
}
|
|
600
|
+
return { state: withAct(st, next), events: opened.events, duties: opened.duties };
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
/** The own key-agreement id a label derives under, from `addresses`. */
|
|
604
|
+
function rkidOf(state, label) {
|
|
605
|
+
for (const [rkid, k] of Object.entries(state.addresses.keys))
|
|
606
|
+
if (k.label === label)
|
|
607
|
+
return rkid;
|
|
608
|
+
throw new Error(`introduction: no address key for ${label}`);
|
|
609
|
+
}
|
|
610
|
+
// ── the ack at the requester (8680) ────────────────────────────────────
|
|
611
|
+
const ackTask = {
|
|
612
|
+
proof: 'absent',
|
|
613
|
+
// on the tuple the act pinned, even one a chaining deactivated since (8270)
|
|
614
|
+
pinned: true,
|
|
615
|
+
decode: (p) => (introSchemaOk('visibility-payload-introduction-ack.schema.json', p) ? p : refuse('malformed')),
|
|
616
|
+
async prepare(ctx, doc, arr) {
|
|
617
|
+
const q = held(ctx.snapshot, 'requester', doc.payload.introduction.body.act);
|
|
618
|
+
const at = arrival(ctx.snapshot, arr.rkid);
|
|
619
|
+
if (!q || q.pins.mediator !== at.label)
|
|
620
|
+
return refuse('validation-failed', 'RLTP-TRU-8680');
|
|
621
|
+
// after the absolute lifetime, whether or not tick ran (8300)
|
|
622
|
+
if (lifetimeOver(q, ctx.when))
|
|
623
|
+
return refuse('validation-failed', 'RLTP-TRU-8300');
|
|
624
|
+
const key = await pinKey(ctx, q.pins.mediator, MAC_ACK_INFO);
|
|
625
|
+
if (!key || !(await introMacOk(doc.payload.introduction, key, ctx.crypto.subtle)))
|
|
626
|
+
return refuse('validation-failed', 'RLTP-TRU-8680');
|
|
627
|
+
return { ok: true };
|
|
628
|
+
},
|
|
629
|
+
// sent → acked; absorbed after completion (8330); ignored after failed (8710)
|
|
630
|
+
apply(state, _p, doc, arr) {
|
|
631
|
+
const q = held(state, 'requester', doc.payload.introduction.body.act);
|
|
632
|
+
if (requesterStatus(q, arr.when) !== 'sent')
|
|
633
|
+
return { state: state };
|
|
634
|
+
return { state: withAct(state, { ...q, status: 'acked' }) };
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
// ── vouchers at both sides (8.5) ───────────────────────────────────────
|
|
638
|
+
const voucherMatches = (a, v) => {
|
|
639
|
+
const own = a.role === 'requester';
|
|
640
|
+
const requesterPair = a.requestCard.anchor;
|
|
641
|
+
const targetPair = a.replyCard?.anchor;
|
|
642
|
+
return v.body.requesterPair === requesterPair && targetPair !== undefined && v.body.targetPair === targetPair && (own || a.role === 'target');
|
|
643
|
+
};
|
|
644
|
+
/** The relationship the act made, marked as vouched by the mediator. */
|
|
645
|
+
function markVouched(state, a) {
|
|
646
|
+
const at = a.ownLabel ? tupleByLabel(state, a.ownLabel) : null;
|
|
647
|
+
if (!at)
|
|
648
|
+
return state;
|
|
649
|
+
const rel = at.rel;
|
|
650
|
+
return { ...state, relationships: { ...state.relationships, [rel.id]: { ...rel, memory: { ...rel.memory, vouchedBy: arrivalContact(state, a.pins.mediator), vouchedPin: a.pins.mediator } } } };
|
|
651
|
+
}
|
|
652
|
+
const voucherTask = {
|
|
653
|
+
proof: 'absent',
|
|
654
|
+
// on the tuple the act pinned, even one a chaining deactivated since (8270)
|
|
655
|
+
pinned: true,
|
|
656
|
+
decode: (p) => (introSchemaOk('visibility-payload-introduction-voucher.schema.json', p) ? p : refuse('malformed')),
|
|
657
|
+
async prepare(ctx, doc, arr) {
|
|
658
|
+
const act = doc.payload.voucher.body.act;
|
|
659
|
+
const at = arrival(ctx.snapshot, arr.rkid);
|
|
660
|
+
const a = [held(ctx.snapshot, 'requester', act), held(ctx.snapshot, 'target', act)].find((x) => x && x.pins.mediator === at.label);
|
|
661
|
+
// a held act within its lifetime, role-terminal states included (8760, 8290)
|
|
662
|
+
if (!a || lifetimeOver(a, ctx.when))
|
|
663
|
+
return refuse('validation-failed', 'RLTP-TRU-8760');
|
|
664
|
+
const key = await pinKey(ctx, a.pins.mediator, MAC_VOUCHER_INFO);
|
|
665
|
+
if (!key || !(await introMacOk(doc.payload.voucher, key, ctx.crypto.subtle)))
|
|
666
|
+
return refuse('validation-failed', 'RLTP-TRU-8730');
|
|
667
|
+
return { role: a.role };
|
|
668
|
+
},
|
|
669
|
+
// 8260: a repeat is byte-identical in JCS(body) (8210) and ignored, a
|
|
670
|
+
// differing one rejected — against the accepted voucher, retained until
|
|
671
|
+
// the lifetime (8200), and against a pending one alike
|
|
672
|
+
apply(state, prepared, doc) {
|
|
673
|
+
const v = doc.payload.voucher;
|
|
674
|
+
const a = held(state, prepared.value.role, v.body.act);
|
|
675
|
+
const kept = (a.voucher ?? a.pendingVoucher);
|
|
676
|
+
if (kept)
|
|
677
|
+
return jcs(kept.body) === jcs(v.body) ? { state: state } : refuse('validation-failed', 'RLTP-TRU-8260');
|
|
678
|
+
if (a.reply === null)
|
|
679
|
+
return { state: withAct(state, { ...a, pendingVoucher: v }) }; // 8770
|
|
680
|
+
if (!voucherMatches(a, v))
|
|
681
|
+
return refuse('validation-failed', 'RLTP-TRU-8760');
|
|
682
|
+
const next = { ...a, voucher: v, vouched: true };
|
|
683
|
+
return { state: withAct(markVouched(state, next), next) };
|
|
684
|
+
},
|
|
685
|
+
};
|
|
686
|
+
// ── time: the transitions and the discard (8140, 8340, 8310) ───────────
|
|
687
|
+
function retainActs(state, when) {
|
|
688
|
+
let out = null;
|
|
689
|
+
for (const a of Object.values(state.encounter.introductions)) {
|
|
690
|
+
if (lifetimeOver(a, when)) {
|
|
691
|
+
out ??= { ...state.encounter.introductions };
|
|
692
|
+
delete out[a.key];
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (a.role === 'requester') {
|
|
696
|
+
const st = requesterStatus(a, when);
|
|
697
|
+
if (st !== a.status)
|
|
698
|
+
(out ??= { ...state.encounter.introductions })[a.key] = { ...a, status: st };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// offers leave with their prompt
|
|
702
|
+
const offers = Object.values(state.encounter.offers).filter((o) => !Object.hasOwn(state.prompts, o.id));
|
|
703
|
+
if (!out && !offers.length)
|
|
704
|
+
return state;
|
|
705
|
+
const kept = { ...state.encounter.offers };
|
|
706
|
+
for (const o of offers)
|
|
707
|
+
delete kept[o.id];
|
|
708
|
+
return { ...state, encounter: { ...state.encounter, introductions: out ?? state.encounter.introductions, offers: kept } };
|
|
709
|
+
}
|
|
710
|
+
/** When retainActs changes something: a requester's timeout fires, an
|
|
711
|
+
* act's absolute lifetime ends (the timer contract, engine.ts). */
|
|
712
|
+
function actDeadlines(state) {
|
|
713
|
+
const out = [];
|
|
714
|
+
for (const a of Object.values(state.encounter.introductions)) {
|
|
715
|
+
out.push(actFiresAt(a.issuedAt + ACT_EXPIRY + ACT_SKEW));
|
|
716
|
+
if (a.role === 'requester' && (a.status === 'sent' || a.status === 'acked'))
|
|
717
|
+
out.push(actFiresAt(a.at + ACT_EXPIRY));
|
|
718
|
+
}
|
|
719
|
+
return out;
|
|
720
|
+
}
|
|
721
|
+
/** The own rkids of every tuple a held act pinned (8270): a chaining
|
|
722
|
+
* during the act deactivates such a tuple, but the act's artifacts stay
|
|
723
|
+
* on it and verify under its key until the absolute lifetime (8290), when
|
|
724
|
+
* retainActs discards the act and with it this claim. */
|
|
725
|
+
function pinnedKeys(state) {
|
|
726
|
+
const out = new Set();
|
|
727
|
+
for (const a of Object.values(state.encounter.introductions)) {
|
|
728
|
+
for (const label of [a.pins.requester, a.pins.target, a.pins.mediator]) {
|
|
729
|
+
const at = label === undefined ? null : tupleByLabel(state, label);
|
|
730
|
+
if (at)
|
|
731
|
+
out.add(at.tuple.rkid);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return [...out];
|
|
735
|
+
}
|
|
736
|
+
/** The introduction module createHolder registers. */
|
|
737
|
+
export function introductionModule() {
|
|
738
|
+
return Object.freeze({
|
|
739
|
+
name: 'introduction',
|
|
740
|
+
commands: Object.freeze({ [INTRODUCTION_COMMANDS.introduce]: introduceDef }),
|
|
741
|
+
tasks: Object.freeze({
|
|
742
|
+
[DECLARATION_TYPE]: declarationTask,
|
|
743
|
+
[OFFER_TYPE]: offerTask,
|
|
744
|
+
[INTRODUCTION_TYPES.request]: requestTask,
|
|
745
|
+
[INTRODUCTION_TYPES.forward]: forwardTask,
|
|
746
|
+
[INTRODUCTION_TYPES.reply]: replyTask,
|
|
747
|
+
[INTRODUCTION_TYPES.ack]: ackTask,
|
|
748
|
+
[INTRODUCTION_TYPES.voucher]: voucherTask,
|
|
749
|
+
}),
|
|
750
|
+
duties: Object.freeze({ 'intro-ack': ackDuty }),
|
|
751
|
+
decisions: Object.freeze({ introduction: introductionDecision }),
|
|
752
|
+
retain: retainActs,
|
|
753
|
+
deadlines: actDeadlines,
|
|
754
|
+
keepKeys: pinnedKeys,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
//# sourceMappingURL=holder.js.map
|