@real-life/trust-protocol 0.4.0 → 0.6.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 (91) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +91 -24
  3. package/dist/adapters/vti-mediator.d.ts +36 -8
  4. package/dist/adapters/vti-mediator.d.ts.map +1 -1
  5. package/dist/adapters/vti-mediator.js +193 -29
  6. package/dist/adapters/vti-mediator.js.map +1 -1
  7. package/dist/codec.d.ts +2 -1
  8. package/dist/codec.d.ts.map +1 -1
  9. package/dist/codec.js +75 -26
  10. package/dist/codec.js.map +1 -1
  11. package/dist/engine.d.ts +77 -4
  12. package/dist/engine.d.ts.map +1 -1
  13. package/dist/engine.js +419 -31
  14. package/dist/engine.js.map +1 -1
  15. package/dist/entropy.d.ts +1 -1
  16. package/dist/entropy.d.ts.map +1 -1
  17. package/dist/entropy.js +1 -1
  18. package/dist/entropy.js.map +1 -1
  19. package/dist/events.d.ts +21 -0
  20. package/dist/events.d.ts.map +1 -1
  21. package/dist/events.js.map +1 -1
  22. package/dist/holder-api.d.ts +44 -2
  23. package/dist/holder-api.d.ts.map +1 -1
  24. package/dist/holder-api.js +40 -5
  25. package/dist/holder-api.js.map +1 -1
  26. package/dist/{holder-state-v1.d.ts → holder-state-v3.d.ts} +261 -201
  27. package/dist/holder-state-v3.d.ts.map +1 -0
  28. package/dist/holder-state-v3.js +2 -0
  29. package/dist/holder-state-v3.js.map +1 -0
  30. package/dist/index.d.ts +3 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +10 -6
  33. package/dist/index.js.map +1 -1
  34. package/dist/internal.d.ts +2 -2
  35. package/dist/internal.d.ts.map +1 -1
  36. package/dist/internal.js +1 -1
  37. package/dist/internal.js.map +1 -1
  38. package/dist/introduction/holder.d.ts.map +1 -1
  39. package/dist/introduction/holder.js +7 -0
  40. package/dist/introduction/holder.js.map +1 -1
  41. package/dist/invariants.d.ts.map +1 -1
  42. package/dist/invariants.js +70 -0
  43. package/dist/invariants.js.map +1 -1
  44. package/dist/membership/holder.d.ts +3 -5
  45. package/dist/membership/holder.d.ts.map +1 -1
  46. package/dist/membership/holder.js +165 -88
  47. package/dist/membership/holder.js.map +1 -1
  48. package/dist/membership.d.ts +31 -0
  49. package/dist/membership.d.ts.map +1 -1
  50. package/dist/membership.js +25 -2
  51. package/dist/membership.js.map +1 -1
  52. package/dist/message/holder.d.ts +49 -0
  53. package/dist/message/holder.d.ts.map +1 -0
  54. package/dist/message/holder.js +290 -0
  55. package/dist/message/holder.js.map +1 -0
  56. package/dist/message.d.ts +43 -0
  57. package/dist/message.d.ts.map +1 -0
  58. package/dist/message.js +51 -0
  59. package/dist/message.js.map +1 -0
  60. package/dist/receive.d.ts +4 -3
  61. package/dist/receive.d.ts.map +1 -1
  62. package/dist/receive.js +23 -4
  63. package/dist/receive.js.map +1 -1
  64. package/dist/results.d.ts +22 -3
  65. package/dist/results.d.ts.map +1 -1
  66. package/dist/results.js.map +1 -1
  67. package/dist/schemas.d.ts.map +1 -1
  68. package/dist/schemas.js +9 -6
  69. package/dist/schemas.js.map +1 -1
  70. package/dist/state.d.ts +88 -14
  71. package/dist/state.d.ts.map +1 -1
  72. package/dist/state.js +2 -1
  73. package/dist/state.js.map +1 -1
  74. package/dist/trust/continuity.d.ts.map +1 -1
  75. package/dist/trust/continuity.js +7 -1
  76. package/dist/trust/continuity.js.map +1 -1
  77. package/dist/trust/entries.d.ts.map +1 -1
  78. package/dist/trust/entries.js +103 -2
  79. package/dist/trust/entries.js.map +1 -1
  80. package/dist/views.d.ts +34 -2
  81. package/dist/views.d.ts.map +1 -1
  82. package/dist/views.js +19 -1
  83. package/dist/views.js.map +1 -1
  84. package/dist/wire.d.ts +47 -6
  85. package/dist/wire.d.ts.map +1 -1
  86. package/dist/wire.js.map +1 -1
  87. package/package.json +6 -4
  88. package/schemas/{holder-state-v1.schema.json → holder-state-v3.schema.json} +300 -12
  89. package/dist/holder-state-v1.d.ts.map +0 -1
  90. package/dist/holder-state-v1.js +0 -2
  91. package/dist/holder-state-v1.js.map +0 -1
