@interop/wallet-core 0.39.0 → 0.41.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 (139) hide show
  1. package/README.md +5 -3
  2. package/dist/clients/listing.d.ts +4 -3
  3. package/dist/clients/listing.d.ts.map +1 -1
  4. package/dist/clients/listing.js.map +1 -1
  5. package/dist/clients/policy.d.ts +3 -2
  6. package/dist/clients/policy.d.ts.map +1 -1
  7. package/dist/clients/policy.js +3 -3
  8. package/dist/clients/policy.js.map +1 -1
  9. package/dist/clients/revocation.d.ts +14 -4
  10. package/dist/clients/revocation.d.ts.map +1 -1
  11. package/dist/clients/revocation.js +32 -22
  12. package/dist/clients/revocation.js.map +1 -1
  13. package/dist/enrollment/enrollment.d.ts +30 -2
  14. package/dist/enrollment/enrollment.d.ts.map +1 -1
  15. package/dist/enrollment/enrollment.js +44 -4
  16. package/dist/enrollment/enrollment.js.map +1 -1
  17. package/dist/enrollment/index.d.ts +4 -1
  18. package/dist/enrollment/index.d.ts.map +1 -1
  19. package/dist/enrollment/index.js +4 -1
  20. package/dist/enrollment/index.js.map +1 -1
  21. package/dist/keyring/fetch.d.ts +2 -1
  22. package/dist/keyring/fetch.d.ts.map +1 -1
  23. package/dist/keyring/fetch.js +7 -2
  24. package/dist/keyring/fetch.js.map +1 -1
  25. package/dist/keyring/index.d.ts +16 -9
  26. package/dist/keyring/index.d.ts.map +1 -1
  27. package/dist/keyring/index.js +15 -8
  28. package/dist/keyring/index.js.map +1 -1
  29. package/dist/keyring/kdf.d.ts +3 -1
  30. package/dist/keyring/kdf.d.ts.map +1 -1
  31. package/dist/keyring/kdf.js +16 -2
  32. package/dist/keyring/kdf.js.map +1 -1
  33. package/dist/keyring/record.d.ts +236 -50
  34. package/dist/keyring/record.d.ts.map +1 -1
  35. package/dist/keyring/record.js +348 -46
  36. package/dist/keyring/record.js.map +1 -1
  37. package/dist/keyring/unlockSpace.d.ts +25 -0
  38. package/dist/keyring/unlockSpace.d.ts.map +1 -1
  39. package/dist/keyring/unlockSpace.js +31 -2
  40. package/dist/keyring/unlockSpace.js.map +1 -1
  41. package/dist/keys/index.d.ts +4 -2
  42. package/dist/keys/index.d.ts.map +1 -1
  43. package/dist/keys/index.js +2 -1
  44. package/dist/keys/index.js.map +1 -1
  45. package/dist/keys/rosterLogStore.d.ts +18 -1
  46. package/dist/keys/rosterLogStore.d.ts.map +1 -1
  47. package/dist/keys/rosterLogStore.js +34 -6
  48. package/dist/keys/rosterLogStore.js.map +1 -1
  49. package/dist/keys/userKeyCascade.d.ts +10 -3
  50. package/dist/keys/userKeyCascade.d.ts.map +1 -1
  51. package/dist/keys/userKeyCascade.js +10 -4
  52. package/dist/keys/userKeyCascade.js.map +1 -1
  53. package/dist/keys/userKeyRoster.d.ts +39 -24
  54. package/dist/keys/userKeyRoster.d.ts.map +1 -1
  55. package/dist/keys/userKeyRoster.js +28 -77
  56. package/dist/keys/userKeyRoster.js.map +1 -1
  57. package/dist/recovery/index.d.ts +16 -3
  58. package/dist/recovery/index.d.ts.map +1 -1
  59. package/dist/recovery/index.js +14 -2
  60. package/dist/recovery/index.js.map +1 -1
  61. package/dist/recovery/recoveryCode.d.ts +7 -0
  62. package/dist/recovery/recoveryCode.d.ts.map +1 -1
  63. package/dist/recovery/recoveryCode.js +10 -0
  64. package/dist/recovery/recoveryCode.js.map +1 -1
  65. package/dist/recovery/recoveryDelegation.d.ts +120 -0
  66. package/dist/recovery/recoveryDelegation.d.ts.map +1 -0
  67. package/dist/recovery/recoveryDelegation.js +216 -0
  68. package/dist/recovery/recoveryDelegation.js.map +1 -0
  69. package/dist/recovery/recoveryRecord.d.ts +138 -46
  70. package/dist/recovery/recoveryRecord.d.ts.map +1 -1
  71. package/dist/recovery/recoveryRecord.js +231 -30
  72. package/dist/recovery/recoveryRecord.js.map +1 -1
  73. package/dist/recovery/recoveryWebvh.d.ts.map +1 -1
  74. package/dist/recovery/recoveryWebvh.js +20 -14
  75. package/dist/recovery/recoveryWebvh.js.map +1 -1
  76. package/dist/request/index.d.ts +1 -1
  77. package/dist/request/index.js +1 -1
  78. package/dist/request/matching.d.ts +24 -5
  79. package/dist/request/matching.d.ts.map +1 -1
  80. package/dist/request/matching.js +36 -39
  81. package/dist/request/matching.js.map +1 -1
  82. package/dist/request/walletInput.d.ts.map +1 -1
  83. package/dist/request/walletInput.js +3 -6
  84. package/dist/request/walletInput.js.map +1 -1
  85. package/dist/resourceLog/controller.d.ts +7 -4
  86. package/dist/resourceLog/controller.d.ts.map +1 -1
  87. package/dist/resourceLog/controller.js +17 -35
  88. package/dist/resourceLog/controller.js.map +1 -1
  89. package/dist/resourceLog/index.d.ts +1 -0
  90. package/dist/resourceLog/index.d.ts.map +1 -1
  91. package/dist/resourceLog/index.js +1 -0
  92. package/dist/resourceLog/index.js.map +1 -1
  93. package/dist/resourceLog/seal.d.ts +9 -1
  94. package/dist/resourceLog/seal.d.ts.map +1 -1
  95. package/dist/resourceLog/seal.js +21 -11
  96. package/dist/resourceLog/seal.js.map +1 -1
  97. package/dist/resourceLog/verify.d.ts.map +1 -1
  98. package/dist/resourceLog/verify.js +26 -9
  99. package/dist/resourceLog/verify.js.map +1 -1
  100. package/dist/resourceLog/vmFragment.d.ts +33 -0
  101. package/dist/resourceLog/vmFragment.d.ts.map +1 -0
  102. package/dist/resourceLog/vmFragment.js +39 -0
  103. package/dist/resourceLog/vmFragment.js.map +1 -0
  104. package/dist/space/publicLink.d.ts +2 -0
  105. package/dist/space/publicLink.d.ts.map +1 -1
  106. package/dist/space/publicLink.js +13 -1
  107. package/dist/space/publicLink.js.map +1 -1
  108. package/dist/space/wasLink.d.ts +5 -0
  109. package/dist/space/wasLink.d.ts.map +1 -1
  110. package/dist/space/wasLink.js +7 -2
  111. package/dist/space/wasLink.js.map +1 -1
  112. package/dist/webvh/didWeb.d.ts +3 -11
  113. package/dist/webvh/didWeb.d.ts.map +1 -1
  114. package/dist/webvh/didWeb.js +5 -1
  115. package/dist/webvh/didWeb.js.map +1 -1
  116. package/dist/webvh/didWebvh.d.ts +72 -1
  117. package/dist/webvh/didWebvh.d.ts.map +1 -1
  118. package/dist/webvh/didWebvh.js +111 -12
  119. package/dist/webvh/didWebvh.js.map +1 -1
  120. package/dist/webvh/index.d.ts +9 -2
  121. package/dist/webvh/index.d.ts.map +1 -1
  122. package/dist/webvh/index.js +8 -2
  123. package/dist/webvh/index.js.map +1 -1
  124. package/dist/webvh/keyAgreement.d.ts +56 -0
  125. package/dist/webvh/keyAgreement.d.ts.map +1 -0
  126. package/dist/webvh/keyAgreement.js +47 -0
  127. package/dist/webvh/keyAgreement.js.map +1 -0
  128. package/dist/webvh/listClients.d.ts +69 -18
  129. package/dist/webvh/listClients.d.ts.map +1 -1
  130. package/dist/webvh/listClients.js +141 -36
  131. package/dist/webvh/listClients.js.map +1 -1
  132. package/dist/webvh/revokeClient.d.ts +21 -11
  133. package/dist/webvh/revokeClient.d.ts.map +1 -1
  134. package/dist/webvh/revokeClient.js +31 -14
  135. package/dist/webvh/revokeClient.js.map +1 -1
  136. package/dist/webvh/verifyLog.d.ts.map +1 -1
  137. package/dist/webvh/verifyLog.js +5 -1
  138. package/dist/webvh/verifyLog.js.map +1 -1
  139. package/package.json +4 -4
