@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,662 @@
1
+ // encounter/ceremony — the Encounter ceremony on the holder (lib Stage 2,
2
+ // PR 7a; Encounter 0.31 §5–§7 on Delivery 0.81 §4.1–4.3). Graduated from
3
+ // probe/encounter.ts (show, scan, counter, captureSentCard,
4
+ // receiveEncounter, resolve) onto HolderState and the engine; the probe
5
+ // stays as it is for the simulators until PR 7b. The pure checks are the
6
+ // root module's (encounter.ts).
7
+ //
8
+ // What it registers (encounterModule):
9
+ // human acts encounter.show a displayed card under a fresh pair
10
+ // label with a fresh own challenge (5.3):
11
+ // the label and its relationship nonce
12
+ // into the register before display (4110,
13
+ // Identity 6.1), the key's lastShown in
14
+ // `addresses` (Delivery §5), the challenge
15
+ // held open; event encounter.shown carries
16
+ // the card, the optical payload
17
+ // encounter.scan the scanner's step (5.8 trunk 2–3):
18
+ // the displayed card checked, a fresh
19
+ // label and own challenge c_A, the record
20
+ // (5.5), the step credential and the sent
21
+ // card, the fresh tuple (Trust openTuple:
22
+ // its prior candidates frozen, its
23
+ // continuity probe pending, 6820) and the
24
+ // bundle into the outbox, in one commit;
25
+ // event encounter.scanned carries the
26
+ // sent card for the optical leg
27
+ // encounter.capture the optical leg (5.8, 5710): a sent
28
+ // card presented to the displayer
29
+ // encounter.counter the counter-step (5.8 step 4)
30
+ // tasks encounter-bundle/0.1 (proof absent, Delivery 4.1; channel
31
+ // binding: the bundle establishes the tuple) and
32
+ // encounter-credential-delivery/0.1 (proof absent, 4.3;
33
+ // the tuple binding); both signature-class: their acks are
34
+ // signed (receive.ts)
35
+ // decision encounter-confirm: accept issues the counter-step, decline
36
+ // closes the prompt (the edge stays one-sided, 5750)
37
+ // retain tick drops the challenges past their age bound (5200,
38
+ // 5210, 5230)
39
+ //
40
+ // The commit discipline of the probe (prepare, verify, commit under a
41
+ // lock set) is the engine's: preparation is async against a snapshot,
42
+ // apply is synchronous on that same snapshot, and the commit is a CAS on
43
+ // its version (engine.ts). A state that moved is a CAS conflict and the
44
+ // receive re-enters at stage 1, which re-checks the effect cache at stage
45
+ // 4 (Delivery 6.2's waiter rule). So the provisional resolution of 4.1
46
+ // and the authoritative one coincide: apply resolves once, on the
47
+ // snapshot the commit lands on, and selects the branch.
48
+ //
49
+ // Choices this file makes where spec and note leave them open:
50
+ // - ids: the contact and the relationship of a ceremony-born tuple are
51
+ // named after the own fresh pair label, `c-<digest>` and `r-<digest>`
52
+ // (the label's digest component): unique by freshness (4.4)
53
+ // - the aging latch (5260, 5270): an aged value is removed (5210, 5230:
54
+ // no challenge history beyond open values and records), so it never
55
+ // resolves open again. tick removes every aged value; a bundle or an
56
+ // optical card refused because its authoritative resolution found its
57
+ // bound value aged commits that removal and the raised lastWhen even
58
+ // though it refuses — the Encounter latch exception of the engine
59
+ // (receive.ts TaskRefusal.latch): no cache entry, no ack, no journal
60
+ // - the scanner's own challenge c_A goes straight to its record: the
61
+ // record and the issuance commit together (5630: record, then issue);
62
+ // nothing is released before the commit, so "record before issuing"
63
+ // holds and no resumable half-state exists (the probe's bundlePending)
64
+ // - the bundle's effect on `open` creates the record, accepts the
65
+ // credential, opens the fresh tuple and the confirm prompt in one
66
+ // commit; the prompt is "arrival is not acceptance: this device
67
+ // decides" — accept issues the counter-step. It expires when the
68
+ // counter credential could no longer be accepted: the end of the
69
+ // issuance window of the challenge it binds, c_A (5.6 step 6)
70
+ // - the credential delivery's defined effect (4.3, buffering) and the
71
+ // 5.6 acceptance commit together; the ack does not depend on the
72
+ // acceptance's outcome and the outcome is never signaled (7.4): the
73
+ // document is acknowledged and its thread reserved either way
74
+ // - encounter.counter picks the newest record of the contact on the
75
+ // displayer side whose own credential is not issued; a new issuance
76
+ // needs an active tuple (tuple-deactivated), its document waits for
77
+ // the bundle's thread where the record arose on the optical leg (5780)
78
+ // and goes out with the bundle's record-aware effect
79
+ // - encounter.capture of a card already recorded with the same material
80
+ // is an idempotent success (5430): the act commits its journal entry only
81
+ // - no automatic optical switch (5690 RECOMMENDS ack-wait as its
82
+ // trigger): the app shows the sent card (encounter.scanned) when it
83
+ // sees the bundle undelivered (deliveryStatus)
84
+ import { b64uOf, iso, jcs, makeValidator, sameDigest } from '../core.js';
85
+ import { diSignWith, diVerify, digestBytes, digestDoc } from '../crypto.js';
86
+ import { seal } from '../delivery.js';
87
+ import { BUNDLE_TYPE, CEREMONY, CHALLENGE_MAX_AGE, SKEW_TOLERANCE, DELIVERY_TYPE, acceptTail, bundleForm, cardBody, cardOk, challengeAged, challengeOf, credHead, credentialBody, deliveryForm, dispositionOf, futureOk, issuanceEnd, issuanceOpen, windowOk, } from '../encounter.js';
88
+ import { SCHEMAS } from '../schemas.js';
89
+ import { withPairNonce } from '../identity-state.js';
90
+ import { tupleByRkid } from '../receive.js';
91
+ import { openTuple } from '../trust/continuity.js';
92
+ import { entryOf, findContact, tupleByLabel } from '../trust/state.js';
93
+ export { BUNDLE_TYPE, DELIVERY_TYPE };
94
+ /** The command names of the human acts. */
95
+ export const ENCOUNTER_COMMANDS = Object.freeze({
96
+ show: 'encounter.show',
97
+ scan: 'encounter.scan',
98
+ capture: 'encounter.capture',
99
+ counter: 'encounter.counter',
100
+ });
101
+ const V = makeValidator(SCHEMAS);
102
+ const schemaOk = (file, data) => V.validate(data, SCHEMAS[file], SCHEMAS[file]).length === 0;
103
+ const refuse = (reason, ref) => ({ ok: false, reason, ...(ref ? { ref } : {}) });
104
+ const no = (code, ref) => ({ ok: false, code, ...(ref ? { ref } : {}) });
105
+ const isRefusal = (x) => x?.ok === false;
106
+ /** The contact and relationship ids of the tuple under an own pair label. */
107
+ export const idsOf = (label) => {
108
+ const d = label.slice('pair/'.length);
109
+ return { contactId: 'c-' + d, relId: 'r-' + d };
110
+ };
111
+ /** The prompt that asks to confirm the enactment of record `own`. */
112
+ export const confirmPromptId = (own) => 'encounter:' + own;
113
+ /** The own-challenge resolution (5240): recorded, open, else unknown. */
114
+ export function resolve(state, value, when) {
115
+ if (Object.hasOwn(state.encounter.records, value))
116
+ return 'recorded';
117
+ const c = Object.hasOwn(state.encounter.challenges, value) ? state.encounter.challenges[value] : undefined;
118
+ return !c || challengeAged(c.tCh, when) ? 'unknown' : 'open';
119
+ }
120
+ /** The aging latch (5260, 5270): where a resolution finds a held value
121
+ * past its age bound, the state with that value removed — committed even
122
+ * by a refusal (receive.ts, the Encounter latch exception), so a clock
123
+ * that moves back never resolves it open again; else undefined. */
124
+ function latchOf(state, value, when) {
125
+ const c = Object.hasOwn(state.encounter.challenges, value) ? state.encounter.challenges[value] : undefined;
126
+ if (!c || !challengeAged(c.tCh, when) || Object.hasOwn(state.encounter.records, value))
127
+ return undefined;
128
+ const challenges = { ...state.encounter.challenges };
129
+ delete challenges[value];
130
+ return { ...state, encounter: { ...state.encounter, challenges } };
131
+ }
132
+ /** 5170: a challenge value consumed or held — on either side of any
133
+ * record, or among the own open challenges — enters no new enactment. */
134
+ const challengeUsed = (state, value) => Object.hasOwn(state.encounter.challenges, value) || Object.values(state.encounter.records).some((r) => r.own === value || r.peer === value);
135
+ const facts = (r) => ({ counterpart: r.counterpart, ceremony: r.ceremony, tCh: r.tCh, bind: r.bind });
136
+ /** Is the thread used: by a record (its bundle's or a later one) or the outbox? (Delivery 3, 4.1) */
137
+ function threadInUse(state, id) {
138
+ for (const r of Object.values(state.encounter.records))
139
+ if (r.thread === id || Object.hasOwn(r.threads, id))
140
+ return true;
141
+ return Object.values(state.outbox).some((o) => o.threadId === id);
142
+ }
143
+ /** The original bundle of a thread, re-evaluated (4.2: after the cache's
144
+ * retention) or re-issued on its own fresh thread: not a new document on
145
+ * a used thread. */
146
+ function sameOpeningBundle(state, anchor, threadId, digest) {
147
+ for (const r of Object.values(state.encounter.records)) {
148
+ if (r.counterpart !== anchor)
149
+ continue;
150
+ if (r.thread === threadId && r.bundleDigest !== null && sameDigest(r.bundleDigest, digest))
151
+ return true;
152
+ if (Object.hasOwn(r.threads, threadId) && sameDigest(r.threads[threadId], digest))
153
+ return true;
154
+ }
155
+ return false;
156
+ }
157
+ /** The record of the tuple under an own pair label. */
158
+ const recordOfLabel = (state, label) => Object.values(state.encounter.records).find((r) => r.label === label);
159
+ const withRecord = (state, r) => ({ ...state, encounter: { ...state.encounter, records: { ...state.encounter.records, [r.own]: r } } });
160
+ /** The entry a record's tuple feeds, and its relationship (by the tuple: a
161
+ * chaining may have moved it, 6220). */
162
+ function placeOf(state, r) {
163
+ const at = tupleByLabel(state, r.label);
164
+ if (!at)
165
+ return { contactId: idsOf(r.label).contactId, relationshipId: r.relId };
166
+ return { contactId: entryOf(state, at.rel)?.id ?? at.tuple.entryId, relationshipId: at.rel.id };
167
+ }
168
+ const sealWith = (ctx, doc, keyAgreement) => seal(doc, keyAgreement, { ephSeed: ctx.entropy.bytes('seal.eph', 32), nonce: ctx.entropy.bytes('seal.nonce', 12) }, ctx.crypto.subtle);
169
+ const signer = (ctx, label) => (b) => ctx.keys.sign(label, b);
170
+ /** The display name a card carries: the input's, else the own profile's, at most 200 characters (6090). */
171
+ const nameFor = (state, name) => {
172
+ const n = typeof name === 'string' ? name : state.profile.body?.name;
173
+ return typeof n === 'string' && n.length ? [...n].slice(0, 200).join('') : undefined;
174
+ };
175
+ /** A fresh pair label from 32 fresh nonce bytes (Identity 6.1, Encounter 4.4). */
176
+ async function freshLabel(ctx) {
177
+ const n = ctx.entropy.bytes('founding.nonce', 32);
178
+ return { label: 'pair/' + await digestBytes(n, ctx.crypto.subtle), nonce: b64uOf(n) };
179
+ }
180
+ const showDef = {
181
+ human: true,
182
+ async prepare(ctx, input) {
183
+ const { label, nonce } = await freshLabel(ctx);
184
+ const own = await ctx.keys.anchorOf(label);
185
+ const value = challengeOf(ctx.entropy.bytes('challenge', 17));
186
+ const at = iso(ctx.when);
187
+ const name = nameFor(ctx.snapshot, input?.name);
188
+ const body = cardBody(own, { ...(name !== undefined ? { name } : {}), challenge: { value, issuedAt: at } });
189
+ const card = await diSignWith(own.anchor, signer(ctx, label), body, at, ctx.crypto.subtle);
190
+ return { label, nonce, rkid: own.keyAgreement, value, card };
191
+ },
192
+ apply(state, prepared, _input, when) {
193
+ const p = prepared.value;
194
+ // fresh-always (4.4, 4150) and single use (5140): fresh entropy never
195
+ // repeats; a repeat is a broken entropy source, not a human's refusal
196
+ if (state.register.labels.includes(p.label) || tupleByLabel(state, p.label))
197
+ throw new Error(`encounter.show: the pair label ${p.label} is not fresh`);
198
+ if (resolve(state, p.value, when) !== 'unknown' || Object.hasOwn(state.encounter.challenges, p.value))
199
+ throw new Error('encounter.show: the challenge value is not fresh');
200
+ return {
201
+ state: {
202
+ ...state,
203
+ // 4110: the label in the register before the card is displayed
204
+ register: withPairNonce(state.register, p.label, p.nonce),
205
+ addresses: { ...state.addresses, keys: { ...state.addresses.keys, [p.rkid]: { label: p.label, lastShown: when } } },
206
+ encounter: { ...state.encounter, challenges: { ...state.encounter.challenges, [p.value]: { value: p.value, tCh: when, label: p.label, card: p.card } } },
207
+ },
208
+ events: [{ type: 'encounter.shown', challenge: p.value, card: p.card }],
209
+ };
210
+ },
211
+ };
212
+ const scanDef = {
213
+ human: true,
214
+ async prepare(ctx, input) {
215
+ const peer = input?.card;
216
+ if ((await cardOk(peer, ctx.crypto.subtle)) !== null)
217
+ return no('card-invalid', 'RLTP-ENC-6010');
218
+ // a displayed card carries neither sentTo nor boundTo (6070)
219
+ if (peer.sentTo !== undefined || peer.boundTo !== undefined)
220
+ return no('card-invalid', 'RLTP-ENC-6070');
221
+ const S = ctx.crypto.subtle;
222
+ const { label, nonce } = await freshLabel(ctx);
223
+ const me = await ctx.keys.anchorOf(label);
224
+ // c_A: fresh, created now, never the displayed value (5150, 5160)
225
+ let own = challengeOf(ctx.entropy.bytes('challenge', 17));
226
+ for (let i = 0; own === peer.challenge.value && i < 4; i++)
227
+ own = challengeOf(ctx.entropy.bytes('challenge', 17));
228
+ if (own === peer.challenge.value)
229
+ throw new Error('encounter.scan: no fresh challenge distinct from the displayed one');
230
+ const bind = await digestDoc({ ceremony: CEREMONY, challenges: [own, peer.challenge.value].sort() }, S);
231
+ const at = iso(ctx.when);
232
+ const sign = signer(ctx, label);
233
+ const credential = await diSignWith(me.anchor, sign, credentialBody(me.anchor, peer.anchor, CEREMONY, peer.challenge.value, bind, at), at, S);
234
+ const name = nameFor(ctx.snapshot, input?.name);
235
+ const sent = cardBody(me, { ...(name !== undefined ? { name } : {}), challenge: { value: own, issuedAt: at }, sentTo: peer.anchor, boundTo: peer.challenge.value });
236
+ const sentCard = await diSignWith(me.anchor, sign, sent, at, S);
237
+ const threadId = ctx.entropy.id('thread.id');
238
+ const doc = { id: ctx.entropy.id('doc.id'), type: BUNDLE_TYPE, issuer: me.anchor, recipient: peer.anchor, threadId, issuedAt: at, payload: { card: sentCard, credential } };
239
+ return {
240
+ peer, label, nonce, rkid: me.keyAgreement, own, bind, threadId, credential, credDigest: await digestDoc(credential, S), sentCard,
241
+ doc: { digest: await digestDoc(doc, S), envelope: await sealWith(ctx, doc, peer.keyAgreement) },
242
+ };
243
+ },
244
+ apply(state, prepared, _input, when) {
245
+ const p = prepared.value;
246
+ if (isRefusal(p))
247
+ return p;
248
+ const peerValue = p.peer.challenge.value;
249
+ // 5.3 single use from the scanner's side: a displayed value already in
250
+ // one of the own records never enters a second enactment (5170)
251
+ if (challengeUsed(state, peerValue))
252
+ return no('already-scanned', 'RLTP-ENC-5170');
253
+ if (state.register.labels.includes(p.label) || tupleByLabel(state, p.label))
254
+ throw new Error(`encounter.scan: the pair label ${p.label} is not fresh`);
255
+ if (resolve(state, p.own, when) !== 'unknown')
256
+ throw new Error('encounter.scan: the own challenge is not fresh');
257
+ if (threadInUse(state, p.threadId))
258
+ throw new Error('encounter.scan: the thread is not fresh');
259
+ // the credential binds c_B: issued only inside its window (5.6 step 6)
260
+ if (!issuanceOpen(Date.parse(p.peer.challenge.issuedAt), when))
261
+ return no('stale-issuance', 'RLTP-ENC-5530');
262
+ const ids = idsOf(p.label);
263
+ const opened = openTuple(state, {
264
+ relId: ids.relId, contactId: ids.contactId, ownPairLabel: p.label, counterpartAnchor: p.peer.anchor,
265
+ counterpartKeyAgreement: p.peer.keyAgreement, provenance: { kind: 'ceremony' }, nonce: p.nonce,
266
+ }, p.rkid, when);
267
+ const record = {
268
+ ceremony: CEREMONY, own: p.own, tCh: when, peer: peerValue, counterpart: p.peer.anchor, card: p.peer, bind: p.bind,
269
+ at: when, label: p.label, relId: ids.relId, role: 'scanner', out: { digest: p.credDigest, credential: p.credential }, in: null,
270
+ thread: p.threadId, bundleDigest: p.doc.digest, threads: {}, sentCard: p.sentCard, counterPending: false,
271
+ };
272
+ return {
273
+ state: withRecord(opened.state, record),
274
+ outbound: [{
275
+ digest: p.doc.digest, envelope: p.doc.envelope, type: BUNDLE_TYPE, threadId: p.threadId,
276
+ recipient: { contactId: ids.contactId, rkid: p.rkid, keyAgreement: p.peer.keyAgreement },
277
+ }],
278
+ events: [
279
+ ...(opened.events ?? []),
280
+ { type: 'encounter.recorded', contactId: ids.contactId, relationshipId: ids.relId, role: 'scanner' },
281
+ { type: 'encounter.scanned', contactId: ids.contactId, card: p.sentCard },
282
+ ],
283
+ duties: opened.duties,
284
+ };
285
+ },
286
+ };
287
+ const captureDef = {
288
+ human: true,
289
+ async prepare(ctx, input) {
290
+ const card = input?.card;
291
+ if ((await cardOk(card, ctx.crypto.subtle)) !== null)
292
+ return no('card-invalid', 'RLTP-ENC-6010');
293
+ if (typeof card.sentTo !== 'string' || typeof card.boundTo !== 'string')
294
+ return no('card-invalid', 'RLTP-ENC-6030');
295
+ if (card.challenge.value === card.boundTo)
296
+ return no('card-invalid', 'RLTP-ENC-5160');
297
+ // sentTo = the own anchor that displayed boundTo (6050, 4.4)
298
+ const s = ctx.snapshot.encounter;
299
+ const label = Object.hasOwn(s.records, card.boundTo) ? s.records[card.boundTo].label
300
+ : Object.hasOwn(s.challenges, card.boundTo) ? s.challenges[card.boundTo].label : null;
301
+ if (label !== null && (await ctx.keys.anchorOf(label)).anchor !== card.sentTo)
302
+ return no('card-invalid', 'RLTP-ENC-6050');
303
+ return { card, label: label ?? '', bind: await digestDoc({ ceremony: CEREMONY, challenges: [card.boundTo, card.challenge.value].sort() }, ctx.crypto.subtle) };
304
+ },
305
+ apply(state, prepared, _input, when) {
306
+ const p = prepared.value;
307
+ if (isRefusal(p))
308
+ return p;
309
+ const { card } = p;
310
+ const st = resolve(state, card.boundTo, when);
311
+ if (st === 'unknown') {
312
+ const latch = latchOf(state, card.boundTo, when);
313
+ return { ...no('gate-expired', 'RLTP-ENC-5460'), ...(latch ? { latch } : {}) };
314
+ }
315
+ if (st === 'recorded') {
316
+ // the taxonomy of 5.5 (5430–5450)
317
+ const r = state.encounter.records[card.boundTo];
318
+ if (r.counterpart !== card.anchor)
319
+ return no('consumed-challenge', 'RLTP-ENC-5440');
320
+ if (jcs(r.card) !== jcs(card))
321
+ return no('card-mismatch', 'RLTP-ENC-5450');
322
+ return { state: state };
323
+ }
324
+ const ch = state.encounter.challenges[card.boundTo];
325
+ if (challengeUsed(state, card.challenge.value))
326
+ return no('consumed-challenge', 'RLTP-ENC-5170');
327
+ if (!futureOk(ch.tCh, when))
328
+ return no('gate-future', 'RLTP-ENC-5390');
329
+ return recordCreating(state, { label: ch.label, own: ch.value, tCh: ch.tCh, card, bind: p.bind, rkid: rkidOf(state, ch.label), when, in: null, thread: null, digest: null });
330
+ },
331
+ };
332
+ /** The own key-agreement id a label derives under, from `addresses`. */
333
+ function rkidOf(state, label) {
334
+ for (const [rkid, k] of Object.entries(state.addresses.keys))
335
+ if (k.label === label)
336
+ return rkid;
337
+ throw new Error(`encounter: no address key for ${label}`);
338
+ }
339
+ /** The displayer's record-creating effect (5.5, 5650, 5710): the record,
340
+ * the fresh tuple, the challenge superseded (5220), the confirm prompt. */
341
+ function recordCreating(state, x) {
342
+ const ids = idsOf(x.label);
343
+ const nonce = Object.hasOwn(state.register.pairNonces, x.label) ? state.register.pairNonces[x.label] : undefined;
344
+ const opened = openTuple(state, {
345
+ relId: ids.relId, contactId: ids.contactId, ownPairLabel: x.label, counterpartAnchor: x.card.anchor,
346
+ counterpartKeyAgreement: x.card.keyAgreement, provenance: { kind: 'ceremony' }, ...(nonce !== undefined ? { nonce } : {}),
347
+ }, x.rkid, x.when);
348
+ const record = {
349
+ ceremony: CEREMONY, own: x.own, tCh: x.tCh, peer: x.card.challenge.value, counterpart: x.card.anchor, card: x.card, bind: x.bind,
350
+ at: x.when, label: x.label, relId: ids.relId, role: 'displayer', out: null, in: x.in, thread: x.thread, bundleDigest: x.digest,
351
+ threads: {}, sentCard: null, counterPending: false,
352
+ };
353
+ const challenges = { ...opened.state.encounter.challenges };
354
+ delete challenges[x.own];
355
+ const prompt = {
356
+ id: confirmPromptId(x.own), kind: 'encounter-confirm', contactId: ids.contactId, openedAt: x.when,
357
+ // the counter credential binds c_A: acceptable only inside its window (5.6 step 6)
358
+ expiresAt: issuanceEnd(Date.parse(x.card.challenge.issuedAt)),
359
+ };
360
+ const next = withRecord({ ...opened.state, encounter: { ...opened.state.encounter, challenges } }, record);
361
+ return {
362
+ state: { ...next, prompts: { ...next.prompts, [prompt.id]: prompt } },
363
+ events: [
364
+ ...(opened.events ?? []),
365
+ { type: 'encounter.recorded', contactId: ids.contactId, relationshipId: ids.relId, role: 'displayer' },
366
+ ...(x.in ? [{ type: 'encounter.accepted', contactId: ids.contactId, relationshipId: ids.relId, mutual: false }] : []),
367
+ { type: 'prompt.opened', promptId: prompt.id, kind: 'encounter-confirm' },
368
+ ],
369
+ duties: opened.duties,
370
+ };
371
+ }
372
+ /** The counter-step's delivery document in the bundle's thread (4.3, 5760). */
373
+ async function counterDoc(ctx, r, issuer, credential, threadId) {
374
+ const doc = {
375
+ id: ctx.entropy.id('doc.id'), type: DELIVERY_TYPE, issuer, recipient: r.counterpart, threadId, issuedAt: iso(ctx.when),
376
+ ceremony: { step: 'counter', enactment: r.bind }, payload: { credential },
377
+ };
378
+ return { digest: await digestDoc(doc, ctx.crypto.subtle), envelope: await sealWith(ctx, doc, r.card.keyAgreement), threadId };
379
+ }
380
+ /** The displayer's record of a contact the counter-step issues on: the
381
+ * newest whose own credential is not issued. */
382
+ function counterRecord(state, contactId) {
383
+ const c = findContact(state, contactId);
384
+ if (!c)
385
+ return no('unknown-contact');
386
+ const ids = new Set([c.id, ...c.aliases]);
387
+ const mine = Object.values(state.encounter.records).filter((r) => r.role === 'displayer' && ids.has(placeOf(state, r).contactId));
388
+ if (!mine.length)
389
+ return no('no-record');
390
+ const open = mine.filter((r) => r.out === null).sort((a, b) => b.at - a.at);
391
+ return open.length ? open[0] : no('already-issued');
392
+ }
393
+ async function prepareCounter(ctx, r) {
394
+ const S = ctx.crypto.subtle;
395
+ const me = await ctx.keys.anchorOf(r.label);
396
+ const at = iso(ctx.when);
397
+ // the subject's challenge is c_A, the record's peer value (5320)
398
+ const credential = await diSignWith(me.anchor, signer(ctx, r.label), credentialBody(me.anchor, r.counterpart, CEREMONY, r.peer, r.bind, at), at, S);
399
+ return { own: r.own, credential, digest: await digestDoc(credential, S), doc: r.thread !== null ? await counterDoc(ctx, r, me.anchor, credential, r.thread) : null };
400
+ }
401
+ /** The counter-step's effect: the own credential committed (5770), its
402
+ * document out where the bundle's thread is known, else pending (5780);
403
+ * the confirm prompt of the record closes. */
404
+ function applyCounter(state, p, when, closePrompt) {
405
+ if (isRefusal(p))
406
+ return p;
407
+ const r = state.encounter.records[p.own];
408
+ if (!r || r.role !== 'displayer')
409
+ return no('no-record');
410
+ if (r.out !== null)
411
+ return no('already-issued');
412
+ const at = tupleByLabel(state, r.label);
413
+ if (!at || at.tuple.deactivated)
414
+ return no('tuple-deactivated');
415
+ if (!issuanceOpen(Date.parse(r.card.challenge.issuedAt), when))
416
+ return no('stale-issuance', 'RLTP-ENC-5530');
417
+ if ((p.doc === null) !== (r.thread === null))
418
+ throw new Error('encounter.counter: prepared on another thread state');
419
+ const place = placeOf(state, r);
420
+ let next = withRecord(state, { ...r, out: { digest: p.digest, credential: p.credential }, counterPending: p.doc === null });
421
+ const events = [];
422
+ const pid = confirmPromptId(r.own);
423
+ if (closePrompt && Object.hasOwn(next.prompts, pid)) {
424
+ const prompts = { ...next.prompts };
425
+ delete prompts[pid];
426
+ next = { ...next, prompts };
427
+ events.push({ type: 'prompt.decided', promptId: pid, kind: 'encounter-confirm', choice: 'accept' });
428
+ }
429
+ return {
430
+ state: next,
431
+ outbound: p.doc ? [{ digest: p.doc.digest, envelope: p.doc.envelope, type: DELIVERY_TYPE, threadId: p.doc.threadId, recipient: { contactId: place.contactId, rkid: at.tuple.rkid, keyAgreement: at.tuple.counterpartKeyAgreement } }] : [],
432
+ events,
433
+ };
434
+ }
435
+ const counterDef = {
436
+ human: true,
437
+ async prepare(ctx, input) {
438
+ const r = counterRecord(ctx.snapshot, input?.contactId);
439
+ return isRefusal(r) ? r : prepareCounter(ctx, r);
440
+ },
441
+ apply: (state, prepared, _input, when) => applyCounter(state, prepared.value, when, true),
442
+ };
443
+ /** encounter-confirm: accept issues the counter-step; decline only closes. */
444
+ const confirmDecision = {
445
+ async prepare(ctx, prompt, choice) {
446
+ if (choice !== 'accept')
447
+ return null;
448
+ const r = ctx.snapshot.encounter.records[prompt.id.slice('encounter:'.length)];
449
+ return r ? prepareCounter(ctx, r) : no('no-record');
450
+ },
451
+ apply(state, prompt, choice, when, prepared) {
452
+ if (choice === 'decline')
453
+ return { state: state };
454
+ // the prompts module closes the prompt and announces the decision
455
+ return applyCounter(state, prepared.value ?? no('no-record'), when, false);
456
+ },
457
+ };
458
+ /** Delivery 4.1 checks 5–6 under a resolution that found the value held:
459
+ * the ceremony (5520), the enacting anchor (4100), the enactment binding
460
+ * (5560), the issuance window (5530); null when they pass. */
461
+ function checks56(state, credential, own, st, bind, rkid) {
462
+ const held = st === 'open' ? state.encounter.challenges[own] : state.encounter.records[own];
463
+ if (credential.credentialSubject.ceremony !== CEREMONY)
464
+ return refuse('validation-failed', 'RLTP-ENC-5520');
465
+ // the enacting anchor is the one that displayed the bound challenge (4.4)
466
+ if (held.label !== state.addresses.keys[rkid]?.label)
467
+ return refuse('validation-failed', 'RLTP-ENC-4100');
468
+ if (!sameDigest(credential.credentialSubject.enactmentBinding, bind))
469
+ return refuse('validation-failed', 'RLTP-ENC-5560');
470
+ if (!windowOk(credential, held.tCh))
471
+ return refuse('stale-issuance', 'RLTP-ENC-5530');
472
+ return null;
473
+ }
474
+ const bundleTask = {
475
+ proof: 'absent',
476
+ decode: (payload) => (schemaOk('payload-encounter-bundle.schema.json', payload) ? payload : refuse('malformed')),
477
+ // check 1 (structure, outer/inner, credential format), then the binding:
478
+ // the arrival tuple where the record exists and the issuer is its
479
+ // counterpart, else the bundle itself — it establishes the tuple (4.1),
480
+ // its issuer = card.anchor = credential.issuer
481
+ channel(state, doc, rkid) {
482
+ const f = bundleForm(doc);
483
+ if (f)
484
+ return refuse(f.reason);
485
+ const at = tupleByRkid(state, rkid);
486
+ if (at && doc.issuer === at.tuple.counterpartAnchor)
487
+ return { keyAgreement: at.tuple.counterpartKeyAgreement, contactId: entryOf(state, at.rel)?.id ?? at.rel.contactId };
488
+ // another issuer on a recorded display key: the record decides at stage
489
+ // 9 (consumed-challenge, 4.1); its ack is never produced
490
+ const k = state.addresses.keys[rkid];
491
+ if (!k || !k.label.startsWith('pair/'))
492
+ return refuse('validation-failed');
493
+ return { keyAgreement: doc.payload.card.keyAgreement, contactId: idsOf(k.label).contactId };
494
+ },
495
+ // checks 2–4: card proof, credential proof, addressee (no state touched);
496
+ // then check 5 the provisional resolution and, where it found the value
497
+ // held, checks 5–6 at stage 8 (an unknown value goes to stage 9)
498
+ async prepare(ctx, doc, arrival) {
499
+ const { card, credential } = doc.payload;
500
+ if (!(await diVerify(card, card.anchor, ctx.crypto.subtle)))
501
+ return refuse('validation-failed', 'RLTP-ENC-6020');
502
+ const h = await credHead(credential, doc.recipient, false, ctx.crypto.subtle);
503
+ if (h)
504
+ return refuse(dispositionOf(h), errRef(h));
505
+ const S = ctx.crypto.subtle;
506
+ const own = credential.credentialSubject.challenge;
507
+ const r = Object.hasOwn(ctx.snapshot.encounter.records, own) ? ctx.snapshot.encounter.records[own] : undefined;
508
+ // a counter-step issued on the optical leg waited for this thread (5780)
509
+ const counter = r && r.counterPending && r.out && r.counterpart === card.anchor
510
+ ? await counterDoc(ctx, r, doc.recipient, r.out.credential, r.thread ?? doc.threadId) : null;
511
+ const bind = await digestDoc({ ceremony: CEREMONY, challenges: [own, card.challenge.value].sort() }, S);
512
+ const st = resolve(ctx.snapshot, own, ctx.when);
513
+ if (st !== 'unknown') {
514
+ const bad = checks56(ctx.snapshot, credential, own, st, bind, arrival.rkid);
515
+ if (bad)
516
+ return bad;
517
+ }
518
+ return { bind, credDigest: await digestDoc(credential, S), counter, st };
519
+ },
520
+ // the authoritative resolution and the branch it selects (4.1); checks
521
+ // 5–6 again only where it differs from the provisional one
522
+ apply(state, prepared, doc, arrival) {
523
+ const p = prepared.value;
524
+ const { card, credential } = doc.payload;
525
+ const when = arrival.when;
526
+ const own = credential.credentialSubject.challenge;
527
+ const st = resolve(state, own, when);
528
+ if (st === 'unknown') {
529
+ const latch = latchOf(state, own, when);
530
+ return { ...refuse('validation-failed', 'RLTP-ENC-5240'), ...(latch ? { latch } : {}) };
531
+ }
532
+ const held = st === 'open' ? state.encounter.challenges[own] : state.encounter.records[own];
533
+ if (st !== p.st) {
534
+ const bad = checks56(state, credential, own, st, p.bind, arrival.rkid);
535
+ if (bad)
536
+ return bad;
537
+ }
538
+ if (threadInUse(state, doc.threadId) && !sameOpeningBundle(state, card.anchor, doc.threadId, arrival.digest))
539
+ return refuse('validation-failed');
540
+ if (st === 'open') {
541
+ if (challengeUsed(state, card.challenge.value))
542
+ return refuse('validation-failed', 'RLTP-ENC-5170');
543
+ if (!futureOk(held.tCh, when))
544
+ return refuse('gate-future', 'RLTP-ENC-5390');
545
+ return recordCreating(state, {
546
+ label: held.label, own, tCh: held.tCh, card, bind: p.bind, rkid: arrival.rkid, when,
547
+ in: { digest: p.credDigest, credential: credential }, thread: doc.threadId, digest: arrival.digest,
548
+ });
549
+ }
550
+ // recorded: the record decides (4.1)
551
+ const r = held;
552
+ if (r.counterpart !== card.anchor)
553
+ return refuse('consumed-challenge', 'RLTP-ENC-5440');
554
+ if (jcs(r.card) !== jcs(card))
555
+ return refuse('validation-failed', 'RLTP-ENC-5730');
556
+ const e = acceptTail(credential, facts(r), 'bundle');
557
+ if (e)
558
+ return refuse(dispositionOf(e), errRef(e));
559
+ if (r.in && !sameDigest(r.in.digest, p.credDigest))
560
+ return refuse('validation-failed', 'RLTP-ENC-5570');
561
+ // the record-aware effect: no gate, no second record (5740)
562
+ const thread = r.thread ?? doc.threadId;
563
+ const next = {
564
+ ...r,
565
+ in: r.in ?? { digest: p.credDigest, credential: credential },
566
+ thread,
567
+ bundleDigest: r.bundleDigest ?? arrival.digest,
568
+ threads: thread === doc.threadId ? r.threads : { ...r.threads, [doc.threadId]: arrival.digest },
569
+ counterPending: r.counterPending && !p.counter,
570
+ };
571
+ const place = placeOf(state, r);
572
+ const at = tupleByLabel(state, r.label);
573
+ const events = r.in ? [] : [{ type: 'encounter.accepted', contactId: place.contactId, relationshipId: place.relationshipId, mutual: r.out !== null }];
574
+ const outbound = p.counter && at
575
+ ? [{ digest: p.counter.digest, envelope: p.counter.envelope, type: DELIVERY_TYPE, threadId: p.counter.threadId, recipient: { contactId: place.contactId, rkid: at.tuple.rkid, keyAgreement: at.tuple.counterpartKeyAgreement } }]
576
+ : [];
577
+ return { state: withRecord(state, next), events, outbound };
578
+ },
579
+ };
580
+ const errRef = (e) => ({
581
+ ERR_VERSION: 'RLTP-ENC-5480', ERR_SIG: 'RLTP-ENC-5490', ERR_ADDRESSEE: 'RLTP-ENC-5500', ERR_NO_RECORD: 'RLTP-ENC-5510',
582
+ ERR_CEREMONY: 'RLTP-ENC-5520', ERR_STALE_ISSUANCE: 'RLTP-ENC-5530', ERR_BINDING: 'RLTP-ENC-5560', ERR_CONFLICT: 'RLTP-ENC-5570',
583
+ }[e]);
584
+ const deliveryTask = {
585
+ proof: 'absent',
586
+ // bound to the enactment record on its tuple (below): a late counter
587
+ // credential is taken against the record even where a chaining has
588
+ // deactivated that tuple since (4.2; encounter review 2/14); no new
589
+ // issuance goes out on it
590
+ pinned: true,
591
+ decode: (payload) => (schemaOk('payload-encounter-credential-delivery.schema.json', payload) ? payload : refuse('malformed')),
592
+ async prepare(ctx, doc, arrival) {
593
+ const f = deliveryForm(doc);
594
+ if (f)
595
+ return refuse('validation-failed', 'RLTP-ENC-5760');
596
+ // the relationship channel to the issuer: a record on the arrival tuple
597
+ const at = tupleByRkid(ctx.snapshot, arrival.rkid);
598
+ if (!at || !recordOfLabel(ctx.snapshot, at.tuple.ownPairLabel))
599
+ return refuse('validation-failed');
600
+ // 5.6 steps 1–3, no state touched; the rest runs in the transaction
601
+ return { verdict: await credHead(doc.payload.credential, doc.recipient, true, ctx.crypto.subtle), credDigest: await digestDoc(doc.payload.credential, ctx.crypto.subtle) };
602
+ },
603
+ apply(state, prepared, doc, arrival) {
604
+ const at = tupleByRkid(state, arrival.rkid);
605
+ const r = recordOfLabel(state, at.tuple.ownPairLabel);
606
+ // the thread rule of 4.3: counter in the bundle's thread, deliver outside it
607
+ const step = doc.ceremony?.step;
608
+ const inBundle = r.thread !== null && doc.threadId === r.thread;
609
+ if (step === 'counter' && !inBundle)
610
+ return refuse('validation-failed', 'RLTP-ENC-5760');
611
+ if (step === 'deliver' && inBundle)
612
+ return refuse('validation-failed');
613
+ if (!inBundle && threadInUse(state, doc.threadId))
614
+ return refuse('validation-failed');
615
+ // the defined effect: buffering (the thread reserved); acceptance (5.6)
616
+ // in the same transaction, its outcome never signaled (7.4)
617
+ const cred = doc.payload.credential;
618
+ const p = prepared.value;
619
+ const bound = Object.hasOwn(state.encounter.records, cred.credentialSubject.challenge) ? state.encounter.records[cred.credentialSubject.challenge] : undefined;
620
+ let e = p.verdict ?? acceptTail(cred, bound && bound.label === r.label ? facts(bound) : undefined, 'credential');
621
+ if (!e && r.in && !sameDigest(r.in.digest, p.credDigest))
622
+ e = 'ERR_CONFLICT';
623
+ let next = inBundle ? r : { ...r, threads: { ...r.threads, [doc.threadId]: arrival.digest } };
624
+ const events = [];
625
+ if (!e && !r.in) {
626
+ next = { ...next, in: { digest: p.credDigest, credential: cred } };
627
+ const place = placeOf(state, r);
628
+ events.push({ type: 'encounter.accepted', contactId: place.contactId, relationshipId: place.relationshipId, mutual: r.out !== null });
629
+ }
630
+ return { state: next === r ? state : withRecord(state, next), events };
631
+ },
632
+ };
633
+ // ── retention: the aging latch's end (5200–5230) ───────────────────────
634
+ function retainChallenges(state, when) {
635
+ const aged = Object.values(state.encounter.challenges).filter((c) => challengeAged(c.tCh, when));
636
+ if (!aged.length)
637
+ return state;
638
+ const challenges = { ...state.encounter.challenges };
639
+ for (const c of aged)
640
+ delete challenges[c.value];
641
+ return { ...state, encounter: { ...state.encounter, challenges } };
642
+ }
643
+ /** When retainChallenges drops an aged challenge (5.3): the first whole
644
+ * second past its maximum age and the skew (the timer contract, engine.ts). */
645
+ const challengeDeadlines = (state) => Object.values(state.encounter.challenges).map((c) => (Math.floor(c.tCh / 1000) + (CHALLENGE_MAX_AGE + SKEW_TOLERANCE) / 1000 + 1) * 1000);
646
+ /** The encounter module createHolder registers. */
647
+ export function encounterModule() {
648
+ return Object.freeze({
649
+ name: 'encounter',
650
+ commands: Object.freeze({
651
+ [ENCOUNTER_COMMANDS.show]: showDef,
652
+ [ENCOUNTER_COMMANDS.scan]: scanDef,
653
+ [ENCOUNTER_COMMANDS.capture]: captureDef,
654
+ [ENCOUNTER_COMMANDS.counter]: counterDef,
655
+ }),
656
+ tasks: Object.freeze({ [BUNDLE_TYPE]: bundleTask, [DELIVERY_TYPE]: deliveryTask }),
657
+ decisions: Object.freeze({ 'encounter-confirm': confirmDecision }),
658
+ retain: retainChallenges,
659
+ deadlines: challengeDeadlines,
660
+ });
661
+ }
662
+ //# sourceMappingURL=ceremony.js.map