@real-life/trust-protocol 0.3.2 → 0.5.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 (277) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +209 -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 +169 -0
  8. package/dist/adapters/vti-mediator.d.ts.map +1 -0
  9. package/dist/adapters/vti-mediator.js +659 -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 +54 -0
  29. package/dist/codec.d.ts.map +1 -0
  30. package/dist/codec.js +494 -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 +303 -0
  56. package/dist/engine.d.ts.map +1 -0
  57. package/dist/engine.js +1509 -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 +198 -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 +165 -0
  80. package/dist/holder-api.d.ts.map +1 -0
  81. package/dist/holder-api.js +142 -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/holder-state-v2.d.ts +858 -0
  88. package/dist/holder-state-v2.d.ts.map +1 -0
  89. package/dist/holder-state-v2.js +2 -0
  90. package/dist/holder-state-v2.js.map +1 -0
  91. package/dist/identity-state.d.ts +43 -0
  92. package/dist/identity-state.d.ts.map +1 -0
  93. package/dist/identity-state.js +345 -0
  94. package/dist/identity-state.js.map +1 -0
  95. package/dist/identity.d.ts +24 -2
  96. package/dist/identity.d.ts.map +1 -1
  97. package/dist/identity.js +70 -11
  98. package/dist/identity.js.map +1 -1
  99. package/dist/index.d.ts +27 -1
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +81 -29
  102. package/dist/index.js.map +1 -1
  103. package/dist/internal.d.ts +37 -0
  104. package/dist/internal.d.ts.map +1 -0
  105. package/dist/internal.js +22 -0
  106. package/dist/internal.js.map +1 -0
  107. package/dist/introduction/holder.d.ts +18 -0
  108. package/dist/introduction/holder.d.ts.map +1 -0
  109. package/dist/introduction/holder.js +764 -0
  110. package/dist/introduction/holder.js.map +1 -0
  111. package/dist/introduction.d.ts +101 -0
  112. package/dist/introduction.d.ts.map +1 -0
  113. package/dist/introduction.js +132 -0
  114. package/dist/introduction.js.map +1 -0
  115. package/dist/invariants.d.ts +43 -0
  116. package/dist/invariants.d.ts.map +1 -0
  117. package/dist/invariants.js +953 -0
  118. package/dist/invariants.js.map +1 -0
  119. package/dist/keys-seed.d.ts +7 -0
  120. package/dist/keys-seed.d.ts.map +1 -0
  121. package/dist/keys-seed.js +133 -0
  122. package/dist/keys-seed.js.map +1 -0
  123. package/dist/log-memory.d.ts +12 -0
  124. package/dist/log-memory.d.ts.map +1 -0
  125. package/dist/log-memory.js +101 -0
  126. package/dist/log-memory.js.map +1 -0
  127. package/dist/membership/holder.d.ts +33 -0
  128. package/dist/membership/holder.d.ts.map +1 -0
  129. package/dist/membership/holder.js +929 -0
  130. package/dist/membership/holder.js.map +1 -0
  131. package/dist/membership.d.ts +123 -0
  132. package/dist/membership.d.ts.map +1 -0
  133. package/dist/membership.js +180 -0
  134. package/dist/membership.js.map +1 -0
  135. package/dist/message/holder.d.ts +49 -0
  136. package/dist/message/holder.d.ts.map +1 -0
  137. package/dist/message/holder.js +290 -0
  138. package/dist/message/holder.js.map +1 -0
  139. package/dist/message.d.ts +43 -0
  140. package/dist/message.d.ts.map +1 -0
  141. package/dist/message.js +51 -0
  142. package/dist/message.js.map +1 -0
  143. package/dist/ports.d.ts +237 -0
  144. package/dist/ports.d.ts.map +1 -0
  145. package/dist/ports.js +2 -0
  146. package/dist/ports.js.map +1 -0
  147. package/dist/profile.d.ts +56 -0
  148. package/dist/profile.d.ts.map +1 -0
  149. package/dist/profile.js +193 -0
  150. package/dist/profile.js.map +1 -0
  151. package/dist/prompts.d.ts +14 -0
  152. package/dist/prompts.d.ts.map +1 -0
  153. package/dist/prompts.js +69 -0
  154. package/dist/prompts.js.map +1 -0
  155. package/dist/receive.d.ts +134 -0
  156. package/dist/receive.d.ts.map +1 -0
  157. package/dist/receive.js +317 -0
  158. package/dist/receive.js.map +1 -0
  159. package/dist/results.d.ts +173 -0
  160. package/dist/results.d.ts.map +1 -0
  161. package/dist/results.js +21 -0
  162. package/dist/results.js.map +1 -0
  163. package/dist/schemas.d.ts.map +1 -1
  164. package/dist/schemas.js +45 -38
  165. package/dist/schemas.js.map +1 -1
  166. package/dist/sha256.d.ts +3 -0
  167. package/dist/sha256.d.ts.map +1 -0
  168. package/dist/sha256.js +69 -0
  169. package/dist/sha256.js.map +1 -0
  170. package/dist/star.d.ts +31 -0
  171. package/dist/star.d.ts.map +1 -0
  172. package/dist/star.js +56 -0
  173. package/dist/star.js.map +1 -0
  174. package/dist/state-builder.d.ts +42 -0
  175. package/dist/state-builder.d.ts.map +1 -0
  176. package/dist/state-builder.js +100 -0
  177. package/dist/state-builder.js.map +1 -0
  178. package/dist/state.d.ts +914 -0
  179. package/dist/state.d.ts.map +1 -0
  180. package/dist/state.js +37 -0
  181. package/dist/state.js.map +1 -0
  182. package/dist/store-contract.d.ts +11 -0
  183. package/dist/store-contract.d.ts.map +1 -0
  184. package/dist/store-contract.js +189 -0
  185. package/dist/store-contract.js.map +1 -0
  186. package/dist/store-memory.d.ts +8 -0
  187. package/dist/store-memory.d.ts.map +1 -0
  188. package/dist/store-memory.js +17 -0
  189. package/dist/store-memory.js.map +1 -0
  190. package/dist/testing.d.ts +22 -0
  191. package/dist/testing.d.ts.map +1 -0
  192. package/dist/testing.js +21 -0
  193. package/dist/testing.js.map +1 -0
  194. package/dist/trust/continuity.d.ts +71 -0
  195. package/dist/trust/continuity.d.ts.map +1 -0
  196. package/dist/trust/continuity.js +1222 -0
  197. package/dist/trust/continuity.js.map +1 -0
  198. package/dist/trust/duties.d.ts +67 -0
  199. package/dist/trust/duties.d.ts.map +1 -0
  200. package/dist/trust/duties.js +179 -0
  201. package/dist/trust/duties.js.map +1 -0
  202. package/dist/trust/entries.d.ts +139 -0
  203. package/dist/trust/entries.d.ts.map +1 -0
  204. package/dist/trust/entries.js +795 -0
  205. package/dist/trust/entries.js.map +1 -0
  206. package/dist/trust/group-star.d.ts +43 -0
  207. package/dist/trust/group-star.d.ts.map +1 -0
  208. package/dist/trust/group-star.js +188 -0
  209. package/dist/trust/group-star.js.map +1 -0
  210. package/dist/trust/mapping.d.ts +56 -0
  211. package/dist/trust/mapping.d.ts.map +1 -0
  212. package/dist/trust/mapping.js +584 -0
  213. package/dist/trust/mapping.js.map +1 -0
  214. package/dist/trust/profile.d.ts +25 -0
  215. package/dist/trust/profile.d.ts.map +1 -0
  216. package/dist/trust/profile.js +249 -0
  217. package/dist/trust/profile.js.map +1 -0
  218. package/dist/trust/star.d.ts +208 -0
  219. package/dist/trust/star.d.ts.map +1 -0
  220. package/dist/trust/star.js +588 -0
  221. package/dist/trust/star.js.map +1 -0
  222. package/dist/trust/state.d.ts +83 -0
  223. package/dist/trust/state.d.ts.map +1 -0
  224. package/dist/trust/state.js +154 -0
  225. package/dist/trust/state.js.map +1 -0
  226. package/dist/util-freeze.d.ts +3 -0
  227. package/dist/util-freeze.d.ts.map +1 -0
  228. package/dist/util-freeze.js +11 -0
  229. package/dist/util-freeze.js.map +1 -0
  230. package/dist/views.d.ts +179 -0
  231. package/dist/views.d.ts.map +1 -0
  232. package/dist/views.js +286 -0
  233. package/dist/views.js.map +1 -0
  234. package/dist/visibility.d.ts +5 -5
  235. package/dist/visibility.d.ts.map +1 -1
  236. package/dist/visibility.js +22 -28
  237. package/dist/visibility.js.map +1 -1
  238. package/dist/wire.d.ts +218 -83
  239. package/dist/wire.d.ts.map +1 -1
  240. package/dist/wire.js.map +1 -1
  241. package/package.json +60 -10
  242. package/schemas/holder-state-v1.schema.json +3880 -0
  243. package/schemas/holder-state-v2.schema.json +4123 -0
  244. package/dist/holder.d.ts.map +0 -1
  245. package/dist/holder.js.map +0 -1
  246. package/dist/probe/acks.d.ts +0 -27
  247. package/dist/probe/acks.d.ts.map +0 -1
  248. package/dist/probe/acks.js +0 -152
  249. package/dist/probe/acks.js.map +0 -1
  250. package/dist/probe/continuity.d.ts +0 -20
  251. package/dist/probe/continuity.d.ts.map +0 -1
  252. package/dist/probe/continuity.js +0 -805
  253. package/dist/probe/continuity.js.map +0 -1
  254. package/dist/probe/deps.d.ts +0 -61
  255. package/dist/probe/deps.d.ts.map +0 -1
  256. package/dist/probe/deps.js +0 -54
  257. package/dist/probe/deps.js.map +0 -1
  258. package/dist/probe/encounter.d.ts +0 -26
  259. package/dist/probe/encounter.d.ts.map +0 -1
  260. package/dist/probe/encounter.js +0 -938
  261. package/dist/probe/encounter.js.map +0 -1
  262. package/dist/probe/introduce.d.ts +0 -62
  263. package/dist/probe/introduce.d.ts.map +0 -1
  264. package/dist/probe/introduce.js +0 -306
  265. package/dist/probe/introduce.js.map +0 -1
  266. package/dist/probe/membership.d.ts +0 -96
  267. package/dist/probe/membership.d.ts.map +0 -1
  268. package/dist/probe/membership.js +0 -467
  269. package/dist/probe/membership.js.map +0 -1
  270. package/dist/probe/trust.d.ts +0 -156
  271. package/dist/probe/trust.d.ts.map +0 -1
  272. package/dist/probe/trust.js +0 -1233
  273. package/dist/probe/trust.js.map +0 -1
  274. package/dist/probe.d.ts +0 -7
  275. package/dist/probe.d.ts.map +0 -1
  276. package/dist/probe.js +0 -42
  277. package/dist/probe.js.map +0 -1
