@interop/wallet-core 0.69.0 → 0.71.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 (171) hide show
  1. package/README.md +22 -36
  2. package/dist/clientAnnex/ladder.d.ts +103 -57
  3. package/dist/clientAnnex/ladder.d.ts.map +1 -1
  4. package/dist/clientAnnex/ladder.js +254 -335
  5. package/dist/clientAnnex/ladder.js.map +1 -1
  6. package/dist/clientAnnex/ladderAnchored.d.ts.map +1 -1
  7. package/dist/clientAnnex/ladderAnchored.js +10 -5
  8. package/dist/clientAnnex/ladderAnchored.js.map +1 -1
  9. package/dist/clientAnnex/recoveryLadderAnchored.d.ts +3 -15
  10. package/dist/clientAnnex/recoveryLadderAnchored.d.ts.map +1 -1
  11. package/dist/clientAnnex/recoveryLadderAnchored.js +22 -3
  12. package/dist/clientAnnex/recoveryLadderAnchored.js.map +1 -1
  13. package/dist/clientAnnex/selfEnroll.js +1 -1
  14. package/dist/clientAnnex/selfEnroll.js.map +1 -1
  15. package/dist/clientAnnex/zcap.js +1 -1
  16. package/dist/clientAnnex/zcap.js.map +1 -1
  17. package/dist/clients/grantState.d.ts +67 -0
  18. package/dist/clients/grantState.d.ts.map +1 -0
  19. package/dist/clients/grantState.js +72 -0
  20. package/dist/clients/grantState.js.map +1 -0
  21. package/dist/clients/index.d.ts +7 -0
  22. package/dist/clients/index.d.ts.map +1 -1
  23. package/dist/clients/index.js +6 -0
  24. package/dist/clients/index.js.map +1 -1
  25. package/dist/enrollment/connectCode.d.ts +4 -3
  26. package/dist/enrollment/connectCode.d.ts.map +1 -1
  27. package/dist/enrollment/connectCode.js +4 -3
  28. package/dist/enrollment/connectCode.js.map +1 -1
  29. package/dist/enrollment/enrollment.js +1 -1
  30. package/dist/enrollment/enrollment.js.map +1 -1
  31. package/dist/enrollment/index.d.ts +2 -2
  32. package/dist/enrollment/index.js +2 -2
  33. package/dist/enrollment/onboardingInvite.d.ts +1 -1
  34. package/dist/enrollment/onboardingInvite.js +1 -1
  35. package/dist/enrollment/onboardingResponse.js +1 -1
  36. package/dist/index.d.ts +4 -11
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +4 -11
  39. package/dist/index.js.map +1 -1
  40. package/dist/keyring/index.d.ts +1 -1
  41. package/dist/keyring/index.js +1 -1
  42. package/dist/keyring/kdf.d.ts +34 -20
  43. package/dist/keyring/kdf.d.ts.map +1 -1
  44. package/dist/keyring/kdf.js +37 -26
  45. package/dist/keyring/kdf.js.map +1 -1
  46. package/dist/keys/userKey.js +1 -1
  47. package/dist/keys/userKey.js.map +1 -1
  48. package/dist/recovery/continuation.d.ts +5 -4
  49. package/dist/recovery/continuation.d.ts.map +1 -1
  50. package/dist/recovery/continuation.js +14 -13
  51. package/dist/recovery/continuation.js.map +1 -1
  52. package/dist/resourceLog/document.d.ts +10 -2
  53. package/dist/resourceLog/document.d.ts.map +1 -1
  54. package/dist/resourceLog/document.js +1 -2
  55. package/dist/resourceLog/document.js.map +1 -1
  56. package/dist/space/index.d.ts +4 -2
  57. package/dist/space/index.d.ts.map +1 -1
  58. package/dist/space/index.js +4 -2
  59. package/dist/space/index.js.map +1 -1
  60. package/dist/space/wasLink.d.ts +13 -2
  61. package/dist/space/wasLink.d.ts.map +1 -1
  62. package/dist/space/wasLink.js +28 -2
  63. package/dist/space/wasLink.js.map +1 -1
  64. package/dist/unlock/index.d.ts +1 -1
  65. package/dist/unlock/index.d.ts.map +1 -1
  66. package/dist/unlock/index.js.map +1 -1
  67. package/dist/unlock/standingClient.d.ts +1 -1
  68. package/dist/unlock/standingClient.d.ts.map +1 -1
  69. package/dist/unlock/standingClient.js +3 -3
  70. package/dist/unlock/standingClient.js.map +1 -1
  71. package/dist/unlock/standingWebvh.d.ts +81 -19
  72. package/dist/unlock/standingWebvh.d.ts.map +1 -1
  73. package/dist/unlock/standingWebvh.js +170 -30
  74. package/dist/unlock/standingWebvh.js.map +1 -1
  75. package/dist/webvh/did.d.ts +2 -4
  76. package/dist/webvh/did.d.ts.map +1 -1
  77. package/dist/webvh/did.js +2 -4
  78. package/dist/webvh/did.js.map +1 -1
  79. package/dist/webvh/didWebvh.d.ts +3 -2
  80. package/dist/webvh/didWebvh.d.ts.map +1 -1
  81. package/dist/webvh/didWebvh.js +2 -1
  82. package/dist/webvh/didWebvh.js.map +1 -1
  83. package/dist/webvh/index.d.ts +1 -1
  84. package/dist/webvh/index.js +1 -1
  85. package/dist/webvh/revokeClient.d.ts +13 -7
  86. package/dist/webvh/revokeClient.d.ts.map +1 -1
  87. package/dist/webvh/revokeClient.js +14 -7
  88. package/dist/webvh/revokeClient.js.map +1 -1
  89. package/dist/webvh/zcap.js +1 -1
  90. package/dist/webvh/zcap.js.map +1 -1
  91. package/package.json +2 -23
  92. package/dist/identity/agents.d.ts +0 -100
  93. package/dist/identity/agents.d.ts.map +0 -1
  94. package/dist/identity/agents.js +0 -120
  95. package/dist/identity/agents.js.map +0 -1
  96. package/dist/identity/index.d.ts +0 -24
  97. package/dist/identity/index.d.ts.map +0 -1
  98. package/dist/identity/index.js +0 -23
  99. package/dist/identity/index.js.map +0 -1
  100. package/dist/identity/keyResolver.d.ts +0 -30
  101. package/dist/identity/keyResolver.d.ts.map +0 -1
  102. package/dist/identity/keyResolver.js +0 -25
  103. package/dist/identity/keyResolver.js.map +0 -1
  104. package/dist/request/appKey.d.ts +0 -295
  105. package/dist/request/appKey.d.ts.map +0 -1
  106. package/dist/request/appKey.js +0 -516
  107. package/dist/request/appKey.js.map +0 -1
  108. package/dist/request/capabilityRequest.d.ts +0 -41
  109. package/dist/request/capabilityRequest.d.ts.map +0 -1
  110. package/dist/request/capabilityRequest.js +0 -46
  111. package/dist/request/capabilityRequest.js.map +0 -1
  112. package/dist/request/classify.d.ts +0 -226
  113. package/dist/request/classify.d.ts.map +0 -1
  114. package/dist/request/classify.js +0 -416
  115. package/dist/request/classify.js.map +0 -1
  116. package/dist/request/composeVp.d.ts +0 -52
  117. package/dist/request/composeVp.d.ts.map +0 -1
  118. package/dist/request/composeVp.js +0 -176
  119. package/dist/request/composeVp.js.map +0 -1
  120. package/dist/request/ephemeralExchange.d.ts +0 -100
  121. package/dist/request/ephemeralExchange.d.ts.map +0 -1
  122. package/dist/request/ephemeralExchange.js +0 -230
  123. package/dist/request/ephemeralExchange.js.map +0 -1
  124. package/dist/request/exchangeClient.d.ts +0 -142
  125. package/dist/request/exchangeClient.d.ts.map +0 -1
  126. package/dist/request/exchangeClient.js +0 -241
  127. package/dist/request/exchangeClient.js.map +0 -1
  128. package/dist/request/index.d.ts +0 -59
  129. package/dist/request/index.d.ts.map +0 -1
  130. package/dist/request/index.js +0 -59
  131. package/dist/request/index.js.map +0 -1
  132. package/dist/request/interactionRequest.d.ts +0 -23
  133. package/dist/request/interactionRequest.d.ts.map +0 -1
  134. package/dist/request/interactionRequest.js +0 -46
  135. package/dist/request/interactionRequest.js.map +0 -1
  136. package/dist/request/interactionUrl.d.ts +0 -42
  137. package/dist/request/interactionUrl.d.ts.map +0 -1
  138. package/dist/request/interactionUrl.js +0 -104
  139. package/dist/request/interactionUrl.js.map +0 -1
  140. package/dist/request/matching.d.ts +0 -95
  141. package/dist/request/matching.d.ts.map +0 -1
  142. package/dist/request/matching.js +0 -205
  143. package/dist/request/matching.js.map +0 -1
  144. package/dist/request/onboarding.d.ts +0 -94
  145. package/dist/request/onboarding.d.ts.map +0 -1
  146. package/dist/request/onboarding.js +0 -167
  147. package/dist/request/onboarding.js.map +0 -1
  148. package/dist/request/parse.d.ts +0 -63
  149. package/dist/request/parse.d.ts.map +0 -1
  150. package/dist/request/parse.js +0 -153
  151. package/dist/request/parse.js.map +0 -1
  152. package/dist/request/presentationSuite.d.ts +0 -71
  153. package/dist/request/presentationSuite.d.ts.map +0 -1
  154. package/dist/request/presentationSuite.js +0 -143
  155. package/dist/request/presentationSuite.js.map +0 -1
  156. package/dist/request/processRequest.d.ts +0 -45
  157. package/dist/request/processRequest.d.ts.map +0 -1
  158. package/dist/request/processRequest.js +0 -147
  159. package/dist/request/processRequest.js.map +0 -1
  160. package/dist/request/queryPredicates.d.ts +0 -98
  161. package/dist/request/queryPredicates.d.ts.map +0 -1
  162. package/dist/request/queryPredicates.js +0 -107
  163. package/dist/request/queryPredicates.js.map +0 -1
  164. package/dist/request/types.d.ts +0 -257
  165. package/dist/request/types.d.ts.map +0 -1
  166. package/dist/request/types.js +0 -2
  167. package/dist/request/types.js.map +0 -1
  168. package/dist/request/walletInput.d.ts +0 -93
  169. package/dist/request/walletInput.d.ts.map +0 -1
  170. package/dist/request/walletInput.js +0 -179
  171. package/dist/request/walletInput.js.map +0 -1
