@bsv/wallet-toolbox-mobile 2.4.3 → 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.
- package/out/src/CWIStyleWalletManager.d.ts +52 -5
- package/out/src/CWIStyleWalletManager.d.ts.map +1 -1
- package/out/src/CWIStyleWalletManager.js +500 -128
- package/out/src/CWIStyleWalletManager.js.map +1 -1
- package/out/src/Wallet.d.ts +8 -0
- package/out/src/Wallet.d.ts.map +1 -1
- package/out/src/Wallet.js +23 -9
- package/out/src/Wallet.js.map +1 -1
- package/out/src/WalletAuthenticationManager.d.ts +20 -6
- package/out/src/WalletAuthenticationManager.d.ts.map +1 -1
- package/out/src/WalletAuthenticationManager.js +214 -34
- package/out/src/WalletAuthenticationManager.js.map +1 -1
- package/out/src/index.mobile.d.ts +1 -0
- package/out/src/index.mobile.d.ts.map +1 -1
- package/out/src/index.mobile.js +1 -0
- package/out/src/index.mobile.js.map +1 -1
- package/out/src/monitor/Monitor.d.ts.map +1 -1
- package/out/src/monitor/Monitor.js +4 -3
- package/out/src/monitor/Monitor.js.map +1 -1
- package/out/src/monitor/tasks/TaskCleanupActionBatches.d.ts +13 -0
- package/out/src/monitor/tasks/TaskCleanupActionBatches.d.ts.map +1 -0
- package/out/src/monitor/tasks/TaskCleanupActionBatches.js +24 -0
- package/out/src/monitor/tasks/TaskCleanupActionBatches.js.map +1 -0
- package/out/src/sdk/ActionBatch.interfaces.d.ts +105 -0
- package/out/src/sdk/ActionBatch.interfaces.d.ts.map +1 -0
- package/out/src/sdk/ActionBatch.interfaces.js +3 -0
- package/out/src/sdk/ActionBatch.interfaces.js.map +1 -0
- package/out/src/sdk/WalletStorage.interfaces.d.ts +18 -1
- package/out/src/sdk/WalletStorage.interfaces.d.ts.map +1 -1
- package/out/src/sdk/index.d.ts +1 -0
- package/out/src/sdk/index.d.ts.map +1 -1
- package/out/src/sdk/index.js +1 -0
- package/out/src/sdk/index.js.map +1 -1
- package/out/src/signer/actionBatch/ActionBatchPlanner.d.ts +28 -0
- package/out/src/signer/actionBatch/ActionBatchPlanner.d.ts.map +1 -0
- package/out/src/signer/actionBatch/ActionBatchPlanner.js +313 -0
- package/out/src/signer/actionBatch/ActionBatchPlanner.js.map +1 -0
- package/out/src/signer/actionBatch/ActionBatchWorkspace.d.ts +33 -0
- package/out/src/signer/actionBatch/ActionBatchWorkspace.d.ts.map +1 -0
- package/out/src/signer/actionBatch/ActionBatchWorkspace.js +693 -0
- package/out/src/signer/actionBatch/ActionBatchWorkspace.js.map +1 -0
- package/out/src/signer/methods/buildSignableTransaction.d.ts.map +1 -1
- package/out/src/signer/methods/buildSignableTransaction.js +2 -1
- package/out/src/signer/methods/buildSignableTransaction.js.map +1 -1
- package/out/src/signer/methods/createAction.d.ts.map +1 -1
- package/out/src/signer/methods/createAction.js +4 -1
- package/out/src/signer/methods/createAction.js.map +1 -1
- package/out/src/storage/StorageProvider.d.ts +28 -0
- package/out/src/storage/StorageProvider.d.ts.map +1 -1
- package/out/src/storage/StorageProvider.js +49 -0
- package/out/src/storage/StorageProvider.js.map +1 -1
- package/out/src/storage/WalletStorageManager.d.ts +8 -0
- package/out/src/storage/WalletStorageManager.d.ts.map +1 -1
- package/out/src/storage/WalletStorageManager.js +24 -0
- package/out/src/storage/WalletStorageManager.js.map +1 -1
- package/out/src/storage/methods/actionBatch.d.ts +14 -0
- package/out/src/storage/methods/actionBatch.d.ts.map +1 -0
- package/out/src/storage/methods/actionBatch.js +644 -0
- package/out/src/storage/methods/actionBatch.js.map +1 -0
- package/out/src/storage/methods/actionBatchBlobs.d.ts +12 -0
- package/out/src/storage/methods/actionBatchBlobs.d.ts.map +1 -0
- package/out/src/storage/methods/actionBatchBlobs.js +121 -0
- package/out/src/storage/methods/actionBatchBlobs.js.map +1 -0
- package/out/src/storage/methods/actionBatchValidation.d.ts +15 -0
- package/out/src/storage/methods/actionBatchValidation.d.ts.map +1 -0
- package/out/src/storage/methods/actionBatchValidation.js +310 -0
- package/out/src/storage/methods/actionBatchValidation.js.map +1 -0
- package/out/src/storage/methods/actionPlanning.d.ts +12 -0
- package/out/src/storage/methods/actionPlanning.d.ts.map +1 -0
- package/out/src/storage/methods/actionPlanning.js +48 -0
- package/out/src/storage/methods/actionPlanning.js.map +1 -0
- package/out/src/storage/methods/createAction.d.ts.map +1 -1
- package/out/src/storage/methods/createAction.js +9 -25
- package/out/src/storage/methods/createAction.js.map +1 -1
- package/out/src/storage/remoting/StorageClientBase.d.ts +9 -0
- package/out/src/storage/remoting/StorageClientBase.d.ts.map +1 -1
- package/out/src/storage/remoting/StorageClientBase.js +36 -1
- package/out/src/storage/remoting/StorageClientBase.js.map +1 -1
- package/out/src/storage/schema/tables/TableActionBatch.d.ts +24 -0
- package/out/src/storage/schema/tables/TableActionBatch.d.ts.map +1 -0
- package/out/src/storage/schema/tables/TableActionBatch.js +3 -0
- package/out/src/storage/schema/tables/TableActionBatch.js.map +1 -0
- package/out/src/storage/schema/tables/index.d.ts +1 -0
- package/out/src/storage/schema/tables/index.d.ts.map +1 -1
- package/out/src/storage/schema/tables/index.js +1 -0
- package/out/src/storage/schema/tables/index.js.map +1 -1
- package/out/src/utility/actionBatchDigest.d.ts +9 -0
- package/out/src/utility/actionBatchDigest.d.ts.map +1 -0
- package/out/src/utility/actionBatchDigest.js +48 -0
- package/out/src/utility/actionBatchDigest.js.map +1 -0
- package/out/src/utility/beefForTxids.d.ts +9 -0
- package/out/src/utility/beefForTxids.d.ts.map +1 -0
- package/out/src/utility/beefForTxids.js +35 -0
- package/out/src/utility/beefForTxids.js.map +1 -0
- package/out/src/wab-client/WABClient.d.ts +36 -90
- package/out/src/wab-client/WABClient.d.ts.map +1 -1
- package/out/src/wab-client/WABClient.js +103 -151
- package/out/src/wab-client/WABClient.js.map +1 -1
- package/out/src/wab-client/WABTransport.d.ts +53 -0
- package/out/src/wab-client/WABTransport.d.ts.map +1 -0
- package/out/src/wab-client/WABTransport.js +300 -0
- package/out/src/wab-client/WABTransport.js.map +1 -0
- package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts +10 -10
- package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts.map +1 -1
- package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js +16 -21
- package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js.map +1 -1
- package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts +2 -1
- package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts.map +1 -1
- package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js +14 -0
- package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js.map +1 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
return
|
|
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
|
-
|
|
575
|
+
return tokens;
|
|
576
|
+
}
|
|
577
|
+
parseLookupOutput(output) {
|
|
356
578
|
try {
|
|
357
|
-
const tx = sdk_1.Transaction.fromBEEF(beef);
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
374
|
-
|
|
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:
|
|
607
|
+
currentOutpoint: `${tx.id('hex')}.${output.outputIndex}`
|
|
395
608
|
};
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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 (
|
|
407
|
-
return
|
|
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
|
|
636
|
+
return token;
|
|
426
637
|
}
|
|
427
|
-
catch
|
|
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
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
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
|
|
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 = '
|
|
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
|
|
800
|
+
constructor(adminOriginator, walletBuilder, interactor, recoveryKeySaver, passwordRetriever, newWalletFunder, stateSnapshot, kdfConfig, telemetry) {
|
|
563
801
|
this.adminOriginator = adminOriginator;
|
|
564
802
|
this.walletBuilder = walletBuilder;
|
|
565
|
-
this.
|
|
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:
|
|
573
|
-
iterations: kdfConfig?.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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
|
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 (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = '
|
|
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
|
-
|
|
1138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
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;
|