mindoodb 0.0.50 → 0.0.53

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 (198) hide show
  1. package/README-server.md +11 -2
  2. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +13 -1
  3. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
  4. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +45 -20
  5. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
  6. package/dist/appendonlystores/network/HttpTransport.d.ts +2 -0
  7. package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
  8. package/dist/appendonlystores/network/HttpTransport.js +6 -62
  9. package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
  10. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +24 -0
  11. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
  12. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +75 -24
  13. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
  14. package/dist/browser/index.mjs +72 -0
  15. package/dist/core/BaseMindooDB.d.ts +89 -14
  16. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  17. package/dist/core/BaseMindooDB.js +692 -201
  18. package/dist/core/BaseMindooDB.js.map +1 -1
  19. package/dist/core/BaseMindooTenant.d.ts +59 -7
  20. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  21. package/dist/core/BaseMindooTenant.js +427 -90
  22. package/dist/core/BaseMindooTenant.js.map +1 -1
  23. package/dist/core/BaseMindooTenantDirectory.d.ts +61 -9
  24. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  25. package/dist/core/BaseMindooTenantDirectory.js +363 -35
  26. package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
  27. package/dist/core/BaseMindooTenantFactory.d.ts +17 -1
  28. package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
  29. package/dist/core/BaseMindooTenantFactory.js +292 -16
  30. package/dist/core/BaseMindooTenantFactory.js.map +1 -1
  31. package/dist/core/accesscontrol/DirectoryStateNode.d.ts +68 -1
  32. package/dist/core/accesscontrol/DirectoryStateNode.d.ts.map +1 -1
  33. package/dist/core/accesscontrol/DirectoryStateNode.js +157 -4
  34. package/dist/core/accesscontrol/DirectoryStateNode.js.map +1 -1
  35. package/dist/core/accesscontrol/directoryProjection.d.ts.map +1 -1
  36. package/dist/core/accesscontrol/directoryProjection.js +14 -5
  37. package/dist/core/accesscontrol/directoryProjection.js.map +1 -1
  38. package/dist/core/accesscontrol/grantKeys.d.ts +17 -14
  39. package/dist/core/accesscontrol/grantKeys.d.ts.map +1 -1
  40. package/dist/core/accesscontrol/grantKeys.js +51 -21
  41. package/dist/core/accesscontrol/grantKeys.js.map +1 -1
  42. package/dist/core/accesscontrol/types.d.ts +13 -7
  43. package/dist/core/accesscontrol/types.d.ts.map +1 -1
  44. package/dist/core/accesscontrol/types.js +16 -10
  45. package/dist/core/accesscontrol/types.js.map +1 -1
  46. package/dist/core/appendonlystores/network/AuthenticationService.d.ts +7 -0
  47. package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
  48. package/dist/core/appendonlystores/network/AuthenticationService.js +70 -14
  49. package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
  50. package/dist/core/appendonlystores/network/entryWireCodec.d.ts +51 -0
  51. package/dist/core/appendonlystores/network/entryWireCodec.d.ts.map +1 -0
  52. package/dist/core/appendonlystores/network/entryWireCodec.js +93 -0
  53. package/dist/core/appendonlystores/network/entryWireCodec.js.map +1 -0
  54. package/dist/core/builtinDbInvariants.d.ts +53 -0
  55. package/dist/core/builtinDbInvariants.d.ts.map +1 -0
  56. package/dist/core/builtinDbInvariants.js +147 -0
  57. package/dist/core/builtinDbInvariants.js.map +1 -0
  58. package/dist/core/copy/feasibility.d.ts.map +1 -1
  59. package/dist/core/copy/feasibility.js +3 -0
  60. package/dist/core/copy/feasibility.js.map +1 -1
  61. package/dist/core/copy/flatten.d.ts.map +1 -1
  62. package/dist/core/copy/flatten.js +7 -2
  63. package/dist/core/copy/flatten.js.map +1 -1
  64. package/dist/core/copy/host.d.ts +5 -0
  65. package/dist/core/copy/host.d.ts.map +1 -1
  66. package/dist/core/copy/preflight.d.ts.map +1 -1
  67. package/dist/core/copy/preflight.js +7 -1
  68. package/dist/core/copy/preflight.js.map +1 -1
  69. package/dist/core/copy/types.d.ts +8 -1
  70. package/dist/core/copy/types.d.ts.map +1 -1
  71. package/dist/core/crypto/EntrySignature.d.ts +8 -2
  72. package/dist/core/crypto/EntrySignature.d.ts.map +1 -1
  73. package/dist/core/crypto/EntrySignature.js +14 -5
  74. package/dist/core/crypto/EntrySignature.js.map +1 -1
  75. package/dist/core/crypto/MindooDocSigner.d.ts.map +1 -1
  76. package/dist/core/crypto/MindooDocSigner.js +11 -2
  77. package/dist/core/crypto/MindooDocSigner.js.map +1 -1
  78. package/dist/core/crypto/RSAEncryption.d.ts +19 -0
  79. package/dist/core/crypto/RSAEncryption.d.ts.map +1 -1
  80. package/dist/core/crypto/RSAEncryption.js +36 -3
  81. package/dist/core/crypto/RSAEncryption.js.map +1 -1
  82. package/dist/core/crypto/privateKeyEncryption.js +1 -1
  83. package/dist/core/crypto/privateKeyEncryption.js.map +1 -1
  84. package/dist/core/crypto/rsaOaep3072.d.ts +12 -0
  85. package/dist/core/crypto/rsaOaep3072.d.ts.map +1 -0
  86. package/dist/core/crypto/rsaOaep3072.js +30 -0
  87. package/dist/core/crypto/rsaOaep3072.js.map +1 -0
  88. package/dist/core/databaseIdValidation.d.ts +23 -0
  89. package/dist/core/databaseIdValidation.d.ts.map +1 -1
  90. package/dist/core/databaseIdValidation.js +59 -1
  91. package/dist/core/databaseIdValidation.js.map +1 -1
  92. package/dist/core/index.d.ts +9 -2
  93. package/dist/core/index.d.ts.map +1 -1
  94. package/dist/core/index.js +40 -4
  95. package/dist/core/index.js.map +1 -1
  96. package/dist/core/join/deviceJoinBootstrap.d.ts +23 -0
  97. package/dist/core/join/deviceJoinBootstrap.d.ts.map +1 -0
  98. package/dist/core/join/deviceJoinBootstrap.js +37 -0
  99. package/dist/core/join/deviceJoinBootstrap.js.map +1 -0
  100. package/dist/core/keys/KeyBag.d.ts +20 -2
  101. package/dist/core/keys/KeyBag.d.ts.map +1 -1
  102. package/dist/core/keys/KeyBag.js +22 -10
  103. package/dist/core/keys/KeyBag.js.map +1 -1
  104. package/dist/core/logging/MindooLogger.d.ts.map +1 -1
  105. package/dist/core/logging/MindooLogger.js +65 -40
  106. package/dist/core/logging/MindooLogger.js.map +1 -1
  107. package/dist/core/tenantIdValidation.d.ts.map +1 -1
  108. package/dist/core/tenantIdValidation.js +1 -1
  109. package/dist/core/tenantIdValidation.js.map +1 -1
  110. package/dist/core/tenantSetup.d.ts +109 -0
  111. package/dist/core/tenantSetup.d.ts.map +1 -0
  112. package/dist/core/tenantSetup.js +250 -0
  113. package/dist/core/tenantSetup.js.map +1 -0
  114. package/dist/core/types.d.ts +333 -43
  115. package/dist/core/types.d.ts.map +1 -1
  116. package/dist/core/types.js +27 -9
  117. package/dist/core/types.js.map +1 -1
  118. package/dist/core/uri/joinRequestUri.d.ts +37 -0
  119. package/dist/core/uri/joinRequestUri.d.ts.map +1 -0
  120. package/dist/core/uri/joinRequestUri.js +168 -0
  121. package/dist/core/uri/joinRequestUri.js.map +1 -0
  122. package/dist/core/userid/canonicalUsername.d.ts +66 -0
  123. package/dist/core/userid/canonicalUsername.d.ts.map +1 -0
  124. package/dist/core/userid/canonicalUsername.js +235 -0
  125. package/dist/core/userid/canonicalUsername.js.map +1 -0
  126. package/dist/core/userid/index.d.ts +9 -0
  127. package/dist/core/userid/index.d.ts.map +1 -1
  128. package/dist/core/userid/index.js +14 -0
  129. package/dist/core/userid/index.js.map +1 -1
  130. package/dist/core/userkeys/UserKeyDocument.d.ts +50 -0
  131. package/dist/core/userkeys/UserKeyDocument.d.ts.map +1 -0
  132. package/dist/core/userkeys/UserKeyDocument.js +145 -0
  133. package/dist/core/userkeys/UserKeyDocument.js.map +1 -0
  134. package/dist/core/userkeys/UserKeyManager.d.ts +170 -0
  135. package/dist/core/userkeys/UserKeyManager.d.ts.map +1 -0
  136. package/dist/core/userkeys/UserKeyManager.js +1277 -0
  137. package/dist/core/userkeys/UserKeyManager.js.map +1 -0
  138. package/dist/core/userkeys/fingerprint.d.ts +17 -0
  139. package/dist/core/userkeys/fingerprint.d.ts.map +1 -0
  140. package/dist/core/userkeys/fingerprint.js +59 -0
  141. package/dist/core/userkeys/fingerprint.js.map +1 -0
  142. package/dist/core/userkeys/index.d.ts +11 -0
  143. package/dist/core/userkeys/index.d.ts.map +1 -0
  144. package/dist/core/userkeys/index.js +49 -0
  145. package/dist/core/userkeys/index.js.map +1 -0
  146. package/dist/core/userkeys/recipients.d.ts +51 -0
  147. package/dist/core/userkeys/recipients.d.ts.map +1 -0
  148. package/dist/core/userkeys/recipients.js +292 -0
  149. package/dist/core/userkeys/recipients.js.map +1 -0
  150. package/dist/core/userkeys/resolveUserKeyDocument.d.ts +21 -0
  151. package/dist/core/userkeys/resolveUserKeyDocument.d.ts.map +1 -0
  152. package/dist/core/userkeys/resolveUserKeyDocument.js +98 -0
  153. package/dist/core/userkeys/resolveUserKeyDocument.js.map +1 -0
  154. package/dist/core/userkeys/sealedCrypto.d.ts +21 -0
  155. package/dist/core/userkeys/sealedCrypto.d.ts.map +1 -0
  156. package/dist/core/userkeys/sealedCrypto.js +79 -0
  157. package/dist/core/userkeys/sealedCrypto.js.map +1 -0
  158. package/dist/core/userkeys/sealedTypes.d.ts +72 -0
  159. package/dist/core/userkeys/sealedTypes.d.ts.map +1 -0
  160. package/dist/core/userkeys/sealedTypes.js +14 -0
  161. package/dist/core/userkeys/sealedTypes.js.map +1 -0
  162. package/dist/core/userkeys/types.d.ts +70 -0
  163. package/dist/core/userkeys/types.d.ts.map +1 -0
  164. package/dist/core/userkeys/types.js +48 -0
  165. package/dist/core/userkeys/types.js.map +1 -0
  166. package/dist/core/userkeys/validateUserKeyDocument.d.ts +19 -0
  167. package/dist/core/userkeys/validateUserKeyDocument.d.ts.map +1 -0
  168. package/dist/core/userkeys/validateUserKeyDocument.js +42 -0
  169. package/dist/core/userkeys/validateUserKeyDocument.js.map +1 -0
  170. package/dist/core/utils/idGeneration.d.ts +39 -20
  171. package/dist/core/utils/idGeneration.d.ts.map +1 -1
  172. package/dist/core/utils/idGeneration.js +82 -62
  173. package/dist/core/utils/idGeneration.js.map +1 -1
  174. package/dist/core/utils/index.d.ts +1 -1
  175. package/dist/core/utils/index.d.ts.map +1 -1
  176. package/dist/core/utils/index.js +4 -1
  177. package/dist/core/utils/index.js.map +1 -1
  178. package/dist/node/server/CapabilityMatcher.d.ts +5 -0
  179. package/dist/node/server/CapabilityMatcher.d.ts.map +1 -1
  180. package/dist/node/server/CapabilityMatcher.js +10 -1
  181. package/dist/node/server/CapabilityMatcher.js.map +1 -1
  182. package/dist/node/server/DeviceDiscoveryService.d.ts +36 -0
  183. package/dist/node/server/DeviceDiscoveryService.d.ts.map +1 -0
  184. package/dist/node/server/DeviceDiscoveryService.js +165 -0
  185. package/dist/node/server/DeviceDiscoveryService.js.map +1 -0
  186. package/dist/node/server/MindooDBServer.d.ts +13 -0
  187. package/dist/node/server/MindooDBServer.d.ts.map +1 -1
  188. package/dist/node/server/MindooDBServer.js +99 -61
  189. package/dist/node/server/MindooDBServer.js.map +1 -1
  190. package/dist/node/server/SystemAdminAuth.d.ts +7 -0
  191. package/dist/node/server/SystemAdminAuth.d.ts.map +1 -1
  192. package/dist/node/server/SystemAdminAuth.js +4 -1
  193. package/dist/node/server/SystemAdminAuth.js.map +1 -1
  194. package/dist/node/server/TenantManager.d.ts +29 -0
  195. package/dist/node/server/TenantManager.d.ts.map +1 -1
  196. package/dist/node/server/TenantManager.js +146 -7
  197. package/dist/node/server/TenantManager.js.map +1 -1
  198. package/package.json +5 -5