@@ -1,17 +1,36 @@
1
1
  // RLTP transport adapter: VTI mediator — published as
2
2
  // @real-life/trust-protocol/adapters/vti-mediator (lib Stage 2, note §4.6).
3
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.
4
+ // It takes the holder's documents from the holder's custody (Delivery 0.83
5
+ // RLTP-DEL-6120 to 6135, lib 0.6.0): reconcile(view, gate, now) files every
6
+ // entry of holder.pendingTransport(when) not yet filed, and withdraws every
7
+ // filed submission still before the trias whose digest the view no longer
8
+ // names — a missing entry means stop, never keep sending. Before every
9
+ // deposit it asks gate.mayHandOver(entry, now) (the holder): false
10
+ // withdraws the submission, an error skips the attempt. It keeps no
11
+ // custody of its own: its copies are volatile and bound to the holder's
12
+ // opening: bound to the holder of its first reconcile, it refuses another,
13
+ // so after holder.close() nothing it holds goes out, and the next opening
14
+ // gets a new adapter (restored from ackState like any other). Its give-up horizon counts
15
+ // from the entry's first submission (`since`), never from when it took the
16
+ // entry over (RLTP-DEL-6130). Its reports reach the holder as the ports'
17
+ // DeliveryReport: report(id) maps the adapter's SubmissionState onto
18
+ // DeliveryReport (deliveryReportOf), keyed by the document digest, which
19
+ // the app hands to holder.report(). An adapter's `delivered` is its claim
20
+ // about transport; the holder's receive core verifies the delivery-ack
21
+ // itself.
10
22
  //
11
23
  // Profile: spec/adapter-vti-mediator.md v0.23 (twenty-third casting;
12
24
  // round journals under design/adapter-review*). A
13
25
  // transport adapter contract — NOT a carrier at the port.
14
26
  //
27
+ // Beyond the profile (lib 0.5.0): conclude(collection, now, {keep}) leaves
28
+ // the items whose disposition is transient unconcluded and offers them
29
+ // again (Delivery 0.82 RLTP-DEL-5810, 5820: failed(rate-limited) is never
30
+ // concluded); `concludable(result)` says which receive outcomes are
31
+ // terminal. The profile's §4 concludes a collection whole; its next
32
+ // casting is to take this up.
33
+ //
15
34
  // Structural rules (reviews 3+4):
16
35
  // · No port operation ever awaits the mediator — including
17
36
  // register: keylist synchronization is a recorded intent with a
@@ -26,14 +45,15 @@ import { jcs } from '../core.js';
26
45
  import { sha } from '../crypto.js';
27
46
  import { carrierPrincipal, carrierScopedIdentity } from '../carrier-identity.js';
28
47
  // ── Declared constants (profile §2) — promises of THIS casting ──────