@@ -1,80 +1,172 @@
1
- /*!
2
- * Copyright (c) 2026 Interop Alliance. All rights reserved.
3
- */
1
+ import type { IKeyAgreementKey, IKeyResolver, IZcap } from '@interop/data-integrity-core';
2
+ import type { AccountPointer, RecordSigner, SignedRecord } from '../keyring/record.js';
4
3
  /**
5
- * The recovery keyring record codec: the `{ version, encryption, wrapped }`
6
- * envelope stored as the one resource of a recovery code's unlock Space. Its
7
- * plaintext is the ordinary keyring record's (controller, email, account
8
- * pointer) PLUS the pre-minted PUT-on-`did.jsonl` delegation -- the narrow
9
- * zcap bridge that lets the code-derived client write its self-enrolling log
10
- * continuation. It carries **no key material of any kind**: never a seed,
11
- * never a user key wrap (wraps live doc-and-roster only), so the record stays
12
- * a pure pointer.
13
- *
14
- * The wrap reuses the keyring record construction verbatim (cipher context and
15
- * record-own epoch alike), so a recovery record IS a keyring record to every
16
- * generic consumer (an ordinary `unwrapKeyringRecord` recovers its pointer and
17
- * ignores the extra member) -- only the recovery flow demands the delegation.
4
+ * A recovery record's account binding is absent, malformed, or does not
5
+ * verify under the typed code's binding MAC key. Its own class, distinct from
6
+ * a proof or decrypt failure: this is the refusal that says the record's
7
+ * `{ controller, pointer }` core was not written by a holder of this code --
8
+ * a forged record redirecting recovery at another account, or a record from
9
+ * before the account moved hosts (either way the code cannot recover here
10
+ * and must be re-issued).
18
11
  */
19
- import type { IKeyAgreementKey, IKeyResolver, IZcap } from '@interop/data-integrity-core';
20
- import type { CollectionEncryption } from '@interop/was-client';
21
- import type { AccountPointer } from '../keyring/record.js';
12
+ export declare class RecoveryBindingError extends Error {
13
+ constructor(message: string);
14
+ }
22
15
  /**
23
- * The unwrapped contents of a recovery keyring record: the ordinary record
24
- * members plus the required delegation. `pointer` is required -- a recovery
25
- * record exists only on WAS deployments (there is nothing to recover toward
26
- * without a Space).
16
+ * The unwrapped contents of a recovery keyring record: the code-authenticated
17
+ * account binding (controller + pointer) plus the delegation and bind
18
+ * timestamp. `pointer` is required -- a recovery record exists only on WAS
19
+ * deployments (there is nothing to recover toward without a Space).
27
20
  */
28
21
  export interface RecoveryRecordContents {
29
22
  controller: string;
30
- email?: string;
31
23
  pointer: AccountPointer;
32
24
  delegation: IZcap;
25
+ createdAt: string;
26
+ }
27
+ /**
28
+ * A stored recovery record: the shared signed frame plus the account-binding
29
+ * tag the frame proof also covers.
30
+ */
31
+ export interface SignedRecoveryRecord extends SignedRecord {
32
+ binding: string;
33
33
  }