@@ -0,0 +1,1277 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UserKeyManager = void 0;
4
+ const grantKeys_1 = require("../accesscontrol/grantKeys");
5
+ const privateKeyEncryption_1 = require("../crypto/privateKeyEncryption");
6
+ const RSAEncryption_1 = require("../crypto/RSAEncryption");
7
+ const rsaOaep3072_1 = require("../crypto/rsaOaep3072");
8
+ const types_1 = require("../types");
9
+ const fingerprint_1 = require("./fingerprint");
10
+ const resolveUserKeyDocument_1 = require("./resolveUserKeyDocument");
11
+ const validateUserKeyDocument_1 = require("./validateUserKeyDocument");
12
+ const UserKeyDocument_1 = require("./UserKeyDocument");
13
+ const types_2 = require("./types");
14
+ class UserKeyManager {
15
+ constructor(tenant) {
16
+ this.tenant = tenant;
17
+ this.userDirectoryFetched = false;
18
+ this.lastUserDirectoryFetchError = null;
19
+ this.decryptedUserKeyCache = null;
20
+ this.decryptedUserKeyBytes = null;
21
+ this.seededBytesMatchLocalPublic = null;
22
+ this.seededBytesVerifiedForPublicKey = null;
23
+ this.resolvingOwnDeviceWrap = false;
24
+ this.reconcileInFlight = null;
25
+ }
26
+ noteUserDirectoryFetched() {
27
+ this.userDirectoryFetched = true;
28
+ this.lastUserDirectoryFetchError = null;
29
+ }
30
+ noteUserDirectoryFetchFailed(error) {
31
+ this.lastUserDirectoryFetchError = error;
32
+ }
33
+ /**
34
+ * Inject User-Key PKCS8 bytes already decrypted by the host (Haven unlock).
35
+ * Used when `currentUserPassword` is empty so mint/wrap does not generate a
36
+ * second pair.
37
+ */
38
+ seedDecryptedUserKeyBytes(bytes) {
39
+ this.decryptedUserKeyBytes = bytes.slice();
40
+ this.decryptedUserKeyCache = null;
41
+ this.seededBytesMatchLocalPublic = null;
42
+ this.seededBytesVerifiedForPublicKey = null;
43
+ }
44
+ /**
45
+ * RSA round-trip against `publicKeyPem`: seal a probe with the public key and
46
+ * open it with the seeded private bytes. Cheaper than deriving the public key
47
+ * from PKCS8 and works on every crypto adapter.
48
+ */
49
+ async seededBytesBelongToLocalPublicKey(publicKeyPem) {
50
+ if (this.seededBytesMatchLocalPublic !== null &&
51
+ this.seededBytesVerifiedForPublicKey === publicKeyPem) {
52
+ return this.seededBytesMatchLocalPublic;
53
+ }
54
+ const bytes = this.decryptedUserKeyBytes;
55
+ if (!bytes)
56
+ return false;
57
+ let matches = false;
58
+ try {
59
+ const crypto = this.tenant.getCryptoAdapter();
60
+ const rsa = new RSAEncryption_1.RSAEncryption(crypto);
61
+ const probe = crypto.getRandomValues(new Uint8Array(16));
62
+ const sealed = await rsa.encrypt(probe, publicKeyPem);
63
+ const privateKey = await (0, rsaOaep3072_1.importRsaOaepPrivateKey)(crypto, bytes.slice(), false);
64
+ const opened = await rsa.decrypt(sealed, privateKey);
65
+ matches = opened.length === probe.length && opened.every((b, i) => b === probe[i]);
66
+ }
67
+ catch (error) {
68
+ console.warn("[userkeys] User-Key round-trip check failed", error);
69
+ matches = false;
70
+ }
71
+ const fingerprint = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(publicKeyPem, this.tenant.getCryptoAdapter().getSubtle()).catch(() => "?");
72
+ console.log(matches
73
+ ? "[userkeys] seeded User-Key private bytes match the local public key"
74
+ : "[userkeys] seeded User-Key private bytes do NOT match the local public key", { publicKeyFingerprint: fingerprint });
75
+ this.seededBytesMatchLocalPublic = matches;
76
+ this.seededBytesVerifiedForPublicKey = publicKeyPem;
77
+ return matches;
78
+ }
79
+ hasFetchedUserDirectory() {
80
+ return this.userDirectoryFetched;
81
+ }
82
+ async ensureLocalUserKeyPair(user, password) {
83
+ if (user.userKeyPair) {
84
+ // Host-seeded bytes come first: Haven opens with an empty password, so
85
+ // trying PBKDF2 would only log a decrypt failure per call. They must
86
+ // belong to `userKeyPair.publicKey` — a foreign pair would publish a
87
+ // document whose wraps nobody, not even this device, can open.
88
+ if (this.decryptedUserKeyBytes &&
89
+ (await this.seededBytesBelongToLocalPublicKey(user.userKeyPair.publicKey))) {
90
+ return user.userKeyPair;
91
+ }
92
+ const usable = password
93
+ ? await this.decryptUserKeyPairBytes(user.userKeyPair, password)
94
+ : null;
95
+ if (usable) {
96
+ this.decryptedUserKeyBytes = usable;
97
+ this.seededBytesMatchLocalPublic = true;
98
+ this.seededBytesVerifiedForPublicKey = user.userKeyPair.publicKey;
99
+ return user.userKeyPair;
100
+ }
101
+ // Joined device: the key lives as a wrap to this device's key, and the
102
+ // host may have seeded different bytes for the same identity.
103
+ const fromWrap = await this.ownDeviceWrapPrivateKeyBytes(user.userKeyPair.publicKey);
104
+ if (fromWrap) {
105
+ this.decryptedUserKeyBytes = fromWrap;
106
+ this.decryptedUserKeyCache = null;
107
+ this.seededBytesMatchLocalPublic = true;
108
+ this.seededBytesVerifiedForPublicKey = user.userKeyPair.publicKey;
109
+ return user.userKeyPair;
110
+ }
111
+ if (this.decryptedUserKeyBytes) {
112
+ console.error("[userkeys] seeded User-Key private bytes do not match the identity's User-Key public key");
113
+ }
114
+ throw new types_2.UserKeyDecryptError();
115
+ }
116
+ const crypto = this.tenant.getCryptoAdapter();
117
+ const generated = await (0, rsaOaep3072_1.generateRsaOaep3072)(crypto);
118
+ const encrypted = await (0, privateKeyEncryption_1.encryptPrivateKey)(crypto, generated.privateKeyBytes, password, types_2.USERKEY_PRIVATE_SALT);
119
+ const pair = {
120
+ publicKey: generated.publicKeyPem,
121
+ privateKey: encrypted,
122
+ };
123
+ user.userKeyPair = pair;
124
+ this.decryptedUserKeyBytes = generated.privateKeyBytes;
125
+ this.decryptedUserKeyCache = null;
126
+ this.seededBytesMatchLocalPublic = true;
127
+ this.seededBytesVerifiedForPublicKey = pair.publicKey;
128
+ return pair;
129
+ }
130
+ async getLocalUserPublicKey() {
131
+ const user = this.tenant.getCurrentPrivateUser();
132
+ if (user.userKeyPair?.publicKey)
133
+ return user.userKeyPair.publicKey;
134
+ return null;
135
+ }
136
+ async decryptUserKeyPairBytes(pair, password) {
137
+ try {
138
+ return new Uint8Array(await this.tenant.decryptPrivateKey(pair.privateKey, password, types_2.USERKEY_PRIVATE_SALT));
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ }
144
+ async getDecryptedUserKeyBytes() {
145
+ const user = this.tenant.getCurrentPrivateUser();
146
+ const pair = user.userKeyPair;
147
+ if (this.decryptedUserKeyBytes) {
148
+ if (!pair)
149
+ return this.decryptedUserKeyBytes;
150
+ // Host-seeded bytes are only usable if they open the published pair;
151
+ // wrapping with a foreign key produces wraps nobody can unwrap. On a
152
+ // mismatch fall through to the password path rather than giving up.
153
+ if (await this.seededBytesBelongToLocalPublicKey(pair.publicKey)) {
154
+ return this.decryptedUserKeyBytes;
155
+ }
156
+ }
157
+ if (!pair)
158
+ return null;
159
+ const password = this.tenant.getCurrentUserPassword();
160
+ // Haven live-bag opens with "". PBKDF2 then fails loudly, so skip it and
161
+ // let the device wrap supply the key.
162
+ const bytes = password ? await this.decryptUserKeyPairBytes(pair, password) : null;
163
+ if (bytes) {
164
+ this.decryptedUserKeyBytes = bytes;
165
+ this.seededBytesMatchLocalPublic = true;
166
+ this.seededBytesVerifiedForPublicKey = pair.publicKey;
167
+ return bytes;
168
+ }
169
+ // A joined device holds the User-Key only as a wrap to its device key, and
170
+ // the host may have seeded this identity's own (different) bytes over it.
171
+ // Recover from the wrap so wrapping/reconcile is not lost for the session.
172
+ const fromWrap = await this.ownDeviceWrapPrivateKeyBytes(pair.publicKey);
173
+ if (!fromWrap)
174
+ return null;
175
+ this.decryptedUserKeyBytes = fromWrap;
176
+ this.decryptedUserKeyCache = null;
177
+ this.seededBytesMatchLocalPublic = true;
178
+ this.seededBytesVerifiedForPublicKey = pair.publicKey;
179
+ return fromWrap;
180
+ }
181
+ /**
182
+ * The User-Key private bytes taken from THIS device's wrap in the own
183
+ * User-Key document, verified against `publicKeyPem`. Returns null when no
184
+ * wrap for this device exists or it does not open the pair. Kept free of
185
+ * {@link getDecryptedUserKeyBytes} so the two cannot recurse.
186
+ */
187
+ async ownDeviceWrapPrivateKeyBytes(publicKeyPem) {
188
+ if (this.resolvingOwnDeviceWrap)
189
+ return null;
190
+ this.resolvingOwnDeviceWrap = true;
191
+ try {
192
+ const deviceKey = await this.tenant.getEncryptionPrivateKeyForReconcile();
193
+ if (!deviceKey)
194
+ return null;
195
+ const resolved = await this.resolveOwnUserKeyDocument();
196
+ if (!resolved)
197
+ return null;
198
+ const user = this.tenant.getCurrentPrivateUser();
199
+ const deviceFp = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(user.userEncryptionKeyPair.publicKey, this.tenant.getCryptoAdapter().getSubtle());
200
+ for (const epoch of this.epochsNewestFirst(resolved.payload)) {
201
+ const gen = resolved.payload.userKeys[epoch];
202
+ if (gen?.publicKey !== publicKeyPem)
203
+ continue;
204
+ const wrappedKeyB64 = gen.deviceWraps?.[deviceFp]?.wrappedKey;
205
+ if (!wrappedKeyB64)
206
+ continue;
207
+ try {
208
+ const bytes = await (0, UserKeyDocument_1.unwrapPrivateKeyFromDevice)({
209
+ cryptoAdapter: this.tenant.getCryptoAdapter(),
210
+ wrappedKeyB64,
211
+ deviceEncryptionPrivateKey: deviceKey,
212
+ });
213
+ console.log("[userkeys] recovered User-Key private bytes from own device wrap", {
214
+ epoch,
215
+ ownDeviceFingerprint: deviceFp,
216
+ });
217
+ return bytes;
218
+ }
219
+ catch (error) {
220
+ console.warn("[userkeys] own device wrap unwrap failed", { epoch }, error);
221
+ }
222
+ }
223
+ return null;
224
+ }
225
+ finally {
226
+ this.resolvingOwnDeviceWrap = false;
227
+ }
228
+ }
229
+ async getDecryptedUserKey() {
230
+ if (this.decryptedUserKeyCache)
231
+ return this.decryptedUserKeyCache;
232
+ const bytes = await this.getDecryptedUserKeyBytes();
233
+ if (!bytes)
234
+ return null;
235
+ const key = await (0, rsaOaep3072_1.importRsaOaepPrivateKey)(this.tenant.getCryptoAdapter(), bytes, false);
236
+ this.decryptedUserKeyCache = key;
237
+ return key;
238
+ }
239
+ /**
240
+ * All epochs this device can open, newest first. Used by key-distribution
241
+ * reconcile so wraps addressed to a retired fingerprint still import.
242
+ */
243
+ async getUserKeyCryptoKeysForReconcile() {
244
+ const keys = [];
245
+ const seen = new Set();
246
+ let usedPublishedDocument = false;
247
+ // Prefer the published User-Key (possibly a different pair than this
248
+ // device generated). Opening userdirectory during directory bring-up
249
+ // races a second Automerge WASM heap — only do it when that DB is
250
+ // already fetched or already open on this tenant instance.
251
+ const canUseUserDirectory = this.canReconcileAgainstUserDirectory();
252
+ let resolvedDocId;
253
+ if (canUseUserDirectory) {
254
+ try {
255
+ const resolved = await this.resolveOwnUserKeyDocument();
256
+ if (resolved) {
257
+ usedPublishedDocument = true;
258
+ resolvedDocId = resolved.doc.getId();
259
+ for (const epoch of this.epochsNewestFirst(resolved.payload)) {
260
+ const bytes = await this.privateKeyBytesForEpoch(resolved.payload, epoch);
261
+ if (!bytes)
262
+ continue;
263
+ const fingerprint = resolved.payload.userKeys[epoch]?.fingerprint;
264
+ if (!fingerprint || seen.has(fingerprint))
265
+ continue;
266
+ seen.add(fingerprint);
267
+ keys.push(await (0, rsaOaep3072_1.importRsaOaepPrivateKey)(this.tenant.getCryptoAdapter(), bytes.slice(), false));
268
+ }
269
+ }
270
+ }
271
+ catch (error) {
272
+ console.warn("[userkeys] getUserKeyCryptoKeysForReconcile: published lookup failed", error);
273
+ }
274
+ }
275
+ if (keys.length === 0) {
276
+ const current = await this.getDecryptedUserKey();
277
+ if (current)
278
+ keys.push(current);
279
+ }
280
+ console.log("[userkeys] getUserKeyCryptoKeysForReconcile", {
281
+ canUseUserDirectory,
282
+ usedPublishedDocument,
283
+ resolvedDocId,
284
+ userKeyFingerprints: [...seen],
285
+ keyCount: keys.length,
286
+ });
287
+ return keys;
288
+ }
289
+ async openUserDirectory() {
290
+ return this.tenant.openDB(types_1.USER_DIRECTORY_DB_ID);
291
+ }
292
+ async resolveOwnUserKeyDocument() {
293
+ const db = await this.openUserDirectory();
294
+ const directory = await this.directory();
295
+ const username = (await this.tenant.getCurrentUserId()).username;
296
+ const hashes = await directory.getUsernameHashCandidates(username);
297
+ const grants = await directory.findGrantAccessDocuments(username);
298
+ return (0, resolveUserKeyDocument_1.resolveUserKeyDocument)({
299
+ db,
300
+ directory,
301
+ username,
302
+ usernameHashCandidates: hashes,
303
+ grantDocIds: grants.map((g) => g.getId()),
304
+ adminPublicKey: this.tenant.getAdministrationPublicKey(),
305
+ });
306
+ }
307
+ /**
308
+ * Admin path: create a pending userkey document from a join-request public key.
309
+ */
310
+ async createPendingFromJoin(input) {
311
+ const binding = await this.mintBindingForUsername(input.username);
312
+ if (!binding) {
313
+ throw new Error(`Cannot create user key: no grant for "${input.username}"`);
314
+ }
315
+ const { grantDocId, usernameHash, usernameHashVersion } = binding;
316
+ const fingerprint = await (0, UserKeyDocument_1.fingerprintUserPublicKey)(input.userPublicKey, this.tenant.getCryptoAdapter());
317
+ const db = await this.openUserDirectory();
318
+ try {
319
+ const existing = await db.getDocument((0, types_2.userKeyDocumentId)(grantDocId));
320
+ if (existing && !existing.isDeleted())
321
+ return existing;
322
+ }
323
+ catch {
324
+ // missing
325
+ }
326
+ return (0, UserKeyDocument_1.createPendingUserKeyDocument)({
327
+ db,
328
+ grantDocId,
329
+ usernameHash,
330
+ usernameHashVersion,
331
+ publicKey: input.userPublicKey,
332
+ fingerprint,
333
+ signingKeyPair: {
334
+ publicKey: input.signingKeyPair.publicKey,
335
+ privateKey: input.signingKeyPair.privateKey,
336
+ },
337
+ signingKeyPassword: input.signingKeyPassword,
338
+ });
339
+ }
340
+ /**
341
+ * Self-create for legacy users who never sent a User-Key in a join request.
342
+ * Refuses unless userdirectory has been fetched this session (Trap 1).
343
+ * On mint, wraps the new private key to every active grant device so already
344
+ * enrolled devices can import without an approval dialog. Devices added to
345
+ * the grant afterwards still require explicit approval.
346
+ */
347
+ async ensureOwnUserKeyDocument(options) {
348
+ const existing = await this.resolveOwnUserKeyDocument();
349
+ if (existing)
350
+ return existing;
351
+ const allow = options?.allowSelfCreate === true || this.userDirectoryFetched;
352
+ if (!allow)
353
+ return null;
354
+ // Additional device: `acl_keydistribution_default` is already wrapped to
355
+ // the published User-Key. Minting a second pair (this device's locally
356
+ // generated key) desyncs dist — Device 2 would publish a key that cannot
357
+ // unwrap `default`. Wait for the userkey document and import from wrap.
358
+ if (await this.defaultAlreadyWrappedForCurrentUser()) {
359
+ return null;
360
+ }
361
+ const user = this.tenant.getCurrentPrivateUser();
362
+ let pair;
363
+ try {
364
+ pair = await this.ensureLocalUserKeyPair(user, this.tenant.getCurrentUserPassword());
365
+ }
366
+ catch (error) {
367
+ if (error instanceof types_2.UserKeyDecryptError)
368
+ return null;
369
+ throw error;
370
+ }
371
+ const userId = await this.tenant.getCurrentUserId();
372
+ const binding = await this.mintBindingForUsername(userId.username, userId.userSigningPublicKey);
373
+ if (!binding)
374
+ return null;
375
+ const { grantDocId, usernameHash, usernameHashVersion } = binding;
376
+ const fingerprint = await (0, UserKeyDocument_1.fingerprintUserPublicKey)(pair.publicKey, this.tenant.getCryptoAdapter());
377
+ const db = await this.openUserDirectory();
378
+ let doc;
379
+ let mintedHere = false;
380
+ try {
381
+ const occupied = await db.getDocument((0, types_2.userKeyDocumentId)(grantDocId));
382
+ if (occupied && !occupied.isDeleted() && (0, validateUserKeyDocument_1.asUserKeyPayload)(occupied.getData())) {
383
+ doc = occupied;
384
+ }
385
+ }
386
+ catch {
387
+ // missing
388
+ }
389
+ if (!doc) {
390
+ const occupiedInvalid = await db.getDocument((0, types_2.userKeyDocumentId)(grantDocId)).catch(() => null);
391
+ if (occupiedInvalid && !occupiedInvalid.isDeleted() && !(0, validateUserKeyDocument_1.asUserKeyPayload)(occupiedInvalid.getData())) {
392
+ const built = (0, UserKeyDocument_1.buildPendingUserKeyPayload)({
393
+ usernameHash,
394
+ usernameHashVersion,
395
+ publicKey: pair.publicKey,
396
+ fingerprint,
397
+ createdAt: Date.now(),
398
+ });
399
+ doc = await db.createDocument({
400
+ idPrefix: "userkey",
401
+ decryptionKeyId: "$publicinfos",
402
+ initialValues: built,
403
+ });
404
+ }
405
+ else {
406
+ doc = await (0, UserKeyDocument_1.createPendingUserKeyDocument)({
407
+ db,
408
+ grantDocId,
409
+ usernameHash,
410
+ usernameHashVersion,
411
+ publicKey: pair.publicKey,
412
+ fingerprint,
413
+ });
414
+ }
415
+ mintedHere = true;
416
+ }
417
+ const payload = (0, validateUserKeyDocument_1.asUserKeyPayload)(doc.getData());
418
+ if (!payload)
419
+ return null;
420
+ // Occupied valid document: another replica won the mint. Do not wrap-all
421
+ // onto it — that would auto-approve later grant devices.
422
+ if (!mintedHere) {
423
+ return { doc, payload };
424
+ }
425
+ try {
426
+ await this.wrapPrivateKeyForActiveGrantDevices(doc, payload);
427
+ }
428
+ catch (error) {
429
+ if (!(error instanceof types_2.UserKeyMismatchError))
430
+ throw error;
431
+ }
432
+ // Grant overview can be empty (directory not pulled yet). Still wrap this
433
+ // device so the document does not stay pending with `deviceWraps: {}`.
434
+ await this.writeOwnDeviceWrap(doc, payload);
435
+ const refreshed = await db.getDocument(doc.getId());
436
+ const next = (0, validateUserKeyDocument_1.asUserKeyPayload)(refreshed.getData());
437
+ return next ? { doc: refreshed, payload: next } : { doc, payload };
438
+ }
439
+ /**
440
+ * Bootstrap / recreate: wrap the new private key to every active grant
441
+ * device. Later devices added to the grant still require explicit approval.
442
+ *
443
+ * `legacyUnstampedOnly` wraps only grant devices with no `addedAt` (pre-User-Keys
444
+ * fleet). Devices stamped with `addedAt` after the userkey document exists still
445
+ * need an explicit approve.
446
+ */
447
+ async wrapPrivateKeyForActiveGrantDevices(doc, payload, options) {
448
+ const bytes = await this.getDecryptedUserKeyBytes();
449
+ if (!bytes) {
450
+ throw new types_2.UserKeyMismatchError("Local User-Key private material is unavailable");
451
+ }
452
+ const live = (0, validateUserKeyDocument_1.asUserKeyPayload)(doc.getData()) ?? payload;
453
+ const localPublic = await this.getLocalUserPublicKey();
454
+ const epoch = (0, types_2.currentUserKeyEpoch)(live);
455
+ if (!epoch || !localPublic)
456
+ return;
457
+ const gen = live.userKeys[epoch];
458
+ if (!gen || gen.publicKey !== localPublic)
459
+ return;
460
+ const directory = await this.directory();
461
+ const signer = await this.tenant.getCurrentUserId();
462
+ const user = this.tenant.getCurrentPrivateUser();
463
+ const overview = await directory.getUserGrantOverview(signer.username);
464
+ const now = Date.now();
465
+ const deviceWraps = {
466
+ ...(gen.deviceWraps ?? {}),
467
+ };
468
+ let changed = false;
469
+ const devices = [...overview.activeDevices];
470
+ if (!devices.some((device) => device.encryptionPublicKey === user.userEncryptionKeyPair.publicKey)) {
471
+ devices.push({
472
+ signingPublicKey: user.userSigningKeyPair.publicKey,
473
+ encryptionPublicKey: user.userEncryptionKeyPair.publicKey,
474
+ wipeRequested: false,
475
+ });
476
+ }
477
+ for (const device of devices) {
478
+ if (device.wipeRequested)
479
+ continue;
480
+ if (options?.legacyUnstampedOnly && typeof device.addedAt === "number")
481
+ continue;
482
+ const deviceFp = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(device.encryptionPublicKey, this.tenant.getCryptoAdapter().getSubtle());
483
+ const existing = deviceWraps[deviceFp];
484
+ if (existing) {
485
+ if (!existing.label && device.label) {
486
+ deviceWraps[deviceFp] = { ...existing, label: device.label };
487
+ changed = true;
488
+ }
489
+ continue;
490
+ }
491
+ deviceWraps[deviceFp] = {
492
+ wrappedKey: await (0, UserKeyDocument_1.wrapPrivateKeyForDevice)({
493
+ cryptoAdapter: this.tenant.getCryptoAdapter(),
494
+ privateKeyBytes: bytes,
495
+ deviceEncryptionPublicKey: device.encryptionPublicKey,
496
+ }),
497
+ ...(device.label ? { label: device.label } : {}),
498
+ approvedAt: now,
499
+ approvedBySigningPublicKey: signer.userSigningPublicKey,
500
+ };
501
+ changed = true;
502
+ }
503
+ if (!changed)
504
+ return;
505
+ await (0, UserKeyDocument_1.replaceUserKeys)(doc.getDatabase(), doc, {
506
+ ...live.userKeys,
507
+ [epoch]: { ...gen, deviceWraps },
508
+ });
509
+ }
510
+ async tryImportUserKeyFromWrap(payload) {
511
+ const user = this.tenant.getCurrentPrivateUser();
512
+ const deviceKey = await this.tenant.getEncryptionPrivateKeyForReconcile();
513
+ if (!deviceKey)
514
+ return false;
515
+ const deviceFp = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(user.userEncryptionKeyPair.publicKey, this.tenant.getCryptoAdapter().getSubtle());
516
+ for (const epoch of this.epochsNewestFirst(payload)) {
517
+ const gen = payload.userKeys[epoch];
518
+ const wrap = gen?.deviceWraps?.[deviceFp];
519
+ if (!wrap?.wrappedKey)
520
+ continue;
521
+ try {
522
+ const bytes = await (0, UserKeyDocument_1.unwrapPrivateKeyFromDevice)({
523
+ cryptoAdapter: this.tenant.getCryptoAdapter(),
524
+ wrappedKeyB64: wrap.wrappedKey,
525
+ deviceEncryptionPrivateKey: deviceKey,
526
+ });
527
+ const encrypted = await (0, privateKeyEncryption_1.encryptPrivateKey)(this.tenant.getCryptoAdapter(), bytes, this.tenant.getCurrentUserPassword(), types_2.USERKEY_PRIVATE_SALT);
528
+ user.userKeyPair = {
529
+ publicKey: gen.publicKey,
530
+ privateKey: encrypted,
531
+ };
532
+ this.decryptedUserKeyBytes = bytes;
533
+ this.decryptedUserKeyCache = null;
534
+ this.seededBytesMatchLocalPublic = true;
535
+ this.seededBytesVerifiedForPublicKey = gen.publicKey;
536
+ return true;
537
+ }
538
+ catch {
539
+ // wrap is not for this device or is corrupt; try next generation
540
+ }
541
+ }
542
+ return false;
543
+ }
544
+ async writeOwnDeviceWrap(doc, payload) {
545
+ const localPublic = await this.getLocalUserPublicKey();
546
+ if (!localPublic) {
547
+ throw new types_2.UserKeyMismatchError("This device has no local User-Key to compare against the published document");
548
+ }
549
+ const live = (0, validateUserKeyDocument_1.asUserKeyPayload)(doc.getData()) ?? payload;
550
+ const currentEpoch = (0, types_2.currentUserKeyEpoch)(live);
551
+ if (!currentEpoch) {
552
+ throw new types_2.UserKeyMismatchError("Published user-key document has no generations");
553
+ }
554
+ const published = live.userKeys[currentEpoch]?.publicKey;
555
+ if (published !== localPublic) {
556
+ throw new types_2.UserKeyMismatchError();
557
+ }
558
+ const user = this.tenant.getCurrentPrivateUser();
559
+ const deviceFp = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(user.userEncryptionKeyPair.publicKey, this.tenant.getCryptoAdapter().getSubtle());
560
+ const signer = await this.tenant.getCurrentUserId();
561
+ const ownLabel = await this.labelForOwnEncryptionKey();
562
+ const db = doc.getDatabase();
563
+ for (const epoch of Object.keys(live.userKeys)) {
564
+ const gen = live.userKeys[epoch];
565
+ if (gen.publicKey !== localPublic)
566
+ continue;
567
+ if (gen.deviceWraps?.[deviceFp]) {
568
+ if (!gen.deviceWraps[deviceFp].label && ownLabel) {
569
+ await (0, UserKeyDocument_1.writeDeviceWrap)({
570
+ db,
571
+ doc,
572
+ epoch,
573
+ deviceFingerprint: deviceFp,
574
+ wrap: { ...gen.deviceWraps[deviceFp], label: ownLabel },
575
+ });
576
+ }
577
+ continue;
578
+ }
579
+ const bytes = await this.privateKeyBytesForEpoch(live, epoch);
580
+ if (!bytes) {
581
+ throw new types_2.UserKeyMismatchError("Local User-Key private material is unavailable");
582
+ }
583
+ const wrapped = await (0, UserKeyDocument_1.wrapPrivateKeyForDevice)({
584
+ cryptoAdapter: this.tenant.getCryptoAdapter(),
585
+ privateKeyBytes: bytes,
586
+ deviceEncryptionPublicKey: user.userEncryptionKeyPair.publicKey,
587
+ });
588
+ await (0, UserKeyDocument_1.writeDeviceWrap)({
589
+ db,
590
+ doc,
591
+ epoch,
592
+ deviceFingerprint: deviceFp,
593
+ wrap: {
594
+ wrappedKey: wrapped,
595
+ ...(ownLabel ? { label: ownLabel } : {}),
596
+ approvedAt: Date.now(),
597
+ approvedBySigningPublicKey: signer.userSigningPublicKey,
598
+ },
599
+ });
600
+ }
601
+ }
602
+ /**
603
+ * A wrap-less userkey document cannot be imported by anyone. Replace its
604
+ * planted public key with this device's local pair and wrap the current
605
+ * grant (same outcome as a first mint).
606
+ */
607
+ async adoptPendingUserKeyDocument(doc, payload) {
608
+ const user = this.tenant.getCurrentPrivateUser();
609
+ const pair = await this.ensureLocalUserKeyPair(user, this.tenant.getCurrentUserPassword());
610
+ const epoch = (0, types_2.currentUserKeyEpoch)(payload) ?? "1";
611
+ const gen = payload.userKeys[epoch];
612
+ const fingerprint = await (0, UserKeyDocument_1.fingerprintUserPublicKey)(pair.publicKey, this.tenant.getCryptoAdapter());
613
+ await (0, UserKeyDocument_1.replaceUserKeys)(doc.getDatabase(), doc, {
614
+ ...payload.userKeys,
615
+ [epoch]: {
616
+ publicKey: pair.publicKey,
617
+ fingerprint,
618
+ createdAt: gen?.createdAt ?? Date.now(),
619
+ deviceWraps: {},
620
+ },
621
+ });
622
+ const db = await this.openUserDirectory();
623
+ const refreshed = await db.getDocument(doc.getId());
624
+ const nextPayload = (0, validateUserKeyDocument_1.asUserKeyPayload)(refreshed.getData()) ?? payload;
625
+ const nextDoc = refreshed;
626
+ await this.wrapPrivateKeyForActiveGrantDevices(nextDoc, nextPayload);
627
+ const wrappedDoc = await db.getDocument(nextDoc.getId());
628
+ const wrappedPayload = (0, validateUserKeyDocument_1.asUserKeyPayload)(wrappedDoc.getData()) ?? nextPayload;
629
+ await this.writeOwnDeviceWrap(wrappedDoc, wrappedPayload);
630
+ const afterDoc = await db.getDocument(wrappedDoc.getId());
631
+ const afterPayload = (0, validateUserKeyDocument_1.asUserKeyPayload)(afterDoc.getData());
632
+ return afterPayload
633
+ ? { doc: afterDoc, payload: afterPayload }
634
+ : { doc: wrappedDoc, payload: wrappedPayload };
635
+ }
636
+ /**
637
+ * A wrap-less document cannot be used by any device. Either wrap it with the
638
+ * local pair that matches the planted public key, or replace that key and wrap.
639
+ * This is the recovery path for an already-minted pending document (the user's
640
+ * leftover `deviceWraps: {}` row).
641
+ */
642
+ async sealPendingUserKeyDocument(doc, payload) {
643
+ const user = this.tenant.getCurrentPrivateUser();
644
+ await this.ensureLocalUserKeyPair(user, this.tenant.getCurrentUserPassword());
645
+ const localPublic = await this.getLocalUserPublicKey();
646
+ const epoch = (0, types_2.currentUserKeyEpoch)(payload);
647
+ const published = epoch ? payload.userKeys[epoch]?.publicKey : undefined;
648
+ const bytes = await this.getDecryptedUserKeyBytes();
649
+ const canUsePlantedKey = Boolean(localPublic && published && localPublic === published && bytes);
650
+ try {
651
+ if (canUsePlantedKey) {
652
+ await this.writeOwnDeviceWrap(doc, payload);
653
+ await this.wrapPrivateKeyForActiveGrantDevices(doc, payload, {
654
+ legacyUnstampedOnly: true,
655
+ });
656
+ }
657
+ else {
658
+ console.warn("[userkeys] sealing pending document by adopting a new pair", {
659
+ hasLocalPublic: Boolean(localPublic),
660
+ publicKeysMatch: localPublic === published,
661
+ hasPrivateBytes: Boolean(bytes),
662
+ });
663
+ return await this.adoptPendingUserKeyDocument(doc, payload);
664
+ }
665
+ }
666
+ catch (error) {
667
+ console.warn("[userkeys] wrap of pending document failed, adopting", error);
668
+ return await this.adoptPendingUserKeyDocument(doc, payload);
669
+ }
670
+ const db = await this.openUserDirectory();
671
+ const afterDoc = await db.getDocument(doc.getId());
672
+ const afterPayload = (0, validateUserKeyDocument_1.asUserKeyPayload)(afterDoc.getData());
673
+ return afterPayload ? { doc: afterDoc, payload: afterPayload } : { doc, payload };
674
+ }
675
+ isReconciling() {
676
+ return this.reconcileInFlight !== null;
677
+ }
678
+ /**
679
+ * True when userdirectory has been fetched this session, or is already fully
680
+ * initialized. In-flight `openDB` does **not** count: treating that as ready
681
+ * starts reconcile during initialize, which re-enters `updateUnifiedCache`
682
+ * and unbounded-allocates Automerge/AES heaps.
683
+ */
684
+ canReconcileAgainstUserDirectory() {
685
+ if (this.userDirectoryFetched)
686
+ return true;
687
+ if (typeof this.tenant.isDatabaseReady === "function") {
688
+ return this.tenant.isDatabaseReady(types_1.USER_DIRECTORY_DB_ID);
689
+ }
690
+ return this.tenant.isDatabaseOpen?.(types_1.USER_DIRECTORY_DB_ID) === true;
691
+ }
692
+ async reconcile(options) {
693
+ if (this.reconcileInFlight) {
694
+ const inFlight = await this.reconcileInFlight;
695
+ if (options?.allowSelfCreate !== true)
696
+ return inFlight;
697
+ }
698
+ const run = this.reconcileInner(options).finally(() => {
699
+ if (this.reconcileInFlight === run)
700
+ this.reconcileInFlight = null;
701
+ });
702
+ this.reconcileInFlight = run;
703
+ return run;
704
+ }
705
+ async reconcileInner(options) {
706
+ try {
707
+ if (!this.canReconcileAgainstUserDirectory()) {
708
+ return this.enrollmentStatus(null, "unknown");
709
+ }
710
+ const db = await this.openUserDirectory();
711
+ await db.syncStoreChanges();
712
+ if (this.userDirectoryFetched) {
713
+ this.lastUserDirectoryFetchError = null;
714
+ }
715
+ let resolved = await this.resolveOwnUserKeyDocument();
716
+ if (!resolved) {
717
+ resolved = await this.ensureOwnUserKeyDocument(options);
718
+ }
719
+ if (!resolved) {
720
+ return this.enrollmentStatus(null, "unknown");
721
+ }
722
+ if ((0, types_2.isPendingUserKeyDocument)(resolved.payload)) {
723
+ resolved = await this.sealPendingUserKeyDocument(resolved.doc, resolved.payload);
724
+ }
725
+ const localPublic = await this.getLocalUserPublicKey();
726
+ if (!localPublic) {
727
+ await this.tryImportUserKeyFromWrap(resolved.payload);
728
+ }
729
+ let afterImport = await this.getLocalUserPublicKey();
730
+ if (afterImport) {
731
+ const epoch = (0, types_2.currentUserKeyEpoch)(resolved.payload);
732
+ const published = epoch ? resolved.payload.userKeys[epoch]?.publicKey : undefined;
733
+ if (published && published !== afterImport) {
734
+ await this.tryImportUserKeyFromWrap(resolved.payload);
735
+ afterImport = await this.getLocalUserPublicKey();
736
+ }
737
+ const user = this.tenant.getCurrentPrivateUser();
738
+ const deviceFp = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(user.userEncryptionKeyPair.publicKey, this.tenant.getCryptoAdapter().getSubtle());
739
+ const hasOwnWrap = Object.values(resolved.payload.userKeys).some((gen) => !!gen.deviceWraps?.[deviceFp]);
740
+ if (published && afterImport && published !== afterImport) {
741
+ const matchesSomeEpoch = Object.values(resolved.payload.userKeys).some((gen) => gen.publicKey === afterImport);
742
+ if (!matchesSomeEpoch) {
743
+ if (hasOwnWrap) {
744
+ throw new types_2.UserKeyMismatchError();
745
+ }
746
+ await this.dropRevokedDeviceWraps(resolved.doc, resolved.payload);
747
+ return this.enrollmentStatus(resolved.payload, "waiting");
748
+ }
749
+ }
750
+ if (!hasOwnWrap && afterImport === published) {
751
+ await this.writeOwnDeviceWrap(resolved.doc, resolved.payload);
752
+ resolved = (await this.resolveOwnUserKeyDocument()) ?? resolved;
753
+ }
754
+ if (afterImport === published) {
755
+ await this.wrapPrivateKeyForActiveGrantDevices(resolved.doc, resolved.payload, {
756
+ legacyUnstampedOnly: true,
757
+ });
758
+ resolved = (await this.resolveOwnUserKeyDocument()) ?? resolved;
759
+ }
760
+ }
761
+ await this.dropRevokedDeviceWraps(resolved.doc, resolved.payload);
762
+ return this.enrollmentStatus(resolved.payload, this.waitState());
763
+ }
764
+ catch (error) {
765
+ if (error instanceof types_2.UserKeyMismatchError || error instanceof types_2.UserKeyDecryptError)
766
+ throw error;
767
+ this.lastUserDirectoryFetchError = error;
768
+ console.warn("[userkeys] reconcile failed", error);
769
+ return this.enrollmentStatus(null, "unknown");
770
+ }
771
+ }
772
+ async listPendingUserKeyDevices() {
773
+ const resolved = await this.resolveOwnUserKeyDocument();
774
+ if (!resolved)
775
+ return [];
776
+ const directory = await this.directory();
777
+ const username = (await this.tenant.getCurrentUserId()).username;
778
+ const overview = await directory.getUserGrantOverview(username);
779
+ const fallbackAddedAt = await this.grantCreatedAtFallback(username);
780
+ const wrapped = this.wrappedDeviceFingerprints(resolved.payload);
781
+ const rejected = new Set(Object.keys(resolved.payload.rejectedDevices ?? {}));
782
+ const pending = [];
783
+ for (const device of overview.activeDevices) {
784
+ const fp = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(device.encryptionPublicKey, this.tenant.getCryptoAdapter().getSubtle());
785
+ if (wrapped.has(fp) || rejected.has(fp))
786
+ continue;
787
+ pending.push({
788
+ fingerprint: fp,
789
+ signingPublicKey: device.signingPublicKey,
790
+ encryptionPublicKey: device.encryptionPublicKey,
791
+ label: device.label,
792
+ addedAt: device.addedAt ?? fallbackAddedAt,
793
+ });
794
+ }
795
+ return pending;
796
+ }
797
+ async listUserKeyDevices() {
798
+ const resolved = await this.resolveOwnUserKeyDocument();
799
+ if (!resolved)
800
+ return [];
801
+ const directory = await this.directory();
802
+ const username = (await this.tenant.getCurrentUserId()).username;
803
+ const overview = await directory.getUserGrantOverview(username);
804
+ const fallbackAddedAt = await this.grantCreatedAtFallback(username);
805
+ const wrapped = this.wrappedDeviceFingerprints(resolved.payload);
806
+ const rejected = resolved.payload.rejectedDevices ?? {};
807
+ const rows = [];
808
+ const seen = new Set();
809
+ for (const device of overview.activeDevices) {
810
+ const fp = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(device.encryptionPublicKey, this.tenant.getCryptoAdapter().getSubtle());
811
+ seen.add(fp);
812
+ const status = wrapped.has(fp) ? "approved" : rejected[fp] ? "declined" : "pending";
813
+ rows.push({
814
+ fingerprint: fp,
815
+ signingPublicKey: device.signingPublicKey,
816
+ encryptionPublicKey: device.encryptionPublicKey,
817
+ label: device.label,
818
+ addedAt: device.addedAt ?? fallbackAddedAt,
819
+ status,
820
+ });
821
+ }
822
+ for (const [fp, wrap] of Object.entries(Object.values(resolved.payload.userKeys ?? {}).reduce((acc, gen) => Object.assign(acc, gen.deviceWraps ?? {}), {}))) {
823
+ if (seen.has(fp))
824
+ continue;
825
+ rows.push({
826
+ fingerprint: fp,
827
+ label: wrap.label,
828
+ addedAt: wrap.approvedAt,
829
+ status: "approved",
830
+ });
831
+ }
832
+ for (const fp of Object.keys(rejected)) {
833
+ if (seen.has(fp))
834
+ continue;
835
+ rows.push({ fingerprint: fp, status: "declined" });
836
+ }
837
+ return rows;
838
+ }
839
+ /**
840
+ * Wrap every User-Key epoch to `encryptionPublicKey` (the joining device).
841
+ * Does not look up the grant overview — join approval just registered the
842
+ * device, and the directory cache can still miss it.
843
+ */
844
+ async wrapUserKeyForGrantDevice(encryptionPublicKey, label) {
845
+ const fingerprint = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(encryptionPublicKey, this.tenant.getCryptoAdapter().getSubtle());
846
+ const resolved = await this.resolveOwnUserKeyDocument();
847
+ console.log("[userkeys] wrapUserKeyForGrantDevice: start", {
848
+ joiningDeviceFingerprint: fingerprint,
849
+ hasUserKeyDocument: !!resolved,
850
+ docId: resolved?.doc.getId(),
851
+ epochs: resolved ? this.epochsNewestFirst(resolved.payload) : [],
852
+ });
853
+ if (!resolved) {
854
+ throw new Error("Cannot wrap User-Key for joining device: no user-key document");
855
+ }
856
+ const signer = await this.tenant.getCurrentUserId();
857
+ const db = resolved.doc.getDatabase();
858
+ let wroteWrap = false;
859
+ for (const epoch of this.epochsNewestFirst(resolved.payload).reverse()) {
860
+ const bytes = await this.privateKeyBytesForEpoch(resolved.payload, epoch);
861
+ console.log("[userkeys] wrapUserKeyForGrantDevice: epoch", {
862
+ epoch,
863
+ hasPrivateKeyBytes: !!bytes,
864
+ });
865
+ if (!bytes)
866
+ continue;
867
+ const wrapped = await (0, UserKeyDocument_1.wrapPrivateKeyForDevice)({
868
+ cryptoAdapter: this.tenant.getCryptoAdapter(),
869
+ privateKeyBytes: bytes,
870
+ deviceEncryptionPublicKey: encryptionPublicKey,
871
+ });
872
+ await (0, UserKeyDocument_1.writeDeviceWrap)({
873
+ db,
874
+ doc: resolved.doc,
875
+ epoch,
876
+ deviceFingerprint: fingerprint,
877
+ wrap: {
878
+ wrappedKey: wrapped,
879
+ ...(label ? { label } : {}),
880
+ approvedAt: Date.now(),
881
+ approvedBySigningPublicKey: signer.userSigningPublicKey,
882
+ },
883
+ });
884
+ wroteWrap = true;
885
+ }
886
+ if (!wroteWrap) {
887
+ throw new Error("Cannot wrap User-Key for joining device: local User-Key is locked");
888
+ }
889
+ console.log("[userkeys] wrapUserKeyForGrantDevice: wrote wrap", {
890
+ joiningDeviceFingerprint: fingerprint,
891
+ });
892
+ }
893
+ async approveUserKeyDevice(fingerprint) {
894
+ const resolved = await this.resolveOwnUserKeyDocument();
895
+ if (!resolved) {
896
+ throw new Error("Cannot approve a device: no user-key document");
897
+ }
898
+ const directory = await this.directory();
899
+ const username = (await this.tenant.getCurrentUserId()).username;
900
+ const overview = await directory.getUserGrantOverview(username);
901
+ const match = await this.findActiveDeviceByFingerprint(overview.activeDevices, fingerprint);
902
+ if (!match) {
903
+ throw new Error(`Cannot approve device "${fingerprint}": not an active grant device`);
904
+ }
905
+ const signer = await this.tenant.getCurrentUserId();
906
+ const db = resolved.doc.getDatabase();
907
+ let wroteWrap = false;
908
+ for (const epoch of this.epochsNewestFirst(resolved.payload).reverse()) {
909
+ const bytes = await this.privateKeyBytesForEpoch(resolved.payload, epoch);
910
+ if (!bytes)
911
+ continue;
912
+ const wrapped = await (0, UserKeyDocument_1.wrapPrivateKeyForDevice)({
913
+ cryptoAdapter: this.tenant.getCryptoAdapter(),
914
+ privateKeyBytes: bytes,
915
+ deviceEncryptionPublicKey: match.encryptionPublicKey,
916
+ });
917
+ await (0, UserKeyDocument_1.writeDeviceWrap)({
918
+ db,
919
+ doc: resolved.doc,
920
+ epoch,
921
+ deviceFingerprint: fingerprint,
922
+ wrap: {
923
+ wrappedKey: wrapped,
924
+ ...(match.label ? { label: match.label } : {}),
925
+ approvedAt: Date.now(),
926
+ approvedBySigningPublicKey: signer.userSigningPublicKey,
927
+ },
928
+ });
929
+ wroteWrap = true;
930
+ }
931
+ if (!wroteWrap) {
932
+ throw new Error("Cannot approve a device: local User-Key is locked");
933
+ }
934
+ if (resolved.payload.rejectedDevices?.[fingerprint]) {
935
+ const next = { ...resolved.payload.rejectedDevices };
936
+ delete next[fingerprint];
937
+ await (0, UserKeyDocument_1.replaceRejectedDevices)(db, resolved.doc, next);
938
+ }
939
+ }
940
+ async declineUserKeyDevice(fingerprint) {
941
+ const resolved = await this.resolveOwnUserKeyDocument();
942
+ if (!resolved) {
943
+ throw new Error("Cannot decline a device: no user-key document");
944
+ }
945
+ const signer = await this.tenant.getCurrentUserId();
946
+ await (0, UserKeyDocument_1.replaceRejectedDevices)(resolved.doc.getDatabase(), resolved.doc, {
947
+ ...(resolved.payload.rejectedDevices ?? {}),
948
+ [fingerprint]: {
949
+ rejectedAt: Date.now(),
950
+ rejectedBySigningPublicKey: signer.userSigningPublicKey,
951
+ },
952
+ });
953
+ }
954
+ async undoDeclineUserKeyDevice(fingerprint) {
955
+ const resolved = await this.resolveOwnUserKeyDocument();
956
+ if (!resolved)
957
+ return;
958
+ if (!resolved.payload.rejectedDevices?.[fingerprint])
959
+ return;
960
+ const next = { ...resolved.payload.rejectedDevices };
961
+ delete next[fingerprint];
962
+ await (0, UserKeyDocument_1.replaceRejectedDevices)(resolved.doc.getDatabase(), resolved.doc, next);
963
+ }
964
+ async rotateUserKey() {
965
+ const resolved = await this.resolveOwnUserKeyDocument();
966
+ if (!resolved) {
967
+ throw new Error("Cannot rotate: no user-key document");
968
+ }
969
+ const current = (0, types_2.currentUserKeyEpoch)(resolved.payload);
970
+ if (!current) {
971
+ throw new Error("Cannot rotate: user-key document has no generations");
972
+ }
973
+ const nextEpoch = (BigInt(current) + 1n).toString();
974
+ if (resolved.payload.userKeys[nextEpoch]) {
975
+ throw new Error(`Cannot rotate: epoch ${nextEpoch} already exists`);
976
+ }
977
+ const user = this.tenant.getCurrentPrivateUser();
978
+ const generated = await (0, rsaOaep3072_1.generateRsaOaep3072)(this.tenant.getCryptoAdapter());
979
+ const encrypted = await (0, privateKeyEncryption_1.encryptPrivateKey)(this.tenant.getCryptoAdapter(), generated.privateKeyBytes, this.tenant.getCurrentUserPassword(), types_2.USERKEY_PRIVATE_SALT);
980
+ user.userKeyPair = {
981
+ publicKey: generated.publicKeyPem,
982
+ privateKey: encrypted,
983
+ };
984
+ this.decryptedUserKeyBytes = generated.privateKeyBytes;
985
+ this.decryptedUserKeyCache = null;
986
+ const fingerprint = await (0, UserKeyDocument_1.fingerprintUserPublicKey)(generated.publicKeyPem, this.tenant.getCryptoAdapter());
987
+ const signer = await this.tenant.getCurrentUserId();
988
+ const now = Date.now();
989
+ const directory = await this.directory();
990
+ const overview = await directory.getUserGrantOverview(signer.username);
991
+ const previousWraps = resolved.payload.userKeys[current]?.deviceWraps ?? {};
992
+ const deviceWraps = {};
993
+ for (const device of overview.activeDevices) {
994
+ const deviceFp = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(device.encryptionPublicKey, this.tenant.getCryptoAdapter().getSubtle());
995
+ if (!previousWraps[deviceFp])
996
+ continue;
997
+ deviceWraps[deviceFp] = {
998
+ wrappedKey: await (0, UserKeyDocument_1.wrapPrivateKeyForDevice)({
999
+ cryptoAdapter: this.tenant.getCryptoAdapter(),
1000
+ privateKeyBytes: generated.privateKeyBytes,
1001
+ deviceEncryptionPublicKey: device.encryptionPublicKey,
1002
+ }),
1003
+ ...(device.label ? { label: device.label } : {}),
1004
+ approvedAt: now,
1005
+ approvedBySigningPublicKey: signer.userSigningPublicKey,
1006
+ };
1007
+ }
1008
+ await (0, UserKeyDocument_1.replaceUserKeys)(resolved.doc.getDatabase(), resolved.doc, {
1009
+ ...resolved.payload.userKeys,
1010
+ [current]: { ...resolved.payload.userKeys[current], retiredAt: now },
1011
+ [nextEpoch]: {
1012
+ publicKey: generated.publicKeyPem,
1013
+ fingerprint,
1014
+ createdAt: now,
1015
+ deviceWraps,
1016
+ },
1017
+ });
1018
+ }
1019
+ /**
1020
+ * Admin-only delete of the published user-key document for `username`.
1021
+ * Lookup uses the same grant-binding path as {@link publishedUserKeyFor}.
1022
+ * Recovery path when every device that held the private key is gone.
1023
+ * A remaining enrolled device keeps local access; it does not republish
1024
+ * the tombstone on unlock while `default` is already wrapped to that user.
1025
+ */
1026
+ async deletePublishedUserKeyFor(input) {
1027
+ const resolved = await this.resolvePublishedUserKeyDocument(input.username);
1028
+ if (!resolved) {
1029
+ throw new Error(`No published personal key document for "${input.username}".`);
1030
+ }
1031
+ const db = await this.openUserDirectory();
1032
+ await db.deleteDocument(resolved.doc.getId(), {
1033
+ signingKeyPair: input.signingKeyPair,
1034
+ signingKeyPassword: input.signingKeyPassword,
1035
+ });
1036
+ }
1037
+ async publishedUserKeyFor(username) {
1038
+ const resolved = await this.resolvePublishedUserKeyDocument(username);
1039
+ if (!resolved)
1040
+ return null;
1041
+ const epoch = (0, types_2.currentUserKeyEpoch)(resolved.payload);
1042
+ if (!epoch)
1043
+ return null;
1044
+ const gen = resolved.payload.userKeys[epoch];
1045
+ return {
1046
+ publicKey: gen.publicKey,
1047
+ fingerprint: gen.fingerprint,
1048
+ pending: (0, types_2.isPendingUserKeyDocument)(resolved.payload),
1049
+ };
1050
+ }
1051
+ /**
1052
+ * Lookup only — Trap 1 (no mint from local emptiness) does not apply.
1053
+ * Haven reopens a fresh tenant per action; blocking on
1054
+ * `userDirectoryFetched` made wrapKeyForUser return null so
1055
+ * `acl_keydistribution_default` never gained a User-Key wrap.
1056
+ */
1057
+ async resolvePublishedUserKeyDocument(username) {
1058
+ const directory = await this.directory();
1059
+ const db = await this.openUserDirectory();
1060
+ const hashes = await directory.getUsernameHashCandidates(username);
1061
+ const grants = await directory.findGrantAccessDocuments(username);
1062
+ return (0, resolveUserKeyDocument_1.resolveUserKeyDocument)({
1063
+ db,
1064
+ directory,
1065
+ username,
1066
+ usernameHashCandidates: hashes,
1067
+ grantDocIds: grants.map((g) => g.getId()),
1068
+ adminPublicKey: this.tenant.getAdministrationPublicKey(),
1069
+ });
1070
+ }
1071
+ async getEnrollmentStatus() {
1072
+ if (this.lastUserDirectoryFetchError && !this.userDirectoryFetched) {
1073
+ return this.enrollmentStatus(null, "unknown");
1074
+ }
1075
+ const resolved = await this.resolveOwnUserKeyDocument();
1076
+ if (!resolved) {
1077
+ return this.enrollmentStatus(null, this.userDirectoryFetched ? "waiting" : "unknown");
1078
+ }
1079
+ return this.enrollmentStatus(resolved.payload, this.waitState());
1080
+ }
1081
+ waitState() {
1082
+ if (!this.userDirectoryFetched && this.lastUserDirectoryFetchError)
1083
+ return "unknown";
1084
+ return this.userDirectoryFetched ? "waiting" : "unknown";
1085
+ }
1086
+ async waitStateForDevice(payload) {
1087
+ const user = this.tenant.getCurrentPrivateUser();
1088
+ const fp = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(user.userEncryptionKeyPair.publicKey, this.tenant.getCryptoAdapter().getSubtle());
1089
+ // A wrap on this device is enough to be approved. Do not wait for a
1090
+ // userdirectory fetch flag: reload / local-only mint already has the wrap
1091
+ // in the local replica, and treating that as "unknown" keeps the waiting
1092
+ // banner up after the approve dialog is gone.
1093
+ if (this.wrappedDeviceFingerprints(payload).has(fp))
1094
+ return "approved";
1095
+ if (!this.userDirectoryFetched && this.lastUserDirectoryFetchError)
1096
+ return "unknown";
1097
+ if (!this.userDirectoryFetched)
1098
+ return "unknown";
1099
+ return "waiting";
1100
+ }
1101
+ async enrollmentStatus(payload, fallback) {
1102
+ if (!payload) {
1103
+ return { state: fallback, pending: true, missingKeys: ["default"] };
1104
+ }
1105
+ const state = await this.waitStateForDevice(payload);
1106
+ const pending = (0, types_2.isPendingUserKeyDocument)(payload);
1107
+ const missingKeys = state === "approved" ? [] : ["default"];
1108
+ return { state, pending, missingKeys };
1109
+ }
1110
+ epochsNewestFirst(payload) {
1111
+ return Object.keys(payload.userKeys ?? {})
1112
+ .filter((epoch) => /^\d+$/.test(epoch))
1113
+ .sort((a, b) => (BigInt(a) > BigInt(b) ? -1 : BigInt(a) < BigInt(b) ? 1 : 0));
1114
+ }
1115
+ async privateKeyBytesForEpoch(payload, epoch) {
1116
+ const gen = payload.userKeys[epoch];
1117
+ if (!gen)
1118
+ return null;
1119
+ const localPublic = await this.getLocalUserPublicKey();
1120
+ const publicKeyMatchesLocal = !!localPublic && gen.publicKey === localPublic;
1121
+ if (publicKeyMatchesLocal) {
1122
+ const localBytes = await this.getDecryptedUserKeyBytes();
1123
+ if (localBytes)
1124
+ return localBytes;
1125
+ // Haven restoreTenantWithLiveBag opens with an empty password, so the
1126
+ // identity's password-encrypted User-Key may not unwrap even when the
1127
+ // public key matches. Fall through to the device wrap.
1128
+ }
1129
+ const deviceKey = await this.tenant.getEncryptionPrivateKeyForReconcile();
1130
+ const user = this.tenant.getCurrentPrivateUser();
1131
+ const deviceFp = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(user.userEncryptionKeyPair.publicKey, this.tenant.getCryptoAdapter().getSubtle());
1132
+ const wrap = gen.deviceWraps?.[deviceFp];
1133
+ console.log("[userkeys] privateKeyBytesForEpoch", {
1134
+ epoch,
1135
+ publicKeyMatchesLocal,
1136
+ seededUserKeyBytes: !!this.decryptedUserKeyBytes,
1137
+ ownDeviceFingerprint: deviceFp,
1138
+ wrappedDeviceFingerprints: Object.keys(gen.deviceWraps ?? {}),
1139
+ hasOwnDeviceWrap: !!wrap?.wrappedKey,
1140
+ hasDeviceEncryptionKey: !!deviceKey,
1141
+ });
1142
+ if (!deviceKey)
1143
+ return null;
1144
+ if (!wrap?.wrappedKey)
1145
+ return null;
1146
+ try {
1147
+ return (0, UserKeyDocument_1.unwrapPrivateKeyFromDevice)({
1148
+ cryptoAdapter: this.tenant.getCryptoAdapter(),
1149
+ wrappedKeyB64: wrap.wrappedKey,
1150
+ deviceEncryptionPrivateKey: deviceKey,
1151
+ });
1152
+ }
1153
+ catch (error) {
1154
+ console.warn("[userkeys] privateKeyBytesForEpoch: device wrap unwrap failed", epoch, error);
1155
+ return null;
1156
+ }
1157
+ }
1158
+ async grantCreatedAtFallback(username) {
1159
+ const directory = await this.directory();
1160
+ const grants = await directory.findGrantAccessDocuments(username);
1161
+ let earliest;
1162
+ for (const grant of grants) {
1163
+ const created = grant.getCreatedAt();
1164
+ if (typeof created !== "number")
1165
+ continue;
1166
+ earliest = earliest == null ? created : Math.min(earliest, created);
1167
+ }
1168
+ return earliest;
1169
+ }
1170
+ wrappedDeviceFingerprints(payload) {
1171
+ const fps = new Set();
1172
+ for (const gen of Object.values(payload.userKeys ?? {})) {
1173
+ for (const fp of Object.keys(gen.deviceWraps ?? {}))
1174
+ fps.add(fp);
1175
+ }
1176
+ return fps;
1177
+ }
1178
+ async dropRevokedDeviceWraps(doc, payload) {
1179
+ const directory = await this.directory();
1180
+ const username = (await this.tenant.getCurrentUserId()).username;
1181
+ const grants = await directory.findGrantAccessDocuments(username);
1182
+ const revokedFps = new Set();
1183
+ const crypto = this.tenant.getCryptoAdapter();
1184
+ for (const grant of grants) {
1185
+ for (const pair of (0, grantKeys_1.extractRevokedKeyPairs)(grant.getData())) {
1186
+ if (!pair.encryptionPublicKey)
1187
+ continue;
1188
+ revokedFps.add(await (0, fingerprint_1.fingerprintEncryptionPublicKey)(pair.encryptionPublicKey, crypto.getSubtle()));
1189
+ }
1190
+ }
1191
+ if (revokedFps.size === 0)
1192
+ return;
1193
+ let changed = false;
1194
+ const userKeys = {};
1195
+ for (const [epoch, gen] of Object.entries(payload.userKeys ?? {})) {
1196
+ const deviceWraps = { ...(gen.deviceWraps ?? {}) };
1197
+ for (const fp of Object.keys(deviceWraps)) {
1198
+ if (revokedFps.has(fp)) {
1199
+ delete deviceWraps[fp];
1200
+ changed = true;
1201
+ }
1202
+ }
1203
+ userKeys[epoch] = { ...gen, deviceWraps };
1204
+ }
1205
+ if (!changed)
1206
+ return;
1207
+ await (0, UserKeyDocument_1.replaceUserKeys)(doc.getDatabase(), doc, userKeys);
1208
+ }
1209
+ async findActiveDeviceByFingerprint(devices, fingerprint) {
1210
+ const crypto = this.tenant.getCryptoAdapter();
1211
+ for (const device of devices) {
1212
+ const fp = await (0, fingerprint_1.fingerprintEncryptionPublicKey)(device.encryptionPublicKey, crypto.getSubtle());
1213
+ if (fp === fingerprint)
1214
+ return device;
1215
+ }
1216
+ return null;
1217
+ }
1218
+ /**
1219
+ * Bind a userkey document to the grant that names this person.
1220
+ * Writes the grant's `username_hash` (not the current write-version hash) so
1221
+ * the userdirectory invariant matches `resolveUsernameHashForSigningKey`.
1222
+ * When `signingPublicKey` is set (self-mint), that key must be on the grant.
1223
+ */
1224
+ async mintBindingForUsername(username, signingPublicKey) {
1225
+ const directory = await this.directory();
1226
+ const grants = await directory.findGrantAccessDocuments(username);
1227
+ if (grants.length === 0)
1228
+ return null;
1229
+ let signerHash = null;
1230
+ if (signingPublicKey && typeof directory.resolveUsernameHashForSigningKey === "function") {
1231
+ signerHash = await directory.resolveUsernameHashForSigningKey(signingPublicKey, Number.MAX_SAFE_INTEGER);
1232
+ if (!signerHash)
1233
+ return null;
1234
+ }
1235
+ const grant = (signerHash
1236
+ ? grants.find((g) => g.getData().username_hash === signerHash)
1237
+ : undefined) ?? grants[0];
1238
+ const binding = (0, UserKeyDocument_1.usernameHashBindingFromGrant)(grant.getData());
1239
+ const usernameHash = signerHash ?? binding?.usernameHash;
1240
+ if (!usernameHash)
1241
+ return null;
1242
+ return {
1243
+ grantDocId: grant.getId(),
1244
+ usernameHash,
1245
+ usernameHashVersion: binding?.usernameHashVersion ?? types_2.USERKEY_USERNAME_HASH_VERSION,
1246
+ };
1247
+ }
1248
+ async labelForOwnEncryptionKey() {
1249
+ try {
1250
+ const directory = await this.directory();
1251
+ const signer = await this.tenant.getCurrentUserId();
1252
+ const user = this.tenant.getCurrentPrivateUser();
1253
+ const overview = await directory.getUserGrantOverview(signer.username);
1254
+ return overview.activeDevices.find((device) => device.encryptionPublicKey === user.userEncryptionKeyPair.publicKey)?.label;
1255
+ }
1256
+ catch {
1257
+ return undefined;
1258
+ }
1259
+ }
1260
+ async defaultAlreadyWrappedForCurrentUser() {
1261
+ try {
1262
+ const directory = await this.directory();
1263
+ const username = (await this.tenant.getCurrentUserId()).username;
1264
+ const managed = await directory.getManagedKeyIds(username);
1265
+ return managed.includes(types_1.DEFAULT_TENANT_KEY_ID);
1266
+ }
1267
+ catch {
1268
+ return false;
1269
+ }
1270
+ }
1271
+ async directory() {
1272
+ const directory = await this.tenant.openDirectory();
1273
+ return directory;
1274
+ }
1275
+ }
1276
+ exports.UserKeyManager = UserKeyManager;
1277
+ //# sourceMappingURL=UserKeyManager.js.map