@@ -40,8 +40,7 @@ import { vmFragmentOf } from '@interop/vh-resource-log';
40
40
  import { hkdf } from '@noble/hashes/hkdf.js';
41
41
  import { sha256 } from '@noble/hashes/sha2.js';
42
42
  import { currentLogParameters, effectiveParameters, updateKeyMultibase } from '../webvh/didWebvh.js';
43
- import { listEnrolledWebvhClients } from '../webvh/listClients.js';
44
- import { credentialKeyAgreementMethods, introducedCredentialKeys, ladderVmIds, relationIds, resolvedKeyAgreementMethods, retiredCredentialKeys } from '../resourceLog/document.js';
43
+ import { credentialKeyAgreementMethods, introducedCredentialKeys, ladderVmIds, relationIds } from '../resourceLog/document.js';
45
44
  import { survivingClientKeyProtection } from '../webvh/revokeClient.js';
46
45
  import { log as logger } from '../log.js';
47
46
  import { LADDER_SEED_BYTES } from '../unlock/unlockRecord.js';
@@ -351,6 +350,25 @@ function credentialSurvives({ entry, vmId }) {
351
350
  // commitment.
352
351
  return relationIds(entry.state.keyAgreement).includes(vmId);
353
352
  }
353
+ /**
354
+ * Whether the document an entry publishes carries a ladder VM the previous
355
+ * entry's did not -- the install half of a bind, a split issuance's authority
356
+ * entry, or a reinstall.
357
+ *
358
+ * @param options {object}
359
+ * @param options.log {DIDLog}
360
+ * @param options.index {number} the entry's index in the log
361
+ * @returns {boolean}
362
+ */
363
+ function introducesLadderVm({ log, index }) {
364
+ const doc = log[index]?.state;
365
+ const prevDoc = log[index - 1]?.state;
366
+ if (doc === undefined) {
367
+ return false;
368
+ }
369
+ const standing = new Set(prevDoc === undefined ? [] : ladderVmIds({ doc: prevDoc }));
370
+ return ladderVmIds({ doc }).some(vmId => !standing.has(vmId));
371
+ }
354
372
  /**
355
373
  * The log-derived indexes every attribution walk starts from, computed once
356
374
  * per log rather than once per credential.
@@ -382,23 +400,6 @@ function indexedLadderLog(log) {
382
400
  ladderLogIndexes.set(log, indexed);
383
401
  return indexed;
384
402
  }
385
- /**
386
- * The account's enrolled clients as the log attributes them, memoized on the
387
- * log for the same reason as {@link indexedLadderLog}.
388
- *
389
- * @param log {DIDLog}
390
- * @returns {ReturnType<typeof listEnrolledWebvhClients>}
391
- */
392
- const enrolledClientsByLog = new WeakMap();
393
- function indexedEnrolledClients(log) {
394
- const cached = enrolledClientsByLog.get(log);
395
- if (cached) {
396
- return cached;
397
- }
398
- const clients = listEnrolledWebvhClients({ log });
399
- enrolledClientsByLog.set(log, clients);
400
- return clients;
401
- }
402
403
  /**
403
404
  * The one pre-pass over the log's effective parameters: per-entry facts, plus
404
405
  * the commit index both walks project their positional questions through. The
@@ -451,7 +452,16 @@ function indexLadderLog({ log, params }) {
451
452
  * that entry is the rung before it. The step is taken only when the entry
452
453
  * authorized exactly one key and that key signed the entry, which makes it a
453
454
  * prerotation reveal rather than mere adjacency, and only when the credential
454
- * itself still stands in the entry's document.
455
+ * itself still stands in the entry's document. One more gate tells a climb
456
+ * from a bind. An entry that installs the credential's inventory -- its
457
+ * `keyAgreement` member, or a ladder VM not standing before -- is where the
458
+ * ladder is being bound, and the rung that signs a bind is the credential's
459
+ * own only where the ladder is born there: the signer's own hash is newly
460
+ * committed in that same entry, the ladder-anchored genesis shape. A signer
461
+ * whose hash an earlier entry committed is a sibling ladder's rung revealing
462
+ * itself in the entry it signs on the newcomer's behalf (the ladder-signed
463
+ * `publishUnlockKey` bind, a split issuance's authority entry), and the walk
464
+ * stops rather than annexing it.
455
465
  *
456
466
  * The ADJACENCY rule is a handover. A hash not in last position sits beside
457
467
  * the rung committed immediately before it, and that predecessor is revealed
@@ -463,15 +473,19 @@ function indexLadderLog({ log, params }) {
463
473
  * ours yet, and at the spent recovery code whose reveal entry commits the
464
474
  * REPLACEMENT code's hash last -- without it the replacement's retirement
465
475
  * would recover the spent code's key and go on to strike the fresh
466
- * credential's rungs. That last stop is load-bearing for the anchor rule of
467
- * `decisions/0014`: a replacement code anchored on that last-position hash
468
- * enters this walk through the climb rule, and its member is published one
469
- * entry after the reveal that committed the hash, which is exactly what the
470
- * test refuses on. The single-self-revealing-key test stops it at the bind
476
+ * credential's rungs. That last stop is load-bearing for a replacement code:
477
+ * its member's `ladderCommitment` is that last-position hash, so its walk
478
+ * enters here through the climb rule, and its member is published one entry
479
+ * after the reveal that committed the hash, which is exactly what the test
480
+ * refuses on. The single-self-revealing-key test stops it at the bind
471
481
  * entry an enrolled client signs, which authorizes no key of its own, so the
472
- * binding client's update key is never recovered as a rung. The strictly
473
- * decreasing entry cursor and the already-recovered test keep the walk finite
474
- * and acyclic.
482
+ * binding client's update key is never recovered as a rung. The ladder-birth
483
+ * test stops it at the bind entry a sibling's committed rung signs, which
484
+ * authorizes exactly that rung, so the acting credential's rung is never
485
+ * recovered as the bound credential's -- the over-strike that would leave
486
+ * the acting credential unable to extend the log. The strictly decreasing
487
+ * entry cursor and the already-recovered test keep the walk finite and
488
+ * acyclic.
475
489
  *
476
490
  * @param options {object}
477
491
  * @param options.log {DIDLog}
@@ -498,16 +512,24 @@ async function recoverEarlierRungs({ log, facts, commitIndex, anchorHash, creden
498
512
  if (origin.at === originFacts.addedHashes.length - 1) {
499
513
  // The last-position rule, read backwards: a climb.
500
514
  const predecessor = originFacts.addedKeys[0];
515
+ const originEntry = log[origin.entryIndex];
501
516
  if (originFacts.addedKeys.length !== 1 ||
502
517
  predecessor === undefined ||
503
518
  !originFacts.signers.includes(predecessor) ||
504
- !credentialSurvives({
505
- entry: log[origin.entryIndex],
506
- vmId: credentialVmId
507
- })) {
519
+ !credentialSurvives({ entry: originEntry, vmId: credentialVmId })) {
508
520
  return recovered;
509
521
  }
510
522
  const hash = await deriveNextKeyHash(predecessor);
523
+ // The ladder-birth test: an entry installing the credential's inventory
524
+ // is a bind, whose signer is this ladder's rung only where the ladder
525
+ // is born there -- its own hash committed in the same entry.
526
+ const installsInventory = !credentialSurvives({
527
+ entry: log[origin.entryIndex - 1],
528
+ vmId: credentialVmId
529
+ }) || introducesLadderVm({ log, index: origin.entryIndex });
530
+ if (installsInventory && !originFacts.addedHashes.includes(hash)) {
531
+ return recovered;
532
+ }
511
533
  if (seenHashes.has(hash)) {
512
534
  return recovered;
513
535
  }
@@ -597,84 +619,173 @@ export function assertNextKeyHashesRemain({ nextKeyHashes, ceremony }) {
597
619
  return nextKeyHashes;
598
620
  }
599
621
  /**
600
- * The enrolled-client members an entry INTRODUCES: new `capabilityInvocation`
601
- * ids, and new `keyAgreement` methods the account DID does not control (a
602
- * client's marked twin). The bind-anchor read refuses any entry that
603
- * introduces one, because an entry publishing a client also publishes that
604
- * client's update key, and reading that key as a credential's rung 0 would
605
- * anchor the walk on a surviving client.
622
+ * The anchor a credential's ladder walk starts from when the caller holds no
623
+ * recorded update key -- the log-only anchoring a cold browser needs. Every
624
+ * credential-class `keyAgreement` member names its own ladder's rung-0
625
+ * commitment (`ladderCommitment`, written by every bind site through
626
+ * `unlockKeyVerificationMethod`), so the anchor is read off the member
627
+ * itself rather than inferred from the shape of the entry that introduced
628
+ * it. The value is `hash(rung 0)` in the form `nextKeyHashes` carries, the
629
+ * same commitment the bind puts there.
630
+ *
631
+ * The value is taken from the entry that INTRODUCED the member, and only
632
+ * from an account-controlled member (an enrolled client's marked twin is
633
+ * never a credential). A member is introduced each time it appears after
634
+ * not standing, so a member re-bound after a retirement (the same passphrase
635
+ * added again) anchors on the fresh ladder's commitment, and a log read
636
+ * before the retirement entry that struck it still names it. What a later
637
+ * entry says about a member that already stood is checked, not adopted: the
638
+ * property is a document member any update-key holder can restate, and a
639
+ * value that changes while the member stands continuously is a retargeting
640
+ * no bind performs. Such a member names no anchor for the rest of that
641
+ * standing run, so the retargeting is reported as unclaimed rather than
642
+ * quietly walked from the substituted hash. The write side carries the same
643
+ * rule: `publishUnlockKey`, the one path in this library that rewrites a
644
+ * standing member, refuses a bind whose rung-0 hash differs from the value
645
+ * the member already names, so a retargeting in a served log is a foreign
646
+ * writer's and never a torn re-run's. The guard covers only a
647
+ * continuous run: the same update-key holder can strike the member in one
648
+ * entry and re-add it under a fresh commitment in the next, which reads as a
649
+ * legitimate re-bind and is not distinguishable from one seedlessly. That
650
+ * shape rests on the log's loudness and on `survivingClientKeyProtection`
651
+ * instead. A member without the property, or one nowhere in the log, names
652
+ * no anchor either.
653
+ *
654
+ * The named hash must also be one the log COMMITTED FOR THE MEMBER: newly
655
+ * added to `nextKeyHashes` by the introducing entry (the merged bind, the
656
+ * ladder-anchored genesis), by a later entry of the same standing run (the
657
+ * split bind's authority entry, two versions after its key entry), or by an
658
+ * earlier entry whose signer the introducing entry retires (the recovery
659
+ * continuations' reveal-and-commit entry, signed by the spent code's rung,
660
+ * which the add-and-retire entry introducing the replacement's member and
661
+ * the fresh credential's strikes -- every reveal entry of a torn and resumed
662
+ * continuation is that shape). A member naming a hash that stood in
663
+ * `nextKeyHashes` already, committed by some other entry for something else
664
+ * -- an enrolled client's staged hash, say, which any update-key holder can
665
+ * read off the log and restate as a commitment -- names no anchor. Without
666
+ * the check the walk would claim that hash for the credential, and a
667
+ * retirement that honored the claim would strike a surviving client's
668
+ * staged commitment. The check is necessary rather than sufficient: an
669
+ * approver can name a hash BEFORE the enrollment it approves commits it, and
670
+ * that is the shape `survivingClientKeyProtection`'s positional exemption
671
+ * closes. Every refusal returns `undefined`, which the callers report as
672
+ * unclaimed rather than acting on.
606
673
  *
607
674
  * @param options {object}
608
- * @param options.doc {KeyAgreementDocument} the entry's document
609
- * @param [options.prevDoc] {KeyAgreementDocument} the previous entry's
610
- * @param options.did {string} the account DID
611
- * @returns {boolean}
675
+ * @param options.log {DIDLog} a resolved, caller-verified log
676
+ * @param options.credentialVmId {string} the credential's `keyAgreement`
677
+ * verification-method id
678
+ * @returns {{ anchorHash: string } | undefined}
612
679
  */