34
34
  /**
35
- * Wraps the recovery record: controller, email, pointer, and the pre-minted
35
+ * Where a recovery record's proof stands after the unwrap: `'verified'` when
36
+ * the code-derived unlock key signed it (checked before decryption), or a
37
+ * pending marker naming the signer the caller must still check against the
38
+ * account's verified did:webvh document -- the re-mint case, where an enrolled
39
+ * client signed on the code's behalf. The pending case is a value the caller
40
+ * cannot ignore by accident: the shell is not trustworthy until
41
+ * `verifyRecordProof` is run against the document-listed keys. The account
42
+ * the document belongs to is the one the code-authenticated pointer names --
43
+ * the binding is verified either way, so the pending state defers the shell's
44
+ * authenticity only, never the account identity.
45
+ */
46
+ export type RecoveryRecordProofState = 'verified' | {
47
+ pending: {
48
+ verificationMethod: string;
49
+ keyMultibase: string;
50
+ };
51
+ };
52
+ /**
53
+ * Computes the account-binding tag: HMAC-SHA-256 over the binding values
54
+ * under the code-derived binding MAC key, base64url (no pad). Issuance calls
55
+ * it to stamp the record; recovery recomputes it to verify.
56
+ *
57
+ * @param options {object}
58
+ * @param options.bindingMacKey {Uint8Array} the code-derived MAC key
59
+ * @param options.controller {string} the account did:key
60
+ * @param options.pointer {AccountPointer} the account pointer
61
+ * @returns {string}
62
+ */
63
+ export declare function computeRecoveryBinding({ bindingMacKey, controller, pointer }: {
64
+ bindingMacKey: Uint8Array;
65
+ controller: string;
66
+ pointer: AccountPointer;
67
+ }): string;
68
+ /**
69
+ * Reads the `binding` frame member off a stored recovery record without
70
+ * decrypting anything. The re-mint path uses it to carry the tag forward
71
+ * verbatim -- it cannot recompute the tag (no code bytes) and does not need
72
+ * to. Refuses a record with no binding: such a record predates the
73
+ * code-authenticated core and cannot be re-minted -- its code must be
74
+ * re-issued.
75
+ *
76
+ * @param options {object}
77
+ * @param options.record {unknown} the stored record envelope
78
+ * @returns {string}
79
+ */
80
+ export declare function recoveryRecordBinding({ record }: {
81
+ record: unknown;
82
+ }): string;
83
+ /**
84
+ * Wraps the recovery record: controller, pointer, and the pre-minted
36
85
  * `did.jsonl` PUT delegation, encrypted under the code's unlock KAK via the
37
- * keyring EDV cipher context.
86
+ * keyring EDV cipher context, then signed into the same frame the keyring
87
+ * record uses -- with the account-binding tag as one more frame member the
88
+ * proof covers. Issuance passes the code-derived `bindingMacKey` (the tag is
89
+ * computed here) and the code-derived unlock signer; the revocation cascade's
90
+ * re-mint path passes the standing record's `binding` verbatim and an
91
+ * enrolled client's account signer (see this module's header for the policy
92
+ * the reader applies to the two). Exactly one of the two binding inputs must
93
+ * be given.
38
94
  *
39
95
  * @param options {object}
40
96
  * @param options.controller {string} the account did:key
41
- * @param [options.email] {string} the account email, when known
42
97
  * @param options.pointer {AccountPointer} the account pointer
43
98
  * @param options.delegation {IZcap} the PUT-on-`did.jsonl` delegation to the
44
99
  * code-derived signing DID
45
- * @param options.keyAgreementKey {IKeyAgreementKey} the code's unlock KAK
46
- * @param options.keyResolver {IKeyResolver}
47
- * @returns {Promise<{ version: number, encryption: CollectionEncryption,
48
- * wrapped: unknown }>}
100
+ * @param options.keyAgreementKey {IKeyAgreementKey} the code's unlock KAK --
101
+ * its public half is all the wrap uses (sealing needs no key-agreement
102
+ * secret), which is exactly what lets the re-mint path re-seal a record it
103
+ * can never open
104
+ * @param options.signer {RecordSigner} the signing key: the code's unlock
105
+ * key at issuance, an enrolled client's account key on a re-mint
106
+ * @param [options.bindingMacKey] {Uint8Array} the code-derived binding MAC
107
+ * key -- the issuance path, which computes the tag
108
+ * @param [options.binding] {string} the standing record's tag, carried
109
+ * forward verbatim -- the re-mint path, which cannot recompute it
110
+ * @param [options.createdAt] {string} the bind timestamp to stamp, as an ISO
111
+ * string; defaults to now. Supplied by a caller that pins record freshness.
112
+ * @returns {Promise<SignedRecoveryRecord>}
49
113
  */
50
- export declare function wrapRecoveryRecord({ controller, email, pointer, delegation, keyAgreementKey, keyResolver }: {
114
+ export declare function wrapRecoveryRecord({ controller, pointer, delegation, keyAgreementKey, signer, bindingMacKey, binding, createdAt }: {
51
115
  controller: string;
52
- email?: string;
53
116
  pointer: AccountPointer;
54
117
  delegation: IZcap;
55
118
  keyAgreementKey: IKeyAgreementKey;
56
- keyResolver: IKeyResolver;
57
- }): Promise<{
58
- version: number;
59
- encryption: CollectionEncryption;
60
- wrapped: unknown;
61
- }>;
119
+ signer: RecordSigner;
120
+ bindingMacKey?: Uint8Array;
121
+ binding?: string;
122
+ createdAt?: string;
123
+ }): Promise<SignedRecoveryRecord>;
62
124
  /**
63
125
  * Unwraps and validates a recovery record: the ordinary keyring-record checks
64
- * plus the required pointer and delegation. A record without a delegation is
65
- * not a recovery record (an ordinary keyring record found under a code's
66
- * unlock Space would mean a corrupted issuance) and is refused.
126
+ * plus the required pointer, delegation, and account binding. A record
127
+ * without a delegation is not a recovery record (an ordinary keyring record
128
+ * found under a code's unlock Space would mean a corrupted issuance) and is
129
+ * refused.
130
+ *
131
+ * The account binding is verified before the contents are returned: the
132
+ * decrypted `{ controller, pointer }` must carry the tag the code-derived
133
+ * MAC key computes over them, or the record is refused as forged
134
+ * ({@link RecoveryBindingError}) -- the check that closes the host-forgery
135
+ * redirect, since the host never holds the MAC key. Nothing downstream may
136
+ * trust the pointer before this returns.
137
+ *
138
+ * Proof verification is mixed-signer and ordered deliberately. A proof by the
139
+ * code's own unlock key is verified BEFORE decryption -- the strong path,
140
+ * where the typed code alone establishes what may have signed the record. A
141
+ * proof by any other key can only be checked after decryption, because the
142
+ * re-minting client is knowable only once the code-authenticated pointer says
143
+ * which account this is and that account's log has been verified; the
144
+ * contents come back with a pending proof state naming the signer, and the
145
+ * caller completes the check with {@link verifyRecordProof} against the
146
+ * document's keys. That second phase is what makes an unexpected signer
147
+ * refuse: a record whose proof belongs to neither class ends in a
148
+ * `RecordProofError` there.
67
149
  *
68
150
  * @param options {object}
69
151
  * @param options.record {unknown} the stored `{ version, encryption,
70
- * wrapped }` envelope
152
+ * wrapped, binding, proof }` envelope
71
153
  * @param options.keyAgreementKey {IKeyAgreementKey} the code's unlock KAK
72
154
  * @param options.keyResolver {IKeyResolver}
73
- * @returns {Promise<RecoveryRecordContents>}
155
+ * @param options.expectedKeyMultibase {string} the code-derived unlock
156
+ * signing key's multibase
157
+ * @param options.bindingMacKey {Uint8Array} the code-derived binding MAC
158
+ * key the record's account binding must verify under
159
+ * @returns {Promise<{ contents: RecoveryRecordContents,
160
+ * proofState: RecoveryRecordProofState }>}
74
161
  */