@@ -0,0 +1,659 @@
1
+ // RLTP transport adapter: VTI mediator — published as
2
+ // @real-life/trust-protocol/adapters/vti-mediator (lib Stage 2, note §4.6).
3
+ //
4
+ // Its reports reach the holder as the ports' DeliveryReport: submitOutbound
5
+ // files an Outbound under its document digest and returns the
6
+ // SubmissionRef (submission id ↔ digest); report(id) maps the adapter's
7
+ // SubmissionState onto DeliveryReport (deliveryReportOf), which the app
8
+ // hands to holder.report(). An adapter's `delivered` is its claim about
9
+ // transport; the holder's receive core verifies the delivery-ack itself.
10
+ //
11
+ // Profile: spec/adapter-vti-mediator.md v0.23 (twenty-third casting;
12
+ // round journals under design/adapter-review*). A
13
+ // transport adapter contract — NOT a carrier at the port.
14
+ //
15
+ // Beyond the profile (lib 0.5.0): conclude(collection, now, {keep}) leaves
16
+ // the items whose disposition is transient unconcluded and offers them
17
+ // again (Delivery 0.82 RLTP-DEL-5810, 5820: failed(rate-limited) is never
18
+ // concluded); `concludable(result)` says which receive outcomes are
19
+ // terminal. The profile's §4 concludes a collection whole; its next
20
+ // casting is to take this up.
21
+ //
22
+ // Structural rules (reviews 3+4):
23
+ // · No port operation ever awaits the mediator — including
24
+ // register: keylist synchronization is a recorded intent with a
25
+ // tracked background task, retried by advance.
26
+ // · No report is invented, and absence is ABSENT: before the first
27
+ // honest report exists, status() returns null (§6.1 grants the
28
+ // window; representing it as a queryable fourth value was
29
+ // review-4 B2). has() distinguishes an unknown id.
30
+ // · A retired instance mutates nothing and refuses everything
31
+ // except reading its ack state.
32
+ import { jcs } from '../core.js';
33
+ import { sha } from '../crypto.js';
34
+ import { carrierPrincipal, carrierScopedIdentity } from '../carrier-identity.js';
35
+ // ── Declared constants (profile §2) — promises of THIS casting ──────
36
+ // frozen: the declared promises cannot be changed at run time either
37
+ export const DECLARED = Object.freeze({
38
+ giveUpHorizonMs: 72 * 3_600_000,
39
+ statusHorizonMs: 60_000,
40
+ queueFloorCount: 64,
41
+ queueFloorBytes: 1_048_576,
42
+ maxQueueBytes: 16_777_216,
43
+ duplicateWindowMs: 30 * 86_400_000,
44
+ });
45
+ export class WireError extends Error {
46
+ kind;
47
+ constructor(kind, message) {
48
+ super(message ?? kind);
49
+ this.kind = kind;
50
+ }
51
+ }
52
+ // ── Sender states (§6.1 — the closed sets, nothing else) ────────────
53
+ // ReceiverRefusalCode is the ports' (one closed set, one place)
54
+ const RECEIVER_REFUSAL_CODES = new Set([
55
+ 'malformedRequest', 'unsupportedType', 'proofRequired',
56
+ 'proofInvalid', 'wrongRecipient', 'identityMismatch',
57
+ ]);
58
+ /** The ports' DeliveryReport for a document `digest` in adapter state `st`
59
+ * (Delivery §6.1): one-to-one, `late` and the refusal code carried over. */
60
+ export function deliveryReportOf(digest, st) {
61
+ switch (st.state) {
62
+ case 'awaiting-transport': return { digest, status: 'awaiting-transport', reason: st.reason };
63
+ case 'accepted': return { digest, status: 'accepted' };
64
+ case 'delivered': return { digest, status: 'delivered', late: st.late };
65
+ case 'failed': return st.reason === 'rejected-by-receiver'
66
+ ? { digest, status: 'failed', reason: 'rejected-by-receiver', code: st.code }
67
+ : { digest, status: 'failed', reason: st.reason };
68
+ }
69
+ }
70
+ /** Whether a receive outcome is terminal and its item may be concluded
71
+ * (Delivery 5a.8): every disposition but `failed(rate-limited)`, the one
72
+ * failure that is transient (RLTP-DEL-5820). An InfraError of the
73
+ * holder's store is transient as well (RLTP-DEL-5810): an item whose
74
+ * receive threw is kept, too. */
75
+ export function concludable(r) {
76
+ return !(r.disposition === 'failed' && r.reason === 'rate-limited');
77
+ }
78
+ /** The adapter's own copy of a wire's token set: the wire keeps no handle on it. */
79
+ const tokenCopy = (t) => ({ accessToken: t.accessToken, accessExpiresAt: t.accessExpiresAt, refreshToken: t.refreshToken, refreshExpiresAt: t.refreshExpiresAt });
80
+ const hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
81
+ // construction capability: only create() holds it
82
+ const KEY = Symbol('MediatorCarrierAdapter.create');
83
+ // ── The adapter ──────────────────────────────────────────────────────
84
+ export class MediatorCarrierAdapter {
85
+ // JS-hard identity fields: TS `readonly` is a compile-time promise
86
+ // only, and §8 claims runtime unforgeability — so the values live
87
+ // in true privates behind getters (R8-B1)
88
+ #principal;
89
+ #connectionDid;
90
+ #egressDid;
91
+ get principal() { return this.#principal; }
92
+ get connectionDid() { return this.#connectionDid; }
93
+ get egressDid() { return this.#egressDid; }
94
+ #wire;
95
+ #tokens = null;
96
+ #clock = Number.NEGATIVE_INFINITY;
97
+ #closed = false;
98
+ #subs = new Map();
99
+ #inflight = new Set();
100
+ #nextId = 1;
101
+ #desiredRkids = null;
102
+ #keylistSynced = false;
103
+ #keylistSyncing = false;
104
+ #inbox = [];
105
+ #inboxBytes = 0;
106
+ #inboxDigests = new Set();
107
+ #outstanding = new Map();
108
+ #outstandingBytes = 0;
109
+ #concluded = new Map();
110
+ #pendingAcks = new Set();
111
+ #flightAcks = new Set(); // ids inside the in-flight flush snapshot
112
+ #flushing = false;
113
+ #nextColId = 1;
114
+ #transitions = [];
115
+ /** The only way to build an adapter: the trio is DERIVED here from
116
+ * one root, one carrier, one nonce — so principal, connection DID
117
+ * and egress identity cannot come from different relationships
118
+ * (R7-B2: the coupling is enforced by construction, not by
119
+ * inspecting caller-supplied identities). */
120
+ static async create(rootIkm, carrier, nonce, wire, restored, restoredAt) {
121
+ // ONE tuple, snapshotted at entry: a caller mutating its buffers
122
+ // between the three awaited derivations must not be able to mix
123
+ // identities from two tuples (R8-B2)
124
+ const ikm = new Uint8Array(rootIkm);
125
+ const n = new Uint8Array(nonce);
126
+ const principal = await carrierPrincipal(ikm, carrier, n);
127
+ const connection = await carrierScopedIdentity(ikm, carrier, n, 'connection');
128
+ const egress = await carrierScopedIdentity(ikm, carrier, n, 'egress');
129
+ return new MediatorCarrierAdapter(KEY, principal, connection, egress, wire, restored, restoredAt);
130
+ }
131
+ constructor(key, principal, connection, egress, wire, restored, restoredAt) {
132
+ if (key !== KEY) {
133
+ throw new Error('use MediatorCarrierAdapter.create — the identity trio must be derived, not supplied (5a.10)');
134
+ }
135
+ if (connection.scope !== 'connection' || egress.scope !== 'egress') {
136
+ throw new Error('scoped identities passed in the wrong roles');
137
+ }
138
+ const three = new Set([principal.principal, connection.did, egress.did]);
139
+ if (three.size !== 3) {
140
+ throw new Error('principal, connection DID and egress identity must be pairwise distinct (5a.10)');
141
+ }
142
+ this.#principal = principal.principal;
143
+ this.#connectionDid = connection.did;
144
+ this.#egressDid = egress.did;
145
+ this.#wire = wire;
146
+ if (restored) {
147
+ // The restore instant anchors the duplicate-window: an expired,
148
+ // debt-free entry is dropped HERE, not at the first advance —
149
+ // otherwise a redelivery arriving before any port call would be
150
+ // absorbed beyond the declared window (review-4 B4).
151
+ if (typeof restoredAt !== 'number' || !Number.isFinite(restoredAt)) {
152
+ throw new Error('restoring ack state requires a finite restoredAt instant');
153
+ }
154
+ this.#now(restoredAt);
155
+ const pending = new Set(restored.pending);
156
+ const known = new Set(restored.concluded.map((c) => c.queueId));
157
+ for (const id of pending) {
158
+ if (!known.has(id)) {
159
+ throw new Error('ack snapshot violates its invariant: pending intent without a concluded entry');
160
+ }
161
+ }
162
+ for (const { queueId, concludedAt } of restored.concluded) {
163
+ if (!Number.isFinite(concludedAt) || concludedAt > restoredAt) {
164
+ // a conclusion "from the future" would stretch the window
165
+ // past its declared span (R5-B4) — fail closed
166
+ throw new Error('ack snapshot: conclusion instant is non-finite or after restoredAt');
167
+ }
168
+ // the window beats everything (round-14 B): an expired entry
169
+ // falls at the restore instant, owed or not — a phantom debt
170
+ // from an ack that landed just before a close can never
171
+ // outlive the window and delete a fresh copy (R14-B2)
172
+ if (restoredAt - concludedAt >= DECLARED.duplicateWindowMs)
173
+ continue;
174
+ this.#concluded.set(queueId, concludedAt);
175
+ if (pending.has(queueId))
176
+ this.#pendingAcks.add(queueId);
177
+ }
178
+ }
179
+ this.#wire.onDeliver(async (raw) => {
180
+ if (this.#closed)
181
+ return;
182
+ // a plain copy of its own: a Node Buffer's slice() would share memory
183
+ const bytes = new Uint8Array(raw);
184
+ const queueId = hex(await sha(bytes));
185
+ if (this.#closed)
186
+ return;
187
+ // Absorption is WINDOW-checked at the moment it happens
188
+ // (R11-B1), and the window beats everything (round-14 B):
189
+ // beyond it, entry AND any leftover intent fall, and the frame
190
+ // is a new item — so no acknowledgement, owed or phantom, can
191
+ // ever delete the only fresh copy.
192
+ const concludedAt = this.#concluded.get(queueId);
193
+ if (concludedAt !== undefined) {
194
+ if (this.#flightAcks.has(queueId) ||
195
+ this.#clock - concludedAt < DECLARED.duplicateWindowMs) {
196
+ // inside the window — or deferred: an id inside an
197
+ // in-flight flush keeps absorbing until that flush ends,
198
+ // because its irrecallable ack may still land (R15-B1)
199
+ this.#pendingAcks.add(queueId);
200
+ return;
201
+ }
202
+ this.#concluded.delete(queueId);
203
+ this.#pendingAcks.delete(queueId);
204
+ }
205
+ if (this.#inboxDigests.has(queueId))
206
+ return;
207
+ for (const col of this.#outstanding.values())
208
+ if (col.has(queueId))
209
+ return;
210
+ const held = this.#inboxBytes + this.#outstandingBytes;
211
+ const underFloor = this.#inbox.length < DECLARED.queueFloorCount &&
212
+ held + bytes.length <= DECLARED.queueFloorBytes;
213
+ const underCap = held + bytes.length <= DECLARED.maxQueueBytes;
214
+ if (!underFloor && !underCap)
215
+ return;
216
+ this.#inbox.push({ bytes, queueId });
217
+ this.#inboxBytes += bytes.length;
218
+ this.#inboxDigests.add(queueId);
219
+ });
220
+ }
221
+ #now(raw) {
222
+ if (!Number.isFinite(raw))
223
+ throw new Error('non-finite time');
224
+ if (raw > this.#clock)
225
+ this.#clock = raw;
226
+ return this.#clock;
227
+ }
228
+ #open() { if (this.#closed)
229
+ throw new Error('retired (5a.10 lifecycle)'); }
230
+ #track(p) {
231
+ const t = p.catch(() => { });
232
+ this.#inflight.add(t);
233
+ void t.finally(() => this.#inflight.delete(t));
234
+ }
235
+ // ── auth: silent carrier policy ─────────────────────────────────
236
+ // Single-flight: concurrent callers share one handshake — the ATM
237
+ // challenge/response is a session, and two interleaved sessions
238
+ // under one connection DID would answer each other's challenges.
239
+ #authing = null;
240
+ #ensureAuth(now) {
241
+ if (this.#tokens && now < this.#tokens.accessExpiresAt)
242
+ return Promise.resolve(this.#tokens.accessToken);
243
+ if (this.#authing)
244
+ return this.#authing;
245
+ const run = (async () => {
246
+ if (this.#tokens && now < this.#tokens.refreshExpiresAt) {
247
+ try {
248
+ const refreshed = await this.#wire.refresh(this.#tokens.refreshToken);
249
+ if (this.#closed)
250
+ throw new Error('retired'); // as on the handshake path (R6-B1)
251
+ this.#tokens = tokenCopy(refreshed);
252
+ return this.#tokens.accessToken;
253
+ }
254
+ catch (e) {
255
+ if (this.#closed)
256
+ throw e;
257
+ // a transient failure (mediator unreachable, retriable refusal)
258
+ // says nothing about the token: keep it for the next attempt (#9)
259
+ if (e instanceof WireError && (e.kind === 'unreachable' || e.kind === 'refused-retriable'))
260
+ throw e;
261
+ // a refused refresh (revoked before its expiry) is never replayed:
262
+ // drop the tokens and take the challenge handshake below
263
+ this.#tokens = null;
264
+ }
265
+ }
266
+ const { challenge, sessionId } = await this.#wire.authChallenge(this.#connectionDid);
267
+ if (this.#closed)
268
+ throw new Error('retired'); // nothing new starts, not even the answer (R6-B1)
269
+ this.#tokens = tokenCopy(await this.#wire.authenticate({ did: this.#connectionDid, challenge, sessionId }));
270
+ return this.#tokens.accessToken;
271
+ })();
272
+ this.#authing = run.finally(() => { this.#authing = null; });
273
+ return this.#authing;
274
+ }
275
+ // ── transport registration (profile §3 — NOT §5a.3) ─────────────
276
+ /** Records the desired keylist and returns; synchronization runs
277
+ * as a tracked background task and advance retries it until it
278
+ * lands (review-4 B1). */
279
+ register(rkids, rawNow) {
280
+ this.#open();
281
+ const now = this.#now(rawNow);
282
+ this.#desiredRkids = [...rkids];
283
+ this.#keylistSynced = false;
284
+ this.#startKeylistSync(now);
285
+ }
286
+ get keylistSynced() {
287
+ this.#open();
288
+ return this.#keylistSynced;
289
+ }
290
+ #startKeylistSync(now) {
291
+ if (this.#keylistSyncing || this.#keylistSynced || this.#desiredRkids === null)
292
+ return;
293
+ this.#keylistSyncing = true;
294
+ const wanted = [...this.#desiredRkids];
295
+ this.#track((async () => {
296
+ try {
297
+ const token = await this.#ensureAuth(now);
298
+ if (this.#closed)
299
+ return;
300
+ await this.#wire.keylistUpdate(token, [...wanted]); // the wire's own list: `wanted` is compared below
301
+ // land only if the desire has not moved meanwhile
302
+ if (!this.#closed && this.#desiredRkids !== null &&
303
+ wanted.join(' ') === this.#desiredRkids.join(' ')) {
304
+ this.#keylistSynced = true;
305
+ }
306
+ }
307
+ catch {
308
+ // intent survives; advance retries
309
+ }
310
+ finally {
311
+ this.#keylistSyncing = false;
312
+ }
313
+ })());
314
+ }
315
+ // ── sender side ─────────────────────────────────────────────────
316
+ /** Returns the submission id synchronously. Before the first
317
+ * honest report exists, status() returns null — §6.1's own
318
+ * pre-report window, represented as absence, not as a value. */
319
+ submit(envelopeBytes, rawNow) {
320
+ this.#open();
321
+ const now = this.#now(rawNow);
322
+ const id = 's' + this.#nextId++;
323
+ const knownOffline = this.#wire.offline?.() === true;
324
+ const sub = {
325
+ // a plain copy of its own (a Node Buffer's slice() shares memory)
326
+ bytes: new Uint8Array(envelopeBytes),
327
+ submittedAt: now,
328
+ attemptStartedAt: null,
329
+ lastKind: null,
330
+ st: null,
331
+ };
332
+ this.#subs.set(id, sub);
333
+ if (!knownOffline)
334
+ this.#startAttempt(sub, now);
335
+ return id;
336
+ }
337
+ /** Files an Outbound (its envelope as JCS bytes) and returns the
338
+ * SubmissionRef the holder's reports are keyed by: submission id ↔
339
+ * document digest (note §4.3). */
340
+ submitOutbound(outbound, rawNow) {
341
+ this.#open();
342
+ if (typeof outbound?.digest !== 'string' || outbound.digest === '')
343
+ throw new Error('an outbound needs its document digest');
344
+ const submissionId = this.submit(new TextEncoder().encode(jcs(outbound.envelope)), rawNow);
345
+ this.#subs.get(submissionId).digest = outbound.digest;
346
+ return { submissionId, digest: outbound.digest };
347
+ }
348
+ /** The current state as the ports' DeliveryReport for holder.report(),
349
+ * or null while no honest report exists (and for an unknown id). A
350
+ * submission filed without a digest (submit) has no report form. */
351
+ report(id) {
352
+ const st = this.status(id);
353
+ if (st === null)
354
+ return null;
355
+ const digest = this.#subs.get(id).digest;
356
+ if (digest === undefined)
357
+ throw new Error('submitted without a document digest: use submitOutbound');
358
+ return deliveryReportOf(digest, st);
359
+ }
360
+ has(id) {
361
+ this.#open();
362
+ return this.#subs.has(id);
363
+ }
364
+ #startAttempt(sub, now) {
365
+ if (sub.attemptStartedAt !== null)
366
+ return;
367
+ sub.attemptStartedAt = now;
368
+ this.#track(this.#trySend(sub, now).finally(() => { sub.attemptStartedAt = null; }));
369
+ }
370
+ async #trySend(sub, now) {
371
+ if (sub.st !== null && sub.st.state !== 'awaiting-transport')
372
+ return;
373
+ let next;
374
+ try {
375
+ const token = await this.#ensureAuth(now);
376
+ if (this.#closed)
377
+ return; // retirement between auth and deposit: nothing new starts (R5-B2)
378
+ // each attempt hands the wire a copy: what it does with it never reaches the next
379
+ await this.#wire.deposit(token, new Uint8Array(sub.bytes), this.#egressDid);
380
+ sub.lastKind = null;
381
+ next = { state: 'accepted' };
382
+ }
383
+ catch (e) {
384
+ const kind = e instanceof WireError ? e.kind : 'unreachable';
385
+ sub.lastKind = kind;
386
+ next = kind === 'oversize'
387
+ ? { state: 'failed', reason: 'oversize' }
388
+ : {
389
+ state: 'awaiting-transport',
390
+ reason: kind === 'unreachable' ? 'transport-unreachable' : 'carrier-refused-retriable',
391
+ };
392
+ }
393
+ if (this.#closed)
394
+ return;
395
+ // An interim report (including a horizon-corrected one) never
396
+ // outranks the attempt's real outcome; only a TERMINAL state —
397
+ // a give-up, a refusal, a delivery — stands (R5-B1). Without
398
+ // this, an accepted deposit would stay reported unreachable and
399
+ // be sent a second time.
400
+ if (sub.st === null || sub.st.state === 'awaiting-transport')
401
+ sub.st = next;
402
+ }
403
+ /** Host teardown aid, NOT a port operation (profile §6): awaits
404
+ * the in-flight background tasks so a host can drain before
405
+ * persisting ackState. On a hanging wire it waits as long as the
406
+ * wire does — a host bounds it externally (Promise.race) exactly
407
+ * because this library holds no clock of its own. */
408
+ async settle() {
409
+ while (this.#inflight.size > 0)
410
+ await Promise.all([...this.#inflight]);
411
+ }
412
+ /** The §6.1 report, or null while none exists yet (has()
413
+ * distinguishes an unknown id). status is a PORT: after close it
414
+ * refuses like every other port operation (R5-B3).
415
+ *
416
+ * The pre-transport report is an HONEST SNAPSHOT, derived at the
417
+ * moment of observation by one precedence order (Option B of the
418
+ * round-14 halt) — never a stored patchwork:
419
+ * (a) the wire knows itself offline → offline
420
+ * (b) an attempt is overdue → transport-unreachable
421
+ * (c) the last completed attempt's kind → its report
422
+ * (d) otherwise → no report yet (null)
423
+ * Terminal states and `accepted` are stored and stand. */
424
+ status(id) {
425
+ this.#open();
426
+ const sub = this.#subs.get(id);
427
+ if (!sub)
428
+ return null;
429
+ if (sub.st !== null && sub.st.state !== 'awaiting-transport')
430
+ return { ...sub.st };
431
+ if (this.#wire.offline?.() === true) {
432
+ return { state: 'awaiting-transport', reason: 'offline' };
433
+ }
434
+ if (sub.attemptStartedAt !== null &&
435
+ this.#clock - sub.attemptStartedAt >= DECLARED.statusHorizonMs) {
436
+ return { state: 'awaiting-transport', reason: 'transport-unreachable' };
437
+ }
438
+ return sub.st === null ? null : { ...sub.st };
439
+ }
440
+ acknowledged(id) {
441
+ if (this.#closed)
442
+ return;
443
+ const sub = this.#subs.get(id);
444
+ if (!sub)
445
+ return;
446
+ if (sub.st?.state === 'delivered')
447
+ return; // idempotent: a duplicate ack never rewrites `late` (R9-M4)
448
+ const late = sub.st?.state === 'failed';
449
+ sub.st = { state: 'delivered', late };
450
+ if (late)
451
+ this.#transitions.push({ id, to: 'delivered' });
452
+ }
453
+ /** Refusals act on live submissions only (§6.1 knows exactly one
454
+ * late correction — the valid ack). The code set is closed and
455
+ * TERMINAL: retryable counterpart conditions (unavailable,
456
+ * internalError) are transport outcomes, never receiver verdicts
457
+ * — they have no entry here, and an unknown code is refused
458
+ * fail-closed rather than minted into a verdict (R6-B2). */
459
+ receiverRefused(id, code) {
460
+ if (this.#closed)
461
+ return;
462
+ if (!RECEIVER_REFUSAL_CODES.has(code)) {
463
+ throw new Error('not a terminal receiver refusal code — retryable conditions are transport outcomes');
464
+ }
465
+ const sub = this.#subs.get(id);
466
+ if (!sub)
467
+ return;
468
+ // A VALIDATED refusal is itself the proof of receipt — the
469
+ // mediator can have accepted and forwarded while our deposit
470
+ // promise is still unresolved, so the report-less phase is a
471
+ // legitimate moment for it (R9-B2, reversing the R8-B3
472
+ // overcorrection). Only terminal states stand against it.
473
+ if (sub.st !== null && sub.st.state !== 'awaiting-transport' && sub.st.state !== 'accepted')
474
+ return;
475
+ sub.st = { state: 'failed', reason: 'rejected-by-receiver', code };
476
+ }
477
+ drainTransitions() {
478
+ this.#open();
479
+ return this.#transitions.splice(0);
480
+ }
481
+ // ── receiver side ───────────────────────────────────────────────
482
+ collect(rawNow) {
483
+ this.#open();
484
+ this.#now(rawNow);
485
+ if (this.#inbox.length === 0)
486
+ return { id: 'c-empty', items: [] };
487
+ const items = this.#inbox.splice(0);
488
+ this.#outstandingBytes += this.#inboxBytes;
489
+ this.#inboxBytes = 0;
490
+ this.#inboxDigests.clear();
491
+ const id = 'c' + this.#nextColId++;
492
+ this.#outstanding.set(id, new Map(items.map((i) => [i.queueId, i])));
493
+ // the caller gets its own copies: the outstanding entries stay intact,
494
+ // whatever it does with the bytes, for a conclusion that keeps them
495
+ return { id, items: items.map((i) => ({ ...i, bytes: new Uint8Array(i.bytes) })) };
496
+ }
497
+ /** Concludes a collection (profile §4, Delivery 5a.8): its items'
498
+ * digests become ack intent. `keep` names the items whose disposition
499
+ * is transient (Delivery RLTP-DEL-5810, 5820: `failed(rate-limited)`,
500
+ * or a transaction that could not commit, see `concludable`): they are
501
+ * not concluded and not acknowledged, they go back to the buffer and the
502
+ * next collect offers them again, and the mediator keeps its copy. */
503
+ conclude(collection, rawNow, opts = {}) {
504
+ this.#open();
505
+ const now = this.#now(rawNow);
506
+ const col = this.#outstanding.get(collection.id);
507
+ if (!col)
508
+ return;
509
+ this.#outstanding.delete(collection.id);
510
+ const keep = new Set(opts.keep ?? []);
511
+ for (const item of col.values()) {
512
+ this.#outstandingBytes -= item.bytes.length;
513
+ if (keep.has(item.queueId)) {
514
+ // transient: back to the buffer, offered again, never acknowledged
515
+ this.#inbox.push(item);
516
+ this.#inboxBytes += item.bytes.length;
517
+ this.#inboxDigests.add(item.queueId);
518
+ continue;
519
+ }
520
+ this.#concluded.set(item.queueId, now);
521
+ this.#pendingAcks.add(item.queueId);
522
+ }
523
+ this.#startFlush(now);
524
+ }
525
+ ackState() {
526
+ return {
527
+ concluded: [...this.#concluded].map(([queueId, concludedAt]) => ({ queueId, concludedAt })),
528
+ pending: [...this.#pendingAcks],
529
+ };
530
+ }
531
+ #startFlush(now) {
532
+ if (this.#flushing || this.#pendingAcks.size === 0)
533
+ return;
534
+ this.#flushing = true;
535
+ const snapshot = new Set(this.#pendingAcks);
536
+ this.#track(this.#flushAcks(now, [...snapshot]).finally(() => {
537
+ this.#flushing = false;
538
+ // lost-wake-up guard (review-4 M6): intents that arrived while
539
+ // this flush ran get their own flush now — but only newcomers,
540
+ // so a failing wire waits for the next advance instead of
541
+ // spinning
542
+ if (!this.#closed) {
543
+ for (const id of this.#pendingAcks) {
544
+ if (!snapshot.has(id)) {
545
+ this.#startFlush(this.#clock);
546
+ break;
547
+ }
548
+ }
549
+ }
550
+ }));
551
+ }
552
+ async #flushAcks(now, ids) {
553
+ try {
554
+ const token = await this.#ensureAuth(now);
555
+ if (this.#closed)
556
+ return;
557
+ // Only NOW does anything become irrecallable (R16-B1): the
558
+ // stay of expiry begins at the wire call itself, never at the
559
+ // snapshot — a hanging auth extends no window. And "live" is
560
+ // WINDOW-live (R17-B1): an entry whose window already closed —
561
+ // even if no prune has swept it yet — is never sent.
562
+ const live = ids.filter((id) => {
563
+ const at = this.#concluded.get(id);
564
+ return at !== undefined && this.#clock - at < DECLARED.duplicateWindowMs;
565
+ });
566
+ if (live.length === 0)
567
+ return;
568
+ for (const id of live)
569
+ this.#flightAcks.add(id);
570
+ try {
571
+ // the wire gets its own list: what it does with it never reaches the bookkeeping below
572
+ await this.#wire.ackReceived(token, [...live]);
573
+ if (!this.#closed)
574
+ for (const id of live)
575
+ this.#pendingAcks.delete(id);
576
+ }
577
+ finally {
578
+ for (const id of live)
579
+ this.#flightAcks.delete(id);
580
+ // deferred window sweep: entries whose window closed while
581
+ // the ack flew fall now — intent, entry and absorption
582
+ // together (R15-B1). Never on a retired instance: after
583
+ // close, ackState() is the host's final-persist surface and
584
+ // a late-returning wire mutates nothing (R17-B2).
585
+ if (!this.#closed) {
586
+ for (const id of live) {
587
+ const at = this.#concluded.get(id);
588
+ if (at !== undefined && this.#clock - at >= DECLARED.duplicateWindowMs) {
589
+ this.#concluded.delete(id);
590
+ this.#pendingAcks.delete(id);
591
+ }
592
+ }
593
+ }
594
+ }
595
+ }
596
+ catch {
597
+ // intent survives; the next advance starts a fresh flush
598
+ }
599
+ }
600
+ // ── the adapter clock — never awaits the mediator ───────────────
601
+ advance(rawNow) {
602
+ this.#open();
603
+ const now = this.#now(rawNow);
604
+ for (const sub of this.#subs.values()) {
605
+ const live = sub.st === null || sub.st.state === 'awaiting-transport' || sub.st.state === 'accepted';
606
+ if (!live)
607
+ continue;
608
+ if (now - sub.submittedAt >= DECLARED.giveUpHorizonMs) {
609
+ // unroutable only when the LAST COMPLETED attempt was an
610
+ // admission refusal and no attempt is in flight whose outcome
611
+ // is unknown (review-4 B3)
612
+ // the give-up verdict applies the SAME precedence as every
613
+ // observation (R17-B3): a wire that knows itself offline at
614
+ // the horizon outranks any stored refusal class — unroutable
615
+ // needs a completed admission refusal, no unknown outcome in
616
+ // flight, AND no offline signal standing over it
617
+ sub.st = {
618
+ state: 'failed',
619
+ reason: sub.lastKind === 'refused-admission' && sub.attemptStartedAt === null &&
620
+ this.#wire.offline?.() !== true
621
+ ? 'unroutable'
622
+ : 'expired-by-adapter-policy',
623
+ };
624
+ continue;
625
+ }
626
+ if (sub.st !== null && sub.st.state === 'accepted')
627
+ continue;
628
+ // reports are DERIVED at observation (see status()); the clock
629
+ // only decides here whether to start an attempt — and never
630
+ // against a wire that knows itself offline
631
+ if (this.#wire.offline?.() === true)
632
+ continue;
633
+ if (sub.attemptStartedAt === null)
634
+ this.#startAttempt(sub, now);
635
+ }
636
+ // THE WINDOW BEATS EVERYTHING (Option B of the round-14 halt):
637
+ // an ack intent is retried until the window closes; then intent
638
+ // and absorption fall together. There is no ageless debt whose
639
+ // truth the adapter would have to know — the receiver's §6.2
640
+ // absorption owns everything beyond the window.
641
+ for (const [digest, at] of this.#concluded) {
642
+ if (now - at >= DECLARED.duplicateWindowMs && !this.#flightAcks.has(digest)) {
643
+ this.#concluded.delete(digest);
644
+ this.#pendingAcks.delete(digest);
645
+ }
646
+ }
647
+ this.#startKeylistSync(now);
648
+ this.#startFlush(now);
649
+ }
650
+ /** §5a.10 lifecycle boundary: after close, every state port is
651
+ * inert — port operations refuse, inbound reports (acknowledged,
652
+ * receiverRefused) are no-ops, late wire resolutions mutate
653
+ * nothing. Only ackState() stays readable for the host's final
654
+ * persist. */
655
+ close() { this.#closed = true; }
656
+ get closed() { return this.#closed; }
657
+ }
658
+ export const queueIdOf = async (bytes) => hex(await sha(bytes));
659
+ //# sourceMappingURL=vti-mediator.js.map