@bsv/wallet-toolbox 1.7.22 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/docs/README.md +1 -0
  3. package/docs/client.md +135 -0
  4. package/docs/wab-shamir.md +311 -0
  5. package/docs/wallet.md +135 -0
  6. package/out/src/ShamirWalletManager.d.ts +213 -0
  7. package/out/src/ShamirWalletManager.d.ts.map +1 -0
  8. package/out/src/ShamirWalletManager.js +363 -0
  9. package/out/src/ShamirWalletManager.js.map +1 -0
  10. package/out/src/WalletPermissionsManager.d.ts +27 -0
  11. package/out/src/WalletPermissionsManager.d.ts.map +1 -1
  12. package/out/src/WalletPermissionsManager.js +308 -147
  13. package/out/src/WalletPermissionsManager.js.map +1 -1
  14. package/out/src/__tests/ShamirWalletManager.test.d.ts +2 -0
  15. package/out/src/__tests/ShamirWalletManager.test.d.ts.map +1 -0
  16. package/out/src/__tests/ShamirWalletManager.test.js +298 -0
  17. package/out/src/__tests/ShamirWalletManager.test.js.map +1 -0
  18. package/out/src/__tests/WalletPermissionsManager.callbacks.test.js +116 -0
  19. package/out/src/__tests/WalletPermissionsManager.callbacks.test.js.map +1 -1
  20. package/out/src/__tests/WalletPermissionsManager.pmodules.test.js +111 -0
  21. package/out/src/__tests/WalletPermissionsManager.pmodules.test.js.map +1 -1
  22. package/out/src/entropy/EntropyCollector.d.ts +89 -0
  23. package/out/src/entropy/EntropyCollector.d.ts.map +1 -0
  24. package/out/src/entropy/EntropyCollector.js +176 -0
  25. package/out/src/entropy/EntropyCollector.js.map +1 -0
  26. package/out/src/entropy/__tests/EntropyCollector.test.d.ts +2 -0
  27. package/out/src/entropy/__tests/EntropyCollector.test.d.ts.map +1 -0
  28. package/out/src/entropy/__tests/EntropyCollector.test.js +137 -0
  29. package/out/src/entropy/__tests/EntropyCollector.test.js.map +1 -0
  30. package/out/src/index.all.d.ts +2 -0
  31. package/out/src/index.all.d.ts.map +1 -1
  32. package/out/src/index.all.js +2 -0
  33. package/out/src/index.all.js.map +1 -1
  34. package/out/src/sdk/WalletServices.interfaces.d.ts.map +1 -1
  35. package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageBase.d.ts.map +1 -1
  36. package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageBase.js +4 -1
  37. package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageBase.js.map +1 -1
  38. package/out/src/wab-client/WABClient.d.ts +65 -0
  39. package/out/src/wab-client/WABClient.d.ts.map +1 -1
  40. package/out/src/wab-client/WABClient.js +107 -0
  41. package/out/src/wab-client/WABClient.js.map +1 -1
  42. package/out/tsconfig.all.tsbuildinfo +1 -1
  43. package/package.json +5 -1
  44. package/src/ShamirWalletManager.ts +499 -0
  45. package/src/WalletPermissionsManager.ts +368 -181
  46. package/src/__tests/ShamirWalletManager.test.ts +369 -0
  47. package/src/__tests/WalletPermissionsManager.callbacks.test.ts +140 -1
  48. package/src/__tests/WalletPermissionsManager.pmodules.test.ts +152 -0
  49. package/src/entropy/EntropyCollector.ts +228 -0
  50. package/src/entropy/__tests/EntropyCollector.test.ts +182 -0
  51. package/src/index.all.ts +2 -0
  52. package/src/sdk/WalletServices.interfaces.ts +0 -1
  53. package/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageBase.ts +4 -1
  54. package/src/wab-client/WABClient.ts +135 -0
@@ -20,7 +20,9 @@ import {
20
20
  RelinquishOutputArgs,
21
21
  GetPublicKeyArgs,
22
22
  CreateActionArgs,
23
- ListOutputsResult
23
+ ListOutputsResult,
24
+ ListActionsArgs,
25
+ ListActionsResult
24
26
  } from '@bsv/sdk'
