@real-life/trust-protocol 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +187 -78
  3. package/dist/ack.d.ts +84 -0
  4. package/dist/ack.d.ts.map +1 -0
  5. package/dist/ack.js +114 -0
  6. package/dist/ack.js.map +1 -0
  7. package/dist/adapters/vti-mediator.d.ts +154 -0
  8. package/dist/adapters/vti-mediator.d.ts.map +1 -0
  9. package/dist/adapters/vti-mediator.js +621 -0
  10. package/dist/adapters/vti-mediator.js.map +1 -0
  11. package/dist/anchor.d.ts +226 -0
  12. package/dist/anchor.d.ts.map +1 -0
  13. package/dist/anchor.js +347 -0
  14. package/dist/anchor.js.map +1 -0
  15. package/dist/{holder.d.ts → carrier-holder.d.ts} +1 -1
  16. package/dist/carrier-holder.d.ts.map +1 -0
  17. package/dist/{holder.js → carrier-holder.js} +5 -4
  18. package/dist/carrier-holder.js.map +1 -0
  19. package/dist/carrier-identity.js +4 -4
  20. package/dist/carrier-identity.js.map +1 -1
  21. package/dist/carrier.d.ts.map +1 -1
  22. package/dist/carrier.js +4 -3
  23. package/dist/carrier.js.map +1 -1
  24. package/dist/ceremony.d.ts +3 -2
  25. package/dist/ceremony.d.ts.map +1 -1
  26. package/dist/ceremony.js +16 -26
  27. package/dist/ceremony.js.map +1 -1
  28. package/dist/codec.d.ts +46 -0
  29. package/dist/codec.d.ts.map +1 -0
  30. package/dist/codec.js +410 -0
  31. package/dist/codec.js.map +1 -0
  32. package/dist/continuity.d.ts +67 -0
  33. package/dist/continuity.d.ts.map +1 -0
  34. package/dist/continuity.js +110 -0
  35. package/dist/continuity.js.map +1 -0
  36. package/dist/core.d.ts +4 -0
  37. package/dist/core.d.ts.map +1 -1
  38. package/dist/core.js.map +1 -1
  39. package/dist/crypto.d.ts +23 -7
  40. package/dist/crypto.d.ts.map +1 -1
  41. package/dist/crypto.js +54 -20
  42. package/dist/crypto.js.map +1 -1
  43. package/dist/delivery.d.ts +31 -3
  44. package/dist/delivery.d.ts.map +1 -1
  45. package/dist/delivery.js +75 -28
  46. package/dist/delivery.js.map +1 -1
  47. package/dist/encounter/ceremony.d.ts +37 -0
  48. package/dist/encounter/ceremony.d.ts.map +1 -0
  49. package/dist/encounter/ceremony.js +662 -0
  50. package/dist/encounter/ceremony.js.map +1 -0
  51. package/dist/encounter.d.ts +91 -1
  52. package/dist/encounter.d.ts.map +1 -1
  53. package/dist/encounter.js +191 -12
  54. package/dist/encounter.js.map +1 -1
  55. package/dist/engine.d.ts +291 -0
  56. package/dist/engine.d.ts.map +1 -0
  57. package/dist/engine.js +1395 -0
  58. package/dist/engine.js.map +1 -0
  59. package/dist/entropy.d.ts +28 -0
  60. package/dist/entropy.d.ts.map +1 -0
  61. package/dist/entropy.js +96 -0
  62. package/dist/entropy.js.map +1 -0
  63. package/dist/events.d.ts +177 -0
  64. package/dist/events.d.ts.map +1 -0
  65. package/dist/events.js +2 -0
  66. package/dist/events.js.map +1 -0
  67. package/dist/fixtures.d.ts +30 -0
  68. package/dist/fixtures.d.ts.map +1 -0
  69. package/dist/fixtures.js +155 -0
  70. package/dist/fixtures.js.map +1 -0
  71. package/dist/forge.d.ts +47 -0
  72. package/dist/forge.d.ts.map +1 -0
  73. package/dist/forge.js +20 -0
  74. package/dist/forge.js.map +1 -0
  75. package/dist/group-star.d.ts +251 -0
  76. package/dist/group-star.d.ts.map +1 -0
  77. package/dist/group-star.js +348 -0
  78. package/dist/group-star.js.map +1 -0
  79. package/dist/holder-api.d.ts +137 -0
  80. package/dist/holder-api.d.ts.map +1 -0
  81. package/dist/holder-api.js +132 -0
  82. package/dist/holder-api.js.map +1 -0
  83. package/dist/holder-state-v1.d.ts +801 -0
  84. package/dist/holder-state-v1.d.ts.map +1 -0
  85. package/dist/holder-state-v1.js +2 -0
  86. package/dist/holder-state-v1.js.map +1 -0
  87. package/dist/identity-state.d.ts +43 -0
  88. package/dist/identity-state.d.ts.map +1 -0
  89. package/dist/identity-state.js +345 -0
  90. package/dist/identity-state.js.map +1 -0
  91. package/dist/identity.d.ts +24 -2
  92. package/dist/identity.d.ts.map +1 -1
  93. package/dist/identity.js +70 -11
  94. package/dist/identity.js.map +1 -1
  95. package/dist/index.d.ts +26 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +77 -29
  98. package/dist/index.js.map +1 -1
  99. package/dist/internal.d.ts +36 -0
  100. package/dist/internal.d.ts.map +1 -0
  101. package/dist/internal.js +22 -0
  102. package/dist/internal.js.map +1 -0
  103. package/dist/introduction/holder.d.ts +18 -0
  104. package/dist/introduction/holder.d.ts.map +1 -0
  105. package/dist/introduction/holder.js +757 -0
  106. package/dist/introduction/holder.js.map +1 -0
  107. package/dist/introduction.d.ts +101 -0
  108. package/dist/introduction.d.ts.map +1 -0
  109. package/dist/introduction.js +132 -0
  110. package/dist/introduction.js.map +1 -0
  111. package/dist/invariants.d.ts +43 -0
  112. package/dist/invariants.d.ts.map +1 -0
  113. package/dist/invariants.js +883 -0
  114. package/dist/invariants.js.map +1 -0
  115. package/dist/keys-seed.d.ts +7 -0
  116. package/dist/keys-seed.d.ts.map +1 -0
  117. package/dist/keys-seed.js +133 -0
  118. package/dist/keys-seed.js.map +1 -0
  119. package/dist/log-memory.d.ts +12 -0
  120. package/dist/log-memory.d.ts.map +1 -0
  121. package/dist/log-memory.js +101 -0
  122. package/dist/log-memory.js.map +1 -0
  123. package/dist/membership/holder.d.ts +35 -0
  124. package/dist/membership/holder.d.ts.map +1 -0
  125. package/dist/membership/holder.js +852 -0
  126. package/dist/membership/holder.js.map +1 -0
  127. package/dist/membership.d.ts +92 -0
  128. package/dist/membership.d.ts.map +1 -0
  129. package/dist/membership.js +157 -0
  130. package/dist/membership.js.map +1 -0
  131. package/dist/ports.d.ts +237 -0
  132. package/dist/ports.d.ts.map +1 -0
  133. package/dist/ports.js +2 -0
  134. package/dist/ports.js.map +1 -0
  135. package/dist/profile.d.ts +56 -0
  136. package/dist/profile.d.ts.map +1 -0
  137. package/dist/profile.js +193 -0
  138. package/dist/profile.js.map +1 -0
  139. package/dist/prompts.d.ts +14 -0
  140. package/dist/prompts.d.ts.map +1 -0
  141. package/dist/prompts.js +69 -0
  142. package/dist/prompts.js.map +1 -0
  143. package/dist/receive.d.ts +133 -0
  144. package/dist/receive.d.ts.map +1 -0
  145. package/dist/receive.js +298 -0
  146. package/dist/receive.js.map +1 -0
  147. package/dist/results.d.ts +158 -0
  148. package/dist/results.d.ts.map +1 -0
  149. package/dist/results.js +21 -0
  150. package/dist/results.js.map +1 -0
  151. package/dist/schemas.d.ts.map +1 -1
  152. package/dist/schemas.js +42 -38
  153. package/dist/schemas.js.map +1 -1
  154. package/dist/sha256.d.ts +3 -0
  155. package/dist/sha256.d.ts.map +1 -0
  156. package/dist/sha256.js +69 -0
  157. package/dist/sha256.js.map +1 -0
  158. package/dist/star.d.ts +31 -0
  159. package/dist/star.d.ts.map +1 -0
  160. package/dist/star.js +56 -0
  161. package/dist/star.js.map +1 -0
  162. package/dist/state-builder.d.ts +42 -0
  163. package/dist/state-builder.d.ts.map +1 -0
  164. package/dist/state-builder.js +100 -0
  165. package/dist/state-builder.js.map +1 -0
  166. package/dist/state.d.ts +853 -0
  167. package/dist/state.d.ts.map +1 -0
  168. package/dist/state.js +36 -0
  169. package/dist/state.js.map +1 -0
  170. package/dist/store-contract.d.ts +11 -0
  171. package/dist/store-contract.d.ts.map +1 -0
  172. package/dist/store-contract.js +189 -0
  173. package/dist/store-contract.js.map +1 -0
  174. package/dist/store-memory.d.ts +8 -0
  175. package/dist/store-memory.d.ts.map +1 -0
  176. package/dist/store-memory.js +17 -0
  177. package/dist/store-memory.js.map +1 -0
  178. package/dist/testing.d.ts +22 -0
  179. package/dist/testing.d.ts.map +1 -0
  180. package/dist/testing.js +21 -0
  181. package/dist/testing.js.map +1 -0
  182. package/dist/trust/continuity.d.ts +71 -0
  183. package/dist/trust/continuity.d.ts.map +1 -0
  184. package/dist/trust/continuity.js +1216 -0
  185. package/dist/trust/continuity.js.map +1 -0
  186. package/dist/trust/duties.d.ts +67 -0
  187. package/dist/trust/duties.d.ts.map +1 -0
  188. package/dist/trust/duties.js +179 -0
  189. package/dist/trust/duties.js.map +1 -0
  190. package/dist/trust/entries.d.ts +139 -0
  191. package/dist/trust/entries.d.ts.map +1 -0
  192. package/dist/trust/entries.js +694 -0
  193. package/dist/trust/entries.js.map +1 -0
  194. package/dist/trust/group-star.d.ts +43 -0
  195. package/dist/trust/group-star.d.ts.map +1 -0
  196. package/dist/trust/group-star.js +188 -0
  197. package/dist/trust/group-star.js.map +1 -0
  198. package/dist/trust/mapping.d.ts +56 -0
  199. package/dist/trust/mapping.d.ts.map +1 -0
  200. package/dist/trust/mapping.js +584 -0
  201. package/dist/trust/mapping.js.map +1 -0
  202. package/dist/trust/profile.d.ts +25 -0
  203. package/dist/trust/profile.d.ts.map +1 -0
  204. package/dist/trust/profile.js +249 -0
  205. package/dist/trust/profile.js.map +1 -0
  206. package/dist/trust/star.d.ts +208 -0
  207. package/dist/trust/star.d.ts.map +1 -0
  208. package/dist/trust/star.js +588 -0
  209. package/dist/trust/star.js.map +1 -0
  210. package/dist/trust/state.d.ts +83 -0
  211. package/dist/trust/state.d.ts.map +1 -0
  212. package/dist/trust/state.js +154 -0
  213. package/dist/trust/state.js.map +1 -0
  214. package/dist/util-freeze.d.ts +3 -0
  215. package/dist/util-freeze.d.ts.map +1 -0
  216. package/dist/util-freeze.js +11 -0
  217. package/dist/util-freeze.js.map +1 -0
  218. package/dist/views.d.ts +147 -0
  219. package/dist/views.d.ts.map +1 -0
  220. package/dist/views.js +268 -0
  221. package/dist/views.js.map +1 -0
  222. package/dist/visibility.d.ts +5 -5
  223. package/dist/visibility.d.ts.map +1 -1
  224. package/dist/visibility.js +22 -28
  225. package/dist/visibility.js.map +1 -1
  226. package/dist/wire.d.ts +178 -84
  227. package/dist/wire.d.ts.map +1 -1
  228. package/dist/wire.js.map +1 -1
  229. package/package.json +58 -11
  230. package/schemas/holder-state-v1.schema.json +3880 -0
  231. package/dist/holder.d.ts.map +0 -1
  232. package/dist/holder.js.map +0 -1
  233. package/dist/probe/acks.d.ts +0 -27
  234. package/dist/probe/acks.d.ts.map +0 -1
  235. package/dist/probe/acks.js +0 -152
  236. package/dist/probe/acks.js.map +0 -1
  237. package/dist/probe/continuity.d.ts +0 -20
  238. package/dist/probe/continuity.d.ts.map +0 -1
  239. package/dist/probe/continuity.js +0 -805
  240. package/dist/probe/continuity.js.map +0 -1
  241. package/dist/probe/deps.d.ts +0 -61
  242. package/dist/probe/deps.d.ts.map +0 -1
  243. package/dist/probe/deps.js +0 -54
  244. package/dist/probe/deps.js.map +0 -1
  245. package/dist/probe/encounter.d.ts +0 -26
  246. package/dist/probe/encounter.d.ts.map +0 -1
  247. package/dist/probe/encounter.js +0 -938
  248. package/dist/probe/encounter.js.map +0 -1
  249. package/dist/probe/introduce.d.ts +0 -70
  250. package/dist/probe/introduce.d.ts.map +0 -1
  251. package/dist/probe/introduce.js +0 -295
  252. package/dist/probe/introduce.js.map +0 -1
  253. package/dist/probe/membership.d.ts +0 -90
  254. package/dist/probe/membership.d.ts.map +0 -1
  255. package/dist/probe/membership.js +0 -450
  256. package/dist/probe/membership.js.map +0 -1
  257. package/dist/probe/trust.d.ts +0 -156
  258. package/dist/probe/trust.d.ts.map +0 -1
  259. package/dist/probe/trust.js +0 -1233
  260. package/dist/probe/trust.js.map +0 -1
  261. package/dist/probe.d.ts +0 -7
  262. package/dist/probe.d.ts.map +0 -1
  263. package/dist/probe.js +0 -42
  264. package/dist/probe.js.map +0 -1
@@ -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