613
- function introducesEnrolledClient({ doc, prevDoc, did }) {
614
- const beforeInvocation = new Set(relationIds(prevDoc?.capabilityInvocation));
615
- if (relationIds(doc.capabilityInvocation).some(id => !beforeInvocation.has(id))) {
616
- return true;
617
- }
618
- const markedIds = (entryDoc) => {
619
- if (entryDoc === undefined) {
620
- return new Set();
621
- }
622
- const credential = new Set(credentialKeyAgreementMethods({ doc: entryDoc, did }).map(method => method.id));
623
- return new Set(resolvedKeyAgreementMethods({ doc: entryDoc })
624
- .map(method => method.id)
625
- .filter((id) => id !== undefined && !credential.has(id)));
626
- };
627
- const before = markedIds(prevDoc);
628
- return [...markedIds(doc)].some(id => !before.has(id));
680
+ export function credentialLadderAnchor({ log, credentialVmId }) {
681
+ const reading = credentialLadderCommitment({ log, credentialVmId });
682
+ return reading === undefined ||
683
+ reading.named === undefined ||
684
+ reading.retargeted ||
685
+ !reading.committed
686
+ ? undefined
687
+ : { anchorHash: reading.named };
629
688
  }
630
689
  /**
631
- * The anchor a credential's ladder walk starts from when the caller holds no
632
- * recorded update key -- the log-only anchoring a cold browser needs. The
633
- * credential's own `keyAgreement` member id is the anchor: the entry that
634
- * FIRST introduced that member is the credential's bind entry, and what that
635
- * entry did to the standing parameters names rung 0.
636
- *
637
- * Two shapes are read, both fail-closed:
638
- *
639
- * - the entry authorized exactly one update key and that key signed it (a
640
- * prerotation reveal, the ladder-anchored genesis shape), so rung 0 is that
641
- * key outright;
642
- * - the entry authorized no key of its own and newly committed exactly one
643
- * hash (the `publishUnlockKey` bind an enrolled client signs, and the
644
- * recovery-code issuance sharing it), so rung 0's hash is that hash.
645
- *
646
- * A third shape is the recovery add-and-retire entry, which the two above
647
- * cannot read: the transient continuation's introduces the fresh credential's
648
- * member and the replacement code's together, and the remembered
649
- * continuation's introduces the replacement's beside an enrolled client. Both
650
- * are the HANDOVER of `decisions/0007`, read here as the anchor rule of
651
- * `decisions/0014`: the entry authorized exactly one key, that key signed it,
652
- * that key's hash was committed by an earlier reveal-and-commit entry whose
653
- * signer this entry retires, and that earlier entry appended exactly three
654
- * hashes with the successor's first. The reveal entry's LAST addition is then
655
- * the replacement code's rung-0 hash (what an entry hands to a successor
656
- * credential comes last), and the successor key is the fresh credential's
657
- * rung 0. Which member is which is read off the `keyAgreement` relation's
658
- * order, which the emitter fixes: the fresh credential's member precedes the
659
- * replacement code's. So a two-member bind that publishes no enrolled client
660
- * anchors its first member on the successor key and its second on the last
661
- * addition; a one-member bind that publishes an enrolled client anchors that
662
- * member on the last addition alone, since the successor key there is the
663
- * client's.
664
- *
665
- * Anything else is ambiguous and returns `undefined`, which the callers report
666
- * as unclaimed rather than acting on.
690
+ * What the log says about a credential member's `ladderCommitment`, before
691
+ * {@link credentialLadderAnchor} decides whether it is an anchor: the value
692
+ * the entry introducing the member's latest standing run named (`named`),
693
+ * whether the member stands in the head document (`stands`), whether a later
694
+ * entry of that run restated the value (`retargeted`), and whether the log
695
+ * committed the named hash for the member (`committed`). The anchor reader
696
+ * is the refusing composition of these; the bind's write-side check reads
697
+ * them apart, since a split bind's authority entry meets a member that names
698
+ * its hash while the log has not committed it yet.
667
699
  *
668
700
  * @param options {object}
669
701
  * @param options.log {DIDLog} a resolved, caller-verified log
670
702
  * @param options.credentialVmId {string} the credential's `keyAgreement`
671
703
  * verification-method id
672
- * @returns {Promise<{ anchorKeyMultibase?: string, anchorHash?: string } |
673
- * undefined>}
704
+ * @returns {{ named?: string, stands: boolean, retargeted: boolean,
705
+ * committed: boolean } | undefined} `undefined` for a member the log
706
+ * never introduced
674
707
  */
675
- export async function credentialLadderAnchor({ log, credentialVmId }) {
676
- const { facts, commitIndex } = indexedLadderLog(log);
677
- return resolveBindAnchor({ log, facts, commitIndex, credentialVmId });
708
+ export function credentialLadderCommitment({ log, credentialVmId }) {
709
+ const did = credentialVmId.split('#')[0];
710
+ if (did === undefined || did === '') {
711
+ return undefined;
712
+ }
713
+ const { facts } = indexedLadderLog(log);
714
+ let introduced = false;
715
+ let named;
716
+ let stood = false;
717
+ let retargeted = false;
718
+ let committed = false;
719
+ for (const [index, entry] of log.entries()) {
720
+ const doc = entry.state;
721
+ if (doc === undefined) {
722
+ continue;
723
+ }
724
+ const member = credentialKeyAgreementMethods({ doc, did }).find(method => method.id === credentialVmId);
725
+ if (member === undefined) {
726
+ stood = false;
727
+ continue;
728
+ }
729
+ const value = typeof member.ladderCommitment === 'string' &&
730
+ member.ladderCommitment !== ''
731
+ ? member.ladderCommitment
732
+ : undefined;
733
+ const here = facts[index];
734
+ if (!stood) {
735
+ // Introduced here (or re-introduced after a strike): the bind's word.
736
+ introduced = true;
737
+ named = value;
738
+ retargeted = false;
739
+ committed =
740
+ value !== undefined &&
741
+ (here?.addedHashes.includes(value) === true ||
742
+ committedByRetiredSigner({ facts, index, hash: value }));
743
+ }
744
+ else if (value !== named) {
745
+ retargeted = true;
746
+ }
747
+ else if (!committed && value !== undefined) {
748
+ committed = here?.addedHashes.includes(value) === true;
749
+ }
750
+ stood = true;
751
+ }
752
+ return introduced
753
+ ? {
754
+ ...(named !== undefined ? { named } : {}),
755
+ stands: stood,
756
+ retargeted,
757
+ committed
758
+ }
759
+ : undefined;
760
+ }
761
+ /**
762
+ * Whether an entry BEFORE `index` newly committed `hash` and was signed by an
763
+ * update key the entry at `index` retires -- the handover shape the recovery
764
+ * continuations leave: the spent code's rung signs the reveal-and-commit
765
+ * entry that commits the successors' hashes, and the add-and-retire entry
766
+ * that introduces their members strikes that rung. A torn and resumed
767
+ * continuation leaves several reveal entries, every one signed by the same
768
+ * rung, so the earliest one's commitments qualify as well as the last one's.
769
+ *
770
+ * @param options {object}
771
+ * @param options.facts {LadderEntryFacts[]} from {@link indexLadderLog}
772
+ * @param options.index {number} the introducing entry
773
+ * @param options.hash {string} the member's named commitment
774
+ * @returns {boolean}
775
+ */
776
+ function committedByRetiredSigner({ facts, index, hash }) {
777
+ const retired = new Set(facts[index]?.removedKeys ?? []);
778
+ if (retired.size === 0) {
779
+ return false;
780
+ }
781
+ for (let earlier = index - 1; earlier >= 0; earlier--) {
782
+ const entry = facts[earlier];
783
+ if (entry.addedHashes.includes(hash) &&
784
+ entry.signers.some(signer => retired.has(signer))) {
785
+ return true;
786
+ }
787
+ }
788
+ return false;
678
789
  }
679
790
  /**
680
791
  * The ladder VMs the log introduced ALONGSIDE something of this credential's
@@ -770,12 +881,11 @@ export async function ladderVmIdsIntroducedWithCredential({ log, credentialVmId,
770
881
  * another resumed reveal, walked past (a continuation torn at its seam twice
771
882
  * leaves one behind). A three-addition one is an attempt that committed a
772
883
  * replacement, and its last addition is collected. Any other size is no
773
- * attempt of this continuation's, and the answer is refused. The two readers
774
- * ask different questions of the result: the forward walk asks only whether
775
- * an attempt committed a replacement at all (so the two-addition entry's last
776
- * position is the ladder's own rung), while the anchor rule needs exactly one
777
- * such attempt, since more than one means the replacement changed between
778
- * resumes, which the contract forbids and which no last position can settle.
884
+ * attempt of this continuation's, and the answer is refused. The forward walk
885
+ * asks only whether an attempt committed a replacement at all, so the
886
+ * two-addition entry's last position is the ladder's own rung. Which hash is
887
+ * the replacement's is never read from here: its member names it
888
+ * (`ladderCommitment`).
779
889
  *
780
890
  * @param options {object}
781
891
  * @param options.facts {LadderEntryFacts[]} from {@link indexLadderLog}
@@ -802,212 +912,9 @@ function resumedRevealReplacementHashes({ facts, before, retiredSigners }) {
802
912
  }
803
913
  return replacementHashes;
804
914
  }
805
- /**
806
- * The recovery add-and-retire arm of {@link resolveBindAnchor}: the handover
807
- * shape of `decisions/0007` read as an anchor, per `decisions/0014`. Every
808
- * test is fail-closed. A shape that passes them all names the successor key
809
- * the entry authorized and, when the reveal entry that committed its hash
810
- * (or, on a resumed reveal, the attempt before it) ends on one, the
811
- * replacement code's hash. The two are decoupled on purpose: the successor
812
- * key is unambiguous from the bind entry alone, so a replacement lookup that
813
- * refuses leaves the fresh credential anchored and retirable.
814
- *
815
- * The between-entries test is what refuses a remembered continuation whose
816
- * reveal entry was published twice with a DIFFERENT replacement code, which
817
- * the continuation's contract forbids. The retired signer then signed an
818
- * entry after the reveal, and no last addition is the replacement's. A reveal
819
- * published twice with the SAME replacement is the resumed shape
820
- * {@link resumedRevealReplacementHashes} reads.
821
- *
822
- * The retired-member test is what keeps the arm off a self-enrollment's add
823
- * entry, which passes every other clause: its reveal entry also commits
824
- * exactly three hashes with the successor's first, and its add entry also
825
- * authorizes one self-signing key while retiring the reveal's signer. Only a
826
- * spend strikes a credential-class `keyAgreement` member in that entry, and
827
- * a self-enrollment's third hash is the acting ladder's own next rung, not a
828
- * successor's.
829
- *
830
- * @param options {object}
831
- * @param options.facts {LadderEntryFacts[]} from {@link indexLadderLog}
832
- * @param options.commitIndex {Map<string, LadderCommitOrigin>} likewise
833
- * @param options.bind {LadderEntryFacts} the bind entry's facts
834
- * @param options.index {number} the bind entry's index
835
- * @param options.successorKeyMultibase {string} the one key it authorized
836
- * @param options.successorHash {string} that key's hash
837
- * @param options.retiresCredentialMember {boolean} whether the bind entry
838
- * struck a credential-class `keyAgreement` member (the spent code's)
839
- * @returns {{ successorKeyMultibase: string, replacementHash?: string } |
840
- * undefined}
841
- */
842
- function resolveHandoverShape({ facts, commitIndex, bind, index, successorKeyMultibase, successorHash, retiresCredentialMember }) {
843
- if (!retiresCredentialMember ||
844
- bind.addedKeys.length !== 1 ||
845
- !bind.signers.includes(successorKeyMultibase)) {
846
- return undefined;
847
- }
848
- // The reveal entry: it committed the successor's hash first among exactly
849
- // three additions (or two, on a resumed reveal), and this entry retires a
850
- // key that signed it.
851
- const origin = commitIndex.get(successorHash);
852
- if (origin === undefined || origin.entryIndex >= index || origin.at !== 0) {
853
- return undefined;
854
- }
855
- const reveal = facts[origin.entryIndex];
856
- const retiredSigners = reveal.signers.filter(signer => bind.removedKeys.includes(signer));
857
- if (retiredSigners.length === 0) {
858
- return undefined;
859
- }
860
- for (let between = origin.entryIndex + 1; between < index; between++) {
861
- if (facts[between].signers.some(key => retiredSigners.includes(key))) {
862
- return undefined;
863
- }
864
- }
865
- const earlier = reveal.addedHashes.length === 2
866
- ? resumedRevealReplacementHashes({
867
- facts,
868
- before: origin.entryIndex,
869
- retiredSigners
870
- })
871
- : undefined;
872
- const replacementHash = reveal.addedHashes.length === 3
873
- ? reveal.addedHashes[2]
874
- : earlier?.length === 1
875
- ? earlier[0]
876
- : undefined;
877
- return {
878
- successorKeyMultibase,
879
- ...(replacementHash !== undefined ? { replacementHash } : {})
880
- };
881
- }
882
- /**
883
- * The core of {@link credentialLadderAnchor}, over a pre-pass the caller
884
- * already ran.
885
- *
886
- * @param options {object}
887
- * @param options.log {DIDLog}
888
- * @param options.facts {LadderEntryFacts[]} from {@link indexLadderLog}
889
- * @param options.commitIndex {Map<string, LadderCommitOrigin>} likewise
890
- * @param options.credentialVmId {string}
891
- * @returns {Promise<{ anchorKeyMultibase?: string, anchorHash?: string } |
892
- * undefined>}
893
- */
894
- async function resolveBindAnchor({ log, facts, commitIndex, credentialVmId }) {
895
- const did = credentialVmId.split('#')[0];
896
- if (did === undefined || did === '') {
897
- return undefined;
898
- }
899
- // Every update key the log attributes to a client the final document still
900
- // lists, so the self-signed arm can refuse one outright. A client whose
901
- // active key the log cannot attribute leaves that arm unable to refuse
902
- // anything, so no anchor is named at all.
903
- const enrolledClients = indexedEnrolledClients(log);
904
- if (enrolledClients.some(client => client.updateKeyMultibase === undefined)) {
905
- return undefined;
906
- }
907
- const enrolledClientKeys = new Set(enrolledClients
908
- .map(client => client.updateKeyMultibase)
909
- .filter((key) => key !== undefined));
910
- let prevDoc;
911
- for (const [index, entry] of log.entries()) {
912
- const doc = entry.state;
913
- if (doc === undefined) {
914
- continue;
915
- }
916
- const introduced = introducedCredentialKeys({ doc, prevDoc, did });
917
- const prevDocBefore = prevDoc;
918
- prevDoc = doc;
919
- if (!introduced.includes(credentialVmId)) {
920
- continue;
921
- }
922
- // The bind entry.
923
- const bind = facts[index];
924
- if (bind === undefined) {
925
- return undefined;
926
- }
927
- const publishesClient = introducesEnrolledClient({
928
- doc: doc,
929
- prevDoc: prevDocBefore,
930
- did
931
- });
932
- // The recovery add-and-retire arm, on the two shapes the continuations
933
- // write: two credential-class members and no client (the transient
934
- // continuation), or one member beside a client (the remembered one). The
935
- // successor key is an anchor only on the first shape, and only when the
936
- // log attributes it to no enrolled client; on the second it is the
937
- // client's update key, and only the replacement's hash is read.
938
- const successorKey = bind.addedKeys[0];
939
- const handover = successorKey === undefined || introduced.length > 2
940
- ? undefined
941
- : resolveHandoverShape({
942
- facts,
943
- commitIndex,
944
- bind,
945
- index,
946
- successorKeyMultibase: successorKey,
947
- successorHash: await deriveNextKeyHash(successorKey),
948
- retiresCredentialMember: retiredCredentialKeys({ doc, prevDoc: prevDocBefore, did })
949
- .length > 0
950
- });
951
- if (handover !== undefined) {
952
- const replacementAnchor = handover.replacementHash === undefined
953
- ? undefined
954
- : { anchorHash: handover.replacementHash };
955
- if (introduced.length === 2 && !publishesClient) {
956
- if (credentialVmId === introduced[0]) {
957
- return enrolledClientKeys.has(handover.successorKeyMultibase)
958
- ? undefined
959
- : { anchorKeyMultibase: handover.successorKeyMultibase };
960
- }
961
- return replacementAnchor;
962
- }
963
- if (introduced.length === 1 && publishesClient) {
964
- return replacementAnchor;
965
- }
966
- // A handover whose members match neither continuation's shape. The
967
- // continuation refuses to re-bind a standing credential's own member
968
- // (`RecoveryCredentialStandingError`), so no emitter writes a
969
- // one-member transient entry; a log that carries one is refused here
970
- // rather than have the self-signed-key arm below anchor the
971
- // replacement on the fresh credential's rung 0.
972
- return undefined;
973
- }
974
- // More than one credential-class member introduced here and nothing
975
- // below can say which addition is whose.
976
- if (introduced.length !== 1) {
977
- return undefined;
978
- }
979
- // The fourth condition: an entry that also publishes an enrolled client
980
- // names no credential's rung. The remembered recovery's add-and-retire
981
- // entry is this shape -- the new client's key-agreement method is
982
- // client-marked, so the credential-class count above sees only the
983
- // replacement code and the ambiguity guard does not fire, while the one
984
- // key the entry authorizes is the CLIENT's update key -- and the handover
985
- // arm above is the only reading of it.
986
- if (publishesClient) {
987
- return undefined;
988
- }
989
- const revealed = bind.addedKeys[0];
990
- if (bind.addedKeys.length === 1 &&
991
- revealed !== undefined &&
992
- bind.signers.includes(revealed) &&
993
- // Belt and braces beside the condition above: never anchor on a key the
994
- // log attributes to an enrolled client, whichever entry published it.
995
- !enrolledClientKeys.has(revealed)) {
996
- return { anchorKeyMultibase: revealed };
997
- }
998
- if (bind.addedKeys.length === 0 && bind.addedHashes.length === 1) {
999
- // No enrolled-client check of its own: this arm reads a hash rather
1000
- // than a key, and no ceremony fuses a credential bind with a client's
1001
- // hash commitment.
1002
- return { anchorHash: bind.addedHashes[0] };
1003
- }
1004
- return undefined;
1005
- }
1006
- return undefined;
1007
- }
1008
915
  /**
1009
916
  * Whether one retiring credential's rung inventory can be claimed from the log
1010
- * at all: its bind entry must name an anchor, and the walk from that anchor
917
+ * at all: its member must name an anchor, and the walk from that anchor
1011
918
  * must not refuse. This is the log-only test, so it answers the same before
1012
919
  * and after the retirement entry lands -- which is what lets a resumed run
1013
920
  * report the same unclaimed set the first run reported.
@@ -1033,7 +940,7 @@ async function claimLadderInventory({ log, credentialVmId, maxScan }) {
1033
940
  * lists, read off the log alone -- the latent commitments a client removal
1034
941
  * must exclude before it attributes the removed client's staged hash. Each
1035
942
  * credential-class `keyAgreement` member (an unmarked method, verbatim key or
1036
- * commitment) is anchored from its bind entry and walked
943
+ * commitment) is anchored from its member's `ladderCommitment` and walked
1037
944
  * ({@link attributeLadderInventory}); a credential whose anchor or walk
1038
945
  * refuses claims nothing and is named on `unclaimedCredentialVmIds`, so the
1039
946
  * caller can tell "no latent hashes stand" from "one credential's could not
@@ -1147,9 +1054,12 @@ export async function retiredCredentialRungsBeforeKey({ log, authorizedKeyMultib
1147
1054
  * than a property of the walk, because a mis-anchored walk landing on a
1148
1055
  * client's key would otherwise end that client's ability to extend the
1149
1056
  * account log for good. The walks therefore run FIRST, and the hashes they
1150
- * claimed are passed to the protection as known-latent, so a retiring
1057
+ * claimed are passed to the protection as walk-derived, so a retiring
1151
1058
  * credential's own rung cannot make a client's staged attribution ambiguous
1152
- * and get itself protected as a candidate;
1059
+ * and get itself protected as a candidate. The protection never lets such
1060
+ * a claim prune the hash the decision-0007 position names as a client's
1061
+ * staged hash, so a walk anchored on a member that names that hash claims
1062
+ * it and is withheld, rather than striking it;
1153
1063
  * - a listed enrolled client whose ACTIVE update key the log cannot attribute
1154
1064
  * withholds the WHOLE strike: nothing is struck and every credential is
1155
1065
  * reported, since the structural guard cannot say what that client holds.
@@ -1195,7 +1105,7 @@ export async function attributeRetiredCredentialRungs({ log, credentialVmIds, pr
1195
1105
  const surviving = await survivingClientKeyProtection({
1196
1106
  log,
1197
1107
  retiredVmIds: credentialVmIds,
1198
- knownLatentHashes: [...claimedHashes]
1108
+ derivedLatentHashes: [...claimedHashes]
1199
1109
  });
1200
1110
  if (surviving.ambiguous.length > 0) {
1201
1111
  logger.warn('Withholding a credential rung strike: an enrolled client whose ' +
@@ -1340,29 +1250,31 @@ export async function attributeRetiredCredentialRungs({ log, credentialVmIds, pr
1340
1250
  * once matches no legitimate history and fails closed ({@link
1341
1251
  * LadderAttributionError}).
1342
1252
  *
1343
- * One shape is out of reach seedlessly, and it is reachable today. The
1344
- * last-client transition strikes the ladder VM and reinstalls it in the same
1345
- * run (`forgetLastEnrolledClient` stage 1): same seed, the credential's
1346
- * member standing, the acting rung's hash still committed, and no hash added.
1347
- * A later self-enrollment then spends that already-revealed rung, so its
1253
+ * One shape is out of the backward walk's reach. The last-client transition
1254
+ * strikes the ladder VM and reinstalls it in the same run
1255
+ * (`forgetLastEnrolledClient` stage 1): same seed, the credential's member
1256
+ * standing, the acting rung's hash still committed, and no hash added. A
1257
+ * later self-enrollment then spends that already-revealed rung, so its
1348
1258
  * reveal-and-commit entry authorizes no key while committing the next rung's
1349
1259
  * hash, and the registry anchor advances to that next rung. The backward walk
1350
1260
  * climbs from the anchor by asking which key the entry that committed its
1351
1261
  * hash authorized; that entry authorized none, so the walk cannot name the
1352
- * rung that signed it, and the earlier rung and the reinstalled VM go
1353
- * unrecovered. A seedless retirement then reports the VM as `unclaimed` and
1354
- * leaves it standing. Tracked as WC-158.
1262
+ * rung that signed it, and a walk anchored on the registry key alone leaves
1263
+ * the earlier rung and the reinstalled VM unrecovered. A walk anchored on the
1264
+ * member's own rung-0 commitment reads that history forward with no climb,
1265
+ * which is why the removal paths walk from both anchors
1266
+ * (`attributeUnlockLadderInventory`) and act on the member's reading.
1355
1267
  *
1356
1268
  * The anchor comes in three forms, and the walk is the same afterwards. A
1357
1269
  * recorded update-key multibase (`anchorKeyMultibase`) is what a caller
1358
1270
  * holding a registry entry passes. A hash (`anchorHash`) is the same anchor
1359
1271
  * with the key withheld: the rung is picked up when the log reveals it, since
1360
1272
  * the reveal test already matches on the commitment. With neither, and a
1361
- * `credentialVmId` in hand, the anchor is read off the credential's bind entry
1362
- * ({@link credentialLadderAnchor}) -- the cold-browser mode, where no registry
1363
- * is readable before the entry is written. An anchor the bind entry cannot
1364
- * name unambiguously refuses with {@link LadderAttributionError} rather than
1365
- * walking from a guess.
1273
+ * `credentialVmId` in hand, the anchor is read off the credential's own
1274
+ * `keyAgreement` member ({@link credentialLadderAnchor}) -- the cold-browser
1275
+ * mode, where no registry is readable before the entry is written. A member
1276
+ * naming no ladder commitment refuses with {@link LadderAttributionError}
1277
+ * rather than walking from a guess.
1366
1278
  *
1367
1279
  * @param options {object}
1368
1280
  * @param options.log {DIDLog} a resolved, caller-verified log
@@ -1386,19 +1298,19 @@ export async function attributeLadderInventory({ log, anchorKeyMultibase, anchor
1386
1298
  const { params, facts, commitIndex } = indexedLadderLog(log);
1387
1299
  // The anchor, in the caller's order of preference: the recorded update key,
1388
1300
  // a hash the caller resolved itself, or -- holding neither, the cold-browser
1389
- // case -- the credential's own bind entry, read off the log.
1390
- let anchorKey = anchorKeyMultibase;
1301
+ // case -- the credential's own member's ladder commitment, read off the
1302
+ // log.
1303
+ const anchorKey = anchorKeyMultibase;
1391
1304
  let anchorHash = suppliedAnchorHash;
1392
1305
  if (anchorKey === undefined && anchorHash === undefined) {
1393
1306
  const resolved = credentialVmId === undefined
1394
1307
  ? undefined
1395
- : await resolveBindAnchor({ log, facts, commitIndex, credentialVmId });
1308
+ : credentialLadderAnchor({ log, credentialVmId });
1396
1309
  if (resolved === undefined) {
1397
- throw new LadderAttributionError('The ladder walk was given no anchor, and the log does not name an ' +
1398
- 'unambiguous bind entry for this credential; refusing to ' +
1399
- 'attribute an ambiguous history.');
1310
+ throw new LadderAttributionError('The ladder walk was given no anchor, and the log names no ladder ' +
1311
+ "commitment on this credential's keyAgreement member; refusing " +
1312
+ 'to attribute an unanchored history.');
1400
1313
  }
1401
- anchorKey = resolved.anchorKeyMultibase;
1402
1314
  anchorHash = resolved.anchorHash;
1403
1315
  }
1404
1316
  if (anchorHash === undefined) {
@@ -1540,8 +1452,15 @@ export async function attributeLadderInventory({ log, anchorKeyMultibase, anchor
1540
1452
  if (reveals.length === 1) {
1541
1453
  const key = reveals[0];
1542
1454
  ladderKeys.add(key);
1543
- pending = { key, claims: [...addedHashes] };
1544
- for (const hash of addedHashes) {
1455
+ // The same foreign-inventory narrowing the signed-while-revealed
1456
+ // branch below applies: a rung revealing itself in the entry that
1457
+ // binds another credential (the ladder-branch bind, a code's split
1458
+ // issuance) commits that credential's rung hash, not this ladder's
1459
+ // next one. Claiming it would strike the newcomer's commitment at this
1460
+ // credential's retirement, silently ending its update authority.
1461
+ const claims = installsForeignInventory ? [] : [...addedHashes];
1462
+ pending = { key, claims };
1463
+ for (const hash of claims) {
1545
1464
  ladderHashes.add(hash);
1546
1465
  }
1547
1466
  // The HANDOVER: a rung whose hash some OTHER key committed earlier, in