25
27
 
26
28
  ////// TODO: ADD SUPPORT FOR ADMIN COUNTERPARTIES BASED ON WALLET STORAGE
@@ -618,6 +620,80 @@ export class WalletPermissionsManager implements WalletInterface {
618
620
  })
619
621
  }
620
622
 
623
+ /**
624
+ * Adds a permission module for the given schemeID if needed, throwing if unsupported.
625
+ */
626
+ private addPModuleByScheme(
627
+ schemeID: string,
628
+ kind: 'label' | 'basket',
629
+ pModulesByScheme: Map<string, PermissionsModule>
630
+ ): void {
631
+ if (pModulesByScheme.has(schemeID)) return
632
+ const module = this.config.permissionModules?.[schemeID]
633
+ if (!module) {
634
+ throw new Error(`Unsupported P-${kind} scheme: p ${schemeID}`)
635
+ }
636
+ pModulesByScheme.set(schemeID, module)
637
+ }
638
+
639
+ /**
640
+ * Splits labels into P and non-P lists, registering any P-modules encountered.
641
+ *
642
+ * P-labels follow BRC-111 format: `p <moduleId> <payload>`
643
+ * - Must start with "p " (lowercase p + space)
644
+ * - Module ID must be at least 1 character with no spaces
645
+ * - Single space separates module ID from payload
646
+ * - Payload must be at least 1 character
647
+ *
648
+ * @example Valid: "p btms token123", "p invoicing invoice 2026-02-02"
649
+ * @example Invalid: "p btms" (no payload), "p btms " (empty payload), "p data" (empty moduleId)
650
+ *
651
+ * @param labels - Array of label strings to process
652
+ * @param pModulesByScheme - Map to populate with discovered p-modules
653
+ * @returns Array of non-P labels for normal permission checks
654
+ * @throws Error if p-label format is invalid or module is unsupported
655
+ */
656
+ private splitLabelsByPermissionModule(
657
+ labels: string[] | undefined,
658
+ pModulesByScheme: Map<string, PermissionsModule>
659
+ ): string[] {
660
+ const nonPLabels: string[] = []
661
+ if (!labels) return nonPLabels
662
+
663
+ for (const label of labels) {
664
+ if (label.startsWith('p ')) {
665
+ // Remove "p " prefix to get "moduleId payload"
666
+ const remainder = label.slice(2)
667
+
668
+ // Find the space that separates moduleId from payload
669
+ const separatorIndex = remainder.indexOf(' ')
670
+
671
+ // Validate: must have a space (separatorIndex > 0) and payload after it
672
+ // separatorIndex <= 0 means no space found or moduleId is empty
673
+ // separatorIndex === remainder.length - 1 means space is last char (no payload)
674
+ if (separatorIndex <= 0 || separatorIndex === remainder.length - 1) {
675
+ throw new Error(`Invalid P-label format: ${label}`)
676
+ }
677
+
678
+ // Reject double spaces after moduleId (payload can't start with space)
679
+ if (remainder[separatorIndex + 1] === ' ') {
680
+ throw new Error(`Invalid P-label format: ${label}`)
681
+ }
682
+
683
+ // Extract moduleId (substring before first space)
684
+ const schemeID = remainder.slice(0, separatorIndex)
685
+
686
+ // Register the module (throws if unsupported)
687
+ this.addPModuleByScheme(schemeID, 'label', pModulesByScheme)
688
+ } else {
689
+ // Regular label - add to list for normal permission checks
690
+ nonPLabels.push(label)
691
+ }
692
+ }
693
+
694
+ return nonPLabels
695
+ }
696
+
621
697
  /**
622
698
  * Decrypts custom instructions in listOutputs results if encryption is configured.
623
699
  */
@@ -634,6 +710,43 @@ export class WalletPermissionsManager implements WalletInterface {
634
710
  return results
635
711
  }
636
712
 
