@bsv/wallet-toolbox-mobile 2.4.2 → 2.4.4

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 (115) hide show
  1. package/out/src/CWIStyleWalletManager.d.ts +52 -5
  2. package/out/src/CWIStyleWalletManager.d.ts.map +1 -1
  3. package/out/src/CWIStyleWalletManager.js +500 -128
  4. package/out/src/CWIStyleWalletManager.js.map +1 -1
  5. package/out/src/Wallet.d.ts +8 -0
  6. package/out/src/Wallet.d.ts.map +1 -1
  7. package/out/src/Wallet.js +23 -9
  8. package/out/src/Wallet.js.map +1 -1
  9. package/out/src/WalletAuthenticationManager.d.ts +20 -6
  10. package/out/src/WalletAuthenticationManager.d.ts.map +1 -1
  11. package/out/src/WalletAuthenticationManager.js +214 -34
  12. package/out/src/WalletAuthenticationManager.js.map +1 -1
  13. package/out/src/WalletPermissionsManager.d.ts +15 -0
  14. package/out/src/WalletPermissionsManager.d.ts.map +1 -1
  15. package/out/src/WalletPermissionsManager.js +76 -28
  16. package/out/src/WalletPermissionsManager.js.map +1 -1
  17. package/out/src/index.mobile.d.ts +1 -0
  18. package/out/src/index.mobile.d.ts.map +1 -1
  19. package/out/src/index.mobile.js +1 -0
  20. package/out/src/index.mobile.js.map +1 -1
  21. package/out/src/monitor/Monitor.d.ts.map +1 -1
  22. package/out/src/monitor/Monitor.js +4 -3
  23. package/out/src/monitor/Monitor.js.map +1 -1
  24. package/out/src/monitor/tasks/TaskCleanupActionBatches.d.ts +13 -0
  25. package/out/src/monitor/tasks/TaskCleanupActionBatches.d.ts.map +1 -0
  26. package/out/src/monitor/tasks/TaskCleanupActionBatches.js +24 -0
  27. package/out/src/monitor/tasks/TaskCleanupActionBatches.js.map +1 -0
  28. package/out/src/sdk/ActionBatch.interfaces.d.ts +105 -0
  29. package/out/src/sdk/ActionBatch.interfaces.d.ts.map +1 -0
  30. package/out/src/sdk/ActionBatch.interfaces.js +3 -0
  31. package/out/src/sdk/ActionBatch.interfaces.js.map +1 -0
  32. package/out/src/sdk/WalletStorage.interfaces.d.ts +18 -1
  33. package/out/src/sdk/WalletStorage.interfaces.d.ts.map +1 -1
  34. package/out/src/sdk/index.d.ts +1 -0
  35. package/out/src/sdk/index.d.ts.map +1 -1
  36. package/out/src/sdk/index.js +1 -0
  37. package/out/src/sdk/index.js.map +1 -1
  38. package/out/src/signer/actionBatch/ActionBatchPlanner.d.ts +28 -0
  39. package/out/src/signer/actionBatch/ActionBatchPlanner.d.ts.map +1 -0
  40. package/out/src/signer/actionBatch/ActionBatchPlanner.js +313 -0
  41. package/out/src/signer/actionBatch/ActionBatchPlanner.js.map +1 -0
  42. package/out/src/signer/actionBatch/ActionBatchWorkspace.d.ts +33 -0
  43. package/out/src/signer/actionBatch/ActionBatchWorkspace.d.ts.map +1 -0
  44. package/out/src/signer/actionBatch/ActionBatchWorkspace.js +693 -0
  45. package/out/src/signer/actionBatch/ActionBatchWorkspace.js.map +1 -0
  46. package/out/src/signer/methods/buildSignableTransaction.d.ts.map +1 -1
  47. package/out/src/signer/methods/buildSignableTransaction.js +2 -1
  48. package/out/src/signer/methods/buildSignableTransaction.js.map +1 -1
  49. package/out/src/signer/methods/createAction.d.ts.map +1 -1
  50. package/out/src/signer/methods/createAction.js +4 -1
  51. package/out/src/signer/methods/createAction.js.map +1 -1
  52. package/out/src/storage/StorageProvider.d.ts +28 -0
  53. package/out/src/storage/StorageProvider.d.ts.map +1 -1
  54. package/out/src/storage/StorageProvider.js +49 -0
  55. package/out/src/storage/StorageProvider.js.map +1 -1
  56. package/out/src/storage/WalletStorageManager.d.ts +8 -0
  57. package/out/src/storage/WalletStorageManager.d.ts.map +1 -1
  58. package/out/src/storage/WalletStorageManager.js +24 -0
  59. package/out/src/storage/WalletStorageManager.js.map +1 -1
  60. package/out/src/storage/methods/actionBatch.d.ts +14 -0
  61. package/out/src/storage/methods/actionBatch.d.ts.map +1 -0
  62. package/out/src/storage/methods/actionBatch.js +644 -0
  63. package/out/src/storage/methods/actionBatch.js.map +1 -0
  64. package/out/src/storage/methods/actionBatchBlobs.d.ts +12 -0
  65. package/out/src/storage/methods/actionBatchBlobs.d.ts.map +1 -0
  66. package/out/src/storage/methods/actionBatchBlobs.js +121 -0
  67. package/out/src/storage/methods/actionBatchBlobs.js.map +1 -0
  68. package/out/src/storage/methods/actionBatchValidation.d.ts +15 -0
  69. package/out/src/storage/methods/actionBatchValidation.d.ts.map +1 -0
  70. package/out/src/storage/methods/actionBatchValidation.js +310 -0
  71. package/out/src/storage/methods/actionBatchValidation.js.map +1 -0
  72. package/out/src/storage/methods/actionPlanning.d.ts +12 -0
  73. package/out/src/storage/methods/actionPlanning.d.ts.map +1 -0
  74. package/out/src/storage/methods/actionPlanning.js +48 -0
  75. package/out/src/storage/methods/actionPlanning.js.map +1 -0
  76. package/out/src/storage/methods/createAction.d.ts.map +1 -1
  77. package/out/src/storage/methods/createAction.js +9 -25
  78. package/out/src/storage/methods/createAction.js.map +1 -1
  79. package/out/src/storage/remoting/StorageClientBase.d.ts +9 -0
  80. package/out/src/storage/remoting/StorageClientBase.d.ts.map +1 -1
  81. package/out/src/storage/remoting/StorageClientBase.js +36 -1
  82. package/out/src/storage/remoting/StorageClientBase.js.map +1 -1
  83. package/out/src/storage/schema/tables/TableActionBatch.d.ts +24 -0
  84. package/out/src/storage/schema/tables/TableActionBatch.d.ts.map +1 -0
  85. package/out/src/storage/schema/tables/TableActionBatch.js +3 -0
  86. package/out/src/storage/schema/tables/TableActionBatch.js.map +1 -0
  87. package/out/src/storage/schema/tables/index.d.ts +1 -0
  88. package/out/src/storage/schema/tables/index.d.ts.map +1 -1
  89. package/out/src/storage/schema/tables/index.js +1 -0
  90. package/out/src/storage/schema/tables/index.js.map +1 -1
  91. package/out/src/utility/actionBatchDigest.d.ts +9 -0
  92. package/out/src/utility/actionBatchDigest.d.ts.map +1 -0
  93. package/out/src/utility/actionBatchDigest.js +48 -0
  94. package/out/src/utility/actionBatchDigest.js.map +1 -0
  95. package/out/src/utility/beefForTxids.d.ts +9 -0
  96. package/out/src/utility/beefForTxids.d.ts.map +1 -0
  97. package/out/src/utility/beefForTxids.js +35 -0
  98. package/out/src/utility/beefForTxids.js.map +1 -0
  99. package/out/src/wab-client/WABClient.d.ts +36 -90
  100. package/out/src/wab-client/WABClient.d.ts.map +1 -1
  101. package/out/src/wab-client/WABClient.js +103 -151
  102. package/out/src/wab-client/WABClient.js.map +1 -1
  103. package/out/src/wab-client/WABTransport.d.ts +53 -0
  104. package/out/src/wab-client/WABTransport.d.ts.map +1 -0
  105. package/out/src/wab-client/WABTransport.js +300 -0
  106. package/out/src/wab-client/WABTransport.js.map +1 -0
  107. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts +10 -10
  108. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts.map +1 -1
  109. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js +16 -21
  110. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js.map +1 -1
  111. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts +2 -1
  112. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts.map +1 -1
  113. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js +14 -0
  114. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js.map +1 -1
  115. package/package.json +8 -3
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CWIStyleWalletManager = exports.OverlayUMPTokenInteractor = exports.DEFAULT_PROFILE_ID = exports.ARGON2ID_DEFAULT_HASH_LENGTH = exports.ARGON2ID_DEFAULT_PARALLELISM = exports.ARGON2ID_DEFAULT_MEMORY_KIB = exports.ARGON2ID_DEFAULT_ITERATIONS = exports.PBKDF2_NUM_ROUNDS = void 0;
3
+ exports.CWIStyleWalletManager = exports.OverlayUMPTokenInteractor = exports.UMPTokenLookupError = exports.DEFAULT_PROFILE_ID = exports.MAX_STATE_SNAPSHOT_BYTES = exports.PBKDF2_MAX_ITERATIONS = exports.KDF_MAX_HASH_LENGTH = exports.ARGON2ID_MAX_PARALLELISM = exports.ARGON2ID_MAX_MEMORY_KIB = exports.ARGON2ID_MAX_ITERATIONS = exports.ARGON2ID_DEFAULT_HASH_LENGTH = exports.ARGON2ID_DEFAULT_PARALLELISM = exports.ARGON2ID_DEFAULT_MEMORY_KIB = exports.ARGON2ID_DEFAULT_ITERATIONS = exports.PBKDF2_NUM_ROUNDS = void 0;
4
4
  const sdk_1 = require("@bsv/sdk");
