@bsv/wallet-toolbox 1.2.36 → 1.2.38

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.
@@ -70,32 +70,67 @@ import { PrivilegedKeyManager } from './sdk/PrivilegedKeyManager'
70
70
  */
71
71
  export const PBKDF2_NUM_ROUNDS = 7777
72
72
 
73
+ /**
74
+ * Unique Identifier for the default profile (16 zero bytes).
75
+ */
76
+ export const DEFAULT_PROFILE_ID = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
77
+
78
+ /**
79
+ * Describes the structure of a user profile within the wallet.
80
+ */
81
+ export interface Profile {
82
+ /**
83
+ * User-defined name for the profile.
84
+ */
85
+ name: string
86
+
87
+ /**
88
+ * Unique 16-byte identifier for the profile.
89
+ */
90
+ id: number[]
91
+
92
+ /**
93
+ * 32-byte random pad XOR'd with the root primary key to derive the profile's primary key.
94
+ */
95
+ primaryPad: number[]
96
+
97
+ /**
98
+ * 32-byte random pad XOR'd with the root privileged key to derive the profile's privileged key.
99
+ */
100
+ privilegedPad: number[]
101
+
102
+ /**
103
+ * Timestamp (seconds since epoch) when the profile was created.
104
+ */
105
+ createdAt: number
106
+ }
107
+
73
108
  /**
74
109
  * Describes the structure of a User Management Protocol (UMP) token.
75
110
  */