713
+ /**
714
+ * Decrypts metadata in listActions results if encryption is configured.
715
+ */
716
+ private async decryptListActionsMetadata(results: ListActionsResult): Promise<ListActionsResult> {
717
+ if (results.actions) {
718
+ for (let i = 0; i < results.actions.length; i++) {
719
+ if (results.actions[i].description) {
720
+ results.actions[i].description = await this.maybeDecryptMetadata(results.actions[i].description)
721
+ }
722
+ if (results.actions[i].inputs) {
723
+ for (let j = 0; j < results.actions[i].inputs!.length; j++) {
724
+ if (results.actions[i].inputs![j].inputDescription) {
725
+ results.actions[i].inputs![j].inputDescription = await this.maybeDecryptMetadata(
726
+ results.actions[i].inputs![j].inputDescription
727
+ )
728
+ }
729
+ }
730
+ }
731
+ if (results.actions[i].outputs) {
732
+ for (let j = 0; j < results.actions[i].outputs!.length; j++) {
733
+ if (results.actions[i].outputs![j].outputDescription) {
734
+ results.actions[i].outputs![j].outputDescription = await this.maybeDecryptMetadata(
735
+ results.actions[i].outputs![j].outputDescription
736
+ )
737
+ }
738
+ if (results.actions[i].outputs![j].customInstructions) {
739
+ results.actions[i].outputs![j].customInstructions = await this.maybeDecryptMetadata(
740
+ results.actions[i].outputs![j].customInstructions!
741
+ )
742
+ }
743
+ }
744
+ }
745
+ }
746
+ }
747
+ return results
748
+ }
749
+
637
750
  /* ---------------------------------------------------------------------
638
751
  * 1) PUBLIC API FOR REGISTERING CALLBACKS (UI PROMPTS, LOGGING, ETC.)
639
752
  * --------------------------------------------------------------------- */
@@ -800,118 +913,128 @@ export class WalletPermissionsManager implements WalletInterface {
800
913
  throw new Error('Request ID not found.')
801
914
  }
802
915
 