75
- export declare function unwrapRecoveryRecord({ record, keyAgreementKey, keyResolver }: {
162
+ export declare function unwrapRecoveryRecord({ record, keyAgreementKey, keyResolver, expectedKeyMultibase, bindingMacKey }: {
76
163
  record: unknown;
77
164
  keyAgreementKey: IKeyAgreementKey;
78
165
  keyResolver: IKeyResolver;
79
- }): Promise<RecoveryRecordContents>;
166
+ expectedKeyMultibase: string;
167
+ bindingMacKey: Uint8Array;
168
+ }): Promise<{
169
+ contents: RecoveryRecordContents;
170
+ proofState: RecoveryRecordProofState;
171
+ }>;
80
172
  //# sourceMappingURL=recoveryRecord.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"recoveryRecord.d.ts","sourceRoot":"","sources":["../../src/recovery/recoveryRecord.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EACZ,KAAK,EACN,MAAM,8BAA8B,CAAA;AACrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAQ/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,cAAc,CAAA;IACvB,UAAU,EAAE,KAAK,CAAA;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,kBAAkB,CAAC,EACvC,UAAU,EACV,KAAK,EACL,OAAO,EACP,UAAU,EACV,eAAe,EACf,WAAW,EACZ,EAAE;IACD,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,cAAc,CAAA;IACvB,UAAU,EAAE,KAAK,CAAA;IACjB,eAAe,EAAE,gBAAgB,CAAA;IACjC,WAAW,EAAE,YAAY,CAAA;CAC1B,GAAG,OAAO,CAAC;IACV,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,oBAAoB,CAAA;IAChC,OAAO,EAAE,OAAO,CAAA;CACjB,CAAC,CAsBD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,MAAM,EACN,eAAe,EACf,WAAW,EACZ,EAAE;IACD,MAAM,EAAE,OAAO,CAAA;IACf,eAAe,EAAE,gBAAgB,CAAA;IACjC,WAAW,EAAE,YAAY,CAAA;CAC1B,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAyClC"}