5
5
  const hash_wasm_1 = require("hash-wasm");
6
6
  const PrivilegedKeyManager_1 = require("./sdk/PrivilegedKeyManager");
@@ -15,6 +15,30 @@ exports.ARGON2ID_DEFAULT_ITERATIONS = 7;
15
15
  exports.ARGON2ID_DEFAULT_MEMORY_KIB = 131072;
16
16
  exports.ARGON2ID_DEFAULT_PARALLELISM = 1;
17
17
  exports.ARGON2ID_DEFAULT_HASH_LENGTH = 32;
18
+ exports.ARGON2ID_MAX_ITERATIONS = 20;
19
+ exports.ARGON2ID_MAX_MEMORY_KIB = 262144;
20
+ exports.ARGON2ID_MAX_PARALLELISM = 16;
21
+ exports.KDF_MAX_HASH_LENGTH = 64;
22
+ exports.PBKDF2_MAX_ITERATIONS = 10_000_000;
23
+ exports.MAX_STATE_SNAPSHOT_BYTES = 16 * 1024 * 1024;
24
+ function isPositiveIntegerInRange(value, maximum) {
25
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 && value <= maximum;
26
+ }
27
+ function isValidKdfConfig(kdf) {
28
+ if (kdf == null)
29
+ return false;
30
+ if (!isPositiveIntegerInRange(kdf.iterations, kdf.algorithm === 'argon2id' ? exports.ARGON2ID_MAX_ITERATIONS : exports.PBKDF2_MAX_ITERATIONS)) {
31
+ return false;
32
+ }
33
+ if (kdf.algorithm === 'pbkdf2-sha512') {
34
+ return kdf.hashLength === undefined || isPositiveIntegerInRange(kdf.hashLength, exports.KDF_MAX_HASH_LENGTH);
35
+ }
36
+ if (kdf.algorithm !== 'argon2id')
37
+ return false;
38
+ return isPositiveIntegerInRange(kdf.memoryKiB, exports.ARGON2ID_MAX_MEMORY_KIB) &&
39
+ isPositiveIntegerInRange(kdf.parallelism, exports.ARGON2ID_MAX_PARALLELISM) &&
40
+ isPositiveIntegerInRange(kdf.hashLength, exports.KDF_MAX_HASH_LENGTH);
41
+ }
18
42
  function isLikelyDerSignatureField(field) {
19
43
  if (!field || field.length < 8 || field.length > 80) {
20
44
  return false;
@@ -82,10 +106,9 @@ async function pbkdf2NativeOrWasm(passwordBytes, salt, iterations, keyLen, hash
82
106
  }, baseKey, keyLen * 8);
83
107
  return Array.from(new Uint8Array(bits));
84
108
  }
85
- catch (webCryptoErr) {
109
+ catch {
86
110
  // WebCrypto is unavailable or refused the algorithm (e.g. non-secure context, old
87
111
  // Safari). Fall through to the pure-JS hash-wasm implementation below.
88
- console.debug('[pbkdf2] WebCrypto path failed, falling back to JS implementation', webCryptoErr);
89
112
  }
90
113
  }
91
114
  const hashFunction = hash === 'sha256' ? (0, hash_wasm_1.createSHA256)() : (0, hash_wasm_1.createSHA512)();