76
111
  export interface UMPToken {
77
112
  /**
78
- * Primary key encrypted by the XOR of the password and presentation keys.
113
+ * Root Primary key encrypted by the XOR of the password and presentation keys.
79
114
  */
80
115
  passwordPresentationPrimary: number[]
81
116
 
82
117
  /**
83
- * Primary key encrypted by the XOR of the password and recovery keys.
118
+ * Root Primary key encrypted by the XOR of the password and recovery keys.
84
119
  */
85
120
  passwordRecoveryPrimary: number[]
86
121
 
87
122
  /**
88
- * Primary key encrypted by the XOR of the presentation and recovery keys.
123
+ * Root Primary key encrypted by the XOR of the presentation and recovery keys.
89
124
  */
90
125
  presentationRecoveryPrimary: number[]
91
126
 
92
127
  /**
93
- * Privileged key encrypted by the XOR of the password and primary keys.
128
+ * Root Privileged key encrypted by the XOR of the password and primary keys.
94
129
  */
95
130
  passwordPrimaryPrivileged: number[]
96
131
 
97
132
  /**
98
- * Privileged key encrypted by the XOR of the presentation and recovery keys.
133
+ * Root Privileged key encrypted by the XOR of the presentation and recovery keys.
99
134
  */
100
135
  presentationRecoveryPrivileged: number[]
101
136
 
@@ -115,20 +150,26 @@ export interface UMPToken {
115
150
  recoveryHash: number[]
116
151
 
117
152
  /**
118
- * A copy of the presentation key encrypted with the privileged key.
153
+ * A copy of the presentation key encrypted with the root privileged key.
119
154
  */
120
155
  presentationKeyEncrypted: number[]
121
156
 
122
157
  /**
123
- * A copy of the recovery key encrypted with the privileged key.
158
+ * A copy of the recovery key encrypted with the root privileged key.
124
159
  */
125
160
  recoveryKeyEncrypted: number[]
126
161
 
127
162
  /**
128
- * A copy of the password key encrypted with the privileged key.
163
+ * A copy of the password key encrypted with the root privileged key.
129
164
  */
130
165
  passwordKeyEncrypted: number[]
131
166
 
167
+ /**
168
+ * Optional field containing the encrypted profile data.
169
+ * JSON string -> Encrypted Bytes using root privileged key.
170
+ */
171
+ profilesEncrypted?: number[]
172
+
132
173
  /**
133
174
  * Describes the token's location on-chain, if it's already been published.
134
175
  */
@@ -160,14 +201,14 @@ export interface UMPTokenInteractor {
160
201
  /**
161
202
  * Creates (and optionally consumes the previous version of) a UMP token on-chain.
162
203
  *
163
- * @param wallet The wallet that might be used to create a new token.
204
+ * @param wallet The wallet that might be used to create a new token (MUST be operating under the DEFAULT profile).
164
205
  * @param adminOriginator The domain name of the administrative originator.
165
206
  * @param token The new UMP token to create.
166
207
  * @param oldTokenToConsume If provided, the old token that must be consumed in the same transaction.
167
208
  * @returns The newly created outpoint.
168
209
  */
169
210
  buildAndSend: (
170
- wallet: WalletInterface,
211
+ wallet: WalletInterface, // This wallet MUST be the one built for the default profile
171
212
  adminOriginator: OriginatorDomainNameStringUnder250Bytes,
172
213
  token: UMPToken,
173
214
  oldTokenToConsume?: UMPToken
@@ -251,34 +292,20 @@ export class OverlayUMPTokenInteractor implements UMPTokenInteractor {
251
292
  * then broadcast and published under the `tm_users` topic using a SHIP broadcast, ensuring
252
293
  * overlay participants see the updated token.
253
294
  *
254
- * @param wallet The wallet used to build and sign the transaction.
295
+ * @param wallet The wallet used to build and sign the transaction (MUST be operating under the DEFAULT profile).
255
296
  * @param adminOriginator The domain/FQDN of the administrative originator (wallet operator).
256
297
  * @param token The new UMPToken to create on-chain.
257
298
  * @param oldTokenToConsume Optionally, an existing token to consume/spend in the same transaction.
258
299
  * @returns The outpoint of the newly created UMP token (e.g. "abcd1234...ef.0").
259
300
  */
260
301
  public async buildAndSend(
261
- wallet: WalletInterface,
302
+ wallet: WalletInterface, // This wallet MUST be the one built for the default profile
262
303
  adminOriginator: OriginatorDomainNameStringUnder250Bytes,
263
304
  token: UMPToken,
264
305
  oldTokenToConsume?: UMPToken
265
306
  ): Promise<OutpointString> {
266
- // 1) Construct the data fields for the new UMP token in the same
267
- // 11-field order used by the UMP protocol's PushDrop definition.
268
- const fields: number[][] = new Array(11)
269
-
270
- // See: UMP field ordering
271
- // 0 => passwordSalt
272
- // 1 => passwordPresentationPrimary
273
- // 2 => passwordRecoveryPrimary
274
- // 3 => presentationRecoveryPrimary
275
- // 4 => passwordPrimaryPrivileged
276
- // 5 => presentationRecoveryPrivileged
277
- // 6 => presentationHash
278
- // 7 => recoveryHash
279
- // 8 => presentationKeyEncrypted
280
- // 9 => passwordKeyEncrypted
281
- // 10 => recoveryKeyEncrypted
307
+ // 1) Construct the data fields for the new UMP token.
308
+ const fields: number[][] = []
282
309
 
283
310
  fields[0] = token.passwordSalt
284
311
  fields[1] = token.passwordPresentationPrimary
@@ -292,7 +319,12 @@ export class OverlayUMPTokenInteractor implements UMPTokenInteractor {
292
319
  fields[9] = token.passwordKeyEncrypted
293
320
  fields[10] = token.recoveryKeyEncrypted
294
321
 
295
- // 2) Create a PushDrop script referencing these fields, locked with the admin key (for easy revocation).
322
+ // Optional field (11) for encrypted profiles
323
+ if (token.profilesEncrypted) {
324
+ fields[11] = token.profilesEncrypted
325
+ }
326
+
327
+ // 2) Create a PushDrop script referencing these fields, locked with the admin key.
296
328
  const script = await new PushDrop(wallet, adminOriginator).lock(
297
329
  fields,
298
330
  [2, 'admin user management token'], // protocolID
@@ -302,7 +334,7 @@ export class OverlayUMPTokenInteractor implements UMPTokenInteractor {
302
334
  /*includeSignature=*/ true
303
335
  )
304
336
 
305
- // 3) Prepare the createAction call. If oldTokenToConsume is provided, we gather the outpoint.
337
+ // 3) Prepare the createAction call. If oldTokenToConsume is provided, gather the outpoint.
306
338
  const inputs: CreateActionInput[] = []
307
339
  let inputToken: { beef: number[]; outputIndex: number } | undefined
308
340
  if (oldTokenToConsume?.currentOutpoint) {
@@ -333,8 +365,7 @@ export class OverlayUMPTokenInteractor implements UMPTokenInteractor {
333
365
  adminOriginator
334
366
  )
335
367
 
336
- // If the transaction is fully processed by the wallet (some wallets might do signAndProcess automatically),
337
- // we retrieve the final TXID from the result.
368
+ // If the transaction is fully processed by the wallet
338
369
  if (!createResult.signableTransaction) {
339
370
  const finalTxid =
340
371
  createResult.txid || (createResult.tx ? Transaction.fromAtomicBEEF(createResult.tx).id('hex') : undefined)
@@ -377,19 +408,25 @@ export class OverlayUMPTokenInteractor implements UMPTokenInteractor {
377
408
  }
378
409
  // 6) Broadcast to `tm_users`
379
410
  const finalAtomicTx = signResult.tx
380
- const broadcastTx = Transaction.fromAtomicBEEF(finalAtomicTx!)
411
+ if (!finalAtomicTx) {
412
+ throw new Error('Final transaction data missing after signing renewed UMP token.')
413
+ }
414
+ const broadcastTx = Transaction.fromAtomicBEEF(finalAtomicTx)
381
415
  const result = await this.broadcaster.broadcast(broadcastTx)
382
416
  console.log('BROADCAST RESULT', result)
383
417
  return `${finalTxid}.0`
384
418
  } else {
385
- // Fallbaack
419
+ // Fallback for creating a new token (no input spending)
386
420
  const signResult = await wallet.signAction({ reference, spends: {} }, adminOriginator)
387
421
  finalTxid = signResult.txid || (signResult.tx ? Transaction.fromAtomicBEEF(signResult.tx).id('hex') : '')
388
422
  if (!finalTxid) {
389
423
  throw new Error('Failed to finalize new UMP token transaction.')
390
424
  }
391
425
  const finalAtomicTx = signResult.tx
392
- const broadcastTx = Transaction.fromAtomicBEEF(finalAtomicTx!)
426
+ if (!finalAtomicTx) {
427
+ throw new Error('Final transaction data missing after signing new UMP token.')
428
+ }
429
+ const broadcastTx = Transaction.fromAtomicBEEF(finalAtomicTx)
393
430
  const result = await this.broadcaster.broadcast(broadcastTx)
394
431
  console.log('BROADCAST RESULT', result)
395
432
  return `${finalTxid}.0`
@@ -412,8 +449,6 @@ export class OverlayUMPTokenInteractor implements UMPTokenInteractor {
412
449
  return undefined
413
450
  }
414
451
 
415
- // We expect only one relevant UMP token in most queries, so let's parse the first.
416
- // If multiple are returned, we can parse the first.
417
452
  const { beef, outputIndex } = answer.outputs[0]
418
453
  try {
419
454
  const tx = Transaction.fromBEEF(beef)
@@ -421,11 +456,15 @@ export class OverlayUMPTokenInteractor implements UMPTokenInteractor {
421
456
 
422
457
  const decoded = PushDrop.decode(tx.outputs[outputIndex].lockingScript)
423
458
 
424
- // Expecting 11 fields for UMP
425
- if (!decoded.fields || decoded.fields.length < 11) return undefined
459
+ // Expecting 11 or more fields for UMP
460
+ if (!decoded.fields || decoded.fields.length < 11) {
461
+ console.warn(`Unexpected number of fields in UMP token: ${decoded.fields?.length}`)
462
+ return undefined
463
+ }
426
464
 
427
465
  // Build the UMP token from these fields, preserving outpoint
428
466
  const t: UMPToken = {
467
+ // Order matches buildAndSend and serialize/deserialize
429
468
  passwordSalt: decoded.fields[0],
430
469
  passwordPresentationPrimary: decoded.fields[1],
431
470
  passwordRecoveryPrimary: decoded.fields[2],
@@ -437,11 +476,12 @@ export class OverlayUMPTokenInteractor implements UMPTokenInteractor {
437
476
  presentationKeyEncrypted: decoded.fields[8],
438
477
  passwordKeyEncrypted: decoded.fields[9],
439
478
  recoveryKeyEncrypted: decoded.fields[10],
479
+ profilesEncrypted: decoded.fields[12] ? decoded.fields[11] : undefined, // If there's a signature in field 12, use field 11
440
480
  currentOutpoint: outpoint
441
481
  }
442
482
  return t
443
483
  } catch (e) {
444
- // If we fail to parse or decode, return undefined
484
+ console.error('Failed to parse or decode UMP token:', e)
445
485
  return undefined
446
486
  }
447
487
  }
@@ -461,7 +501,7 @@ export class OverlayUMPTokenInteractor implements UMPTokenInteractor {
461
501
  if (results.type !== 'output-list') {
462
502
  return undefined
463
503
  }
464
- if (!results.outputs.length) {
504
+ if (!results.outputs || !results.outputs.length) {
465
505
  return undefined
466
506
  }
467
507
  return results.outputs[0]
@@ -470,18 +510,19 @@ export class OverlayUMPTokenInteractor implements UMPTokenInteractor {
470
510
 
471
511
  /**
472
512
  * Manages a "CWI-style" wallet that uses a UMP token and a
473
- * multi-key authentication scheme (password, presentation key, and recovery key).
513
+ * multi-key authentication scheme (password, presentation key, and recovery key),
514
+ * supporting multiple user profiles under a single account.
474
515
  */
475
516
  export class CWIStyleWalletManager implements WalletInterface {
476
517
  /**
477
- * Whether the user is currently authenticated.
518
+ * Whether the user is currently authenticated (i.e., root keys are available).
478
519
  */
479
520
  authenticated: boolean
480
521
 
481
522
  /**
482
523
  * The domain name of the administrative originator (wallet operator / vendor, or your own).
483
524
  */
484
- private adminOriginator: string
525
+ private adminOriginator: OriginatorDomainNameStringUnder250Bytes
485
526
 
486
527
  /**
487
528
  * The system that locates and publishes UMP tokens on-chain.
@@ -503,26 +544,25 @@ export class CWIStyleWalletManager implements WalletInterface {
503
544
  private passwordRetriever: (reason: string, test: (passwordCandidate: string) => boolean) => Promise<string>
504
545
 
505
546
  /**
506
- * An optional function that funds a new Wallet after the new-user flow, before the system proceeds.
507
- * Allows integration with faucets, and provides the presentation key for use in claiming faucet funds
508
- * that may be bound to it.
547
+ * Optional function to fund a new Wallet after the new-user flow.
509
548
  */
510
549
  private newWalletFunder?: (
511
550
  presentationKey: number[],
512
- wallet: WalletInterface,
551
+ wallet: WalletInterface, // The default profile wallet
513
552
  adminOriginator: OriginatorDomainNameStringUnder250Bytes
514
553
  ) => Promise<void>
515
554
 
516
555
  /**
517
- * Builds the underlying wallet once the user has been authenticated.
556
+ * Builds the underlying wallet for a specific profile.
518
557
  */
519
- private walletBuilder: (primaryKey: number[], privilegedKeyManager: PrivilegedKeyManager) => Promise<WalletInterface>
558
+ private walletBuilder: (
559
+ profilePrimaryKey: number[],
560
+ profilePrivilegedKeyManager: PrivilegedKeyManager,
561
+ profileId: number[]
562
+ ) => Promise<WalletInterface>
520
563
 
521
564
  /**
522
- * The current mode of authentication:
523
- * - 'presentation-key-and-password'
524
- * - 'presentation-key-and-recovery-key'
525
- * - 'recovery-key-and-password'
565
+ * Current mode of authentication.
526
566
  */
527
567
  authenticationMode:
528
568
  | 'presentation-key-and-password'
@@ -530,66 +570,74 @@ export class CWIStyleWalletManager implements WalletInterface {
530
570
  | 'recovery-key-and-password' = 'presentation-key-and-password'
531
571
 
532
572
  /**
533
- * Indicates whether this is a new user or an existing user flow:
534
- * - 'new-user'
535
- * - 'existing-user'
573
+ * Indicates new user or existing user flow.
536
574
  */
537
575
  authenticationFlow: 'new-user' | 'existing-user' = 'new-user'
538
576
 
539
577
  /**
540
- * The current UMP token in use (representing the user's keys on-chain).
578
+ * The current UMP token in use.
541
579
  */
542
580
  private currentUMPToken?: UMPToken
543
581
 
544
582
  /**
545
- * The presentation key, temporarily retained after being provided until authenticated.
583
+ * Temporarily retained presentation key.
546
584
  */
547
585
  private presentationKey?: number[]
548
586
 
549
587
  /**
550
- * The recovery key, temporarily retained after being provided until authenticated.
588
+ * Temporarily retained recovery key.
551
589
  */
552
590
  private recoveryKey?: number[]
553
591
 
554
592
  /**
555
- * The user's primary key, which is used to operate the underlying wallet.
556
- * It is also stored within state snapshots.
593
+ * The user's *root* primary key, derived from authentication factors.
594
+ */
595
+ private rootPrimaryKey?: number[]
596
+
597
+ /**
598
+ * The currently active profile ID (null or DEFAULT_PROFILE_ID means default profile).
599
+ */
600
+ private activeProfileId: number[] = DEFAULT_PROFILE_ID
601
+
602
+ /**
603
+ * List of loaded non-default profiles.
557
604
  */
558
- private primaryKey?: number[]
605
+ private profiles: Profile[] = []
559
606
 
560
607
  /**
561
- * The underlying wallet that handles the
562
- * actual signing, encryption, and other wallet operations.
608
+ * The underlying wallet instance for the *active* profile.
563
609
  */
564
610
  private underlying?: WalletInterface
565
611
 
566
612
  /**
567
- * Privileged key manager associated with the underlying wallet, used for
568
- * short-term administrative tasks (e.g. re-wrapping or rotating keys).
613
+ * Privileged key manager associated with the *root* keys, aware of the active profile.
569
614
  */
570
- private underlyingPrivilegedKeyManager?: PrivilegedKeyManager
615
+ private rootPrivilegedKeyManager?: PrivilegedKeyManager
571
616
 
572
617
  /**
573
618
  * Constructs a new CWIStyleWalletManager.
574
619
  *
575
620
  * @param adminOriginator The domain name of the administrative originator.
576
- * @param walletBuilder A function that can build an underlying wallet instance
577
- * from a primary key and a privileged key manager
578
- * @param interactor An instance of UMPTokenInteractor capable of managing UMP tokens.
579
- * @param recoveryKeySaver A function that can persist or display a newly generated recovery key.
580
- * @param passwordRetriever A function to request the user's password, given a reason and a test function.
581
- * @param newWalletFunder An optional function called with the presentation key and a new Wallet post-construction to fund it before use.
582
- * @param stateSnapshot If provided, a previously saved snapshot of the wallet's state.
621
+ * @param walletBuilder A function that can build an underlying wallet instance for a profile.
622
+ * @param interactor An instance of UMPTokenInteractor.
623
+ * @param recoveryKeySaver A function to persist a new recovery key.
624
+ * @param passwordRetriever A function to request the user's password.
625
+ * @param newWalletFunder Optional function to fund a new wallet.
626
+ * @param stateSnapshot Optional previously saved state snapshot.
583
627
  */
584
628
  constructor(
585
629
  adminOriginator: OriginatorDomainNameStringUnder250Bytes,
586
- walletBuilder: (primaryKey: number[], privilegedKeyManager: PrivilegedKeyManager) => Promise<WalletInterface>,
630
+ walletBuilder: (
631
+ profilePrimaryKey: number[],
632
+ profilePrivilegedKeyManager: PrivilegedKeyManager,
633
+ profileId: number[]
634
+ ) => Promise<WalletInterface>,
587
635
  interactor: UMPTokenInteractor = new OverlayUMPTokenInteractor(),
588
636
  recoveryKeySaver: (key: number[]) => Promise<true>,
589
637
  passwordRetriever: (reason: string, test: (passwordCandidate: string) => boolean) => Promise<string>,
590
638
  newWalletFunder?: (
591
639
  presentationKey: number[],
592
- wallet: WalletInterface,
640
+ wallet: WalletInterface, // Default profile wallet
593
641
  adminOriginator: OriginatorDomainNameStringUnder250Bytes
594
642
  ) => Promise<void>,
595
643
  stateSnapshot?: number[]
@@ -603,18 +651,22 @@ export class CWIStyleWalletManager implements WalletInterface {
603
651
  this.newWalletFunder = newWalletFunder
604
652
 
605
653
  // If a saved snapshot is provided, attempt to load it.
654
+ // Note: loadSnapshot now returns a promise. We don't await it here,
655
+ // as the constructor must be synchronous. The caller should check
656
+ // `this.authenticated` after construction if a snapshot was provided.
606
657
  if (stateSnapshot) {
607
- this.loadSnapshot(stateSnapshot)
658
+ this.loadSnapshot(stateSnapshot).catch(err => {
659
+ console.error('Failed to load snapshot during construction:', err)
660
+ // Clear potentially partially loaded state
661
+ this.destroy()
662
+ })
608
663
  }
609
664
  }
610
665
 
666
+ // --- Authentication Methods ---
667
+
611
668
  /**
612
- * Provides the presentation key in an authentication mode that requires it.
613
- * If a UMP token is found based on the key's hash, this is an existing-user flow.
614
- * Otherwise, it is treated as a new-user flow.
615
- *
616
- * @param key The user's presentation key (32 bytes).
617
- * @throws {Error} if user is already authenticated, or if the current mode does not require a presentation key.
669
+ * Provides the presentation key.
618
670
  */
619
671
  async providePresentationKey(key: number[]): Promise<void> {
620
672
  if (this.authenticated) {
@@ -640,17 +692,7 @@ export class CWIStyleWalletManager implements WalletInterface {
640
692
  }
641
693
 
642
694
  /**
643
- * Provides the password in an authentication mode that requires it.
644
- *
645
- * - **Existing user**:
646
- * Decrypts the primary key using the provided password (and either the presentation key or recovery key, depending on the mode).
647
- * Then builds the underlying wallet, marking the user as authenticated.
648
- *
649
- * - **New user**:
650
- * Generates a new UMP token with fresh keys (primary, privileged, recovery). Publishes it on-chain and builds the wallet.
651
- *
652
- * @param password The user's password as a string.
653
- * @throws {Error} If the user is already authenticated, if the mode does not use a password, or if required keys are missing.
695
+ * Provides the password.
654
696
  */
655
697
  async providePassword(password: string): Promise<void> {
656
698
  if (this.authenticated) {
@@ -660,12 +702,10 @@ export class CWIStyleWalletManager implements WalletInterface {
660
702
  throw new Error('Password is not needed in this mode')
661
703
  }
662
704
 
663
- // If we detect an existing user flow:
664
705
  if (this.authenticationFlow === 'existing-user') {
706
+ // Existing user flow
665
707
  if (!this.currentUMPToken) {
666
- throw new Error(
667
- 'Provide either a presentation key or a recovery key first, depending on the authentication mode.'
668
- )
708
+ throw new Error('Provide presentation or recovery key first.')
669
709
  }
670
710
  const derivedPasswordKey = Hash.pbkdf2(
671
711
  Utils.toArray(password, 'utf8'),
@@ -675,140 +715,123 @@ export class CWIStyleWalletManager implements WalletInterface {
675
715
  'sha512'
676
716
  )
677
717
 
678
- if (this.authenticationMode === 'presentation-key-and-password') {
679
- if (!this.presentationKey) {
680
- throw new Error('No presentation key found!')
681
- }
718
+ let rootPrimaryKey: number[]
719
+ let rootPrivilegedKey: number[] | undefined // Only needed for recovery mode
682
720
 
683
- // Decrypt the primary key with XOR(presentationKey, derivedPasswordKey).
721
+ if (this.authenticationMode === 'presentation-key-and-password') {
722
+ if (!this.presentationKey) throw new Error('No presentation key found!')
684
723
  const xorKey = this.XOR(this.presentationKey, derivedPasswordKey)
685
- const decryptedPrimary = new SymmetricKey(xorKey).decrypt(
686
- this.currentUMPToken.passwordPresentationPrimary
687
- ) as number[]
688
-
689
- await this.buildUnderlying(decryptedPrimary)
724
+ rootPrimaryKey = new SymmetricKey(xorKey).decrypt(this.currentUMPToken.passwordPresentationPrimary) as number[]
690
725
  } else {
691
- // 'recovery-key-and-password' mode
692
- if (!this.recoveryKey) {
693
- throw new Error('No recovery key found!')
694
- }
695
-
696
- // Decrypt the primary key with XOR(recoveryKey, derivedPasswordKey).
726
+ // 'recovery-key-and-password'
727
+ if (!this.recoveryKey) throw new Error('No recovery key found!')
697
728
  const primaryDecryptionKey = this.XOR(this.recoveryKey, derivedPasswordKey)
698
- const decryptedPrimary = new SymmetricKey(primaryDecryptionKey).decrypt(
729
+ rootPrimaryKey = new SymmetricKey(primaryDecryptionKey).decrypt(
699
730
  this.currentUMPToken.passwordRecoveryPrimary
700
731
  ) as number[]
701
-
702
- // Decrypt the privileged key for immediate use.
703
- const privilegedDecryptionKey = this.XOR(decryptedPrimary, derivedPasswordKey)
704
- const decryptedPrivileged = new SymmetricKey(privilegedDecryptionKey).decrypt(
732
+ const privilegedDecryptionKey = this.XOR(rootPrimaryKey, derivedPasswordKey)
733
+ rootPrivilegedKey = new SymmetricKey(privilegedDecryptionKey).decrypt(
705
734
  this.currentUMPToken.passwordPrimaryPrivileged
706
735
  ) as number[]
707
-
708
- await this.buildUnderlying(decryptedPrimary, decryptedPrivileged)
736
+ }
737
+ // Build root infrastructure, load profiles, and switch to default profile initially
738
+ await this.setupRootInfrastructure(rootPrimaryKey, rootPrivilegedKey)
739
+ await this.switchProfile(this.activeProfileId)
740
+ } else {
741
+ // New user flow (only 'presentation-key-and-password')
742
+ if (this.authenticationMode !== 'presentation-key-and-password') {
743
+ throw new Error('New-user flow requires presentation key and password mode.')
744
+ }
745
+ if (!this.presentationKey) {
746
+ throw new Error('No presentation key provided for new-user flow.')
709
747
  }
710
748
 
711
- return
712
- }
713
-
714
- // Otherwise, handle new user flow (only valid in 'presentation-key-and-password').
715
- if (this.authenticationMode !== 'presentation-key-and-password') {
716
- throw new Error('New-user flow requires presentation key and password, not recovery key mode.')
717
- }
718
-
719
- if (!this.presentationKey) {
720
- throw new Error('No presentation key provided for new-user flow.')
721
- }
722
-
723
- // Generate new random keys/salt and create a new UMP token.
724
- const recoveryKey = Random(32)
725
- await this.recoveryKeySaver(recoveryKey)
726
-
727
- const passwordSalt = Random(32)
728
- const passwordKey = Hash.pbkdf2(Utils.toArray(password, 'utf8'), passwordSalt, PBKDF2_NUM_ROUNDS, 32, 'sha512')
729
-
730
- const primaryKey = Random(32)
731
- const privilegedKey = Random(32)
732
-
733
- // Build XOR-based symmetrical keys:
734
- const presentationPassword = new SymmetricKey(this.XOR(this.presentationKey, passwordKey))
735
- const presentationRecovery = new SymmetricKey(this.XOR(this.presentationKey, recoveryKey))
736
- const recoveryPassword = new SymmetricKey(this.XOR(recoveryKey, passwordKey))
737
- const primaryPassword = new SymmetricKey(this.XOR(primaryKey, passwordKey))
749
+ // Generate new keys/salt
750
+ const recoveryKey = Random(32)
751
+ await this.recoveryKeySaver(recoveryKey)
752
+ const passwordSalt = Random(32)
753
+ const passwordKey = Hash.pbkdf2(Utils.toArray(password, 'utf8'), passwordSalt, PBKDF2_NUM_ROUNDS, 32, 'sha512')
754
+ const rootPrimaryKey = Random(32)
755
+ const rootPrivilegedKey = Random(32)
756
+
757
+ // Build XOR keys
758
+ const presentationPassword = new SymmetricKey(this.XOR(this.presentationKey, passwordKey))
759
+ const presentationRecovery = new SymmetricKey(this.XOR(this.presentationKey, recoveryKey))
760
+ const recoveryPassword = new SymmetricKey(this.XOR(recoveryKey, passwordKey))
761
+ const primaryPassword = new SymmetricKey(this.XOR(rootPrimaryKey, passwordKey))
762
+
763
+ // Temp manager for encryption
764
+ const tempPrivilegedKeyManager = new PrivilegedKeyManager(async () => new PrivateKey(rootPrivilegedKey))
765
+
766
+ // Build new UMP token (no profiles initially)
767
+ const newToken: UMPToken = {
768
+ passwordSalt,
769
+ passwordPresentationPrimary: presentationPassword.encrypt(rootPrimaryKey) as number[],
770
+ passwordRecoveryPrimary: recoveryPassword.encrypt(rootPrimaryKey) as number[],
771
+ presentationRecoveryPrimary: presentationRecovery.encrypt(rootPrimaryKey) as number[],
772
+ passwordPrimaryPrivileged: primaryPassword.encrypt(rootPrivilegedKey) as number[],
773
+ presentationRecoveryPrivileged: presentationRecovery.encrypt(rootPrivilegedKey) as number[],
774
+ presentationHash: Hash.sha256(this.presentationKey),
775
+ recoveryHash: Hash.sha256(recoveryKey),
776
+ presentationKeyEncrypted: (
777
+ await tempPrivilegedKeyManager.encrypt({
778
+ plaintext: this.presentationKey,
779
+ protocolID: [2, 'admin key wrapping'],
780
+ keyID: '1'
781
+ })
782
+ ).ciphertext,
783
+ passwordKeyEncrypted: (
784
+ await tempPrivilegedKeyManager.encrypt({
785
+ plaintext: passwordKey,
786
+ protocolID: [2, 'admin key wrapping'],
787
+ keyID: '1'
788
+ })
789
+ ).ciphertext,
790
+ recoveryKeyEncrypted: (
791
+ await tempPrivilegedKeyManager.encrypt({
792
+ plaintext: recoveryKey,
793
+ protocolID: [2, 'admin key wrapping'],
794
+ keyID: '1'
795
+ })
796
+ ).ciphertext,
797
+ profilesEncrypted: undefined // No profiles yet
798
+ }
799
+ this.currentUMPToken = newToken
738
800
 
739
- // Temporarily create a privileged key manager for encrypting the keys in the token.
740
- const tempPrivilegedKeyManager = new PrivilegedKeyManager(async () => new PrivateKey(privilegedKey))
801
+ // Setup root infrastructure and switch to default profile
802
+ await this.setupRootInfrastructure(rootPrimaryKey)
803
+ await this.switchProfile(DEFAULT_PROFILE_ID)
741
804
 
742
- // Build the new UMP token:
743
- const newToken: UMPToken = {
744
- passwordSalt,
745
- passwordPresentationPrimary: presentationPassword.encrypt(primaryKey) as number[],
746
- passwordRecoveryPrimary: recoveryPassword.encrypt(primaryKey) as number[],
747
- presentationRecoveryPrimary: presentationRecovery.encrypt(primaryKey) as number[],
748
- passwordPrimaryPrivileged: primaryPassword.encrypt(privilegedKey) as number[],
749
- presentationRecoveryPrivileged: presentationRecovery.encrypt(privilegedKey) as number[],
750
- presentationHash: Hash.sha256(this.presentationKey),
751
- recoveryHash: Hash.sha256(recoveryKey),
752
- presentationKeyEncrypted: (
753
- await tempPrivilegedKeyManager.encrypt({
754
- plaintext: this.presentationKey,
755
- protocolID: [2, 'admin key wrapping'],
756
- keyID: '1'
757
- })
758
- ).ciphertext,
759
- passwordKeyEncrypted: (
760
- await tempPrivilegedKeyManager.encrypt({
761
- plaintext: passwordKey,
762
- protocolID: [2, 'admin key wrapping'],
763
- keyID: '1'
764
- })
765
- ).ciphertext,
766
- recoveryKeyEncrypted: (
767
- await tempPrivilegedKeyManager.encrypt({
768
- plaintext: recoveryKey,
769
- protocolID: [2, 'admin key wrapping'],
770
- keyID: '1'
771
- })
772
- ).ciphertext
773
- }
774
-
775
- // Now, we can create our new wallet!
776
- this.currentUMPToken = newToken
777
- await this.buildUnderlying(primaryKey)
805
+ // Fund the *default* wallet if funder provided
806
+ if (this.newWalletFunder && this.underlying) {
807
+ try {
808
+ await this.newWalletFunder(this.presentationKey, this.underlying, this.adminOriginator)
809
+ } catch (e) {
810
+ console.error('Error funding new wallet:', e)
811
+ // Decide if this should halt the process or just log
812
+ }
813
+ }
778
814
 
779
- // Before we do anything, the new wallet is most likely empty right now.
780
- // We want to provide a chance for someone to fund it, if they want.
781
- if (this.newWalletFunder) {
782
- try {
783
- await this.newWalletFunder(this.presentationKey, this.underlying!, this.adminOriginator)
784
- } catch (e) {
785
- // swallow error
786
- // TODO: Implement better error handling
787
- console.error(e)
815
+ // Publish the new UMP token *after* potentially funding
816
+ // We need the default profile wallet to sign the UMP creation TX
817
+ if (!this.underlying) {
818
+ throw new Error('Default profile wallet not built before attempting to publish UMP token.')
788
819
  }
820
+ this.currentUMPToken.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(
821
+ this.underlying, // Use the default profile wallet
822
+ this.adminOriginator,
823
+ newToken
824
+ )
789
825
  }
790
-
791
- // Publish the new UMP token on-chain and store the resulting outpoint.
792
- this.currentUMPToken.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(
793
- this.underlying!,
794
- this.adminOriginator,
795
- newToken
796
- )
797
826
  }
798
827
 
799
828
  /**
800
- * Provides the recovery key in an authentication flow that requires it.
801
- *
802
- * @param recoveryKey The user's recovery key (32 bytes).
803
- * @throws {Error} if user is already authenticated, if the mode does not use a recovery key,
804
- * or if a required presentation key is missing in "presentation-key-and-recovery-key" mode.
829
+ * Provides the recovery key.
805
830
  */
806
831
  async provideRecoveryKey(recoveryKey: number[]): Promise<void> {
807
832
  if (this.authenticated) {
808
833
  throw new Error('Already authenticated')
809
834
  }
810
-
811
- // Cannot use recovery key in a new-user flow
812
835
  if (this.authenticationFlow === 'new-user') {
813
836
  throw new Error('Do not submit recovery key in new-user flow')
814
837
  }
@@ -816,390 +839,605 @@ export class CWIStyleWalletManager implements WalletInterface {
816
839
  if (this.authenticationMode === 'presentation-key-and-password') {
817
840
  throw new Error('No recovery key required in this mode')
818
841
  } else if (this.authenticationMode === 'recovery-key-and-password') {
819
- // We will need to wait until the user provides the password as well.
842
+ // Wait for password
820
843
  const hash = Hash.sha256(recoveryKey)
821
844
  const token = await this.UMPTokenInteractor.findByRecoveryKeyHash(hash)
822
- if (!token) {
823
- throw new Error('No user found with this key')
824
- }
845
+ if (!token) throw new Error('No user found with this recovery key')
825
846
  this.recoveryKey = recoveryKey
826
847
  this.currentUMPToken = token
827
848
  } else {
828
849
  // 'presentation-key-and-recovery-key'
829
- if (!this.presentationKey) {
830
- throw new Error('Provide the presentation key first')
831
- }
832
- if (!this.currentUMPToken) {
833
- throw new Error('Current UMP token not found')
834
- }
850
+ if (!this.presentationKey) throw new Error('Provide the presentation key first')
851
+ if (!this.currentUMPToken) throw new Error('Current UMP token not found')
835
852
 
836
- // Decrypt the primary key:
837
853
  const xorKey = this.XOR(this.presentationKey, recoveryKey)
838
- const primaryKey = new SymmetricKey(xorKey).decrypt(this.currentUMPToken.presentationRecoveryPrimary) as number[]
839
-
840
- // Decrypt the privileged key (for account recovery).
841
- const privilegedKey = new SymmetricKey(xorKey).decrypt(
854
+ const rootPrimaryKey = new SymmetricKey(xorKey).decrypt(
855
+ this.currentUMPToken.presentationRecoveryPrimary
856
+ ) as number[]
857
+ const rootPrivilegedKey = new SymmetricKey(xorKey).decrypt(
842
858
  this.currentUMPToken.presentationRecoveryPrivileged
843
859
  ) as number[]
844
860
 
845
- await this.buildUnderlying(primaryKey, privilegedKey)
861
+ // Build root infrastructure, load profiles, switch to default
862
+ await this.setupRootInfrastructure(rootPrimaryKey, rootPrivilegedKey)
863
+ await this.switchProfile(this.activeProfileId)
846
864
  }
847
865
  }
848
866
 
867
+ // --- State Management Methods ---
868
+
849
869
  /**
850
- * Saves the current wallet state (including the current UMP token and primary key)
851
- * into an encrypted snapshot. This snapshot can be stored locally and later passed
852
- * to `loadSnapshot` to restore the wallet state without re-authenticating manually.
853
- *
854
- * @remarks
855
- * Storing the snapshot provides a fully authenticated state.
856
- * This **must** be securely stored (e.g. system keychain or encrypted file).
857
- * If attackers gain access to this snapshot, they can fully control the wallet.
870
+ * Saves the current wallet state (root key, UMP token, active profile) into an encrypted snapshot.
871
+ * Version 2 format: [1 byte version=2] + [32 byte snapshot key] + [16 byte activeProfileId] + [encrypted payload]
872
+ * Encrypted Payload: [32 byte rootPrimaryKey] + [varint token length + serialized UMP token]
858
873
  *
859
- * @returns An array of bytes representing the encrypted snapshot.
860
- * @throws {Error} if no primary key or token is currently set.
874
+ * @returns Encrypted snapshot bytes.
861
875
  */
862
876
  saveSnapshot(): number[] {
863
- if (!this.primaryKey || !this.currentUMPToken) {
864
- throw new Error('No primary key or current UMP token set')
877
+ if (!this.rootPrimaryKey || !this.currentUMPToken) {
878
+ throw new Error('No root primary key or current UMP token set')
865
879
  }
866
880
 
867
- // Generate a random snapshot encryption key:
868
881
  const snapshotKey = Random(32)
869
-
870
- // Serialize the relevant data to a preimage buffer:
871
882
  const snapshotPreimageWriter = new Utils.Writer()
872
883
 
873
- // Write the primary key (32 bytes):
874
- snapshotPreimageWriter.write(this.primaryKey)
884
+ // Write root primary key
885
+ snapshotPreimageWriter.write(this.rootPrimaryKey)
875
886
 
876
- // Write the serialized UMP token:
887
+ // Write serialized UMP token (must have outpoint)
888
+ if (!this.currentUMPToken.currentOutpoint) {
889
+ throw new Error('UMP token cannot be saved without a current outpoint.')
890
+ }
877
891
  const serializedToken = this.serializeUMPToken(this.currentUMPToken)
892
+ snapshotPreimageWriter.writeVarIntNum(serializedToken.length)
878
893
  snapshotPreimageWriter.write(serializedToken)
879
894
 
880
- // Encrypt the combined data with the snapshotKey:
895
+ // Encrypt the payload
881
896
  const snapshotPreimage = snapshotPreimageWriter.toArray()
882
897
  const snapshotPayload = new SymmetricKey(snapshotKey).encrypt(snapshotPreimage) as number[]
883
898
 
884
- // Build the final snapshot structure: [snapshotKey (32 bytes) + encryptedPayload]
899
+ // Build final snapshot (Version 2)
885
900
  const snapshotWriter = new Utils.Writer()
901
+ snapshotWriter.writeUInt8(2) // Version
886
902
  snapshotWriter.write(snapshotKey)
887
- snapshotWriter.write(snapshotPayload)
903
+ snapshotWriter.write(this.activeProfileId) // Active profile ID
904
+ snapshotWriter.write(snapshotPayload) // Encrypted data
888
905
 
889
906
  return snapshotWriter.toArray()
890
907
  }
891
908
 
892
909
  /**
893
- * Loads a previously saved state snapshot (e.g. from `saveSnapshot`).
894
- * Upon success, the wallet becomes authenticated without needing to re-enter keys.
910
+ * Loads a previously saved state snapshot. Restores root key, UMP token, profiles, and active profile.
911
+ * Handles Version 1 (legacy) and Version 2 formats.
895
912
  *
896
- * @param snapshot An array of bytes that was previously produced by `saveSnapshot`.
897
- * @throws {Error} If the snapshot format is invalid or decryption fails.
913
+ * @param snapshot Encrypted snapshot bytes.
898
914
  */
899
915
  async loadSnapshot(snapshot: number[]): Promise<void> {
900
916
  try {
901
917
  const reader = new Utils.Reader(snapshot)
918
+ const version = reader.readUInt8()
919
+
920
+ let snapshotKey: number[]
921
+ let encryptedPayload: number[]
922
+ let activeProfileId = DEFAULT_PROFILE_ID // Default for V1
923
+
924
+ if (version === 1) {
925
+ snapshotKey = reader.read(32)
926
+ encryptedPayload = reader.read()
927
+ } else if (version === 2) {
928
+ snapshotKey = reader.read(32)
929
+ activeProfileId = reader.read(16) // Read active profile ID
930
+ encryptedPayload = reader.read()
931
+ } else {
932
+ throw new Error(`Unsupported snapshot version: ${version}`)
933
+ }
902
934
 
903
- // First 32 bytes is the snapshotKey:
904
- const snapshotKey = reader.read(32)
905
-
906
- // The rest is the encrypted payload:
907
- const encryptedPayload = reader.read()
908
-
909
- // Decrypt the payload:
935
+ // Decrypt payload
910
936
  const decryptedPayload = new SymmetricKey(snapshotKey).decrypt(encryptedPayload) as number[]
911
-
912
937
  const payloadReader = new Utils.Reader(decryptedPayload)
913
938
 
914
- // Read the primary key (32 bytes):
915
- const primaryKey = payloadReader.read(32)
939
+ // Read root primary key
940
+ const rootPrimaryKey = payloadReader.read(32)
916
941
 
917
- // Read the remainder as the serialized UMP token:
918
- const tokenBytes = payloadReader.read()
942
+ // Read serialized UMP token
943
+ const tokenLen = payloadReader.readVarIntNum()
944
+ const tokenBytes = payloadReader.read(tokenLen)
919
945
  const token = this.deserializeUMPToken(tokenBytes)
920
946
 
921
- // Assign and build:
947
+ // Assign loaded data
922
948
  this.currentUMPToken = token
923
- await this.buildUnderlying(primaryKey)
949
+
950
+ // Setup root infrastructure, load profiles, and switch to the loaded active profile
951
+ await this.setupRootInfrastructure(rootPrimaryKey) // Will automatically load profiles
952
+ await this.switchProfile(activeProfileId) // Switch to the profile saved in the snapshot
953
+
954
+ this.authenticationFlow = 'existing-user' // Loading implies existing user
924
955
  } catch (error) {
956
+ this.destroy() // Clear state on error
925
957
  throw new Error(`Failed to load snapshot: ${(error as Error).message}`)
926
958
  }
927
959
  }
928
960
 
929
961
  /**
930
- * Destroys the underlying wallet, returning to a default state
962
+ * Destroys the wallet state, clearing keys, tokens, and profiles.
931
963
  */
932
964
  destroy(): void {
933
965
  this.underlying = undefined
934
- this.underlyingPrivilegedKeyManager = undefined
966
+ this.rootPrivilegedKeyManager = undefined
935
967
  this.authenticated = false
936
- this.primaryKey = undefined
968
+ this.rootPrimaryKey = undefined
937
969
  this.currentUMPToken = undefined
938
970
  this.presentationKey = undefined
939
971
  this.recoveryKey = undefined
972
+ this.profiles = []
973
+ this.activeProfileId = DEFAULT_PROFILE_ID
940
974
  this.authenticationMode = 'presentation-key-and-password'
941
975
  this.authenticationFlow = 'new-user'
942
976
  }
943
977
 
978
+ // --- Profile Management Methods ---
979
+
944
980
  /**
945
- * Changes the user's password, re-wrapping the primary and privileged keys with the new password factor.
946
- *
947
- * @param newPassword The user's new password as a string.
948
- * @throws {Error} If the user is not authenticated, or if underlying token references are missing.
981
+ * Lists all available profiles, including the default profile.
982
+ * @returns Array of profile info objects, including an 'active' flag.
949
983
  */
950
- async changePassword(newPassword: string): Promise<void> {
984
+ listProfiles(): Array<{ id: number[]; name: string; createdAt: number | null; active: boolean }> {
951
985
  if (!this.authenticated) {
952
986
  throw new Error('Not authenticated.')
953
987
  }
954
- if (!this.currentUMPToken) {
955
- throw new Error('No UMP token to update.')
988
+ const profileList = [
989
+ // Default profile
990
+ {
991
+ id: DEFAULT_PROFILE_ID,
992
+ name: 'default',
993
+ createdAt: null, // Default profile doesn't have a creation timestamp in the same way
994
+ active: this.activeProfileId.every(x => x === 0)
995
+ },
996
+ // Other profiles
997
+ ...this.profiles.map(p => ({
998
+ id: p.id,
999
+ name: p.name,
1000
+ createdAt: p.createdAt,
1001
+ active: this.activeProfileId.every((x, i) => x === p.id[i])
1002
+ }))
1003
+ ]
1004
+ return profileList
1005
+ }
1006
+
1007
+ /**
1008
+ * Adds a new profile with the given name.
1009
+ * Generates necessary pads and updates the UMP token.
1010
+ * Does not switch to the new profile automatically.
1011
+ *
1012
+ * @param name The desired name for the new profile.
1013
+ * @returns The ID of the newly created profile.
1014
+ */
1015
+ async addProfile(name: string): Promise<number[]> {
1016
+ if (!this.authenticated || !this.rootPrimaryKey || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
1017
+ throw new Error('Wallet not fully initialized or authenticated.')
1018
+ }
1019
+
1020
+ // Ensure name is unique (including 'default')
1021
+ if (name === 'default' || this.profiles.some(p => p.name.toLowerCase() === name.toLowerCase())) {
1022
+ throw new Error(`Profile name "${name}" is already in use.`)
1023
+ }
1024
+
1025
+ const newProfile: Profile = {
1026
+ name,
1027
+ id: Random(16),
1028
+ primaryPad: Random(32),
1029
+ privilegedPad: Random(32),
1030
+ createdAt: Math.floor(Date.now() / 1000)
1031
+ }
1032
+
1033
+ this.profiles.push(newProfile)
1034
+
1035
+ // Update the UMP token with the new profile list
1036
+ await this.updateAuthFactors(
1037
+ this.currentUMPToken.passwordSalt,
1038
+ // Need to re-derive/decrypt factors needed for re-encryption
1039
+ await this.getFactor('passwordKey'),
1040
+ await this.getFactor('presentationKey'),
1041
+ await this.getFactor('recoveryKey'),
1042
+ this.rootPrimaryKey,
1043
+ await this.getFactor('privilegedKey', true), // Get ROOT privileged key
1044
+ this.profiles // Pass the updated profile list
1045
+ )
1046
+
1047
+ return newProfile.id
1048
+ }
1049
+
1050
+ /**
1051
+ * Deletes a profile by its ID.
1052
+ * Cannot delete the default profile. If the active profile is deleted,
1053
+ * it switches back to the default profile.
1054
+ *
1055
+ * @param profileId The 16-byte ID of the profile to delete.
1056
+ */
1057
+ async deleteProfile(profileId: number[]): Promise<void> {
1058
+ if (!this.authenticated || !this.rootPrimaryKey || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
1059
+ throw new Error('Wallet not fully initialized or authenticated.')
1060
+ }
1061
+ if (profileId.every(x => x === 0)) {
1062
+ throw new Error('Cannot delete the default profile.')
1063
+ }
1064
+
1065
+ const profileIndex = this.profiles.findIndex(p => p.id.every((x, i) => x === profileId[i]))
1066
+ if (profileIndex === -1) {
1067
+ throw new Error('Profile not found.')
1068
+ }
1069
+
1070
+ // Remove the profile
1071
+ this.profiles.splice(profileIndex, 1)
1072
+
1073
+ // If the deleted profile was active, switch to default
1074
+ if (this.activeProfileId.every((x, i) => x === profileId[i])) {
1075
+ await this.switchProfile(DEFAULT_PROFILE_ID) // This rebuilds the wallet
1076
+ }
1077
+
1078
+ // Update the UMP token
1079
+ await this.updateAuthFactors(
1080
+ this.currentUMPToken.passwordSalt,
1081
+ await this.getFactor('passwordKey'),
1082
+ await this.getFactor('presentationKey'),
1083
+ await this.getFactor('recoveryKey'),
1084
+ this.rootPrimaryKey,
1085
+ await this.getFactor('privilegedKey', true), // Get ROOT privileged key
1086
+ this.profiles // Pass updated list
1087
+ )
1088
+ }
1089
+
1090
+ /**
1091
+ * Switches the active profile. This re-derives keys and rebuilds the underlying wallet.
1092
+ *
1093
+ * @param profileId The 16-byte ID of the profile to switch to (use DEFAULT_PROFILE_ID for default).
1094
+ */
1095
+ async switchProfile(profileId: number[]): Promise<void> {
1096
+ if (!this.authenticated || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
1097
+ throw new Error('Cannot switch profile: Wallet not authenticated or root keys missing.')
1098
+ }
1099
+
1100
+ let profilePrimaryKey: number[]
1101
+ let profilePrivilegedPad: number[] | undefined // Pad for the target profile
1102
+
1103
+ if (profileId.every(x => x === 0)) {
1104
+ // Switching to default profile
1105
+ profilePrimaryKey = this.rootPrimaryKey
1106
+ profilePrivilegedPad = undefined // No pad for default
1107
+ this.activeProfileId = DEFAULT_PROFILE_ID
1108
+ } else {
1109
+ // Switching to a non-default profile
1110
+ const profile = this.profiles.find(p => p.id.every((x, i) => x === profileId[i]))
1111
+ if (!profile) {
1112
+ throw new Error('Profile not found.')
1113
+ }
1114
+ profilePrimaryKey = this.XOR(this.rootPrimaryKey, profile.primaryPad)
1115
+ profilePrivilegedPad = profile.privilegedPad
1116
+ this.activeProfileId = profileId
1117
+ }
1118
+
1119
+ // Create a *profile-specific* PrivilegedKeyManager.
1120
+ // It uses the ROOT manager internally but applies the profile's pad.
1121
+ const profilePrivilegedKeyManager = new PrivilegedKeyManager(async (reason: string) => {
1122
+ // Request the ROOT privileged key using the root manager
1123
+ const rootPrivileged: PrivateKey = await (this.rootPrivilegedKeyManager as any).getPrivilegedKey(reason)
1124
+ const rootPrivilegedBytes = rootPrivileged.toArray()
1125
+
1126
+ // Apply the profile's pad if applicable
1127
+ const profilePrivilegedBytes = profilePrivilegedPad
1128
+ ? this.XOR(rootPrivilegedBytes, profilePrivilegedPad)
1129
+ : rootPrivilegedBytes
1130
+
1131
+ return new PrivateKey(profilePrivilegedBytes)
1132
+ })
1133
+
1134
+ // Build the underlying wallet for the specific profile
1135
+ this.underlying = await this.walletBuilder(
1136
+ profilePrimaryKey,
1137
+ profilePrivilegedKeyManager, // Pass the profile-specific manager
1138
+ this.activeProfileId // Pass the ID of the profile being activated
1139
+ )
1140
+ }
1141
+
1142
+ // --- Key Management Methods ---
1143
+
1144
+ /**
1145
+ * Changes the user's password. Re-wraps keys and updates the UMP token.
1146
+ */
1147
+ async changePassword(newPassword: string): Promise<void> {
1148
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
1149
+ throw new Error('Not authenticated or missing required data.')
956
1150
  }
957
1151
 
958
1152
  const passwordSalt = Random(32)
959
- const passwordKey = Hash.pbkdf2(Utils.toArray(newPassword, 'utf8'), passwordSalt, PBKDF2_NUM_ROUNDS, 32, 'sha512')
960
-
961
- // Decrypt existing factors via the privileged key manager:
962
- const recoveryKey = (
963
- await this.underlyingPrivilegedKeyManager!.decrypt({
964
- ciphertext: this.currentUMPToken.recoveryKeyEncrypted,
965
- protocolID: [2, 'admin key wrapping'],
966
- keyID: '1'
967
- })
968
- ).plaintext
969
- const presentationKey = (
970
- await this.underlyingPrivilegedKeyManager!.decrypt({
971
- ciphertext: this.currentUMPToken.presentationKeyEncrypted,
972
- protocolID: [2, 'admin key wrapping'],
973
- keyID: '1'
974
- })
975
- ).plaintext
976
- const privilegedKey = new SymmetricKey(this.XOR(presentationKey, recoveryKey)).decrypt(
977
- this.currentUMPToken.presentationRecoveryPrivileged
978
- ) as number[]
1153
+ const newPasswordKey = Hash.pbkdf2(
1154
+ Utils.toArray(newPassword, 'utf8'),
1155
+ passwordSalt,
1156
+ PBKDF2_NUM_ROUNDS,
1157
+ 32,
1158
+ 'sha512'
1159
+ )
1160
+
1161
+ // Decrypt existing factors needed for re-encryption, using the *root* privileged key manager
1162
+ const recoveryKey = await this.getFactor('recoveryKey')
1163
+ const presentationKey = await this.getFactor('presentationKey')
1164
+ const rootPrivilegedKey = await this.getFactor('privilegedKey', true) // Get ROOT privileged key
979
1165
 
980
1166
  await this.updateAuthFactors(
981
1167
  passwordSalt,
982
- passwordKey,
1168
+ newPasswordKey,
983
1169
  presentationKey,
984
1170
  recoveryKey,
985
- this.primaryKey!,
986
- privilegedKey
1171
+ this.rootPrimaryKey,
1172
+ rootPrivilegedKey, // Pass the explicitly fetched root key
1173
+ this.profiles // Preserve existing profiles
987
1174
  )
988
1175
  }
989
1176
 
990
1177
  /**
991
- * Retrieves the current recovery key.
992
- *
993
- * @throws {Error} If the user is not authenticated, or if underlying token references are missing.
1178
+ * Retrieves the current recovery key. Requires privileged access.
994
1179
  */
995
1180
  async getRecoveryKey(): Promise<number[]> {
996
- if (!this.authenticated) {
997
- throw new Error('Not authenticated.')
1181
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
1182
+ throw new Error('Not authenticated or missing required data.')
998
1183
  }
999
- if (!this.currentUMPToken) {
1000
- throw new Error('No UMP token!')
1001
- }
1002
- return (
1003
- await this.underlyingPrivilegedKeyManager!.decrypt({
1004
- ciphertext: this.currentUMPToken.recoveryKeyEncrypted,
1005
- protocolID: [2, 'admin key wrapping'],
1006
- keyID: '1'
1007
- })
1008
- ).plaintext
1184
+ return this.getFactor('recoveryKey')
1009
1185
  }
1010
1186
 
1011
1187
  /**
1012
- * Changes the user's recovery key, prompting the user to save the new key.
1013
- *
1014
- * @throws {Error} If the user is not authenticated, or if underlying token references are missing.
1188
+ * Changes the user's recovery key. Prompts user to save the new key.
1015
1189
  */
1016
1190
  async changeRecoveryKey(): Promise<void> {
1017
- if (!this.authenticated) {
1018
- throw new Error('Not authenticated.')
1019
- }
1020
- if (!this.currentUMPToken) {
1021
- throw new Error('No UMP token to update.')
1191
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
1192
+ throw new Error('Not authenticated or missing required data.')
1022
1193
  }
1023
1194
 
1024
- // Decrypt existing password/presentation keys via the privileged key manager:
1025
- const passwordKey = (
1026
- await this.underlyingPrivilegedKeyManager!.decrypt({
1027
- ciphertext: this.currentUMPToken.passwordKeyEncrypted,
1028
- protocolID: [2, 'admin key wrapping'],
1029
- keyID: '1'
1030
- })
1031
- ).plaintext
1032
- const presentationKey = (
1033
- await this.underlyingPrivilegedKeyManager!.decrypt({
1034
- ciphertext: this.currentUMPToken.presentationKeyEncrypted,
1035
- protocolID: [2, 'admin key wrapping'],
1036
- keyID: '1'
1037
- })
1038
- ).plaintext
1039
- const privilegedKey = new SymmetricKey(this.XOR(passwordKey, this.primaryKey!)).decrypt(
1040
- this.currentUMPToken.passwordPrimaryPrivileged
1041
- ) as number[]
1195
+ // Decrypt existing factors needed
1196
+ const passwordKey = await this.getFactor('passwordKey')
1197
+ const presentationKey = await this.getFactor('presentationKey')
1198
+ const rootPrivilegedKey = await this.getFactor('privilegedKey', true) // Get ROOT privileged key
1042
1199
 
1043
- const recoveryKey = Random(32)
1044
- await this.recoveryKeySaver(recoveryKey)
1200
+ // Generate and save new recovery key
1201
+ const newRecoveryKey = Random(32)
1202
+ await this.recoveryKeySaver(newRecoveryKey)
1045
1203
 
1046
1204
  await this.updateAuthFactors(
1047
1205
  this.currentUMPToken.passwordSalt,
1048
1206
  passwordKey,
1049
1207
  presentationKey,
1050
- recoveryKey,
1051
- this.primaryKey!,
1052
- privilegedKey
1208
+ newRecoveryKey, // Use the new key
1209
+ this.rootPrimaryKey,
1210
+ rootPrivilegedKey,
1211
+ this.profiles // Preserve profiles
1053
1212
  )
1054
1213
  }
1055
1214
 
1056
1215
  /**
1057
1216
  * Changes the user's presentation key.
1058
- *
1059
- * @param presentationKey The new presentation key (32 bytes).
1060
- * @throws {Error} If the user is not authenticated, or if underlying token references are missing.
1061
1217
  */
1062
- async changePresentationKey(presentationKey: number[]): Promise<void> {
1063
- if (!this.authenticated) {
1064
- throw new Error('Not authenticated.')
1218
+ async changePresentationKey(newPresentationKey: number[]): Promise<void> {
1219
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
1220
+ throw new Error('Not authenticated or missing required data.')
1065
1221
  }
1066
- if (!this.currentUMPToken) {
1067
- throw new Error('No UMP token to update.')
1222
+ if (newPresentationKey.length !== 32) {
1223
+ throw new Error('Presentation key must be 32 bytes.')
1068
1224
  }
1069
1225
 
1070
- // Decrypt existing password/recovery keys via the privileged key manager:
1071
- const recoveryKey = (
1072
- await this.underlyingPrivilegedKeyManager!.decrypt({
1073
- ciphertext: this.currentUMPToken.recoveryKeyEncrypted,
1074
- protocolID: [2, 'admin key wrapping'],
1075
- keyID: '1'
1076
- })
1077
- ).plaintext
1078
- const passwordKey = (
1079
- await this.underlyingPrivilegedKeyManager!.decrypt({
1080
- ciphertext: this.currentUMPToken.passwordKeyEncrypted,
1081
- protocolID: [2, 'admin key wrapping'],
1082
- keyID: '1'
1083
- })
1084
- ).plaintext
1085
- const privilegedKey = new SymmetricKey(this.XOR(passwordKey, this.primaryKey!)).decrypt(
1086
- this.currentUMPToken.passwordPrimaryPrivileged
1087
- ) as number[]
1226
+ // Decrypt existing factors
1227
+ const recoveryKey = await this.getFactor('recoveryKey')
1228
+ const passwordKey = await this.getFactor('passwordKey')
1229
+ const rootPrivilegedKey = await this.getFactor('privilegedKey', true) // Get ROOT privileged key
1088
1230
 
1089
1231
  await this.updateAuthFactors(
1090
1232
  this.currentUMPToken.passwordSalt,
1091
1233
  passwordKey,
1092
- presentationKey,
1234
+ newPresentationKey, // Use the new key
1093
1235
  recoveryKey,
1094
- this.primaryKey!,
1095
- privilegedKey
1236
+ this.rootPrimaryKey,
1237
+ rootPrivilegedKey,
1238
+ this.profiles // Preserve profiles
1096
1239
  )
1240
+ // Update the temporarily stored key if it was set
1241
+ if (this.presentationKey) {
1242
+ this.presentationKey = newPresentationKey
1243
+ }
1097
1244
  }
1098
1245
 
1246
+ // --- Internal Helper Methods ---
1247
+
1099
1248
  /**
1100
- * Internal helper to recompute a UMP token with updated authentication factors and consume the old token on-chain.
1101
- *
1102
- * @param passwordSalt The PBKDF2 salt for the new password factor.
1103
- * @param passwordKey The PBKDF2-derived password key (32 bytes).
1104
- * @param presentationKey The new or existing presentation key (32 bytes).
1105
- * @param recoveryKey The new or existing recovery key (32 bytes).
1106
- * @param primaryKey The user's primary key for re-wrapping.
1107
- * @param privilegedKey The user's privileged key for re-wrapping.
1108
- * @throws {Error} If the user is not authenticated or if keys are unavailable.
1249
+ * Performs XOR operation on two byte arrays.
1250
+ */
1251
+ private XOR(n1: number[], n2: number[]): number[] {
1252
+ if (n1.length !== n2.length) {
1253
+ // Provide more context in error
1254
+ throw new Error(`XOR length mismatch: ${n1.length} vs ${n2.length}`)
1255
+ }
1256
+ const r = new Array<number>(n1.length)
1257
+ for (let i = 0; i < n1.length; i++) {
1258
+ r[i] = n1[i] ^ n2[i]
1259
+ }
1260
+ return r
1261
+ }
1262
+
1263
+ /**
1264
+ * Helper to decrypt a specific factor (key) stored encrypted in the UMP token.
1265
+ * Requires the root privileged key manager.
1266
+ * @param factorName Name of the factor to decrypt ('passwordKey', 'presentationKey', 'recoveryKey', 'privilegedKey').
1267
+ * @param getRoot If true and factorName is 'privilegedKey', returns the root privileged key bytes directly.
1268
+ * @returns The decrypted key bytes.
1269
+ */
1270
+ private async getFactor(
1271
+ factorName: 'passwordKey' | 'presentationKey' | 'recoveryKey' | 'privilegedKey',
1272
+ getRoot: boolean = false
1273
+ ): Promise<number[]> {
1274
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
1275
+ throw new Error(`Cannot get factor "${factorName}": Wallet not ready.`)
1276
+ }
1277
+
1278
+ const protocolID: [0 | 1 | 2, string] = [2, 'admin key wrapping'] // Protocol used for encrypting factors
1279
+ const keyID = '1' // Key ID used
1280
+
1281
+ try {
1282
+ switch (factorName) {
1283
+ case 'passwordKey':
1284
+ return (
1285
+ await this.rootPrivilegedKeyManager.decrypt({
1286
+ ciphertext: this.currentUMPToken.passwordKeyEncrypted,
1287
+ protocolID,
1288
+ keyID
1289
+ })
1290
+ ).plaintext
1291
+ case 'presentationKey':
1292
+ return (
1293
+ await this.rootPrivilegedKeyManager.decrypt({
1294
+ ciphertext: this.currentUMPToken.presentationKeyEncrypted,
1295
+ protocolID,
1296
+ keyID
1297
+ })
1298
+ ).plaintext
1299
+ case 'recoveryKey':
1300
+ return (
1301
+ await this.rootPrivilegedKeyManager.decrypt({
1302
+ ciphertext: this.currentUMPToken.recoveryKeyEncrypted,
1303
+ protocolID,
1304
+ keyID
1305
+ })
1306
+ ).plaintext
1307
+ case 'privilegedKey': {
1308
+ // This needs careful handling based on whether the ROOT or PROFILE key is needed.
1309
+ // This helper is mostly used for UMP updates, which need the ROOT key.
1310
+ // We retrieve the PrivateKey object first.
1311
+ const pk = await (this.rootPrivilegedKeyManager as any).getPrivilegedKey('UMP token update', true) // Force retrieval of root key
1312
+ return pk.toArray() // Return bytes
1313
+ }
1314
+ default:
1315
+ throw new Error(`Unknown factor name: ${factorName}`)
1316
+ }
1317
+ } catch (error) {
1318
+ console.error(`Error decrypting factor ${factorName}:`, error)
1319
+ throw new Error(`Failed to decrypt factor "${factorName}": ${(error as Error).message}`)
1320
+ }
1321
+ }
1322
+
1323
+ /**
1324
+ * Recomputes UMP token fields with updated factors and profiles, then publishes the update.
1325
+ * This operation requires the *root* privileged key and the *default* profile wallet.
1109
1326
  */
1110
1327
  private async updateAuthFactors(
1111
1328
  passwordSalt: number[],
1112
1329
  passwordKey: number[],
1113
1330
  presentationKey: number[],
1114
1331
  recoveryKey: number[],
1115
- primaryKey: number[],
1116
- privilegedKey: number[]
1332
+ rootPrimaryKey: number[],
1333
+ rootPrivilegedKey: number[], // Explicitly pass the root key bytes
1334
+ profiles?: Profile[] // Pass current/new profiles list
1117
1335
  ): Promise<void> {
1118
- if (!this.authenticated || !this.primaryKey || !this.currentUMPToken) {
1119
- throw new Error('Wallet is not properly authenticated or missing data.')
1336
+ if (!this.authenticated || !this.rootPrimaryKey || !this.currentUMPToken) {
1337
+ throw new Error('Wallet is not properly authenticated or missing data for update.')
1338
+ }
1339
+ // Ensure we have the OLD token to consume
1340
+ const oldTokenToConsume = { ...this.currentUMPToken }
1341
+ if (!oldTokenToConsume.currentOutpoint) {
1342
+ throw new Error('Cannot update UMP token: Old token has no outpoint.')
1120
1343
  }
1121
1344
 
1122
- // Derive symmetrical encryption keys via XOR:
1345
+ // Derive symmetrical encryption keys using XOR for the *root* keys
1123
1346
  const presentationPassword = new SymmetricKey(this.XOR(presentationKey, passwordKey))
1124
1347
  const presentationRecovery = new SymmetricKey(this.XOR(presentationKey, recoveryKey))
1125
1348
  const recoveryPassword = new SymmetricKey(this.XOR(recoveryKey, passwordKey))
1126
- const primaryPassword = new SymmetricKey(this.XOR(this.primaryKey, passwordKey))
1127
-
1128
- // Build a temporary privileged key manager just to encrypt the new fields:
1129
- const tempPrivilegedKeyManager = new PrivilegedKeyManager(async () => new PrivateKey(privilegedKey))
1349
+ const primaryPassword = new SymmetricKey(this.XOR(rootPrimaryKey, passwordKey)) // Use rootPrimaryKey
1350
+
1351
+ // Build a temporary privileged key manager using the explicit ROOT privileged key
1352
+ const tempRootPrivilegedKeyManager = new PrivilegedKeyManager(async () => new PrivateKey(rootPrivilegedKey))
1353
+
1354
+ // Encrypt profiles if provided
1355
+ let profilesEncrypted: number[] | undefined
1356
+ if (profiles && profiles.length > 0) {
1357
+ const profilesJson = JSON.stringify(profiles)
1358
+ const profilesBytes = Utils.toArray(profilesJson, 'utf8')
1359
+ profilesEncrypted = (
1360
+ await tempRootPrivilegedKeyManager.encrypt({
1361
+ plaintext: profilesBytes,
1362
+ protocolID: [2, 'admin profile wrapping'], // Separate protocol for profiles
1363
+ keyID: '1'
1364
+ })
1365
+ ).ciphertext
1366
+ }
1130
1367
 
1131
- // Construct the new UMP token:
1132
- const newToken: UMPToken = {
1368
+ // Construct the new UMP token data
1369
+ const newTokenData: UMPToken = {
1133
1370
  passwordSalt,
1134
- passwordPresentationPrimary: presentationPassword.encrypt(this.primaryKey) as number[],
1135
- passwordRecoveryPrimary: recoveryPassword.encrypt(this.primaryKey) as number[],
1136
- presentationRecoveryPrimary: presentationRecovery.encrypt(this.primaryKey) as number[],
1137
- passwordPrimaryPrivileged: primaryPassword.encrypt(privilegedKey) as number[],
1138
- presentationRecoveryPrivileged: presentationRecovery.encrypt(privilegedKey) as number[],
1371
+ passwordPresentationPrimary: presentationPassword.encrypt(rootPrimaryKey) as number[],
1372
+ passwordRecoveryPrimary: recoveryPassword.encrypt(rootPrimaryKey) as number[],
1373
+ presentationRecoveryPrimary: presentationRecovery.encrypt(rootPrimaryKey) as number[],
1374
+ passwordPrimaryPrivileged: primaryPassword.encrypt(rootPrivilegedKey) as number[],
1375
+ presentationRecoveryPrivileged: presentationRecovery.encrypt(rootPrivilegedKey) as number[],
1139
1376
  presentationHash: Hash.sha256(presentationKey),
1140
1377
  recoveryHash: Hash.sha256(recoveryKey),
1141
1378
  presentationKeyEncrypted: (
1142
- await tempPrivilegedKeyManager.encrypt({
1379
+ await tempRootPrivilegedKeyManager.encrypt({
1143
1380
  plaintext: presentationKey,
1144
1381
  protocolID: [2, 'admin key wrapping'],
1145
1382
  keyID: '1'
1146
1383
  })
1147
1384
  ).ciphertext,
1148
1385
  passwordKeyEncrypted: (
1149
- await tempPrivilegedKeyManager.encrypt({
1386
+ await tempRootPrivilegedKeyManager.encrypt({
1150
1387
  plaintext: passwordKey,
1151
1388
  protocolID: [2, 'admin key wrapping'],
1152
1389
  keyID: '1'
1153
1390
  })
1154
1391
  ).ciphertext,
1155
1392
  recoveryKeyEncrypted: (
1156
- await tempPrivilegedKeyManager.encrypt({
1393
+ await tempRootPrivilegedKeyManager.encrypt({
1157
1394
  plaintext: recoveryKey,
1158
1395
  protocolID: [2, 'admin key wrapping'],
1159
1396
  keyID: '1'
1160
1397
  })
1161
- ).ciphertext
1398
+ ).ciphertext,
1399
+ profilesEncrypted // Add encrypted profiles
1400
+ // currentOutpoint will be set after publishing
1162
1401
  }
1163
1402
 
1164
- // Publish the new token on-chain and consume the old one:
1165
- newToken.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(
1166
- this.underlying!,
1167
- this.adminOriginator,
1168
- newToken,
1169
- this.currentUMPToken
1170
- )
1171
- this.currentUMPToken = newToken
1172
- }
1403
+ // We need the wallet built for the DEFAULT profile to publish the UMP token.
1404
+ // If the current active profile is not default, temporarily switch, publish, then switch back.
1405
+ const currentActiveId = this.activeProfileId
1406
+ let walletToUse: WalletInterface | undefined = this.underlying
1173
1407
 
1174
- /**
1175
- * A helper function to XOR two equal-length byte arrays.
1176
- *
1177
- * @param n1 The first byte array.
1178
- * @param n2 The second byte array.
1179
- * @returns A new byte array which is the element-wise XOR of the two inputs.
1180
- * @throws {Error} if the two arrays are not the same length.
1181
- */
1182
- private XOR(n1: number[], n2: number[]): number[] {
1183
- if (n1.length !== n2.length) {
1184
- throw new Error('lengths mismatch')
1408
+ if (currentActiveId.every(x => x === 0)) {
1409
+ console.log('Temporarily switching to default profile to update UMP token...')
1410
+ await this.switchProfile(DEFAULT_PROFILE_ID) // This rebuilds this.underlying
1411
+ walletToUse = this.underlying
1185
1412
  }
1186
- const r = new Array<number>(n1.length)
1187
- for (let i = 0; i < n1.length; i++) {
1188
- r[i] = n1[i] ^ n2[i]
1413
+
1414
+ if (!walletToUse) {
1415
+ throw new Error('Default profile wallet could not be activated for UMP token update.')
1416
+ }
1417
+
1418
+ // Publish the new token on-chain, consuming the old one
1419
+ try {
1420
+ newTokenData.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(
1421
+ walletToUse, // Use the (potentially temporarily activated) default profile wallet
1422
+ this.adminOriginator,
1423
+ newTokenData,
1424
+ oldTokenToConsume // Consume the previous token
1425
+ )
1426
+ // Update the manager's state
1427
+ this.currentUMPToken = newTokenData
1428
+ // Profiles are already updated in this.profiles if they were passed in
1429
+ } finally {
1430
+ // Switch back if we temporarily switched
1431
+ if (!currentActiveId.every(x => x === 0)) {
1432
+ console.log('Switching back to original profile...')
1433
+ await this.switchProfile(currentActiveId)
1434
+ }
1189
1435
  }
1190
- return r
1191
1436
  }
1192
1437
 
1193
1438
  /**
1194
- * A helper function to serialize a UMP token to a binary format (version=1).
1195
- * The serialization layout is:
1196
- * - [1 byte version (value=1)]
1197
- * - For each array field in the UMP token, [varint length + bytes]
1198
- * - Then [varint length + outpoint string in UTF-8]
1199
- *
1200
- * @param token The UMP token to serialize.
1201
- * @returns A byte array representing the serialized token.
1202
- * @throws {Error} if the token has no currentOutpoint (required for serialization).
1439
+ * Serializes a UMP token to binary format (Version 2 with optional profiles).
1440
+ * Layout: [1 byte version=2] + [11 * (varint len + bytes) for standard fields] + [1 byte profile_flag] + [IF flag=1 THEN varint len + profile bytes] + [varint len + outpoint bytes]
1203
1441
  */
1204
1442
  private serializeUMPToken(token: UMPToken): number[] {
1205
1443
  if (!token.currentOutpoint) {
@@ -1207,29 +1445,35 @@ export class CWIStyleWalletManager implements WalletInterface {
1207
1445
  }
1208
1446
 
1209
1447
  const writer = new Utils.Writer()
1210
- // Write version byte
1211
- writer.writeUInt8(1)
1448
+ writer.writeUInt8(2) // Version 2
1212
1449
 
1213
- // Helper to write array with length prefix
1214
1450
  const writeArray = (arr: number[]) => {
1215
1451
  writer.writeVarIntNum(arr.length)
1216
1452
  writer.write(arr)
1217
1453
  }
1218
1454
 
1219
- // Write each array-based field in the order they appear on UMPToken
1220
- writeArray(token.passwordPresentationPrimary)
1221
- writeArray(token.passwordRecoveryPrimary)
1222
- writeArray(token.presentationRecoveryPrimary)
1223
- writeArray(token.passwordPrimaryPrivileged)
1224
- writeArray(token.presentationRecoveryPrivileged)
1225
- writeArray(token.presentationHash)
1226
- writeArray(token.passwordSalt)
1227
- writeArray(token.recoveryHash)
1228
- writeArray(token.presentationKeyEncrypted)
1229
- writeArray(token.recoveryKeyEncrypted)
1230
- writeArray(token.passwordKeyEncrypted)
1231
-
1232
- // Finally, write the outpoint string:
1455
+ // Write standard fields in specific order
1456
+ writeArray(token.passwordSalt) // 0
1457
+ writeArray(token.passwordPresentationPrimary) // 1
1458
+ writeArray(token.passwordRecoveryPrimary) // 2
1459
+ writeArray(token.presentationRecoveryPrimary) // 3
1460
+ writeArray(token.passwordPrimaryPrivileged) // 4
1461
+ writeArray(token.presentationRecoveryPrivileged) // 5
1462
+ writeArray(token.presentationHash) // 6
1463
+ writeArray(token.recoveryHash) // 7
1464
+ writeArray(token.presentationKeyEncrypted) // 8
1465
+ writeArray(token.passwordKeyEncrypted) // 9 - Swapped order vs original doc comment
1466
+ writeArray(token.recoveryKeyEncrypted) // 10
1467
+
1468
+ // Write optional profiles field
1469
+ if (token.profilesEncrypted && token.profilesEncrypted.length > 0) {
1470
+ writer.writeUInt8(1) // Flag indicating profiles present
1471
+ writeArray(token.profilesEncrypted)
1472
+ } else {
1473
+ writer.writeUInt8(0) // Flag indicating no profiles
1474
+ }
1475
+
1476
+ // Write outpoint string
1233
1477
  const outpointBytes = Utils.toArray(token.currentOutpoint, 'utf8')
1234
1478
  writer.writeVarIntNum(outpointBytes.length)
1235
1479
  writer.write(outpointBytes)
@@ -1238,57 +1482,61 @@ export class CWIStyleWalletManager implements WalletInterface {
1238
1482
  }
1239
1483
 
1240
1484
  /**
1241
- * A helper function to deserialize a UMP token from the format described in `serializeUMPToken`.
1242
- *
1243
- * @param bin The serialized byte array.
1244
- * @returns The reconstructed UMP token.
1245
- * @throws {Error} if the version byte is unexpected or if parsing fails.
1485
+ * Deserializes a UMP token from binary format (Handles Version 1 and 2).
1246
1486
  */
1247
1487
  private deserializeUMPToken(bin: number[]): UMPToken {
1248
1488
  const reader = new Utils.Reader(bin)
1249
-
1250
- // Check version:
1251
1489
  const version = reader.readUInt8()
1252
- if (version !== 1) {
1253
- throw new Error(`Unsupported UMP token version: ${version}`)
1490
+
1491
+ if (version !== 1 && version !== 2) {
1492
+ throw new Error(`Unsupported UMP token serialization version: ${version}`)
1254
1493
  }
1255
1494
 
1256
- // Helper to read an array with length prefix
1257
1495
  const readArray = (): number[] => {
1258
1496
  const length = reader.readVarIntNum()
1259
1497
  return reader.read(length)
1260
1498
  }
1261
1499
 
1262
- // Read in the correct order:
1263
- const passwordPresentationPrimary = readArray()
1264
- const passwordRecoveryPrimary = readArray()
1265
- const presentationRecoveryPrimary = readArray()
1266
- const passwordPrimaryPrivileged = readArray()
1267
- const presentationRecoveryPrivileged = readArray()
1268
- const presentationHash = readArray()
1269
- const passwordSalt = readArray()
1270
- const recoveryHash = readArray()
1271
- const presentationKeyEncrypted = readArray()
1272
- const recoveryKeyEncrypted = readArray()
1273
- const passwordKeyEncrypted = readArray()
1274
-
1275
- // Read outpoint string:
1500
+ // Read standard fields (order matches serialization V2)
1501
+ const passwordSalt = readArray() // 0
1502
+ const passwordPresentationPrimary = readArray() // 1
1503
+ const passwordRecoveryPrimary = readArray() // 2
1504
+ const presentationRecoveryPrimary = readArray() // 3
1505
+ const passwordPrimaryPrivileged = readArray() // 4
1506
+ const presentationRecoveryPrivileged = readArray() // 5
1507
+ const presentationHash = readArray() // 6
1508
+ const recoveryHash = readArray() // 7
1509
+ const presentationKeyEncrypted = readArray() // 8
1510
+ const passwordKeyEncrypted = readArray() // 9
1511
+ const recoveryKeyEncrypted = readArray() // 10
1512
+
1513
+ // Read optional profiles (only in V2)
1514
+ let profilesEncrypted: number[] | undefined
1515
+ if (version === 2) {
1516
+ const profilesFlag = reader.readUInt8()
1517
+ if (profilesFlag === 1) {
1518
+ profilesEncrypted = readArray()
1519
+ }
1520
+ }
1521
+
1522
+ // Read outpoint string
1276
1523
  const outpointLen = reader.readVarIntNum()
1277
1524
  const outpointBytes = reader.read(outpointLen)
1278
1525
  const currentOutpoint = Utils.toUTF8(outpointBytes)
1279
1526
 
1280
1527
  const token: UMPToken = {
1528
+ passwordSalt,
1281
1529
  passwordPresentationPrimary,
1282
1530
  passwordRecoveryPrimary,
1283
1531
  presentationRecoveryPrimary,
1284
1532
  passwordPrimaryPrivileged,
1285
1533
  presentationRecoveryPrivileged,
1286
1534
  presentationHash,
1287
- passwordSalt,
1288
1535
  recoveryHash,
1289
1536
  presentationKeyEncrypted,
1537
+ passwordKeyEncrypted, // Corrected order
1290
1538
  recoveryKeyEncrypted,
1291
- passwordKeyEncrypted,
1539
+ profilesEncrypted, // May be undefined
1292
1540
  currentOutpoint
1293
1541
  }
1294
1542
 
@@ -1296,28 +1544,37 @@ export class CWIStyleWalletManager implements WalletInterface {
1296
1544
  }
1297
1545
 
1298
1546
  /**
1299
- * Builds the underlying wallet once the user is authenticated.
1547
+ * Sets up the root key infrastructure after authentication or loading from snapshot.
1548
+ * Initializes the root primary key, root privileged key manager, loads profiles,
1549
+ * and sets the authenticated flag. Does NOT switch profile initially.
1300
1550
  *
1301
- * @param primaryKey The user's primary key (32 bytes).
1302
- * @param privilegedKey Optionally, a privileged key (for short-term usage in account recovery).
1551
+ * @param rootPrimaryKey The user's root primary key (32 bytes).
1552
+ * @param ephemeralRootPrivilegedKey Optional root privileged key (e.g., during recovery flows).
1303
1553
  */
1304
- private async buildUnderlying(primaryKey: number[], privilegedKey?: number[]): Promise<void> {
1554
+ private async setupRootInfrastructure(
1555
+ rootPrimaryKey: number[],
1556
+ ephemeralRootPrivilegedKey?: number[]
1557
+ ): Promise<void> {
1305
1558
  if (!this.currentUMPToken) {
1306
- throw new Error('A UMP token must exist before building underlying wallet!')
1559
+ throw new Error('A UMP token must exist before setting up root infrastructure!')
1307
1560
  }
1561
+ this.rootPrimaryKey = rootPrimaryKey
1308
1562
 
1309
- this.primaryKey = primaryKey
1563
+ // Store ephemeral key if provided, for one-time use by the manager
1564
+ let oneTimePrivilegedKey: PrivateKey | undefined = ephemeralRootPrivilegedKey
1565
+ ? new PrivateKey(ephemeralRootPrivilegedKey)
1566
+ : undefined
1310
1567
 
1311
- // Create a privileged manager that either uses the ephemeral privilegedKey if provided,
1312
- // or derives it later from the user's password on demand.
1313
- const privilegedManager = new PrivilegedKeyManager(async (reason: string) => {
1314
- if (privilegedKey) {
1315
- // For account recovery: a one-off opportunity to recover.
1316
- const tempKey = new PrivateKey(privilegedKey)
1317
- privilegedKey = undefined
1568
+ // Create the ROOT PrivilegedKeyManager
1569
+ this.rootPrivilegedKeyManager = new PrivilegedKeyManager(async (reason: string) => {
1570
+ // 1. Use one-time key if available (for recovery)
1571
+ if (oneTimePrivilegedKey) {
1572
+ const tempKey = oneTimePrivilegedKey
1573
+ oneTimePrivilegedKey = undefined // Consume it
1318
1574
  return tempKey
1319
1575
  }
1320
- // Otherwise, ask user for their password to decrypt the privileged key.
1576
+
1577
+ // 2. Otherwise, derive from password
1321
1578
  const password = await this.passwordRetriever(reason, (passwordCandidate: string) => {
1322
1579
  try {
1323
1580
  const derivedPasswordKey = Hash.pbkdf2(
@@ -1327,19 +1584,17 @@ export class CWIStyleWalletManager implements WalletInterface {
1327
1584
  32,
1328
1585
  'sha512'
1329
1586
  )
1330
- // Decrypt the privileged key with XOR(primaryKey, derivedPasswordKey).
1331
- const privilegedDecryptor = this.XOR(this.primaryKey!, derivedPasswordKey)
1587
+ const privilegedDecryptor = this.XOR(this.rootPrimaryKey!, derivedPasswordKey)
1332
1588
  const decryptedPrivileged = new SymmetricKey(privilegedDecryptor).decrypt(
1333
1589
  this.currentUMPToken!.passwordPrimaryPrivileged
1334
1590
  ) as number[]
1335
- if (decryptedPrivileged) {
1336
- return true
1337
- }
1338
- return false
1591
+ return !!decryptedPrivileged // Test passes if decryption works
1339
1592
  } catch (e) {
1340
1593
  return false
1341
1594
  }
1342
1595
  })
1596
+
1597
+ // Decrypt the root privileged key using the confirmed password
1343
1598
  const derivedPasswordKey = Hash.pbkdf2(
1344
1599
  Utils.toArray(password, 'utf8'),
1345
1600
  this.currentUMPToken!.passwordSalt,
@@ -1347,39 +1602,67 @@ export class CWIStyleWalletManager implements WalletInterface {
1347
1602
  32,
1348
1603
  'sha512'
1349
1604
  )
1350
- // Decrypt the privileged key with XOR(primaryKey, derivedPasswordKey).
1351
- const privilegedDecryptor = this.XOR(this.primaryKey!, derivedPasswordKey)
1352
- const decryptedPrivileged = new SymmetricKey(privilegedDecryptor).decrypt(
1605
+ const privilegedDecryptor = this.XOR(this.rootPrimaryKey!, derivedPasswordKey)
1606
+ const rootPrivilegedBytes = new SymmetricKey(privilegedDecryptor).decrypt(
1353
1607
  this.currentUMPToken!.passwordPrimaryPrivileged
1354
1608
  ) as number[]
1355
- return new PrivateKey(decryptedPrivileged)
1356
- })
1357
1609
 
1358
- this.underlyingPrivilegedKeyManager = privilegedManager
1610
+ return new PrivateKey(rootPrivilegedBytes) // Return the ROOT key object
1611
+ })
1359
1612
 
1360
- // Build the underlying wallet with the primary key and privileged manager.
1361
- this.underlying = await this.walletBuilder(primaryKey, privilegedManager)
1613
+ // Decrypt and load profiles if present in the token
1614
+ this.profiles = [] // Clear existing profiles before loading
1615
+ if (this.currentUMPToken.profilesEncrypted && this.currentUMPToken.profilesEncrypted.length > 0) {
1616
+ try {
1617
+ const decryptedProfileBytes = (
1618
+ await this.rootPrivilegedKeyManager.decrypt({
1619
+ ciphertext: this.currentUMPToken.profilesEncrypted,
1620
+ protocolID: [2, 'admin profile wrapping'], // Use profile protocol ID
1621
+ keyID: '1'
1622
+ })
1623
+ ).plaintext
1624
+ const profilesJson = Utils.toUTF8(decryptedProfileBytes)
1625
+ this.profiles = JSON.parse(profilesJson) as Profile[]
1626
+ } catch (error) {
1627
+ console.error('Failed to decrypt or parse profiles:', error)
1628
+ // Decide if this should be fatal or just log and continue without profiles
1629
+ this.profiles = [] // Ensure profiles are empty on error
1630
+ // Optionally re-throw or handle more gracefully
1631
+ throw new Error(`Failed to load profiles: ${(error as Error).message}`)
1632
+ }
1633
+ }
1362
1634
 
1363
1635
  this.authenticated = true
1636
+ // Note: We don't call switchProfile here anymore.
1637
+ // It's called by the auth methods (providePassword/provideRecoveryKey) or loadSnapshot after this.
1364
1638
  }
1365
1639
 
1366
1640
  /*
1367
1641
  * ---------------------------------------------------------------------------------------
1368
- * Below are the standard WalletInterface methods that simply proxy through to this.underlying,
1369
- * ensuring that the user is authenticated and that the admin originator is not misused.
1642
+ * Standard WalletInterface methods proxying to the *active* underlying wallet.
1643
+ * Includes authentication checks and admin originator protection.
1370
1644
  * ---------------------------------------------------------------------------------------
1371
1645
  */
1372
1646
 
1373
- async getPublicKey(
1374
- args: GetPublicKeyArgs,
1375
- originator?: OriginatorDomainNameStringUnder250Bytes
1376
- ): Promise<GetPublicKeyResult> {
1647
+ private checkAuthAndUnderlying(originator?: string): void {
1377
1648
  if (!this.authenticated) {
1378
1649
  throw new Error('User is not authenticated.')
1379
1650
  }
1651
+ if (!this.underlying) {
1652
+ // This might happen if authentication succeeded but profile switching failed
1653
+ throw new Error('Underlying wallet for the active profile is not initialized.')
1654
+ }
1380
1655
  if (originator === this.adminOriginator) {
1381
1656
  throw new Error('External applications are not allowed to use the admin originator.')
1382
1657
  }
1658
+ }
1659
+
1660
+ // Example proxy method (repeat pattern for all others)
1661
+ async getPublicKey(
1662
+ args: GetPublicKeyArgs,
1663
+ originator?: OriginatorDomainNameStringUnder250Bytes
1664
+ ): Promise<GetPublicKeyResult> {
1665
+ this.checkAuthAndUnderlying(originator)
1383
1666
  return this.underlying!.getPublicKey(args, originator)
1384
1667
  }
1385
1668
 
@@ -1387,12 +1670,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1387
1670
  args: RevealCounterpartyKeyLinkageArgs,
1388
1671
  originator?: OriginatorDomainNameStringUnder250Bytes
1389
1672
  ): Promise<RevealCounterpartyKeyLinkageResult> {
1390
- if (!this.authenticated) {
1391
- throw new Error('User is not authenticated.')
1392
- }
1393
- if (originator === this.adminOriginator) {
1394
- throw new Error('External applications are not allowed to use the admin originator.')
1395
- }
1673
+ this.checkAuthAndUnderlying(originator)
1396
1674
  return this.underlying!.revealCounterpartyKeyLinkage(args, originator)
1397
1675
  }
1398
1676
 
@@ -1400,12 +1678,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1400
1678
  args: RevealSpecificKeyLinkageArgs,
1401
1679
  originator?: OriginatorDomainNameStringUnder250Bytes
1402
1680
  ): Promise<RevealSpecificKeyLinkageResult> {
1403
- if (!this.authenticated) {
1404
- throw new Error('User is not authenticated.')
1405
- }
1406
- if (originator === this.adminOriginator) {
1407
- throw new Error('External applications are not allowed to use the admin originator.')
1408
- }
1681
+ this.checkAuthAndUnderlying(originator)
1409
1682
  return this.underlying!.revealSpecificKeyLinkage(args, originator)
1410
1683
  }
1411
1684
 
@@ -1413,12 +1686,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1413
1686
  args: WalletEncryptArgs,
1414
1687
  originator?: OriginatorDomainNameStringUnder250Bytes
1415
1688
  ): Promise<WalletEncryptResult> {
1416
- if (!this.authenticated) {
1417
- throw new Error('User is not authenticated.')
1418
- }
1419
- if (originator === this.adminOriginator) {
1420
- throw new Error('External applications are not allowed to use the admin originator.')
1421
- }
1689
+ this.checkAuthAndUnderlying(originator)
1422
1690
  return this.underlying!.encrypt(args, originator)
1423
1691
  }
1424
1692
 
@@ -1426,12 +1694,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1426
1694
  args: WalletDecryptArgs,
1427
1695
  originator?: OriginatorDomainNameStringUnder250Bytes
1428
1696
  ): Promise<WalletDecryptResult> {
1429
- if (!this.authenticated) {
1430
- throw new Error('User is not authenticated.')
1431
- }
1432
- if (originator === this.adminOriginator) {
1433
- throw new Error('External applications are not allowed to use the admin originator.')
1434
- }
1697
+ this.checkAuthAndUnderlying(originator)
1435
1698
  return this.underlying!.decrypt(args, originator)
1436
1699
  }
1437
1700
 
@@ -1439,12 +1702,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1439
1702
  args: CreateHmacArgs,
1440
1703
  originator?: OriginatorDomainNameStringUnder250Bytes
1441
1704
  ): Promise<CreateHmacResult> {
1442
- if (!this.authenticated) {
1443
- throw new Error('User is not authenticated.')
1444
- }
1445
- if (originator === this.adminOriginator) {
1446
- throw new Error('External applications are not allowed to use the admin originator.')
1447
- }
1705
+ this.checkAuthAndUnderlying(originator)
1448
1706
  return this.underlying!.createHmac(args, originator)
1449
1707
  }
1450
1708
 
@@ -1452,12 +1710,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1452
1710
  args: VerifyHmacArgs,
1453
1711
  originator?: OriginatorDomainNameStringUnder250Bytes
1454
1712
  ): Promise<VerifyHmacResult> {
1455
- if (!this.authenticated) {
1456
- throw new Error('User is not authenticated.')
1457
- }
1458
- if (originator === this.adminOriginator) {
1459
- throw new Error('External applications are not allowed to use the admin originator.')
1460
- }
1713
+ this.checkAuthAndUnderlying(originator)
1461
1714
  return this.underlying!.verifyHmac(args, originator)
1462
1715
  }
1463
1716
 
@@ -1465,12 +1718,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1465
1718
  args: CreateSignatureArgs,
1466
1719
  originator?: OriginatorDomainNameStringUnder250Bytes
1467
1720
  ): Promise<CreateSignatureResult> {
1468
- if (!this.authenticated) {
1469
- throw new Error('User is not authenticated.')
1470
- }
1471
- if (originator === this.adminOriginator) {
1472
- throw new Error('External applications are not allowed to use the admin originator.')
1473
- }
1721
+ this.checkAuthAndUnderlying(originator)
1474
1722
  return this.underlying!.createSignature(args, originator)
1475
1723
  }
1476
1724
 
@@ -1478,12 +1726,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1478
1726
  args: VerifySignatureArgs,
1479
1727
  originator?: OriginatorDomainNameStringUnder250Bytes
1480
1728
  ): Promise<VerifySignatureResult> {
1481
- if (!this.authenticated) {
1482
- throw new Error('User is not authenticated.')
1483
- }
1484
- if (originator === this.adminOriginator) {
1485
- throw new Error('External applications are not allowed to use the admin originator.')
1486
- }
1729
+ this.checkAuthAndUnderlying(originator)
1487
1730
  return this.underlying!.verifySignature(args, originator)
1488
1731
  }
1489
1732
 
@@ -1491,12 +1734,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1491
1734
  args: CreateActionArgs,
1492
1735
  originator?: OriginatorDomainNameStringUnder250Bytes
1493
1736
  ): Promise<CreateActionResult> {
1494
- if (!this.authenticated) {
1495
- throw new Error('User is not authenticated.')
1496
- }
1497
- if (originator === this.adminOriginator) {
1498
- throw new Error('External applications are not allowed to use the admin originator.')
1499
- }
1737
+ this.checkAuthAndUnderlying(originator)
1500
1738
  return this.underlying!.createAction(args, originator)
1501
1739
  }
1502
1740
 
@@ -1504,12 +1742,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1504
1742
  args: SignActionArgs,
1505
1743
  originator?: OriginatorDomainNameStringUnder250Bytes
1506
1744
  ): Promise<SignActionResult> {
1507
- if (!this.authenticated) {
1508
- throw new Error('User is not authenticated.')
1509
- }
1510
- if (originator === this.adminOriginator) {
1511
- throw new Error('External applications are not allowed to use the admin originator.')
1512
- }
1745
+ this.checkAuthAndUnderlying(originator)
1513
1746
  return this.underlying!.signAction(args, originator)
1514
1747
  }
1515
1748
 
@@ -1517,12 +1750,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1517
1750
  args: AbortActionArgs,
1518
1751
  originator?: OriginatorDomainNameStringUnder250Bytes
1519
1752
  ): Promise<AbortActionResult> {
1520
- if (!this.authenticated) {
1521
- throw new Error('User is not authenticated.')
1522
- }
1523
- if (originator === this.adminOriginator) {
1524
- throw new Error('External applications are not allowed to use the admin originator.')
1525
- }
1753
+ this.checkAuthAndUnderlying(originator)
1526
1754
  return this.underlying!.abortAction(args, originator)
1527
1755
  }
1528
1756
 
@@ -1530,12 +1758,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1530
1758
  args: ListActionsArgs,
1531
1759
  originator?: OriginatorDomainNameStringUnder250Bytes
1532
1760
  ): Promise<ListActionsResult> {
1533
- if (!this.authenticated) {
1534
- throw new Error('User is not authenticated.')
1535
- }
1536
- if (originator === this.adminOriginator) {
1537
- throw new Error('External applications are not allowed to use the admin originator.')
1538
- }
1761
+ this.checkAuthAndUnderlying(originator)
1539
1762
  return this.underlying!.listActions(args, originator)
1540
1763
  }
1541
1764
 
@@ -1543,12 +1766,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1543
1766
  args: InternalizeActionArgs,
1544
1767
  originator?: OriginatorDomainNameStringUnder250Bytes
1545
1768
  ): Promise<InternalizeActionResult> {
1546
- if (!this.authenticated) {
1547
- throw new Error('User is not authenticated.')
1548
- }
1549
- if (originator === this.adminOriginator) {
1550
- throw new Error('External applications are not allowed to use the admin originator.')
1551
- }
1769
+ this.checkAuthAndUnderlying(originator)
1552
1770
  return this.underlying!.internalizeAction(args, originator)
1553
1771
  }
1554
1772
 
@@ -1556,12 +1774,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1556
1774
  args: ListOutputsArgs,
1557
1775
  originator?: OriginatorDomainNameStringUnder250Bytes
1558
1776
  ): Promise<ListOutputsResult> {
1559
- if (!this.authenticated) {
1560
- throw new Error('User is not authenticated.')
1561
- }
1562
- if (originator === this.adminOriginator) {
1563
- throw new Error('External applications are not allowed to use the admin originator.')
1564
- }
1777
+ this.checkAuthAndUnderlying(originator)
1565
1778
  return this.underlying!.listOutputs(args, originator)
1566
1779
  }
1567
1780
 
@@ -1569,12 +1782,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1569
1782
  args: RelinquishOutputArgs,
1570
1783
  originator?: OriginatorDomainNameStringUnder250Bytes
1571
1784
  ): Promise<RelinquishOutputResult> {
1572
- if (!this.authenticated) {
1573
- throw new Error('User is not authenticated.')
1574
- }
1575
- if (originator === this.adminOriginator) {
1576
- throw new Error('External applications are not allowed to use the admin originator.')
1577
- }
1785
+ this.checkAuthAndUnderlying(originator)
1578
1786
  return this.underlying!.relinquishOutput(args, originator)
1579
1787
  }
1580
1788
 
@@ -1582,12 +1790,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1582
1790
  args: AcquireCertificateArgs,
1583
1791
  originator?: OriginatorDomainNameStringUnder250Bytes
1584
1792
  ): Promise<AcquireCertificateResult> {
1585
- if (!this.authenticated) {
1586
- throw new Error('User is not authenticated.')
1587
- }
1588
- if (originator === this.adminOriginator) {
1589
- throw new Error('External applications are not allowed to use the admin originator.')
1590
- }
1793
+ this.checkAuthAndUnderlying(originator)
1591
1794
  return this.underlying!.acquireCertificate(args, originator)
1592
1795
  }
1593
1796
 
@@ -1595,12 +1798,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1595
1798
  args: ListCertificatesArgs,
1596
1799
  originator?: OriginatorDomainNameStringUnder250Bytes
1597
1800
  ): Promise<ListCertificatesResult> {
1598
- if (!this.authenticated) {
1599
- throw new Error('User is not authenticated.')
1600
- }
1601
- if (originator === this.adminOriginator) {
1602
- throw new Error('External applications are not allowed to use the admin originator.')
1603
- }
1801
+ this.checkAuthAndUnderlying(originator)
1604
1802
  return this.underlying!.listCertificates(args, originator)
1605
1803
  }
1606
1804
 
@@ -1608,12 +1806,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1608
1806
  args: ProveCertificateArgs,
1609
1807
  originator?: OriginatorDomainNameStringUnder250Bytes
1610
1808
  ): Promise<ProveCertificateResult> {
1611
- if (!this.authenticated) {
1612
- throw new Error('User is not authenticated.')
1613
- }
1614
- if (originator === this.adminOriginator) {
1615
- throw new Error('External applications are not allowed to use the admin originator.')
1616
- }
1809
+ this.checkAuthAndUnderlying(originator)
1617
1810
  return this.underlying!.proveCertificate(args, originator)
1618
1811
  }
1619
1812
 
@@ -1621,12 +1814,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1621
1814
  args: RelinquishCertificateArgs,
1622
1815
  originator?: OriginatorDomainNameStringUnder250Bytes
1623
1816
  ): Promise<RelinquishCertificateResult> {
1624
- if (!this.authenticated) {
1625
- throw new Error('User is not authenticated.')
1626
- }
1627
- if (originator === this.adminOriginator) {
1628
- throw new Error('External applications are not allowed to use the admin originator.')
1629
- }
1817
+ this.checkAuthAndUnderlying(originator)
1630
1818
  return this.underlying!.relinquishCertificate(args, originator)
1631
1819
  }
1632
1820
 
@@ -1634,12 +1822,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1634
1822
  args: DiscoverByIdentityKeyArgs,
1635
1823
  originator?: OriginatorDomainNameStringUnder250Bytes
1636
1824
  ): Promise<DiscoverCertificatesResult> {
1637
- if (!this.authenticated) {
1638
- throw new Error('User is not authenticated.')
1639
- }
1640
- if (originator === this.adminOriginator) {
1641
- throw new Error('External applications are not allowed to use the admin originator.')
1642
- }
1825
+ this.checkAuthAndUnderlying(originator)
1643
1826
  return this.underlying!.discoverByIdentityKey(args, originator)
1644
1827
  }
1645
1828
 
@@ -1647,12 +1830,7 @@ export class CWIStyleWalletManager implements WalletInterface {
1647
1830
  args: DiscoverByAttributesArgs,
1648
1831
  originator?: OriginatorDomainNameStringUnder250Bytes
1649
1832
  ): Promise<DiscoverCertificatesResult> {
1650
- if (!this.authenticated) {
1651
- throw new Error('User is not authenticated.')
1652
- }
1653
- if (originator === this.adminOriginator) {
1654
- throw new Error('External applications are not allowed to use the admin originator.')
1655
- }
1833
+ this.checkAuthAndUnderlying(originator)
1656
1834
  return this.underlying!.discoverByAttributes(args, originator)
1657
1835
  }
1658
1836
 
@@ -1673,19 +1851,14 @@ export class CWIStyleWalletManager implements WalletInterface {
1673
1851
  if (originator === this.adminOriginator) {
1674
1852
  throw new Error('External applications are not allowed to use the admin originator.')
1675
1853
  }
1676
- while (!this.authenticated) {
1854
+ while (!this.authenticated || !this.underlying) {
1677
1855
  await new Promise(resolve => setTimeout(resolve, 100))
1678
1856
  }
1679
1857
  return { authenticated: true }
1680
1858
  }
1681
1859
 
1682
1860
  async getHeight(_: {}, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<GetHeightResult> {
1683
- if (!this.authenticated) {
1684
- throw new Error('User is not authenticated.')
1685
- }
1686
- if (originator === this.adminOriginator) {
1687
- throw new Error('External applications are not allowed to use the admin originator.')
1688
- }
1861
+ this.checkAuthAndUnderlying(originator)
1689
1862
  return this.underlying!.getHeight({}, originator)
1690
1863
  }
1691
1864
 
@@ -1693,32 +1866,17 @@ export class CWIStyleWalletManager implements WalletInterface {
1693
1866
  args: GetHeaderArgs,
1694
1867
  originator?: OriginatorDomainNameStringUnder250Bytes
1695
1868
  ): Promise<GetHeaderResult> {
1696
- if (!this.authenticated) {
1697
- throw new Error('User is not authenticated.')
1698
- }
1699
- if (originator === this.adminOriginator) {
1700
- throw new Error('External applications are not allowed to use the admin originator.')
1701
- }
1869
+ this.checkAuthAndUnderlying(originator)
1702
1870
  return this.underlying!.getHeaderForHeight(args, originator)
1703
1871
  }
1704
1872
 
1705
1873
  async getNetwork(_: {}, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<GetNetworkResult> {
1706
- if (!this.authenticated) {
1707
- throw new Error('User is not authenticated.')
1708
- }
1709
- if (originator === this.adminOriginator) {
1710
- throw new Error('External applications are not allowed to use the admin originator.')
1711
- }
1874
+ this.checkAuthAndUnderlying(originator)
1712
1875
  return this.underlying!.getNetwork({}, originator)
1713
1876
  }
1714
1877
 
1715
1878
  async getVersion(_: {}, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<GetVersionResult> {
1716
- if (!this.authenticated) {
1717
- throw new Error('User is not authenticated.')
1718
- }
1719
- if (originator === this.adminOriginator) {
1720
- throw new Error('External applications are not allowed to use the admin originator.')
1721
- }
1879
+ this.checkAuthAndUnderlying(originator)
1722
1880
  return this.underlying!.getVersion({}, originator)
1723
1881
  }
1724
1882
  }