1
+ {"version":3,"file":"recoveryRecord.d.ts","sourceRoot":"","sources":["../../src/recovery/recoveryRecord.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EACZ,KAAK,EACN,MAAM,8BAA8B,CAAA;AAcrC,OAAO,KAAK,EACV,cAAc,EACd,YAAY,EACZ,YAAY,EACb,MAAM,sBAAsB,CAAA;AAQ7B;;;;;;;;GAQG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,cAAc,CAAA;IACvB,UAAU,EAAE,KAAK,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAqB,SAAQ,YAAY;IACxD,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,wBAAwB,GAClC,UAAU,GAAG;IAAE,OAAO,EAAE;QAAE,kBAAkB,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAA;AAyBhF;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,aAAa,EACb,UAAU,EACV,OAAO,EACR,EAAE;IACD,aAAa,EAAE,UAAU,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,cAAc,CAAA;CACxB,GAAG,MAAM,CAIT;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,CAS7E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAsB,kBAAkB,CAAC,EACvC,UAAU,EACV,OAAO,EACP,UAAU,EACV,eAAe,EACf,MAAM,EACN,aAAa,EACb,OAAO,EACP,SAAS,EACV,EAAE;IACD,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,cAAc,CAAA;IACvB,UAAU,EAAE,KAAK,CAAA;IACjB,eAAe,EAAE,gBAAgB,CAAA;IACjC,MAAM,EAAE,YAAY,CAAA;IACpB,aAAa,CAAC,EAAE,UAAU,CAAA;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAiChC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,MAAM,EACN,eAAe,EACf,WAAW,EACX,oBAAoB,EACpB,aAAa,EACd,EAAE;IACD,MAAM,EAAE,OAAO,CAAA;IACf,eAAe,EAAE,gBAAgB,CAAA;IACjC,WAAW,EAAE,YAAY,CAAA;IACzB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,aAAa,EAAE,UAAU,CAAA;CAC1B,GAAG,OAAO,CAAC;IACV,QAAQ,EAAE,sBAAsB,CAAA;IAChC,UAAU,EAAE,wBAAwB,CAAA;CACrC,CAAC,CAqFD"}
@@ -1,61 +1,243 @@
1
- import { KEYRING_RECORD_VERSION, mintRecordEncryption, parseRecordFrame, parseRecordPointer, recordCipher } from '../keyring/record.js';
1
+ /*!
2
+ * Copyright (c) 2026 Interop Alliance. All rights reserved.
3
+ */
2
4
  /**
3
- * Wraps the recovery record: controller, email, pointer, and the pre-minted
5
+ * The recovery keyring record codec: the `{ version, encryption, wrapped,
6
+ * binding, proof }` envelope stored as the one resource of a recovery code's
7
+ * unlock Space. Its plaintext is the ordinary keyring record's (controller,
8
+ * account pointer) PLUS the pre-minted PUT-on-`did.jsonl` delegation -- the
9
+ * narrow zcap bridge that lets the code-derived client write its
10
+ * self-enrolling log continuation. It carries **no key material of any
11
+ * kind**: never a seed, never a user key wrap (wraps live doc-and-roster
12
+ * only), so the record stays a pure pointer.
13
+ *
14
+ * The wrap reuses the keyring record construction (cipher context,
15
+ * record-own epoch, and signed frame alike), so a recovery record IS a
16
+ * keyring record to every generic consumer -- only the recovery flow demands
17
+ * the delegation and the binding.
18
+ *
19
+ * The record splits into a code-authenticated core and a re-mintable shell.
20
+ * The core is the account binding `{ controller, pointer }`, authenticated by
21
+ * the `binding` frame member: an HMAC tag under a key derived from the code
22
+ * bytes, computed at issuance and verified at recovery BEFORE the pointer is
23
+ * trusted. Only the issuer and the code holder ever hold that key -- the
24
+ * storage host never does -- so a host-forged record pointing recovery at
25
+ * another account fails the tag however it is encrypted or signed. The tag
26
+ * rides the frame in the clear (it reveals nothing) so the re-mint path,
27
+ * which cannot decrypt the record or recompute the tag, preserves it
28
+ * verbatim; the binding values themselves stay inside the plaintext. The
29
+ * consequence: a re-mint can never change the pointer, and an account that
30
+ * moves hosts must re-issue its codes.
31
+ *
32
+ * The shell is the delegation and the frame proof, and its signer is mixed.
33
+ * At issuance the record is signed by the code-derived unlock key, the one a
34
+ * typed code re-derives, so recovery verifies the proof before decrypting.
35
+ * The revocation cascade's re-mint path holds only the code's KAK public
36
+ * half plus an enrolled client's account key, so it re-PUTs the record
37
+ * signed by that client's account verification method instead. This codec is
38
+ * agnostic -- it signs with what it is given -- and the reader carries the
39
+ * policy: a proof by the expected unlock key verifies up front, and anything
40
+ * else comes back marked unverified, for the caller to check against the
41
+ * verified did:webvh document of the account the code-authenticated pointer
42
+ * names.
43
+ */
44
+ import { base64urlnopad } from '@scure/base';
45
+ import { equalBytes } from '@noble/ciphers/utils.js';
46
+ import { hmac } from '@noble/hashes/hmac.js';
47
+ import { sha256 } from '@noble/hashes/sha2.js';
48
+ import { KEYRING_RECORD_VERSION, mintRecordEncryption, parseRecordCreatedAt, parseRecordFrame, parseRecordPointer, recordCipher, recordCreatedAtStamp, recordProofKeyMultibase, recordSealCipher, signRecordFrame, verifyRecordProof } from '../keyring/record.js';
49
+ /**
50
+ * The context label mixed into the binding MAC input, versioning the tag
51
+ * construction. Permanent -- changing it orphans every issued code.
52
+ */
53
+ const RECOVERY_BINDING_CONTEXT = 'freewallet/recovery/binding/v1';
54
+ /**
55
+ * A recovery record's account binding is absent, malformed, or does not
56
+ * verify under the typed code's binding MAC key. Its own class, distinct from
57
+ * a proof or decrypt failure: this is the refusal that says the record's
58
+ * `{ controller, pointer }` core was not written by a holder of this code --
59
+ * a forged record redirecting recovery at another account, or a record from
60
+ * before the account moved hosts (either way the code cannot recover here
61
+ * and must be re-issued).
62
+ */
63
+ export class RecoveryBindingError extends Error {
64
+ constructor(message) {
65
+ super(message);
66
+ this.name = 'RecoveryBindingError';
67
+ }
68
+ }
69
+ /**
70
+ * The deterministic MAC input over a record's account binding: a JSON array
71
+ * of the context label and the binding values, so no delimiter ambiguity can
72
+ * make two bindings collide.
73
+ */
74
+ function bindingMacInput({ controller, pointer }) {
75
+ return new TextEncoder().encode(JSON.stringify([
76
+ RECOVERY_BINDING_CONTEXT,
77
+ controller,
78
+ pointer.did ?? '',
79
+ pointer.spaceId,
80
+ pointer.host
81
+ ]));
82
+ }
83
+ /**
84
+ * Computes the account-binding tag: HMAC-SHA-256 over the binding values
85
+ * under the code-derived binding MAC key, base64url (no pad). Issuance calls
86
+ * it to stamp the record; recovery recomputes it to verify.
87
+ *
88
+ * @param options {object}
89
+ * @param options.bindingMacKey {Uint8Array} the code-derived MAC key
90
+ * @param options.controller {string} the account did:key
91
+ * @param options.pointer {AccountPointer} the account pointer
92
+ * @returns {string}
93
+ */
94
+ export function computeRecoveryBinding({ bindingMacKey, controller, pointer }) {
95
+ return base64urlnopad.encode(hmac(sha256, bindingMacKey, bindingMacInput({ controller, pointer })));
96
+ }
97
+ /**
98
+ * Reads the `binding` frame member off a stored recovery record without
99
+ * decrypting anything. The re-mint path uses it to carry the tag forward
100
+ * verbatim -- it cannot recompute the tag (no code bytes) and does not need
101
+ * to. Refuses a record with no binding: such a record predates the
102
+ * code-authenticated core and cannot be re-minted -- its code must be
103
+ * re-issued.
104
+ *
105
+ * @param options {object}
106
+ * @param options.record {unknown} the stored record envelope
107
+ * @returns {string}
108
+ */
109
+ export function recoveryRecordBinding({ record }) {
110
+ const { binding } = (record ?? {});
111
+ if (typeof binding !== 'string' || !binding) {
112
+ throw new RecoveryBindingError('The recovery record carries no code-authenticated account binding; ' +
113
+ 'the recovery code must be re-issued.');
114
+ }
115
+ return binding;
116
+ }
117
+ /**
118
+ * Wraps the recovery record: controller, pointer, and the pre-minted
4
119
  * `did.jsonl` PUT delegation, encrypted under the code's unlock KAK via the
5
- * keyring EDV cipher context.
120
+ * keyring EDV cipher context, then signed into the same frame the keyring
121
+ * record uses -- with the account-binding tag as one more frame member the
122
+ * proof covers. Issuance passes the code-derived `bindingMacKey` (the tag is
123
+ * computed here) and the code-derived unlock signer; the revocation cascade's
124
+ * re-mint path passes the standing record's `binding` verbatim and an
125
+ * enrolled client's account signer (see this module's header for the policy
126
+ * the reader applies to the two). Exactly one of the two binding inputs must
127
+ * be given.
6
128
  *
7
129
  * @param options {object}
8
130
  * @param options.controller {string} the account did:key
9
- * @param [options.email] {string} the account email, when known
10
131
  * @param options.pointer {AccountPointer} the account pointer
11
132
  * @param options.delegation {IZcap} the PUT-on-`did.jsonl` delegation to the
12
133
  * code-derived signing DID
13
- * @param options.keyAgreementKey {IKeyAgreementKey} the code's unlock KAK
14
- * @param options.keyResolver {IKeyResolver}
15
- * @returns {Promise<{ version: number, encryption: CollectionEncryption,
16
- * wrapped: unknown }>}
134
+ * @param options.keyAgreementKey {IKeyAgreementKey} the code's unlock KAK --
135
+ * its public half is all the wrap uses (sealing needs no key-agreement
136
+ * secret), which is exactly what lets the re-mint path re-seal a record it
137
+ * can never open
138
+ * @param options.signer {RecordSigner} the signing key: the code's unlock
139
+ * key at issuance, an enrolled client's account key on a re-mint
140
+ * @param [options.bindingMacKey] {Uint8Array} the code-derived binding MAC
141
+ * key -- the issuance path, which computes the tag
142
+ * @param [options.binding] {string} the standing record's tag, carried
143
+ * forward verbatim -- the re-mint path, which cannot recompute it
144
+ * @param [options.createdAt] {string} the bind timestamp to stamp, as an ISO
145
+ * string; defaults to now. Supplied by a caller that pins record freshness.
146
+ * @returns {Promise<SignedRecoveryRecord>}
17
147
  */
18
- export async function wrapRecoveryRecord({ controller, email, pointer, delegation, keyAgreementKey, keyResolver }) {
148
+ export async function wrapRecoveryRecord({ controller, pointer, delegation, keyAgreementKey, signer, bindingMacKey, binding, createdAt }) {
149
+ if ((bindingMacKey === undefined) === (binding === undefined)) {
150
+ throw new Error('Exactly one of bindingMacKey (issuance) or binding (re-mint) is ' +
151
+ 'required.');
152
+ }
153
+ const tag = bindingMacKey !== undefined
154
+ ? computeRecoveryBinding({ bindingMacKey, controller, pointer })
155
+ : binding;
19
156
  const encryption = await mintRecordEncryption({ keyAgreementKey });
20
- const cipher = await recordCipher({
21
- keyAgreementKey,
22
- keyResolver,
23
- encryption
24
- });
157
+ const cipher = await recordSealCipher({ encryption });
25
158
  const data = {
26
159
  controller,
27
- ...(email ? { email } : {}),
28
160
  pointer: {
29
161
  ...(pointer.did ? { did: pointer.did } : {}),
30
162
  spaceId: pointer.spaceId,
31
163
  host: pointer.host
32
164
  },
33
165
  delegation,
34
- createdAt: new Date().toISOString()
166
+ createdAt: recordCreatedAtStamp({ createdAt })
35
167
  };
36
168
  const { envelope } = await cipher.encrypt({
37
169
  data: data
38
170
  });
39
- return { version: KEYRING_RECORD_VERSION, encryption, wrapped: envelope };
171
+ return (await signRecordFrame({
172
+ version: KEYRING_RECORD_VERSION,
173
+ encryption,
174
+ wrapped: envelope,
175
+ signer,
176
+ members: { binding: tag }
177
+ }));
40
178
  }
41
179
  /**
42
180
  * Unwraps and validates a recovery record: the ordinary keyring-record checks
43
- * plus the required pointer and delegation. A record without a delegation is
44
- * not a recovery record (an ordinary keyring record found under a code's
45
- * unlock Space would mean a corrupted issuance) and is refused.
181
+ * plus the required pointer, delegation, and account binding. A record
182
+ * without a delegation is not a recovery record (an ordinary keyring record
183
+ * found under a code's unlock Space would mean a corrupted issuance) and is
184
+ * refused.
185
+ *
186
+ * The account binding is verified before the contents are returned: the
187
+ * decrypted `{ controller, pointer }` must carry the tag the code-derived
188
+ * MAC key computes over them, or the record is refused as forged
189
+ * ({@link RecoveryBindingError}) -- the check that closes the host-forgery
190
+ * redirect, since the host never holds the MAC key. Nothing downstream may
191
+ * trust the pointer before this returns.
192
+ *
193
+ * Proof verification is mixed-signer and ordered deliberately. A proof by the
194
+ * code's own unlock key is verified BEFORE decryption -- the strong path,
195
+ * where the typed code alone establishes what may have signed the record. A
196
+ * proof by any other key can only be checked after decryption, because the
197
+ * re-minting client is knowable only once the code-authenticated pointer says
198
+ * which account this is and that account's log has been verified; the
199
+ * contents come back with a pending proof state naming the signer, and the
200
+ * caller completes the check with {@link verifyRecordProof} against the
201
+ * document's keys. That second phase is what makes an unexpected signer
202
+ * refuse: a record whose proof belongs to neither class ends in a
203
+ * `RecordProofError` there.
46
204
  *
47
205
  * @param options {object}
48
206
  * @param options.record {unknown} the stored `{ version, encryption,
49
- * wrapped }` envelope
207
+ * wrapped, binding, proof }` envelope
50
208
  * @param options.keyAgreementKey {IKeyAgreementKey} the code's unlock KAK
51
209
  * @param options.keyResolver {IKeyResolver}
52
- * @returns {Promise<RecoveryRecordContents>}
210
+ * @param options.expectedKeyMultibase {string} the code-derived unlock
211
+ * signing key's multibase
212
+ * @param options.bindingMacKey {Uint8Array} the code-derived binding MAC
213
+ * key the record's account binding must verify under
214
+ * @returns {Promise<{ contents: RecoveryRecordContents,
215
+ * proofState: RecoveryRecordProofState }>}
53
216
  */
54
- export async function unwrapRecoveryRecord({ record, keyAgreementKey, keyResolver }) {
55
- const { encryption, wrapped } = parseRecordFrame({
217
+ export async function unwrapRecoveryRecord({ record, keyAgreementKey, keyResolver, expectedKeyMultibase, bindingMacKey }) {
218
+ const { encryption, wrapped, proof } = parseRecordFrame({
56
219
  record,
57
220
  label: 'recovery'
58
221
  });
222
+ const binding = recoveryRecordBinding({ record });
223
+ // `parseRecordFrame` shape-checks the proof of a current-version frame, so
224
+ // it is present here.
225
+ const verificationMethod = proof.verificationMethod;
226
+ const keyMultibase = recordProofKeyMultibase({
227
+ verificationMethod,
228
+ label: 'recovery'
229
+ });
230
+ let proofState = {
231
+ pending: { verificationMethod, keyMultibase }
232
+ };
233
+ if (keyMultibase === expectedKeyMultibase) {
234
+ await verifyRecordProof({
235
+ record,
236
+ allowedKeyMultibases: expectedKeyMultibase,
237
+ label: 'recovery'
238
+ });
239
+ proofState = 'verified';
240
+ }
59
241
  const cipher = await recordCipher({
60
242
  keyAgreementKey,
61
243
  keyResolver,
@@ -75,13 +257,32 @@ export async function unwrapRecoveryRecord({ record, keyAgreementKey, keyResolve
75
257
  typeof plaintext.delegation !== 'object') {
76
258
  throw new Error('Recovery record is missing its did.jsonl delegation.');
77
259
  }
260
+ const createdAt = parseRecordCreatedAt({
261
+ value: plaintext.createdAt,
262
+ label: 'Recovery'
263
+ });
264
+ const expectedTag = hmac(sha256, bindingMacKey, bindingMacInput({ controller: plaintext.controller, pointer }));
265
+ let servedTag;
266
+ try {
267
+ servedTag = base64urlnopad.decode(binding);
268
+ }
269
+ catch {
270
+ servedTag = null;
271
+ }
272
+ // `equalBytes` is @noble/ciphers' authentication-tag comparison (no early
273
+ // exit), so the check leaks nothing through timing.
274
+ if (servedTag === null || !equalBytes(servedTag, expectedTag)) {
275
+ throw new RecoveryBindingError("The recovery record's account binding does not verify under this " +
276
+ 'code; the record is refused as forged.');
277
+ }
78
278
  return {
79
- controller: plaintext.controller,
80
- ...(typeof plaintext.email === 'string' && plaintext.email
81
- ? { email: plaintext.email }
82
- : {}),
83
- pointer,
84
- delegation: plaintext.delegation
279
+ contents: {
280
+ controller: plaintext.controller,
281
+ pointer,
282
+ delegation: plaintext.delegation,
283
+ createdAt
284
+ },
285
+ proofState
85
286
  };
86
287
  }
87
288
  //# sourceMappingURL=recoveryRecord.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"recoveryRecord.js","sourceRoot":"","sources":["../../src/recovery/recoveryRecord.ts"],"names":[],"mappings":"AAwBA,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACb,MAAM,sBAAsB,CAAA;AAgB7B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,EACvC,UAAU,EACV,KAAK,EACL,OAAO,EACP,UAAU,EACV,eAAe,EACf,WAAW,EAQZ;IAKC,MAAM,UAAU,GAAG,MAAM,oBAAoB,CAAC,EAAE,eAAe,EAAE,CAAC,CAAA;IAClE,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC;QAChC,eAAe;QACf,WAAW;QACX,UAAU;KACX,CAAC,CAAA;IACF,MAAM,IAAI,GAAG;QACX,UAAU;QACV,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3B,OAAO,EAAE;YACP,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5C,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,IAAI,EAAE,OAAO,CAAC,IAAI;SACnB;QACD,UAAU;QACV,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAA;IACD,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC;QACxC,IAAI,EAAE,IAA+D;KACtE,CAAC,CAAA;IACF,OAAO,EAAE,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAA;AAC3E,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,EACzC,MAAM,EACN,eAAe,EACf,WAAW,EAKZ;IACC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAC;QAC/C,MAAM;QACN,KAAK,EAAE,UAAU;KAClB,CAAC,CAAA;IACF,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC;QAChC,eAAe;QACf,WAAW;QACX,UAAU;KACX,CAAC,CAAA;IACF,MAAM,SAAS,GAAG,CAAC,MAAM,MAAM,CAAC,OAAO,CAAC;QACtC,QAAQ,EAAE,OAAgB;KAC3B,CAAC,CAKD,CAAA;IAED,IAAI,OAAO,SAAS,CAAC,UAAU,KAAK,QAAQ,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAA;IAC7D,CAAC;IACD,MAAM,OAAO,GAAG,kBAAkB,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;IACrD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAA;IACpE,CAAC;IACD,IACE,SAAS,CAAC,UAAU,KAAK,IAAI;QAC7B,OAAO,SAAS,CAAC,UAAU,KAAK,QAAQ,EACxC,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAA;IACzE,CAAC;IAED,OAAO;QACL,UAAU,EAAE,SAAS,CAAC,UAAU;QAChC,GAAG,CAAC,OAAO,SAAS,CAAC,KAAK,KAAK,QAAQ,IAAI,SAAS,CAAC,KAAK;YACxD,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE;YAC5B,CAAC,CAAC,EAAE,CAAC;QACP,OAAO;QACP,UAAU,EAAE,SAAS,CAAC,UAAmB;KAC1C,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"recoveryRecord.js","sourceRoot":"","sources":["../../src/recovery/recoveryRecord.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"recoveryWebvh.d.ts","sourceRoot":"","sources":["../../src/recovery/recoveryWebvh.ts"],"names":[],"mappings":"AAgDA,OAAO,KAAK,EACV,qBAAqB,EAErB,mBAAmB,EACnB,YAAY,EACb,MAAM,sBAAsB,CAAA;AAE7B;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,EAC3B,GAAG,EACH,wBAAwB,EACzB,EAAE;IACD,GAAG,EAAE,MAAM,CAAA;IACX,wBAAwB,EAAE,MAAM,CAAA;CACjC,GAAG,MAAM,CAET;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,wBAAwB,EAAE,MAAM,CAAA;IAChC,kBAAkB,EAAE,MAAM,CAAA;CAC3B;AAED;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,KAAK;gBAEnD,OAAO,SACuC;CAKjD;AAED;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,YAAY,EACZ,kBAAkB,GAAG,eAAe,CACrC,CAAA;AAsDD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,OAAO,EAAE,YAAY,CAAA;IACrB,UAAU,EAAE,qBAAqB,CAAA;IACjC,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAE3B;AA4ED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,OAAO,EAAE,YAAY,CAAA;IACrB,UAAU,EAAE,qBAAqB,CAAA;IACjC,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAE3B;AAqED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,KAAK,EAAE,gBAAgB,CAAA;IACvB,QAAQ,EAAE,kBAAkB,GAAG;QAAE,UAAU,EAAE,UAAU,CAAA;KAAE,CAAA;IACzD,aAAa,EAAE,mBAAmB,CAAA;IAClC,oBAAoB,EAAE,qBAAqB,CAAA;IAC3C,WAAW,EAAE,kBAAkB,CAAA;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAE5C"}
1
+ {"version":3,"file":"recoveryWebvh.d.ts","sourceRoot":"","sources":["../../src/recovery/recoveryWebvh.ts"],"names":[],"mappings":"AAiDA,OAAO,KAAK,EACV,qBAAqB,EAErB,mBAAmB,EACnB,YAAY,EACb,MAAM,sBAAsB,CAAA;AAE7B;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,EAC3B,GAAG,EACH,wBAAwB,EACzB,EAAE;IACD,GAAG,EAAE,MAAM,CAAA;IACX,wBAAwB,EAAE,MAAM,CAAA;CACjC,GAAG,MAAM,CAET;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,wBAAwB,EAAE,MAAM,CAAA;IAChC,kBAAkB,EAAE,MAAM,CAAA;CAC3B;AAED;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,KAAK;gBAEnD,OAAO,SACuC;CAKjD;AAED;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,YAAY,EACZ,kBAAkB,GAAG,eAAe,CACrC,CAAA;AAsDD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,OAAO,EAAE,YAAY,CAAA;IACrB,UAAU,EAAE,qBAAqB,CAAA;IACjC,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAE3B;AA+ED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,OAAO,EAAE,YAAY,CAAA;IACrB,UAAU,EAAE,qBAAqB,CAAA;IACjC,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAE3B;AAqED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,KAAK,EAAE,gBAAgB,CAAA;IACvB,QAAQ,EAAE,kBAAkB,GAAG;QAAE,UAAU,EAAE,UAAU,CAAA;KAAE,CAAA;IACzD,aAAa,EAAE,mBAAmB,CAAA;IAClC,oBAAoB,EAAE,qBAAqB,CAAA;IAC3C,WAAW,EAAE,kBAAkB,CAAA;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAE5C"}
@@ -34,7 +34,7 @@
34
34
  * with the same key material converges without forking the log.
35
35
  */
36
36
  import { deriveNextKeyHash, updateDID } from '@interop/did-method-webvh';
37
- import { assertCarryOverCommitments, MULTIKEY_VM_TYPE, publishUpdatedLog, putLogResource, readPublishedLog, relationIds, updateKeyMultibase, updateKeySigner, withLogConflictRetry } from '../webvh/didWebvh.js';
37
+ import { assertCarryOverCommitments, markedVerificationMethodPair, MULTIKEY_VM_TYPE, publishUpdatedLog, putLogResource, readPublishedLog, relationIds, updateKeyMultibase, updateKeySigner, withLogConflictRetry } from '../webvh/didWebvh.js';
38
38
  /**
39
39
  * The verification-method id a code's key-agreement key publishes under --
40
40
  * the ordinary `<did>#<multibase>` form, indistinguishable by id from any
@@ -150,6 +150,9 @@ async function publishRecoveryKeyOnce({ idStore, updateKeys, recovery, expectedD
150
150
  }
151
151
  await assertCarryOverCommitments({ published });
152
152
  const existingMethods = (doc.verificationMethod ?? []);
153
+ // Controlled by the account, deliberately unmarked: a recovery code is not
154
+ // a listed client, so its key-agreement method must never carry the
155
+ // controller marker a client listing and a revocation removal match on.
153
156
  const recoveryMethod = {
154
157
  id: vmId,
155
158
  type: MULTIKEY_VM_TYPE,
@@ -355,24 +358,27 @@ async function recoverWebvhClientOnce({ store, recovery, newClientKeys, newClien
355
358
  did,
356
359
  keyAgreementKeyMultibase: replacement.keyAgreementKeyMultibase
357
360
  });
361
+ // A three-way controller split. The new client's signing method and the
362
+ // replacement code's key-agreement method are controlled by the account;
363
+ // the new client's key-agreement method alone carries the controller marker
364
+ // (see clientKeyAgreementController) -- which is exactly what tells the two
365
+ // simultaneously published keyAgreement methods apart. The marked pair goes
366
+ // through the shared builder, which refuses a new client whose key-agreement
367
+ // key is not its signing key's canonical twin; the replacement code's
368
+ // unmarked method is appended after it.
358
369
  const addedMethods = [
359
- {
360
- id: vmId(newClientKeys.signingKeyMultibase),
361
- publicKeyMultibase: newClientKeys.signingKeyMultibase
362
- },
363
- {
364
- id: vmId(newClientKeys.keyAgreementKeyMultibase),
365
- publicKeyMultibase: newClientKeys.keyAgreementKeyMultibase
366
- },
370
+ ...markedVerificationMethodPair({
371
+ controller: did,
372
+ signingKeyMultibase: newClientKeys.signingKeyMultibase,
373
+ keyAgreementKeyMultibase: newClientKeys.keyAgreementKeyMultibase
374
+ }),
367
375
  {
368
376
  id: replacementVmId,
377
+ type: MULTIKEY_VM_TYPE,
378
+ controller: did,
369
379
  publicKeyMultibase: replacement.keyAgreementKeyMultibase
370
380
  }
371
- ].map(method => ({
372
- ...method,
373
- type: MULTIKEY_VM_TYPE,
374
- controller: did
375
- }));
381
+ ];
376
382
  const existingMethods = (doc.verificationMethod ?? []);
377
383
  const verificationMethods = [
378
384
  ...existingMethods.filter(method => method.id !== spentVmId &&