29
- export const DECLARED = {
48
+ // frozen: the declared promises cannot be changed at run time either
49
+ export const DECLARED = Object.freeze({
30
50
  giveUpHorizonMs: 72 * 3_600_000,
31
51
  statusHorizonMs: 60_000,
32
52
  queueFloorCount: 64,
33
53
  queueFloorBytes: 1_048_576,
34
54
  maxQueueBytes: 16_777_216,
35
55
  duplicateWindowMs: 30 * 86_400_000,
36
- };
56
+ });
37
57
  export class WireError extends Error {
38
58
  kind;
39
59
  constructor(kind, message) {
@@ -59,6 +79,16 @@ export function deliveryReportOf(digest, st) {
59
79
  : { digest, status: 'failed', reason: st.reason };
60
80
  }
61
81
  }
82
+ /** Whether a receive outcome is terminal and its item may be concluded
83
+ * (Delivery 5a.8): every disposition but `failed(rate-limited)`, the one
84
+ * failure that is transient (RLTP-DEL-5820). An InfraError of the
85
+ * holder's store is transient as well (RLTP-DEL-5810): an item whose
86
+ * receive threw is kept, too. */
87
+ export function concludable(r) {
88
+ return !(r.disposition === 'failed' && r.reason === 'rate-limited');
89
+ }
90
+ /** The adapter's own copy of a wire's token set: the wire keeps no handle on it. */
91
+ const tokenCopy = (t) => ({ accessToken: t.accessToken, accessExpiresAt: t.accessExpiresAt, refreshToken: t.refreshToken, refreshExpiresAt: t.refreshExpiresAt });
62
92
  const hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
63
93
  // construction capability: only create() holds it
64
94
  const KEY = Symbol('MediatorCarrierAdapter.create');
@@ -77,6 +107,8 @@ export class MediatorCarrierAdapter {
77
107
  #tokens = null;
78
108
  #clock = Number.NEGATIVE_INFINITY;
79
109
  #closed = false;
110
+ /** the opening this adapter serves, bound at its first reconcile */
111
+ #gate = null;
80
112
  #subs = new Map();
81
113
  #inflight = new Set();
82
114
  #nextId = 1;
@@ -161,7 +193,8 @@ export class MediatorCarrierAdapter {
161
193
  this.#wire.onDeliver(async (raw) => {
162
194
  if (this.#closed)
163
195
  return;
164
- const bytes = raw.slice();
196
+ // a plain copy of its own: a Node Buffer's slice() would share memory
197
+ const bytes = new Uint8Array(raw);
165
198
  const queueId = hex(await sha(bytes));
166
199
  if (this.#closed)
167
200
  return;
@@ -229,7 +262,7 @@ export class MediatorCarrierAdapter {
229
262
  const refreshed = await this.#wire.refresh(this.#tokens.refreshToken);
230
263
  if (this.#closed)
231
264
  throw new Error('retired'); // as on the handshake path (R6-B1)
232
- this.#tokens = refreshed;
265
+ this.#tokens = tokenCopy(refreshed);
233
266
  return this.#tokens.accessToken;
234
267
  }
235
268
  catch (e) {
@@ -247,7 +280,7 @@ export class MediatorCarrierAdapter {
247
280
  const { challenge, sessionId } = await this.#wire.authChallenge(this.#connectionDid);
248
281
  if (this.#closed)
249
282
  throw new Error('retired'); // nothing new starts, not even the answer (R6-B1)
250
- this.#tokens = await this.#wire.authenticate({ did: this.#connectionDid, challenge, sessionId });
283
+ this.#tokens = tokenCopy(await this.#wire.authenticate({ did: this.#connectionDid, challenge, sessionId }));
251
284
  return this.#tokens.accessToken;
252
285
  })();
253
286
  this.#authing = run.finally(() => { this.#authing = null; });
@@ -278,7 +311,7 @@ export class MediatorCarrierAdapter {
278
311
  const token = await this.#ensureAuth(now);
279
312
  if (this.#closed)
280
313
  return;
281
- await this.#wire.keylistUpdate(token, wanted);
314
+ await this.#wire.keylistUpdate(token, [...wanted]); // the wire's own list: `wanted` is compared below
282
315
  // land only if the desire has not moved meanwhile
283
316
  if (!this.#closed && this.#desiredRkids !== null &&
284
317
  wanted.join(' ') === this.#desiredRkids.join(' ')) {
@@ -303,7 +336,9 @@ export class MediatorCarrierAdapter {
303
336
  const id = 's' + this.#nextId++;
304
337
  const knownOffline = this.#wire.offline?.() === true;
305
338
  const sub = {
306
- bytes: envelopeBytes.slice(),
339
+ id,
340
+ // a plain copy of its own (a Node Buffer's slice() shares memory)
341
+ bytes: new Uint8Array(envelopeBytes),
307
342
  submittedAt: now,
308
343
  attemptStartedAt: null,
309
344
  lastKind: null,
@@ -314,16 +349,79 @@ export class MediatorCarrierAdapter {
314
349
  this.#startAttempt(sub, now);
315
350
  return id;
316
351
  }
317
- /** Files an Outbound (its envelope as JCS bytes) and returns the
318
- * SubmissionRef the holder's reports are keyed by: submission id ↔
319
- * document digest (note §4.3). */
320
- submitOutbound(outbound, rawNow) {
352
+ /** Takes the holder's custody set (holder.pendingTransport, after a
353
+ * resume of this opening) and the holder as the gate: files every entry
354
+ * not yet filed under its digest (its envelope as JCS bytes; the give-up
355
+ * horizon from entry.since) and returns their SubmissionRefs; withdraws
356
+ * every filed submission still before the trias (no report, or
357
+ * awaiting-transport) whose digest the view does not name. Accepted and
358
+ * terminal submissions stay for their reports. */
359
+ reconcile(view, gate, rawNow) {
321
360
  this.#open();
322
- if (typeof outbound?.digest !== 'string' || outbound.digest === '')
323
- throw new Error('an outbound needs its document digest');
324
- const submissionId = this.submit(new TextEncoder().encode(jcs(outbound.envelope)), rawNow);
325
- this.#subs.get(submissionId).digest = outbound.digest;
326
- return { submissionId, digest: outbound.digest };
361
+ if (view?.ok !== true || !Array.isArray(view.entries))
362
+ throw new Error('reconcile needs a view of holder.pendingTransport');
363
+ if (typeof gate?.handOver !== 'function')
364
+ throw new Error('reconcile needs the holder as its gate');
365
+ // one adapter per opening: its copies and the holder's grants belong to
366
+ // that opening, and the next opening gets an adapter of its own
367
+ if (this.#gate === null)
368
+ this.#gate = gate;
369
+ else if (this.#gate !== gate)
370
+ throw new Error('one adapter per opening: this adapter reconciles with another holder (create a new adapter for the next opening)');
371
+ const now = this.#now(rawNow);
372
+ // a submission's key: the digest, and for a duplicate's answer its
373
+ // grant too — the retries and each answer are hand-overs of their own
374
+ const keyOf = (digest, resend) => resend === undefined ? digest : `${digest}#${resend}`;
375
+ const named = new Set(view.entries.map((e) => keyOf(e.digest, e.resend)));
376
+ const filed = new Map();
377
+ for (const [id, sub] of this.#subs) {
378
+ if (sub.digest === undefined)
379
+ continue;
380
+ const key = keyOf(sub.digest, sub.entry?.resend);
381
+ const before = sub.st === null || sub.st.state === 'awaiting-transport';
382
+ if (before && !named.has(key)) {
383
+ this.#subs.delete(id);
384
+ continue;
385
+ }
386
+ filed.set(key, sub);
387
+ }
388
+ const refs = [];
389
+ for (const entry of view.entries) {
390
+ const had = filed.get(keyOf(entry.digest, entry.resend));
391
+ if (had !== undefined) {
392
+ // the newest entry of a live submission is the one its gate checks
393
+ if (had.st === null || had.st.state === 'awaiting-transport') {
394
+ had.entry = entry;
395
+ had.gate = gate;
396
+ continue;
397
+ }
398
+ // an answer the holder still offers is still owed — its hand-over
399
+ // never began, or the holder could not record that it did: a
400
+ // terminal submission of it is filed again (the holder ends it)
401
+ if (entry.resend === undefined)
402
+ continue;
403
+ this.#subs.delete(had.id);
404
+ }
405
+ const id = 's' + this.#nextId++;
406
+ const sub = {
407
+ id, digest: entry.digest, entry, gate,
408
+ bytes: new TextEncoder().encode(jcs(entry.envelope)),
409
+ // a duplicate's ack is a hand-over of its own, from now
410
+ submittedAt: entry.resend !== undefined ? now : Math.min(entry.since, now),
411
+ attemptStartedAt: null, lastKind: null, st: null,
412
+ };
413
+ this.#subs.set(id, sub);
414
+ refs.push({ submissionId: id, digest: entry.digest });
415
+ // taken over past its horizon (counted from the first submission,
416
+ // RLTP-DEL-6130): given up at once, never attempted
417
+ if (now - sub.submittedAt >= DECLARED.giveUpHorizonMs) {
418
+ sub.st = { state: 'failed', reason: 'expired-by-adapter-policy' };
419
+ continue;
420
+ }
421
+ if (this.#wire.offline?.() !== true)
422
+ this.#startAttempt(sub, now);
423
+ }
424
+ return refs;
327
425
  }
328
426
  /** The current state as the ports' DeliveryReport for holder.report(),
329
427
  * or null while no honest report exists (and for an unknown id). A
@@ -334,7 +432,7 @@ export class MediatorCarrierAdapter {
334
432
  return null;
335
433
  const digest = this.#subs.get(id).digest;
336
434
  if (digest === undefined)
337
- throw new Error('submitted without a document digest: use submitOutbound');
435
+ throw new Error('submitted without a document digest: file holder documents through reconcile');
338
436
  return deliveryReportOf(digest, st);
339
437
  }
340
438
  has(id) {
@@ -355,7 +453,56 @@ export class MediatorCarrierAdapter {
355
453
  const token = await this.#ensureAuth(now);
356
454
  if (this.#closed)
357
455
  return; // retirement between auth and deposit: nothing new starts (R5-B2)
358
- await this.#wire.deposit(token, sub.bytes, this.#egressDid);
456
+ // each attempt hands the wire a copy: what it does with it never reaches the next
457
+ const deposit = () => {
458
+ try {
459
+ return this.#wire.deposit(token, new Uint8Array(sub.bytes), this.#egressDid);
460
+ }
461
+ catch (e) {
462
+ return Promise.reject(e);
463
+ }
464
+ };
465
+ if (sub.gate !== undefined) {
466
+ // the holder decides, and the deposit starts in the same step of its
467
+ // queue (Delivery RLTP-DEL-6125 to 6135); the adapter re-checks its
468
+ // own state there: retired, withdrawn, past the trias, past the
469
+ // entry's deadline or its own horizon → no deposit
470
+ // the deposit's outcome, caught at once: the holder's step may still
471
+ // be committing when the wire answers
472
+ let started = null;
473
+ let declined = false;
474
+ const entry = sub.entry;
475
+ const start = () => {
476
+ if (this.#closed || this.#subs.get(sub.id) !== sub || sub.gate.closed === true ||
477
+ (sub.st !== null && sub.st.state !== 'awaiting-transport') || this.#clock >= entry.notAfter ||
478
+ this.#clock - sub.submittedAt >= DECLARED.giveUpHorizonMs) {
479
+ declined = true;
480
+ return false;
481
+ }
482
+ started = deposit().then(() => null, (e) => ({ failed: e }));
483
+ return true;
484
+ };
485
+ let ok;
486
+ try {
487
+ ok = await sub.gate.handOver(entry, this.#clock, start);
488
+ }
489
+ catch {
490
+ ok = null;
491
+ }
492
+ if (started === null) {
493
+ // the holder said no: withdrawn; the adapter declined or the
494
+ // holder was unreachable: not this attempt
495
+ if (ok === false && !declined && this.#subs.get(sub.id) === sub)
496
+ this.#subs.delete(sub.id);
497
+ return;
498
+ }
499
+ const outcome = await started;
500
+ if (outcome !== null)
501
+ throw outcome.failed;
502
+ }
503
+ else {
504
+ await deposit();
505
+ }
359
506
  sub.lastKind = null;
360
507
  next = { state: 'accepted' };
361
508
  }
@@ -406,7 +553,7 @@ export class MediatorCarrierAdapter {
406
553
  if (!sub)
407
554
  return null;
408
555
  if (sub.st !== null && sub.st.state !== 'awaiting-transport')
409
- return sub.st;
556
+ return { ...sub.st };
410
557
  if (this.#wire.offline?.() === true) {
411
558
  return { state: 'awaiting-transport', reason: 'offline' };
412
559
  }
@@ -414,7 +561,7 @@ export class MediatorCarrierAdapter {
414
561
  this.#clock - sub.attemptStartedAt >= DECLARED.statusHorizonMs) {
415
562
  return { state: 'awaiting-transport', reason: 'transport-unreachable' };
416
563
  }
417
- return sub.st;
564
+ return sub.st === null ? null : { ...sub.st };
418
565
  }
419
566
  acknowledged(id) {
420
567
  if (this.#closed)
@@ -469,17 +616,33 @@ export class MediatorCarrierAdapter {
469
616
  this.#inboxDigests.clear();
470
617
  const id = 'c' + this.#nextColId++;
471
618
  this.#outstanding.set(id, new Map(items.map((i) => [i.queueId, i])));
472
- return { id, items: items.map((i) => ({ ...i })) };
619
+ // the caller gets its own copies: the outstanding entries stay intact,
620
+ // whatever it does with the bytes, for a conclusion that keeps them
621
+ return { id, items: items.map((i) => ({ ...i, bytes: new Uint8Array(i.bytes) })) };
473
622
  }
474
- conclude(collection, rawNow) {
623
+ /** Concludes a collection (profile §4, Delivery 5a.8): its items'
624
+ * digests become ack intent. `keep` names the items whose disposition
625
+ * is transient (Delivery RLTP-DEL-5810, 5820: `failed(rate-limited)`,
626
+ * or a transaction that could not commit, see `concludable`): they are
627
+ * not concluded and not acknowledged, they go back to the buffer and the
628
+ * next collect offers them again, and the mediator keeps its copy. */
629
+ conclude(collection, rawNow, opts = {}) {
475
630
  this.#open();
476
631
  const now = this.#now(rawNow);
477
632
  const col = this.#outstanding.get(collection.id);
478
633
  if (!col)
479
634
  return;
480
635
  this.#outstanding.delete(collection.id);
636
+ const keep = new Set(opts.keep ?? []);
481
637
  for (const item of col.values()) {
482
638
  this.#outstandingBytes -= item.bytes.length;
639
+ if (keep.has(item.queueId)) {
640
+ // transient: back to the buffer, offered again, never acknowledged
641
+ this.#inbox.push(item);
642
+ this.#inboxBytes += item.bytes.length;
643
+ this.#inboxDigests.add(item.queueId);
644
+ continue;
645
+ }
483
646
  this.#concluded.set(item.queueId, now);
484
647
  this.#pendingAcks.add(item.queueId);
485
648
  }
@@ -531,7 +694,8 @@ export class MediatorCarrierAdapter {
531
694
  for (const id of live)
532
695
  this.#flightAcks.add(id);
533
696
  try {
534
- await this.#wire.ackReceived(token, live);
697
+ // the wire gets its own list: what it does with it never reaches the bookkeeping below
698
+ await this.#wire.ackReceived(token, [...live]);
535
699
  if (!this.#closed)
536
700
  for (const id of live)
537
701
  this.#pendingAcks.delete(id);