@@ -111,6 +134,9 @@ async function pbkdf2NativeOrWasm(passwordBytes, salt, iterations, keyLen, hash
111
134
  */
112
135
  async function derivePasswordKey(token, passwordBytes, overrideKdf) {
113
136
  const kdf = overrideKdf || token.passwordKdf;
137
+ if (kdf != null && !isValidKdfConfig(kdf)) {
138
+ throw new Error('UMP token contains unsupported or unsafe KDF parameters.');
139
+ }
114
140
  // Legacy token or explicit PBKDF2 request
115
141
  if ((kdf == null) || kdf.algorithm === 'pbkdf2-sha512') {
116
142
  return await pbkdf2NativeOrWasm(passwordBytes, token.passwordSalt, kdf?.iterations ?? exports.PBKDF2_NUM_ROUNDS, 32, 'sha512');
@@ -138,6 +164,43 @@ async function derivePasswordKey(token, passwordBytes, overrideKdf) {
138
164
  * Unique Identifier for the default profile (16 zero bytes).
139
165
  */
140
166
  exports.DEFAULT_PROFILE_ID = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
167
+ function isByteArrayOfLength(value, length) {
168
+ return Array.isArray(value) &&
169
+ value.length === length &&
170
+ value.every(byte => Number.isInteger(byte) && byte >= 0 && byte <= 255);
171
+ }
172
+ function isValidProfile(value) {
173
+ if (value == null || typeof value !== 'object')
174
+ return false;
175
+ const profile = value;
176
+ return typeof profile.name === 'string' &&
177
+ profile.name.length > 0 &&
178
+ profile.name.length <= 250 &&
179
+ isByteArrayOfLength(profile.id, 16) &&
180
+ isByteArrayOfLength(profile.primaryPad, 32) &&
181
+ isByteArrayOfLength(profile.privilegedPad, 32) &&
182
+ typeof profile.createdAt === 'number' &&
183
+ Number.isFinite(profile.createdAt) &&
184
+ profile.createdAt >= 0;
185
+ }
186
+ /**
187
+ * Raised when UMP absence cannot be established authoritatively.
188
+ *
189
+ * Callers must offer retry/recovery rather than treating this error as a new
190
+ * account. Diagnostics contain counts only and never hashes, keys, or tokens.
191
+ */
192
+ class UMPTokenLookupError extends Error {
193
+ reason;
194
+ diagnostics;
195
+ code = 'WERR_UMP_LOOKUP_INDETERMINATE';
196
+ constructor(reason, diagnostics, options) {
197
+ super('Unable to determine whether the wallet account already exists. Retry or use account recovery.', options);
198
+ this.reason = reason;
199
+ this.diagnostics = diagnostics;
200
+ this.name = 'UMPTokenLookupError';
201
+ }
202
+ }
203
+ exports.UMPTokenLookupError = UMPTokenLookupError;
141
204
  /**
142
205
  * @class OverlayUMPTokenInteractor
143
206
  *
@@ -160,15 +223,17 @@ class OverlayUMPTokenInteractor {
160
223
  * under the `tm_users` topic to overlay service peers.
161
224
  */
162
225
  broadcaster;
226
+ telemetry;
163
227
  /**
164
228
  * Construct a new OverlayUMPTokenInteractor.
165
229
  *
166
230
  * @param resolver A LookupResolver instance for performing overlay queries (ls_users).
167
231
  * @param broadcaster A SHIPBroadcaster instance for sharing new or updated tokens across the `tm_users` overlay.
168
232
  */
169
- constructor(resolver = new sdk_1.LookupResolver(), broadcaster = new sdk_1.SHIPBroadcaster(['tm_users'])) {
233
+ constructor(resolver = new sdk_1.LookupResolver(), broadcaster = new sdk_1.SHIPBroadcaster(['tm_users']), telemetry) {
170
234
  this.resolver = resolver;
171
235
  this.broadcaster = broadcaster;
236
+ this.telemetry = new sdk_1.Telemetry(telemetry);
172
237
  }
173
238
  /**
174
239
  * Finds a UMP token on-chain by the given presentation key hash, if it exists.
@@ -178,13 +243,10 @@ class OverlayUMPTokenInteractor {
178
243
  * @returns A UMPToken object (including currentOutpoint) if found, otherwise undefined.
179
244
  */
180
245
  async findByPresentationKeyHash(hash) {
181
- // Query ls_users for the given presentationHash
182
- const question = {
246
+ return await this.findToken({
183
247
  service: 'ls_users',
184
248
  query: { presentationHash: sdk_1.Utils.toHex(hash) }
185
- };
186
- const answer = await this.resolver.query(question);
187
- return this.parseLookupAnswer(answer);
249
+ }, 'presentation');
188
250
  }
189
251
  /**
190
252
  * Finds a UMP token on-chain by the given recovery key hash, if it exists.
@@ -194,12 +256,141 @@ class OverlayUMPTokenInteractor {
194
256
  * @returns A UMPToken object (including currentOutpoint) if found, otherwise undefined.
195
257
  */
196
258
  async findByRecoveryKeyHash(hash) {
197
- const question = {
259
+ return await this.findToken({
198
260
  service: 'ls_users',
199
261
  query: { recoveryHash: sdk_1.Utils.toHex(hash) }
262
+ }, 'recovery');
263
+ }
264
+ async findToken(question, lookupKind) {
265
+ const correlationId = this.telemetry.enabled === true ? this.telemetry.createCorrelationId() : undefined;
266
+ const startedAt = Date.now();
267
+ this.telemetry.capture({
268
+ name: 'wallet-toolbox.ump.lookup.started',
269
+ component: 'wallet-toolbox.ump',
270
+ severity: 'debug',
271
+ correlationId,
272
+ attributes: { lookupKind }
273
+ });
274
+ let resolution;
275
+ try {
276
+ resolution = await this.resolver.queryDetailed(question, undefined, {
277
+ waitForAllHosts: true,
278
+ correlationId
279
+ });
280
+ }
281
+ catch (error) {
282
+ const diagnostics = this.emptyLookupDiagnostics(correlationId);
283
+ this.captureLookupFailure(lookupKind, 'lookup-unavailable', diagnostics, startedAt, error);
284
+ throw new UMPTokenLookupError('lookup-unavailable', diagnostics, { cause: error });
285
+ }
286
+ const diagnostics = this.toLookupDiagnostics(resolution);
287
+ if (resolution.answer.outputs.length === 0) {
288
+ const authoritative = resolution.progress.isFinal &&
289
+ resolution.progress.hostCount > 0 &&
290
+ resolution.progress.completedHosts === resolution.progress.hostCount &&
291
+ resolution.progress.successfulHosts === resolution.progress.hostCount &&
292
+ resolution.progress.emptyHosts === resolution.progress.hostCount &&
293
+ resolution.progress.failedHosts === 0 &&
294
+ resolution.progress.rejectedHosts === 0 &&
295
+ resolution.progress.freeformHosts === 0;
296
+ if (!authoritative) {
297
+ this.captureLookupFailure(lookupKind, 'lookup-incomplete', diagnostics, startedAt);
298
+ throw new UMPTokenLookupError('lookup-incomplete', diagnostics);
299
+ }
300
+ this.telemetry.capture({
301
+ name: 'wallet-toolbox.ump.lookup.completed',
302
+ component: 'wallet-toolbox.ump',
303
+ severity: 'info',
304
+ correlationId: diagnostics.correlationId,
305
+ attributes: {
306
+ lookupKind,
307
+ result: 'not-found',
308
+ durationMs: Date.now() - startedAt,
309
+ ...this.lookupDiagnosticAttributes(diagnostics)
310
+ }
311
+ });
312
+ return undefined;
313
+ }
314
+ const tokens = this.parseLookupAnswers(resolution.answer);
315
+ const expectedHash = question.query[lookupKind === 'presentation' ? 'presentationHash' : 'recoveryHash'].toLowerCase();
316
+ const everyOutputValidAndMatching = tokens.length === resolution.answer.outputs.length &&
317
+ tokens.every(token => sdk_1.Utils.toHex(lookupKind === 'presentation' ? token.presentationHash : token.recoveryHash).toLowerCase() === expectedHash);
318
+ if (!everyOutputValidAndMatching || tokens.length === 0) {
319
+ this.captureLookupFailure(lookupKind, 'token-malformed', diagnostics, startedAt);
320
+ throw new UMPTokenLookupError('token-malformed', diagnostics);
321
+ }
322
+ if (tokens.length !== 1) {
323
+ const reason = 'token-ambiguous';
324
+ this.captureLookupFailure(lookupKind, reason, diagnostics, startedAt);
325
+ throw new UMPTokenLookupError(reason, diagnostics);
326
+ }
327
+ this.telemetry.capture({
328
+ name: 'wallet-toolbox.ump.lookup.completed',
329
+ component: 'wallet-toolbox.ump',
330
+ severity: 'info',
331
+ correlationId: diagnostics.correlationId,
332
+ attributes: {
333
+ lookupKind,
334
+ result: 'found',
335
+ durationMs: Date.now() - startedAt,
336
+ ...this.lookupDiagnosticAttributes(diagnostics)
337
+ }
338
+ });
339
+ return tokens[0];
340
+ }
341
+ emptyLookupDiagnostics(correlationId) {
342
+ return {
343
+ hostCount: 0,
344
+ completedHosts: 0,
345
+ successfulHosts: 0,
346
+ emptyHosts: 0,
347
+ failedHosts: 0,
348
+ rejectedHosts: 0,
349
+ freeformHosts: 0,
350
+ outputCount: 0,
351
+ ...(correlationId !== undefined ? { correlationId } : {})
352
+ };
353
+ }
354
+ toLookupDiagnostics(resolution) {
355
+ const progress = resolution.progress;
356
+ return {
357
+ hostCount: progress.hostCount,
358
+ completedHosts: progress.completedHosts,
359
+ successfulHosts: progress.successfulHosts,
360
+ emptyHosts: progress.emptyHosts,
361
+ failedHosts: progress.failedHosts,
362
+ rejectedHosts: progress.rejectedHosts,
363
+ freeformHosts: progress.freeformHosts,
364
+ outputCount: resolution.answer.outputs.length,
365
+ ...(progress.correlationId !== undefined ? { correlationId: progress.correlationId } : {})
366
+ };
367
+ }
368
+ lookupDiagnosticAttributes(diagnostics) {
369
+ return {
370
+ hostCount: diagnostics.hostCount,
371
+ completedHosts: diagnostics.completedHosts,
372
+ successfulHosts: diagnostics.successfulHosts,
373
+ emptyHosts: diagnostics.emptyHosts,
374
+ failedHosts: diagnostics.failedHosts,
375
+ rejectedHosts: diagnostics.rejectedHosts,
376
+ freeformHosts: diagnostics.freeformHosts,
377
+ outputCount: diagnostics.outputCount
200
378
  };
201
- const answer = await this.resolver.query(question);
202
- return this.parseLookupAnswer(answer);
379
+ }
380
+ captureLookupFailure(lookupKind, reason, diagnostics, startedAt, error) {
381
+ this.telemetry.capture({
382
+ name: 'wallet-toolbox.ump.lookup.indeterminate',
383
+ component: 'wallet-toolbox.ump',
384
+ severity: 'warn',
385
+ correlationId: diagnostics.correlationId,
386
+ attributes: {
387
+ lookupKind,
388
+ reason,
389
+ durationMs: Date.now() - startedAt,
390
+ ...this.lookupDiagnosticAttributes(diagnostics)
391
+ },
392
+ error
393
+ });
203
394
  }
204
395
  /**
205
396
  * Creates or updates (replaces) a UMP token on-chain. If `oldTokenToConsume` is provided,
@@ -275,11 +466,12 @@ class OverlayUMPTokenInteractor {
275
466
  if (!oldTokenToConsume?.currentOutpoint)
276
467
  return { resolvedOldToken: undefined, inputToken: undefined };
277
468
  const inputToken = await this.findByOutpoint(oldTokenToConsume.currentOutpoint);
278
- if (inputToken == null)
279
- return { resolvedOldToken: undefined, inputToken: undefined };
469
+ if (inputToken == null) {
470
+ throw new Error('The previous UMP token could not be resolved; refusing to publish a duplicate token.');
471
+ }
280
472
  return { resolvedOldToken: oldTokenToConsume, inputToken };
281
473
  }
282
- /** Creates the UMP action; falls back to a bare recovery action on failure. */
474
+ /** Creates the UMP action without dropping a required old-token input on failure. */
283
475
  async createUMPAction(wallet, adminOriginator, inputs, outputs, inputToken, resolvedOldToken) {
284
476
  try {
285
477
  return await wallet.createAction({
@@ -290,13 +482,18 @@ class OverlayUMPTokenInteractor {
290
482
  options: { randomizeOutputs: false, acceptDelayedBroadcast: false }
291
483
  }, adminOriginator);
292
484
  }
293
- catch (e) {
294
- console.error('Error with UMP token update. Attempting a last-ditch effort to get a new one', e);
295
- return await wallet.createAction({
296
- description: 'Recover UMP token',
297
- outputs,
298
- options: { randomizeOutputs: false, acceptDelayedBroadcast: false }
299
- }, adminOriginator);
485
+ catch (error) {
486
+ this.telemetry.capture({
487
+ name: 'wallet-toolbox.ump.action.failed',
488
+ component: 'wallet-toolbox.ump',
489
+ severity: 'error',
490
+ attributes: {
491
+ operation: resolvedOldToken == null ? 'create' : 'renew',
492
+ oldTokenInputRequired: resolvedOldToken != null
493
+ },
494
+ error
495
+ });
496
+ throw error;
300
497
  }
301
498
  }
302
499
  /** Handles a fully-finalized (no signable tx) createAction result — broadcasts and returns outpoint. */
@@ -308,7 +505,7 @@ class OverlayUMPTokenInteractor {
308
505
  throw new Error('No final TX data to broadcast.');
309
506
  const broadcastTx = sdk_1.Transaction.fromAtomicBEEF(createResult.tx);
310
507
  const result = await this.broadcaster.broadcast(broadcastTx);
311
- console.log('BROADCAST RESULT', result);
508
+ this.assertSuccessfulBroadcast(result, 'create-finalized');
312
509
  return `${finalTxid}.0`;
313
510
  }
314
511
  /** Signs the old-token input and broadcasts — used during UMP token renewal. */
@@ -322,7 +519,7 @@ class OverlayUMPTokenInteractor {
322
519
  if (signResult.tx == null)
323
520
  throw new Error('Final transaction data missing after signing renewed UMP token.');
324
521
  const result = await this.broadcaster.broadcast(sdk_1.Transaction.fromAtomicBEEF(signResult.tx));
325
- console.log('BROADCAST RESULT', result);
522
+ this.assertSuccessfulBroadcast(result, 'renew');
326
523
  return `${finalTxid}.0`;
327
524
  }
328
525
  /** Signs without input spending and broadcasts — used when creating a brand-new UMP token. */
@@ -334,9 +531,27 @@ class OverlayUMPTokenInteractor {
334
531
  if (signResult.tx == null)
335
532
  throw new Error('Final transaction data missing after signing new UMP token.');
336
533
  const result = await this.broadcaster.broadcast(sdk_1.Transaction.fromAtomicBEEF(signResult.tx));
337
- console.log('BROADCAST RESULT', result);
534
+ this.assertSuccessfulBroadcast(result, 'create');
338
535
  return `${finalTxid}.0`;
339
536
  }
537
+ assertSuccessfulBroadcast(result, operation) {
538
+ const succeeded = result.status === 'success';
539
+ this.telemetry.capture({
540
+ name: succeeded
541
+ ? 'wallet-toolbox.ump.broadcast.completed'
542
+ : 'wallet-toolbox.ump.broadcast.failed',
543
+ component: 'wallet-toolbox.ump',
544
+ severity: succeeded ? 'info' : 'error',
545
+ attributes: {
546
+ operation,
547
+ outcome: result.status,
548
+ ...(result.status === 'error' ? { code: result.code } : {})
549
+ }
550
+ });
551
+ if (!succeeded) {
552
+ throw new Error(`UMP token broadcast failed (${result.code}).`);
553
+ }
554
+ }
340
555
  /**
341
556
  * Attempts to parse a LookupAnswer from the UMP lookup service. If successful,
342
557
  * extracts the token fields from the resulting transaction and constructs
@@ -346,40 +561,38 @@ class OverlayUMPTokenInteractor {
346
561
  * @returns The parsed UMPToken or `undefined` if none found/decodable.
347
562
  */
348
563
  parseLookupAnswer(answer) {
349
- if (answer.type !== 'output-list') {
350
- return undefined;
351
- }
352
- if (!answer.outputs || answer.outputs.length === 0) {
353
- return undefined;
564
+ return this.parseLookupAnswers(answer)[0];
565
+ }
566
+ parseLookupAnswers(answer) {
567
+ if (answer.type !== 'output-list' || answer.outputs.length === 0)
568
+ return [];
569
+ const tokens = [];
570
+ for (const output of answer.outputs) {
571
+ const token = this.parseLookupOutput(output);
572
+ if (token != null)
573
+ tokens.push(token);
354
574
  }
355
- const { beef, outputIndex } = answer.outputs[0];
575
+ return tokens;
576
+ }
577
+ parseLookupOutput(output) {
356
578
  try {
357
- const tx = sdk_1.Transaction.fromBEEF(beef);
358
- const outpoint = `${tx.id('hex')}.${outputIndex}`;
359
- const decoded = sdk_1.PushDrop.decode(tx.outputs[outputIndex].lockingScript);
360
- // Expecting 11 or more fields for UMP
361
- if (!decoded.fields || decoded.fields.length < 11) {
362
- console.warn(`Unexpected number of fields in UMP token: ${decoded.fields?.length}`);
579
+ const tx = sdk_1.Transaction.fromBEEF(output.beef);
580
+ const txOutput = tx.outputs[output.outputIndex];
581
+ if (txOutput == null)
582
+ return undefined;
583
+ const decoded = sdk_1.PushDrop.decode(txOutput.lockingScript);
584
+ if (decoded.fields == null || decoded.fields.length < 11 || decoded.fields.length > 16) {
363
585
  return undefined;
364
586
  }
365
- // Parse protocol fields, excluding the trailing PushDrop signature only when
366
- // it is cryptographically valid for the preceding field payload.
367
587
  const protocolFields = stripVerifiedPushDropSignature(decoded.fields, decoded.lockingPublicKey);
368
- // Detect v3 token metadata in either layout:
369
- // - with profilesEncrypted present: [0..10]=core, [11]=profiles, [12]=version, [13]=algorithm, [14]=params
370
- // - without profilesEncrypted: [0..10]=core, [11]=version, [12]=algorithm, [13]=params
588
+ if (protocolFields.length < 11 ||
589
+ protocolFields.slice(0, 11).some(field => field.length === 0 || field.length > exports.MAX_STATE_SNAPSHOT_BYTES)) {
590
+ return undefined;
591
+ }
371
592
  const hasV3MetadataWithProfiles = protocolFields.length >= 15 && protocolFields[12]?.length === 1 && protocolFields[12][0] === 3;
372
593
  const hasV3MetadataWithoutProfiles = protocolFields.length >= 14 && protocolFields[11]?.length === 1 && protocolFields[11][0] === 3;
373
- let kdfVersionFieldIndex;
374
- if (hasV3MetadataWithProfiles)
375
- kdfVersionFieldIndex = 12;
376
- else if (hasV3MetadataWithoutProfiles)
377
- kdfVersionFieldIndex = 11;
378
- else
379
- kdfVersionFieldIndex = -1;
380
- // Build the UMP token from these fields, preserving outpoint
381
- const t = {
382
- // Core fields (unchanged for all versions)
594
+ const kdfVersionFieldIndex = hasV3MetadataWithProfiles ? 12 : (hasV3MetadataWithoutProfiles ? 11 : -1);
595
+ const token = {
383
596
  passwordSalt: protocolFields[0],
384
597
  passwordPresentationPrimary: protocolFields[1],
385
598
  passwordRecoveryPrimary: protocolFields[2],
@@ -391,41 +604,38 @@ class OverlayUMPTokenInteractor {
391
604
  presentationKeyEncrypted: protocolFields[8],
392
605
  passwordKeyEncrypted: protocolFields[9],
393
606
  recoveryKeyEncrypted: protocolFields[10],
394
- currentOutpoint: outpoint
607
+ currentOutpoint: `${tx.id('hex')}.${output.outputIndex}`
395
608
  };
396
- // Encrypted profiles (optional, all versions)
397
- const hasProfilesField = hasV3MetadataWithProfiles || (!hasV3MetadataWithoutProfiles && !!protocolFields[11]);
398
- if (hasProfilesField && protocolFields[11] && protocolFields[11].length > 0) {
399
- t.profilesEncrypted = protocolFields[11];
609
+ if (token.presentationHash.length !== 32 || token.recoveryHash.length !== 32)
610
+ return undefined;
611
+ const hasProfilesField = hasV3MetadataWithProfiles || (!hasV3MetadataWithoutProfiles && protocolFields[11] != null);
612
+ if (hasProfilesField && protocolFields[11].length > 0) {
613
+ if (protocolFields[11].length > exports.MAX_STATE_SNAPSHOT_BYTES)
614
+ return undefined;
615
+ token.profilesEncrypted = protocolFields[11];
400
616
  }
401
- // V3 fields: umpVersion, kdfAlgorithm, kdfParams (fields 12, 13, 14)
402
617
  if (kdfVersionFieldIndex !== -1) {
403
- t.umpVersion = protocolFields[kdfVersionFieldIndex][0]; // Single byte value 3
404
618
  const kdfAlgorithmField = protocolFields[kdfVersionFieldIndex + 1];
405
619
  const kdfParamsField = protocolFields[kdfVersionFieldIndex + 2];
406
- if (!kdfAlgorithmField || !kdfParamsField) {
407
- return t;
408
- }
409
- const kdfAlgorithm = sdk_1.Utils.toUTF8(kdfAlgorithmField);
410
- const kdfParamsJson = sdk_1.Utils.toUTF8(kdfParamsField);
411
- try {
412
- const kdfParams = JSON.parse(kdfParamsJson);
413
- t.passwordKdf = {
414
- algorithm: kdfAlgorithm,
415
- iterations: kdfParams.iterations,
416
- memoryKiB: kdfParams.memoryKiB,
417
- parallelism: kdfParams.parallelism,
418
- hashLength: kdfParams.hashLength
419
- };
420
- }
421
- catch (e) {
422
- console.warn('Failed to parse v3 KDF params:', e);
620
+ if (kdfAlgorithmField == null || kdfParamsField == null || kdfParamsField.length > 1024) {
621
+ return undefined;
423
622
  }
623
+ const kdfParams = JSON.parse(sdk_1.Utils.toUTF8(kdfParamsField));
624
+ const passwordKdf = {
625
+ algorithm: sdk_1.Utils.toUTF8(kdfAlgorithmField),
626
+ iterations: kdfParams.iterations,
627
+ memoryKiB: kdfParams.memoryKiB,
628
+ parallelism: kdfParams.parallelism,
629
+ hashLength: kdfParams.hashLength
630
+ };
631
+ if (!isValidKdfConfig(passwordKdf))
632
+ return undefined;
633
+ token.umpVersion = 3;
634
+ token.passwordKdf = passwordKdf;
424
635
  }
425
- return t;
636
+ return token;
426
637
  }
427
- catch (e) {
428
- console.error('Failed to parse or decode UMP token:', e);
638
+ catch {
429
639
  return undefined;
430
640
  }
431
641
  }
@@ -435,19 +645,43 @@ class OverlayUMPTokenInteractor {
435
645
  * @returns The result so that we can use it to unlock the transaction
436
646
  */
437
647
  async findByOutpoint(outpoint) {
438
- const results = await this.resolver.query({
439
- service: 'ls_users',
440
- query: {
441
- outpoint
442
- }
443
- });
444
- if (results.type !== 'output-list') {
445
- return undefined;
648
+ const correlationId = this.telemetry.enabled === true ? this.telemetry.createCorrelationId() : undefined;
649
+ const startedAt = Date.now();
650
+ let resolution;
651
+ try {
652
+ resolution = await this.resolver.queryDetailed({
653
+ service: 'ls_users',
654
+ query: {
655
+ outpoint
656
+ }
657
+ }, undefined, {
658
+ waitForAllHosts: true,
659
+ correlationId
660
+ });
446
661
  }
447
- if (!results.outputs || (results.outputs.length === 0)) {
662
+ catch (error) {
663
+ const diagnostics = this.emptyLookupDiagnostics(correlationId);
664
+ this.captureLookupFailure('outpoint', 'lookup-unavailable', diagnostics, startedAt, error);
665
+ throw new UMPTokenLookupError('lookup-unavailable', diagnostics, { cause: error });
666
+ }
667
+ if (resolution.answer.outputs.length === 0) {
668
+ const p = resolution.progress;
669
+ const authoritative = p.isFinal &&
670
+ p.hostCount > 0 &&
671
+ p.completedHosts === p.hostCount &&
672
+ p.successfulHosts === p.hostCount &&
673
+ p.emptyHosts === p.hostCount &&
674
+ p.failedHosts === 0 &&
675
+ p.rejectedHosts === 0 &&
676
+ p.freeformHosts === 0;
677
+ if (!authoritative) {
678
+ const diagnostics = this.toLookupDiagnostics(resolution);
679
+ this.captureLookupFailure('outpoint', 'lookup-incomplete', diagnostics, startedAt);
680
+ throw new UMPTokenLookupError('lookup-incomplete', diagnostics);
681
+ }
448
682
  return undefined;
449
683
  }
450
- return results.outputs[0];
684
+ return resolution.answer.outputs[0];
451
685
  }
452
686
  }
453
687
  exports.OverlayUMPTokenInteractor = OverlayUMPTokenInteractor;
@@ -483,6 +717,10 @@ class CWIStyleWalletManager {
483
717
  * The system that locates and publishes UMP tokens on-chain.
484
718
  */
485
719
  UMPTokenInteractor;
720
+ /**
721
+ * Privacy-bounded diagnostic channel for wallet state transitions.
722
+ */
723
+ telemetry;
486
724
  /**
487
725
  * A function called to persist the newly generated recovery key.
488
726
  * It should generally trigger a UI prompt where the user is asked to write it down.
@@ -510,7 +748,7 @@ class CWIStyleWalletManager {
510
748
  /**
511
749
  * Indicates new user or existing user flow.
512
750
  */
513
- authenticationFlow = 'new-user';
751
+ authenticationFlow = 'unknown';
514
752
  /**
515
753
  * The current UMP token in use.
516
754
  */
@@ -559,32 +797,55 @@ class CWIStyleWalletManager {
559
797
  * @param stateSnapshot Optional previously saved state snapshot.
560
798
  * @param kdfConfig Optional KDF configuration for new UMP tokens.
561
799
  */
562
- constructor(adminOriginator, walletBuilder, interactor = new OverlayUMPTokenInteractor(), recoveryKeySaver, passwordRetriever, newWalletFunder, stateSnapshot, kdfConfig) {
800
+ constructor(adminOriginator, walletBuilder, interactor, recoveryKeySaver, passwordRetriever, newWalletFunder, stateSnapshot, kdfConfig, telemetry) {
563
801
  this.adminOriginator = adminOriginator;
564
802
  this.walletBuilder = walletBuilder;
565
- this.UMPTokenInteractor = interactor;
803
+ this.telemetry = new sdk_1.Telemetry(telemetry);
804
+ this.UMPTokenInteractor = interactor ?? new OverlayUMPTokenInteractor(undefined, undefined, telemetry);
566
805
  this.recoveryKeySaver = recoveryKeySaver;
567
806
  this.passwordRetriever = passwordRetriever;
568
807
  this.authenticated = false;
569
808
  this.newWalletFunder = newWalletFunder;
570
809
  // Initialize KDF config with Argon2id defaults for v3 tokens
810
+ const kdfAlgorithm = kdfConfig?.algorithm ?? 'argon2id';
571
811
  this.kdfConfig = {
572
- algorithm: kdfConfig?.algorithm ?? 'argon2id',
573
- iterations: kdfConfig?.iterations ?? exports.ARGON2ID_DEFAULT_ITERATIONS,
812
+ algorithm: kdfAlgorithm,
813
+ iterations: kdfConfig?.iterations ??
814
+ (kdfAlgorithm === 'argon2id' ? exports.ARGON2ID_DEFAULT_ITERATIONS : exports.PBKDF2_NUM_ROUNDS),
574
815
  memoryKiB: kdfConfig?.memoryKiB ?? exports.ARGON2ID_DEFAULT_MEMORY_KIB,
575
816
  parallelism: kdfConfig?.parallelism ?? exports.ARGON2ID_DEFAULT_PARALLELISM,
576
817
  hashLength: kdfConfig?.hashLength ?? exports.ARGON2ID_DEFAULT_HASH_LENGTH
577
818
  };
819
+ if (!isValidKdfConfig(this.kdfConfig)) {
820
+ throw new Error('Unsupported or unsafe KDF configuration.');
821
+ }
578
822
  // Store snapshot for lazy init; callers await ready before calling wallet methods.
579
823
  this._initSnapshot = stateSnapshot;
580
824
  }
581
825
  async _init() {
582
826
  if (this._initSnapshot !== undefined) {
583
- await this.loadSnapshot(this._initSnapshot).catch((err) => {
584
- console.error('Failed to load snapshot during construction:', err);
585
- // Clear potentially partially loaded state. destroy() is synchronous.
586
- this.destroy();
827
+ this.telemetry.capture({
828
+ name: 'wallet-toolbox.snapshot.initialization.started',
829
+ component: 'wallet-toolbox.cwi-manager',
830
+ severity: 'debug'
587
831
  });
832
+ try {
833
+ await this.loadSnapshot(this._initSnapshot);
834
+ this.telemetry.capture({
835
+ name: 'wallet-toolbox.snapshot.initialization.completed',
836
+ component: 'wallet-toolbox.cwi-manager',
837
+ severity: 'info'
838
+ });
839
+ }
840
+ catch (error) {
841
+ this.telemetry.capture({
842
+ name: 'wallet-toolbox.snapshot.initialization.failed',
843
+ component: 'wallet-toolbox.cwi-manager',
844
+ severity: 'error',
845
+ error
846
+ });
847
+ throw error;
848
+ }
588
849
  }
589
850
  }
590
851
  // --- Authentication Methods ---
@@ -598,8 +859,35 @@ class CWIStyleWalletManager {
598
859
  if (this.authenticationMode === 'recovery-key-and-password') {
599
860
  throw new Error('Presentation key is not needed in this mode');
600
861
  }
862
+ if (key.length !== 32 || key.some(byte => !Number.isInteger(byte) || byte < 0 || byte > 255)) {
863
+ throw new TypeError('Presentation key must contain exactly 32 bytes.');
864
+ }
865
+ this.authenticationFlow = 'unknown';
601
866
  const hash = sdk_1.Hash.sha256(key);
602
- const token = await this.UMPTokenInteractor.findByPresentationKeyHash(hash);
867
+ const startedAt = Date.now();
868
+ this.telemetry.capture({
869
+ name: 'wallet-toolbox.authentication.account-lookup.started',
870
+ component: 'wallet-toolbox.cwi-manager',
871
+ severity: 'debug',
872
+ attributes: { lookupKind: 'presentation' }
873
+ });
874
+ let token;
875
+ try {
876
+ token = await this.UMPTokenInteractor.findByPresentationKeyHash(hash);
877
+ }
878
+ catch (error) {
879
+ this.telemetry.capture({
880
+ name: 'wallet-toolbox.authentication.account-lookup.failed',
881
+ component: 'wallet-toolbox.cwi-manager',
882
+ severity: 'warn',
883
+ attributes: {
884
+ lookupKind: 'presentation',
885
+ durationMs: Date.now() - startedAt
886
+ },
887
+ error
888
+ });
889
+ throw error;
890
+ }
603
891
  if (token == null) {
604
892
  // No token found -> New user
605
893
  this.authenticationFlow = 'new-user';
@@ -611,6 +899,16 @@ class CWIStyleWalletManager {
611
899
  this.presentationKey = key;
612
900
  this.currentUMPToken = token;
613
901
  }
902
+ this.telemetry.capture({
903
+ name: 'wallet-toolbox.authentication.account-lookup.completed',
904
+ component: 'wallet-toolbox.cwi-manager',
905
+ severity: 'info',
906
+ attributes: {
907
+ lookupKind: 'presentation',
908
+ accountStatus: this.authenticationFlow,
909
+ durationMs: Date.now() - startedAt
910
+ }
911
+ });
614
912
  }
615
913
  /**
616
914
  * Provides the password.
@@ -621,6 +919,9 @@ class CWIStyleWalletManager {
621
919
  if (this.authenticationMode === 'presentation-key-and-recovery-key') {
622
920
  throw new Error('Password is not needed in this mode');
623
921
  }
922
+ if (this.authenticationFlow === 'unknown') {
923
+ throw new Error('Determine account status with a presentation or recovery key before providing a password.');
924
+ }
624
925
  if (this.authenticationFlow === 'existing-user') {
625
926
  await this.handleExistingUserPassword(password);
626
927
  }
@@ -699,10 +1000,15 @@ class CWIStyleWalletManager {
699
1000
  try {
700
1001
  await this.newWalletFunder(this.presentationKey, this.underlying, this.adminOriginator);
701
1002
  }
702
- catch (e) {
703
- console.error('Error funding new wallet:', e);
704
- const message = e instanceof Error ? e.message : String(e);
705
- throw new Error(`Failed to fund new wallet before publishing UMP token: ${message}`);
1003
+ catch (error) {
1004
+ this.telemetry.capture({
1005
+ name: 'wallet-toolbox.authentication.new-wallet-funding.failed',
1006
+ component: 'wallet-toolbox.cwi-manager',
1007
+ severity: 'error',
1008
+ error: new Error('New wallet funding failed.')
1009
+ });
1010
+ const message = error instanceof Error ? error.message : String(error);
1011
+ throw new Error(`Failed to fund new wallet before publishing UMP token: ${message}`, { cause: error });
706
1012
  }
707
1013
  }
708
1014
  if (this.underlying == null)
@@ -722,6 +1028,9 @@ class CWIStyleWalletManager {
722
1028
  if (this.authenticationMode === 'presentation-key-and-password') {
723
1029
  throw new Error('No recovery key required in this mode');
724
1030
  }
1031
+ if (recoveryKey.length !== 32 || recoveryKey.some(byte => !Number.isInteger(byte) || byte < 0 || byte > 255)) {
1032
+ throw new TypeError('Recovery key must contain exactly 32 bytes.');
1033
+ }
725
1034
  if (this.authenticationMode === 'recovery-key-and-password') {
726
1035
  // Wait for password
727
1036
  const hash = sdk_1.Hash.sha256(recoveryKey);
@@ -778,7 +1087,20 @@ class CWIStyleWalletManager {
778
1087
  snapshotWriter.write(snapshotKey);
779
1088
  snapshotWriter.write(this.activeProfileId); // Active profile ID
780
1089
  snapshotWriter.write(snapshotPayload); // Encrypted data
781
- return snapshotWriter.toArray();
1090
+ const snapshot = snapshotWriter.toArray();
1091
+ if (snapshot.length > exports.MAX_STATE_SNAPSHOT_BYTES) {
1092
+ throw new Error('Snapshot exceeds the maximum supported size.');
1093
+ }
1094
+ this.telemetry.capture({
1095
+ name: 'wallet-toolbox.snapshot.saved',
1096
+ component: 'wallet-toolbox.cwi-manager',
1097
+ severity: 'info',
1098
+ attributes: {
1099
+ formatVersion: 2,
1100
+ profileCount: this.profiles.length
1101
+ }
1102
+ });
1103
+ return snapshot;
782
1104
  }
783
1105
  /**
784
1106
  * Loads a previously saved state snapshot. Restores root key, UMP token, profiles, and active profile.
@@ -788,6 +1110,11 @@ class CWIStyleWalletManager {
788
1110
  */
789
1111
  async loadSnapshot(snapshot) {
790
1112
  try {
1113
+ if (snapshot.length === 0 ||
1114
+ snapshot.length > exports.MAX_STATE_SNAPSHOT_BYTES ||
1115
+ snapshot.some(byte => !Number.isInteger(byte) || byte < 0 || byte > 255)) {
1116
+ throw new Error('Snapshot is empty, oversized, or contains invalid bytes.');
1117
+ }
791
1118
  const reader = new sdk_1.Utils.Reader(snapshot);
792
1119
  const version = reader.readUInt8();
793
1120
  let snapshotKey;
@@ -820,10 +1147,26 @@ class CWIStyleWalletManager {
820
1147
  await this.setupRootInfrastructure(rootPrimaryKey); // Will automatically load profiles
821
1148
  await this.switchProfile(activeProfileId); // Switch to the profile saved in the snapshot
822
1149
  this.authenticationFlow = 'existing-user'; // Loading implies existing user
1150
+ this.telemetry.capture({
1151
+ name: 'wallet-toolbox.snapshot.loaded',
1152
+ component: 'wallet-toolbox.cwi-manager',
1153
+ severity: 'info',
1154
+ attributes: {
1155
+ formatVersion: version,
1156
+ profileCount: this.profiles.length
1157
+ }
1158
+ });
823
1159
  }
824
1160
  catch (error) {
825
1161
  this.destroy(); // Clear state on error
826
- throw new Error(`Failed to load snapshot: ${error.message}`);
1162
+ this.telemetry.capture({
1163
+ name: 'wallet-toolbox.snapshot.load-failed',
1164
+ component: 'wallet-toolbox.cwi-manager',
1165
+ severity: 'error',
1166
+ error
1167
+ });
1168
+ const message = error instanceof Error ? error.message : 'Unknown error';
1169
+ throw new Error(`Failed to load snapshot: ${message}`, { cause: error });
827
1170
  }
828
1171
  }
829
1172
  async syncUMPToken() {
@@ -865,7 +1208,7 @@ class CWIStyleWalletManager {
865
1208
  this.profiles = [];
866
1209
  this.activeProfileId = exports.DEFAULT_PROFILE_ID;
867
1210
  this.authenticationMode = 'presentation-key-and-password';
868
- this.authenticationFlow = 'new-user';
1211
+ this.authenticationFlow = 'unknown';
869
1212
  }
870
1213
  // --- Profile Management Methods ---
871
1214
  /**
@@ -1134,8 +1477,15 @@ class CWIStyleWalletManager {
1134
1477
  }
1135
1478
  }
1136
1479
  catch (error) {
1137
- console.error(`Error decrypting factor ${factorName}:`, error);
1138
- throw new Error(`Failed to decrypt factor "${factorName}": ${error.message}`);
1480
+ this.telemetry.capture({
1481
+ name: 'wallet-toolbox.authentication.factor-decryption.failed',
1482
+ component: 'wallet-toolbox.cwi-manager',
1483
+ severity: 'error',
1484
+ attributes: { factor: factorName },
1485
+ error
1486
+ });
1487
+ const message = error instanceof Error ? error.message : 'Unknown error';
1488
+ throw new Error(`Failed to decrypt factor "${factorName}": ${message}`, { cause: error });
1139
1489
  }
1140
1490
  }
1141
1491
  /**
@@ -1210,7 +1560,12 @@ class CWIStyleWalletManager {
1210
1560
  const currentActiveId = this.activeProfileId;
1211
1561
  let walletToUse = this.underlying;
1212
1562
  if (!currentActiveId.every(x => x === 0)) {
1213
- console.log('Temporarily switching to default profile to update UMP token...');
1563
+ this.telemetry.capture({
1564
+ name: 'wallet-toolbox.ump.profile-switch.started',
1565
+ component: 'wallet-toolbox.cwi-manager',
1566
+ severity: 'debug',
1567
+ attributes: { reason: 'token-update' }
1568
+ });
1214
1569
  await this.switchProfile(exports.DEFAULT_PROFILE_ID); // This rebuilds this.underlying
1215
1570
  walletToUse = this.underlying;
1216
1571
  }
@@ -1228,8 +1583,13 @@ class CWIStyleWalletManager {
1228
1583
  finally {
1229
1584
  // Switch back if we temporarily switched
1230
1585
  if (!currentActiveId.every(x => x === 0)) {
1231
- console.log('Switching back to original profile...');
1232
1586
  await this.switchProfile(currentActiveId);
1587
+ this.telemetry.capture({
1588
+ name: 'wallet-toolbox.ump.profile-switch.completed',
1589
+ component: 'wallet-toolbox.cwi-manager',
1590
+ severity: 'debug',
1591
+ attributes: { reason: 'token-update' }
1592
+ });
1233
1593
  }
1234
1594
  }
1235
1595
  }
@@ -1311,6 +1671,9 @@ class CWIStyleWalletManager {
1311
1671
  }
1312
1672
  const readArray = () => {
1313
1673
  const length = reader.readVarIntNum();
1674
+ if (length <= 0 || length > exports.MAX_STATE_SNAPSHOT_BYTES) {
1675
+ throw new Error('UMP token field exceeds allowed bounds.');
1676
+ }
1314
1677
  return reader.read(length);
1315
1678
  };
1316
1679
  // Read standard fields (order matches serialization)
@@ -1344,18 +1707,16 @@ class CWIStyleWalletManager {
1344
1707
  const algorithm = sdk_1.Utils.toUTF8(algorithmBytes);
1345
1708
  const kdfParamsBytes = readArray();
1346
1709
  const kdfParamsJson = sdk_1.Utils.toUTF8(kdfParamsBytes);
1347
- try {
1348
- const kdfParams = JSON.parse(kdfParamsJson);
1349
- passwordKdf = {
1350
- algorithm,
1351
- iterations: kdfParams.iterations,
1352
- memoryKiB: kdfParams.memoryKiB,
1353
- parallelism: kdfParams.parallelism,
1354
- hashLength: kdfParams.hashLength
1355
- };
1356
- }
1357
- catch (e) {
1358
- console.warn('Failed to parse KDF params during deserialization:', e);
1710
+ const kdfParams = JSON.parse(kdfParamsJson);
1711
+ passwordKdf = {
1712
+ algorithm,
1713
+ iterations: kdfParams.iterations,
1714
+ memoryKiB: kdfParams.memoryKiB,
1715
+ parallelism: kdfParams.parallelism,
1716
+ hashLength: kdfParams.hashLength
1717
+ };
1718
+ if (!isValidKdfConfig(passwordKdf)) {
1719
+ throw new Error('Serialized UMP token contains unsupported or unsafe KDF parameters.');
1359
1720
  }
1360
1721
  }
1361
1722
  }
@@ -1363,6 +1724,9 @@ class CWIStyleWalletManager {
1363
1724
  const outpointLen = reader.readVarIntNum();
1364
1725
  const outpointBytes = reader.read(outpointLen);
1365
1726
  const currentOutpoint = sdk_1.Utils.toUTF8(outpointBytes);
1727
+ if (currentOutpoint.length > 128 || !/^[^\s.:]+[.:][0-9]+$/.test(currentOutpoint)) {
1728
+ throw new Error('Serialized UMP token contains an invalid outpoint.');
1729
+ }
1366
1730
  const token = {
1367
1731
  passwordSalt,
1368
1732
  passwordPresentationPrimary,
@@ -1434,14 +1798,22 @@ class CWIStyleWalletManager {
1434
1798
  try {
1435
1799
  const decryptedProfileBytes = new sdk_1.SymmetricKey(rootPrimaryKey).decrypt(this.currentUMPToken.profilesEncrypted);
1436
1800
  const profilesJson = sdk_1.Utils.toUTF8(decryptedProfileBytes);
1437
- this.profiles = JSON.parse(profilesJson);
1801
+ const profiles = JSON.parse(profilesJson);
1802
+ if (!Array.isArray(profiles) || profiles.length > 1000 || !profiles.every(isValidProfile)) {
1803
+ throw new Error('Decrypted profile data is invalid or exceeds supported bounds.');
1804
+ }
1805
+ this.profiles = profiles;
1438
1806
  }
1439
1807
  catch (error) {
1440
- console.error('Failed to decrypt or parse profiles:', error);
1441
- // Decide if this should be fatal or just log and continue without profiles
1442
- this.profiles = []; // Ensure profiles are empty on error
1443
- // Optionally re-throw or handle more gracefully
1444
- throw new Error(`Failed to load profiles: ${error.message}`);
1808
+ this.profiles = [];
1809
+ this.telemetry.capture({
1810
+ name: 'wallet-toolbox.profile.load-failed',
1811
+ component: 'wallet-toolbox.cwi-manager',
1812
+ severity: 'error',
1813
+ error
1814
+ });
1815
+ const message = error instanceof Error ? error.message : 'Unknown error';
1816
+ throw new Error(`Failed to load profiles: ${message}`, { cause: error });
1445
1817
  }
1446
1818
  }
1447
1819
  this.authenticated = true;