803
- const originalRequest = matching.request as {
804
- originator: string
805
- permissions: GroupedPermissions
806
- displayOriginator?: string
807
- }
808
- const { originator, permissions: requestedPermissions, displayOriginator } = originalRequest
809
- const originLookupValues = this.buildOriginatorLookupValues(displayOriginator, originator)
810
-
811
- // --- Validation: Ensure granted permissions are a subset of what was requested ---
812
- if (params.granted.spendingAuthorization && !requestedPermissions.spendingAuthorization) {
813
- throw new Error('Granted spending authorization was not part of the original request.')
814
- }
815
- if (
816
- params.granted.protocolPermissions?.some(
817
- g => !requestedPermissions.protocolPermissions?.find(r => deepEqual(r, g))
818
- )
819
- ) {
820
- throw new Error('Granted protocol permissions are not a subset of the original request.')
821
- }
822
- if (params.granted.basketAccess?.some(g => !requestedPermissions.basketAccess?.find(r => deepEqual(r, g)))) {
823
- throw new Error('Granted basket access permissions are not a subset of the original request.')
824
- }
825
- if (
826
- params.granted.certificateAccess?.some(g => !requestedPermissions.certificateAccess?.find(r => deepEqual(r, g)))
827
- ) {
828
- throw new Error('Granted certificate access permissions are not a subset of the original request.')
829
- }
830
- // --- End Validation ---
831
-
832
- const expiry = params.expiry || 0 // default: never expires
916
+ try {
917
+ const originalRequest = matching.request as {
918
+ originator: string
919
+ permissions: GroupedPermissions
920
+ displayOriginator?: string
921
+ }
922
+ const { originator, permissions: requestedPermissions, displayOriginator } = originalRequest
923
+ const originLookupValues = this.buildOriginatorLookupValues(displayOriginator, originator)
833
924
 
834
- const toCreate: Array<{ request: PermissionRequest; expiry: number; amount?: number }> = []
835
- const toRenew: Array<{ oldToken: PermissionToken; request: PermissionRequest; expiry: number; amount?: number }> =
836
- []
925
+ // --- Validation: Ensure granted permissions are a subset of what was requested ---
926
+ if (params.granted.spendingAuthorization && !requestedPermissions.spendingAuthorization) {
927
+ throw new Error('Granted spending authorization was not part of the original request.')
928
+ }
929
+ if (
930
+ params.granted.protocolPermissions?.some(
931
+ g => !requestedPermissions.protocolPermissions?.find(r => deepEqual(r, g))
932
+ )
933
+ ) {
934
+ throw new Error('Granted protocol permissions are not a subset of the original request.')
935
+ }
936
+ if (params.granted.basketAccess?.some(g => !requestedPermissions.basketAccess?.find(r => deepEqual(r, g)))) {
937
+ throw new Error('Granted basket access permissions are not a subset of the original request.')
938
+ }
939
+ if (
940
+ params.granted.certificateAccess?.some(g => !requestedPermissions.certificateAccess?.find(r => deepEqual(r, g)))
941
+ ) {
942
+ throw new Error('Granted certificate access permissions are not a subset of the original request.')
943
+ }
944
+ // --- End Validation ---
837
945
 
838
- if (params.granted.spendingAuthorization) {
839
- toCreate.push({
840
- request: {
841
- type: 'spending',
842
- originator,
843
- spending: { satoshis: params.granted.spendingAuthorization.amount },
844
- reason: params.granted.spendingAuthorization.description
845
- },
846
- expiry: 0,
847
- amount: params.granted.spendingAuthorization.amount
848
- })
849
- }
946
+ const expiry = params.expiry || 0 // default: never expires
850
947
 
851
- const grantedProtocols = params.granted.protocolPermissions || []
852
- const protocolTokens = await this.mapWithConcurrency(grantedProtocols, 8, async p => {
853
- const token = await this.findProtocolToken(
854
- originator,
855
- false,
856
- p.protocolID,
857
- p.counterparty || 'self',
858
- true,
859
- originLookupValues
860
- )
861
- return { p, token }
862
- })
948
+ const toCreate: Array<{ request: PermissionRequest; expiry: number; amount?: number }> = []
949
+ const toRenew: Array<{ oldToken: PermissionToken; request: PermissionRequest; expiry: number; amount?: number }> =
950
+ []
863
951
 
864
- for (const { p, token } of protocolTokens) {
865
- const request: PermissionRequest = {
866
- type: 'protocol',
867
- originator,
868
- privileged: false,
869
- protocolID: p.protocolID,
870
- counterparty: p.counterparty || 'self',
871
- reason: p.description
872
- }
873
- if (token) {
874
- toRenew.push({ oldToken: token, request, expiry })
875
- } else {
876
- toCreate.push({ request, expiry })
952
+ if (params.granted.spendingAuthorization) {
953
+ toCreate.push({
954
+ request: {
955
+ type: 'spending',
956
+ originator,
957
+ spending: { satoshis: params.granted.spendingAuthorization.amount },
958
+ reason: params.granted.spendingAuthorization.description
959
+ },
960
+ expiry: 0,
961
+ amount: params.granted.spendingAuthorization.amount
962
+ })
877
963
  }
878
- }
879
964
 
880
- for (const b of params.granted.basketAccess || []) {
881
- toCreate.push({
882
- request: { type: 'basket', originator, basket: b.basket, reason: b.description },
883
- expiry
965
+ const grantedProtocols = params.granted.protocolPermissions || []
966
+ const protocolTokens = await this.mapWithConcurrency(grantedProtocols, 8, async p => {
967
+ const token = await this.findProtocolToken(
968
+ originator,
969
+ false,
970
+ p.protocolID,
971
+ p.counterparty || 'self',
972
+ true,
973
+ originLookupValues
974
+ )
975
+ return { p, token }
884
976
  })
885
- }
886
977
 
887
- for (const c of params.granted.certificateAccess || []) {
888
- toCreate.push({
889
- request: {
890
- type: 'certificate',
978
+ for (const { p, token } of protocolTokens) {
979
+ const request: PermissionRequest = {
980
+ type: 'protocol',
891
981
  originator,
892
982
  privileged: false,
893
- certificate: {
894
- verifier: c.verifierPublicKey,
895
- certType: c.type,
896
- fields: c.fields
983
+ protocolID: p.protocolID,
984
+ counterparty: p.counterparty || 'self',
985
+ reason: p.description
986
+ }
987
+ if (token) {
988
+ toRenew.push({ oldToken: token, request, expiry })
989
+ } else {
990
+ toCreate.push({ request, expiry })
991
+ }
992
+ }
993
+
994
+ for (const b of params.granted.basketAccess || []) {
995
+ toCreate.push({
996
+ request: { type: 'basket', originator, basket: b.basket, reason: b.description },
997
+ expiry
998
+ })
999
+ }
1000
+
1001
+ for (const c of params.granted.certificateAccess || []) {
1002
+ toCreate.push({
1003
+ request: {
1004
+ type: 'certificate',
1005
+ originator,
1006
+ privileged: false,
1007
+ certificate: {
1008
+ verifier: c.verifierPublicKey,
1009
+ certType: c.type,
1010
+ fields: c.fields
1011
+ },
1012
+ reason: c.description
897
1013
  },
898
- reason: c.description
899
- },
900
- expiry
901
- })
902
- }
1014
+ expiry
1015
+ })
1016
+ }
903
1017
 
904
- const created = await this.createPermissionTokensBestEffort(toCreate)
905
- const renewed = await this.renewPermissionTokensBestEffort(toRenew)
906
- for (const req of [...created, ...renewed]) {
907
- this.markRecentGrant(req)
908
- }
1018
+ const created = await this.createPermissionTokensBestEffort(toCreate)
1019
+ const renewed = await this.renewPermissionTokensBestEffort(toRenew)
1020
+ for (const req of [...created, ...renewed]) {
1021
+ this.markRecentGrant(req)
1022
+ }
909
1023
 
910
- // Resolve all pending promises for this request
911
- for (const p of matching.pending) {
912
- p.resolve(true)
1024
+ // Success - resolve all pending promises for this request
1025
+ for (const p of matching.pending) {
1026
+ p.resolve(true)
1027
+ }
1028
+ } catch (error) {
1029
+ // Failure - reject all pending promises so callers don't hang forever
1030
+ for (const p of matching.pending) {
1031
+ p.reject(error)
1032
+ }
1033
+ throw error
1034
+ } finally {
1035
+ // Always clean up the request entry
1036
+ this.activeRequests.delete(params.requestID)
913
1037
  }
914
- this.activeRequests.delete(params.requestID)
915
1038
  }
916
1039
 
917
1040
  /**
@@ -3510,22 +3633,17 @@ export class WalletPermissionsManager implements WalletInterface {
3510
3633
  args: Parameters<WalletInterface['createAction']>[0],
3511
3634
  originator?: string
3512
3635
  ): ReturnType<WalletInterface['createAction']> {
3513
- // 1) Identify unique P-modules involved (one per schemeID)
3636
+ // 1) Identify unique P-modules involved (one per schemeID) from both baskets and labels
3514
3637
  const pModulesByScheme = new Map<string, PermissionsModule>()
3515
3638
  const nonPBaskets: string[] = []
3516
3639
 
3640
+ // Check baskets for p modules
3517
3641
  if (args.outputs) {
3518
3642
  for (const out of args.outputs) {
3519
3643
  if (out.basket) {
3520
3644
  if (out.basket.startsWith('p ')) {
3521
3645
  const schemeID = out.basket.split(' ')[1]
3522
- if (!pModulesByScheme.has(schemeID)) {
3523
- const module = this.config.permissionModules?.[schemeID]
3524
- if (!module) {
3525
- throw new Error(`Unsupported P-basket scheme: p ${schemeID}`)
3526
- }
3527
- pModulesByScheme.set(schemeID, module)
3528
- }
3646
+ this.addPModuleByScheme(schemeID, 'basket', pModulesByScheme)
3529
3647
  } else {
3530
3648
  // Track non-P baskets for normal permission checks
3531
3649
  nonPBaskets.push(out.basket)
@@ -3534,6 +3652,9 @@ export class WalletPermissionsManager implements WalletInterface {
3534
3652
  }
3535
3653
  }
3536
3654
 
3655
+ // Check labels for p modules
3656
+ const nonPLabels = this.splitLabelsByPermissionModule(args.labels, pModulesByScheme)
3657
+
3537
3658
  // 2) Check permissions for non-P baskets
3538
3659
  for (const basket of nonPBaskets) {
3539
3660
  await this.ensureBasketAccess({
@@ -3544,15 +3665,14 @@ export class WalletPermissionsManager implements WalletInterface {
3544
3665
  })
3545
3666
  }
3546
3667
 
3547
- if (args.labels) {
3548
- for (const lbl of args.labels) {
3549
- await this.ensureLabelAccess({
3550
- originator: originator!,
3551
- label: lbl,
3552
- reason: args.description,
3553
- usageType: 'apply'
3554
- })
3555
- }
3668
+ // 3) Check permissions for non-P labels
3669
+ for (const lbl of nonPLabels) {
3670
+ await this.ensureLabelAccess({
3671
+ originator: originator!,
3672
+ label: lbl,
3673
+ reason: args.description,
3674
+ usageType: 'apply'
3675
+ })
3556
3676
  }
3557
3677
 
3558
3678
  /**
@@ -3758,91 +3878,154 @@ export class WalletPermissionsManager implements WalletInterface {
3758
3878
  ...args: Parameters<WalletInterface['listActions']>
3759
3879
  ): ReturnType<WalletInterface['listActions']> {
3760
3880
  const [requestArgs, originator] = args
3761
- // for each label, ensure label access
3762
- if (requestArgs.labels) {
3763
- for (const lbl of requestArgs.labels) {
3764
- await this.ensureLabelAccess({
3765
- originator: originator!,
3766
- label: lbl,
3767
- reason: 'listActions',
3768
- usageType: 'list'
3881
+
3882
+ // 1) Identify unique P-modules involved (one per schemeID, preserving label order)
3883
+ const pModulesByScheme = new Map<string, PermissionsModule>()
3884
+ const nonPLabels = this.splitLabelsByPermissionModule(requestArgs.labels, pModulesByScheme)
3885
+
3886
+ // 2) Check permissions for non-P labels
3887
+ for (const lbl of nonPLabels) {
3888
+ await this.ensureLabelAccess({
3889
+ originator: originator!,
3890
+ label: lbl,
3891
+ reason: 'listActions',
3892
+ usageType: 'list'
3893
+ })
3894
+ }
3895
+
3896
+ // 3) Call underlying wallet, with P-module transformations if needed
3897
+ let results: ListActionsResult
3898
+
3899
+ if (pModulesByScheme.size > 0) {
3900
+ // P-modules are involved - chain transformations
3901
+ const pModules = Array.from(pModulesByScheme.values())
3902
+
3903
+ // Chain onRequest calls through all modules in order
3904
+ let transformedArgs: object = requestArgs
3905
+ for (const module of pModules) {
3906
+ const transformed = await module.onRequest({
3907
+ method: 'listActions',
3908
+ args: transformedArgs,
3909
+ originator: originator!
3769
3910
  })
3911
+ transformedArgs = transformed.args
3770
3912
  }
3771
- }
3772
- const results = await this.underlying.listActions(...args)
3773
- // Transparently decrypt transaction metadata, if configured to do so.
3774
- if (results.actions) {
3775
- for (let i = 0; i < results.actions.length; i++) {
3776
- if (results.actions[i].description) {
3777
- results.actions[i].description = await this.maybeDecryptMetadata(results.actions[i].description)
3778
- }
3779
- if (results.actions[i].inputs) {
3780
- for (let j = 0; j < results.actions[i].inputs!.length; j++) {
3781
- if (results.actions[i].inputs![j].inputDescription) {
3782
- results.actions[i].inputs![j].inputDescription = await this.maybeDecryptMetadata(
3783
- results.actions[i].inputs![j].inputDescription
3784
- )
3785
- }
3786
- }
3787
- }
3788
- if (results.actions[i].outputs) {
3789
- for (let j = 0; j < results.actions[i].outputs!.length; j++) {
3790
- if (results.actions[i].outputs![j].outputDescription) {
3791
- results.actions[i].outputs![j].outputDescription = await this.maybeDecryptMetadata(
3792
- results.actions[i].outputs![j].outputDescription
3793
- )
3794
- }
3795
- if (results.actions[i].outputs![j].customInstructions) {
3796
- results.actions[i].outputs![j].customInstructions = await this.maybeDecryptMetadata(
3797
- results.actions[i].outputs![j].customInstructions!
3798
- )
3799
- }
3800
- }
3801
- }
3913
+
3914
+ // Call underlying wallet with transformed args
3915
+ results = await this.underlying.listActions(transformedArgs as ListActionsArgs, originator!)
3916
+
3917
+ // Chain onResponse calls in reverse order
3918
+ for (let i = pModules.length - 1; i >= 0; i--) {
3919
+ results = await pModules[i].onResponse(results, {
3920
+ method: 'listActions',
3921
+ originator: originator!
3922
+ })
3802
3923
  }
3924
+ } else {
3925
+ // No P-modules - call underlying wallet directly
3926
+ results = await this.underlying.listActions(...args)
3803
3927
  }
3804
- return results
3928
+
3929
+ // 4) Transparently decrypt transaction metadata, if configured to do so.
3930
+ return await this.decryptListActionsMetadata(results)
3805
3931
  }
3806
3932
 
3807
3933
  public async internalizeAction(
3808
3934
  ...args: Parameters<WalletInterface['internalizeAction']>
3809
3935
  ): ReturnType<WalletInterface['internalizeAction']> {
3810
3936
  const [requestArgs, originator] = args
3811
- // If the transaction is inserting outputs into baskets, we also ensure basket permission
3937
+
3938
+ // 1) Identify unique P-modules involved (one per schemeID) from both baskets and labels
3939
+ const pModulesByScheme = new Map<string, PermissionsModule>()
3940
+ const nonPBaskets: Array<{ outIndex: string; basket: string; customInstructions?: string }> = []
3941
+
3942
+ // Check baskets for p modules
3812
3943
  for (const outIndex in requestArgs.outputs) {
3813
3944
  const out = requestArgs.outputs[outIndex]
3814
3945
  if (out.protocol === 'basket insertion') {
3815
- // Delegate to permission module if needed
3816
- const pModuleResult = await this.delegateToPModuleIfNeeded(
3817
- out.insertionRemittance!.basket,
3818
- 'internalizeAction',
3819
- requestArgs,
3820
- originator!,
3821
- async transformedArgs => {
3822
- if (out.insertionRemittance!.customInstructions) {
3823
- ;(transformedArgs as InternalizeActionArgs).outputs[outIndex].insertionRemittance!.customInstructions =
3824
- await this.maybeEncryptMetadata(out.insertionRemittance!.customInstructions)
3825
- }
3826
- return await this.underlying.internalizeAction(transformedArgs as InternalizeActionArgs, originator!)
3827
- }
3828
- )
3829
- if (pModuleResult !== null) {
3830
- return pModuleResult
3946
+ const basket = out.insertionRemittance!.basket
3947
+ if (basket.startsWith('p ')) {
3948
+ const schemeID = basket.split(' ')[1]
3949
+ this.addPModuleByScheme(schemeID, 'basket', pModulesByScheme)
3950
+ } else {
3951
+ // Track non-P baskets for normal permission checks
3952
+ nonPBaskets.push({
3953
+ outIndex,
3954
+ basket,
3955
+ customInstructions: out.insertionRemittance!.customInstructions
3956
+ })
3831
3957
  }
3958
+ }
3959
+ }
3832
3960
 
3833
- await this.ensureBasketAccess({
3834
- originator: originator!,
3835
- basket: out.insertionRemittance!.basket,
3836
- reason: requestArgs.description,
3837
- usageType: 'insertion'
3961
+ // Check labels for p modules
3962
+ const nonPLabels = this.splitLabelsByPermissionModule(requestArgs.labels, pModulesByScheme)
3963
+
3964
+ // 2) Check permissions for non-P baskets
3965
+ for (const { outIndex, basket, customInstructions } of nonPBaskets) {
3966
+ await this.ensureBasketAccess({
3967
+ originator: originator!,
3968
+ basket,
3969
+ reason: requestArgs.description,
3970
+ usageType: 'insertion'
3971
+ })
3972
+ if (customInstructions) {
3973
+ requestArgs.outputs[outIndex].insertionRemittance!.customInstructions =
3974
+ await this.maybeEncryptMetadata(customInstructions)
3975
+ }
3976
+ }
3977
+
3978
+ // 3) Check permissions for non-P labels
3979
+ for (const lbl of nonPLabels) {
3980
+ await this.ensureLabelAccess({
3981
+ originator: originator!,
3982
+ label: lbl,
3983
+ reason: requestArgs.description,
3984
+ usageType: 'apply'
3985
+ })
3986
+ }
3987
+
3988
+ // 4) Call underlying wallet, with P-module transformations if needed
3989
+ if (pModulesByScheme.size > 0) {
3990
+ // P-modules are involved - chain transformations
3991
+ const pModules = Array.from(pModulesByScheme.values())
3992
+
3993
+ // Chain onRequest calls through all modules in order
3994
+ let transformedArgs: object = requestArgs
3995
+ for (const module of pModules) {
3996
+ const transformed = await module.onRequest({
3997
+ method: 'internalizeAction',
3998
+ args: transformedArgs,
3999
+ originator: originator!
3838
4000
  })
3839
- if (out.insertionRemittance!.customInstructions) {
3840
- requestArgs.outputs[outIndex].insertionRemittance!.customInstructions = await this.maybeEncryptMetadata(
3841
- out.insertionRemittance!.customInstructions
4001
+ transformedArgs = transformed.args
4002
+ }
4003
+
4004
+ // Encrypt custom instructions for p basket outputs
4005
+ for (const outIndex in (transformedArgs as InternalizeActionArgs).outputs) {
4006
+ const out = (transformedArgs as InternalizeActionArgs).outputs[outIndex]
4007
+ if (out.protocol === 'basket insertion' && out.insertionRemittance?.customInstructions) {
4008
+ out.insertionRemittance.customInstructions = await this.maybeEncryptMetadata(
4009
+ out.insertionRemittance.customInstructions
3842
4010
  )
3843
4011
  }
3844
4012
  }
4013
+
4014
+ // Call underlying wallet with transformed args
4015
+ let results = await this.underlying.internalizeAction(transformedArgs as InternalizeActionArgs, originator!)
4016
+
4017
+ // Chain onResponse calls in reverse order
4018
+ for (let i = pModules.length - 1; i >= 0; i--) {
4019
+ results = await pModules[i].onResponse(results, {
4020
+ method: 'internalizeAction',
4021
+ originator: originator!
4022
+ })
4023
+ }
4024
+
4025
+ return results
3845
4026
  }
4027
+
4028
+ // No P-modules - call underlying wallet directly
3846
4029
  return this.underlying.internalizeAction(...args)
3847
4030
  }
3848
4031
 
@@ -4359,6 +4542,7 @@ export class WalletPermissionsManager implements WalletInterface {
4359
4542
  * Checks if the given label is admin-reserved per BRC-100 rules:
4360
4543
  *
4361
4544
  * - Must not start with `admin` (admin-reserved)
4545
+ * - Must not start with `p ` (permissioned labels requiring a permission module)
4362
4546
  *
4363
4547
  * If it violates these rules and the caller is not admin, we consider it "admin-only."
4364
4548
  */
@@ -4366,6 +4550,9 @@ export class WalletPermissionsManager implements WalletInterface {
4366
4550
  if (label.startsWith('admin')) {
4367
4551
  return true
4368
4552
  }
4553
+ if (label.startsWith('p ')) {
4554
+ return true
4555
+ }
4369
4556
  return false
4370
4557
  }
4371
4558