@bsv/wallet-toolbox-client 1.7.22 → 1.8.1

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.
@@ -163,6 +163,68 @@ class WalletPermissionsManager {
163
163
  originator
164
164
  });
165
165
  }
166
+ /**
167
+ * Adds a permission module for the given schemeID if needed, throwing if unsupported.
168
+ */
169
+ addPModuleByScheme(schemeID, kind, pModulesByScheme) {
170
+ var _a;
171
+ if (pModulesByScheme.has(schemeID))
172
+ return;
173
+ const module = (_a = this.config.permissionModules) === null || _a === void 0 ? void 0 : _a[schemeID];
174
+ if (!module) {
175
+ throw new Error(`Unsupported P-${kind} scheme: p ${schemeID}`);
176
+ }
177
+ pModulesByScheme.set(schemeID, module);
178
+ }
179
+ /**
180
+ * Splits labels into P and non-P lists, registering any P-modules encountered.
181
+ *
182
+ * P-labels follow BRC-111 format: `p <moduleId> <payload>`
183
+ * - Must start with "p " (lowercase p + space)
184
+ * - Module ID must be at least 1 character with no spaces
185
+ * - Single space separates module ID from payload
186
+ * - Payload must be at least 1 character
187
+ *
188
+ * @example Valid: "p btms token123", "p invoicing invoice 2026-02-02"
189
+ * @example Invalid: "p btms" (no payload), "p btms " (empty payload), "p data" (empty moduleId)
190
+ *
191
+ * @param labels - Array of label strings to process
192
+ * @param pModulesByScheme - Map to populate with discovered p-modules
193
+ * @returns Array of non-P labels for normal permission checks
194
+ * @throws Error if p-label format is invalid or module is unsupported
195
+ */
196
+ splitLabelsByPermissionModule(labels, pModulesByScheme) {
197
+ const nonPLabels = [];
198
+ if (!labels)
199
+ return nonPLabels;
200
+ for (const label of labels) {
201
+ if (label.startsWith('p ')) {
202
+ // Remove "p " prefix to get "moduleId payload"
203
+ const remainder = label.slice(2);
204
+ // Find the space that separates moduleId from payload
205
+ const separatorIndex = remainder.indexOf(' ');
206
+ // Validate: must have a space (separatorIndex > 0) and payload after it
207
+ // separatorIndex <= 0 means no space found or moduleId is empty
208
+ // separatorIndex === remainder.length - 1 means space is last char (no payload)
209
+ if (separatorIndex <= 0 || separatorIndex === remainder.length - 1) {
210
+ throw new Error(`Invalid P-label format: ${label}`);
211
+ }
212
+ // Reject double spaces after moduleId (payload can't start with space)
213
+ if (remainder[separatorIndex + 1] === ' ') {
214
+ throw new Error(`Invalid P-label format: ${label}`);
215
+ }
216
+ // Extract moduleId (substring before first space)
217
+ const schemeID = remainder.slice(0, separatorIndex);
218
+ // Register the module (throws if unsupported)
219
+ this.addPModuleByScheme(schemeID, 'label', pModulesByScheme);
220
+ }
221
+ else {
222
+ // Regular label - add to list for normal permission checks
223
+ nonPLabels.push(label);
224
+ }
225
+ }
226
+ return nonPLabels;
227
+ }
166
228
  /**
167
229
  * Decrypts custom instructions in listOutputs results if encryption is configured.
168
230
  */
@@ -176,6 +238,36 @@ class WalletPermissionsManager {
176
238
  }
177
239
  return results;
178
240
  }
241
+ /**
242
+ * Decrypts metadata in listActions results if encryption is configured.
243
+ */
244
+ async decryptListActionsMetadata(results) {
245
+ if (results.actions) {
246
+ for (let i = 0; i < results.actions.length; i++) {
247
+ if (results.actions[i].description) {
248
+ results.actions[i].description = await this.maybeDecryptMetadata(results.actions[i].description);
249
+ }
250
+ if (results.actions[i].inputs) {
251
+ for (let j = 0; j < results.actions[i].inputs.length; j++) {
252
+ if (results.actions[i].inputs[j].inputDescription) {
253
+ results.actions[i].inputs[j].inputDescription = await this.maybeDecryptMetadata(results.actions[i].inputs[j].inputDescription);
254
+ }
255
+ }
256
+ }
257
+ if (results.actions[i].outputs) {
258
+ for (let j = 0; j < results.actions[i].outputs.length; j++) {
259
+ if (results.actions[i].outputs[j].outputDescription) {
260
+ results.actions[i].outputs[j].outputDescription = await this.maybeDecryptMetadata(results.actions[i].outputs[j].outputDescription);
261
+ }
262
+ if (results.actions[i].outputs[j].customInstructions) {
263
+ results.actions[i].outputs[j].customInstructions = await this.maybeDecryptMetadata(results.actions[i].outputs[j].customInstructions);
264
+ }
265
+ }
266
+ }
267
+ }
268
+ }
269
+ return results;
270
+ }
179
271
  /* ---------------------------------------------------------------------
180
272
  * 1) PUBLIC API FOR REGISTERING CALLBACKS (UI PROMPTS, LOGGING, ETC.)
181
273
  * --------------------------------------------------------------------- */
@@ -316,91 +408,103 @@ class WalletPermissionsManager {
316
408
  if (!matching) {
317
409
  throw new Error('Request ID not found.');
318
410
  }
319
- const originalRequest = matching.request;
320
- const { originator, permissions: requestedPermissions, displayOriginator } = originalRequest;
321
- const originLookupValues = this.buildOriginatorLookupValues(displayOriginator, originator);
322
- // --- Validation: Ensure granted permissions are a subset of what was requested ---
323
- if (params.granted.spendingAuthorization && !requestedPermissions.spendingAuthorization) {
324
- throw new Error('Granted spending authorization was not part of the original request.');
325
- }
326
- if ((_a = params.granted.protocolPermissions) === null || _a === void 0 ? void 0 : _a.some(g => { var _a; return !((_a = requestedPermissions.protocolPermissions) === null || _a === void 0 ? void 0 : _a.find(r => deepEqual(r, g))); })) {
327
- throw new Error('Granted protocol permissions are not a subset of the original request.');
328
- }
329
- if ((_b = params.granted.basketAccess) === null || _b === void 0 ? void 0 : _b.some(g => { var _a; return !((_a = requestedPermissions.basketAccess) === null || _a === void 0 ? void 0 : _a.find(r => deepEqual(r, g))); })) {
330
- throw new Error('Granted basket access permissions are not a subset of the original request.');
331
- }
332
- if ((_c = params.granted.certificateAccess) === null || _c === void 0 ? void 0 : _c.some(g => { var _a; return !((_a = requestedPermissions.certificateAccess) === null || _a === void 0 ? void 0 : _a.find(r => deepEqual(r, g))); })) {
333
- throw new Error('Granted certificate access permissions are not a subset of the original request.');
334
- }
335
- // --- End Validation ---
336
- const expiry = params.expiry || 0; // default: never expires
337
- const toCreate = [];
338
- const toRenew = [];
339
- if (params.granted.spendingAuthorization) {
340
- toCreate.push({
341
- request: {
342
- type: 'spending',
343
- originator,
344
- spending: { satoshis: params.granted.spendingAuthorization.amount },
345
- reason: params.granted.spendingAuthorization.description
346
- },
347
- expiry: 0,
348
- amount: params.granted.spendingAuthorization.amount
349
- });
350
- }
351
- const grantedProtocols = params.granted.protocolPermissions || [];
352
- const protocolTokens = await this.mapWithConcurrency(grantedProtocols, 8, async (p) => {
353
- const token = await this.findProtocolToken(originator, false, p.protocolID, p.counterparty || 'self', true, originLookupValues);
354
- return { p, token };
355
- });
356
- for (const { p, token } of protocolTokens) {
357
- const request = {
358
- type: 'protocol',
359
- originator,
360
- privileged: false,
361
- protocolID: p.protocolID,
362
- counterparty: p.counterparty || 'self',
363
- reason: p.description
364
- };
365
- if (token) {
366
- toRenew.push({ oldToken: token, request, expiry });
411
+ try {
412
+ const originalRequest = matching.request;
413
+ const { originator, permissions: requestedPermissions, displayOriginator } = originalRequest;
414
+ const originLookupValues = this.buildOriginatorLookupValues(displayOriginator, originator);
415
+ // --- Validation: Ensure granted permissions are a subset of what was requested ---
416
+ if (params.granted.spendingAuthorization && !requestedPermissions.spendingAuthorization) {
417
+ throw new Error('Granted spending authorization was not part of the original request.');
367
418
  }
368
- else {
369
- toCreate.push({ request, expiry });
419
+ if ((_a = params.granted.protocolPermissions) === null || _a === void 0 ? void 0 : _a.some(g => { var _a; return !((_a = requestedPermissions.protocolPermissions) === null || _a === void 0 ? void 0 : _a.find(r => deepEqual(r, g))); })) {
420
+ throw new Error('Granted protocol permissions are not a subset of the original request.');
370
421
  }
371
- }
372
- for (const b of params.granted.basketAccess || []) {
373
- toCreate.push({
374
- request: { type: 'basket', originator, basket: b.basket, reason: b.description },
375
- expiry
422
+ if ((_b = params.granted.basketAccess) === null || _b === void 0 ? void 0 : _b.some(g => { var _a; return !((_a = requestedPermissions.basketAccess) === null || _a === void 0 ? void 0 : _a.find(r => deepEqual(r, g))); })) {
423
+ throw new Error('Granted basket access permissions are not a subset of the original request.');
424
+ }
425
+ if ((_c = params.granted.certificateAccess) === null || _c === void 0 ? void 0 : _c.some(g => { var _a; return !((_a = requestedPermissions.certificateAccess) === null || _a === void 0 ? void 0 : _a.find(r => deepEqual(r, g))); })) {
426
+ throw new Error('Granted certificate access permissions are not a subset of the original request.');
427
+ }
428
+ // --- End Validation ---
429
+ const expiry = params.expiry || 0; // default: never expires
430
+ const toCreate = [];
431
+ const toRenew = [];
432
+ if (params.granted.spendingAuthorization) {
433
+ toCreate.push({
434
+ request: {
435
+ type: 'spending',
436
+ originator,
437
+ spending: { satoshis: params.granted.spendingAuthorization.amount },
438
+ reason: params.granted.spendingAuthorization.description
439
+ },
440
+ expiry: 0,
441
+ amount: params.granted.spendingAuthorization.amount
442
+ });
443
+ }
444
+ const grantedProtocols = params.granted.protocolPermissions || [];
445
+ const protocolTokens = await this.mapWithConcurrency(grantedProtocols, 8, async (p) => {
446
+ const token = await this.findProtocolToken(originator, false, p.protocolID, p.counterparty || 'self', true, originLookupValues);
447
+ return { p, token };
376
448
  });
377
- }
378
- for (const c of params.granted.certificateAccess || []) {
379
- toCreate.push({
380
- request: {
381
- type: 'certificate',
449
+ for (const { p, token } of protocolTokens) {
450
+ const request = {
451
+ type: 'protocol',
382
452
  originator,
383
453
  privileged: false,
384
- certificate: {
385
- verifier: c.verifierPublicKey,
386
- certType: c.type,
387
- fields: c.fields
454
+ protocolID: p.protocolID,
455
+ counterparty: p.counterparty || 'self',
456
+ reason: p.description
457
+ };
458
+ if (token) {
459
+ toRenew.push({ oldToken: token, request, expiry });
460
+ }
461
+ else {
462
+ toCreate.push({ request, expiry });
463
+ }
464
+ }
465
+ for (const b of params.granted.basketAccess || []) {
466
+ toCreate.push({
467
+ request: { type: 'basket', originator, basket: b.basket, reason: b.description },
468
+ expiry
469
+ });
470
+ }
471
+ for (const c of params.granted.certificateAccess || []) {
472
+ toCreate.push({
473
+ request: {
474
+ type: 'certificate',
475
+ originator,
476
+ privileged: false,
477
+ certificate: {
478
+ verifier: c.verifierPublicKey,
479
+ certType: c.type,
480
+ fields: c.fields
481
+ },
482
+ reason: c.description
388
483
  },
389
- reason: c.description
390
- },
391
- expiry
392
- });
484
+ expiry
485
+ });
486
+ }
487
+ const created = await this.createPermissionTokensBestEffort(toCreate);
488
+ const renewed = await this.renewPermissionTokensBestEffort(toRenew);
489
+ for (const req of [...created, ...renewed]) {
490
+ this.markRecentGrant(req);
491
+ }
492
+ // Success - resolve all pending promises for this request
493
+ for (const p of matching.pending) {
494
+ p.resolve(true);
495
+ }
393
496
  }
394
- const created = await this.createPermissionTokensBestEffort(toCreate);
395
- const renewed = await this.renewPermissionTokensBestEffort(toRenew);
396
- for (const req of [...created, ...renewed]) {
397
- this.markRecentGrant(req);
497
+ catch (error) {
498
+ // Failure - reject all pending promises so callers don't hang forever
499
+ for (const p of matching.pending) {
500
+ p.reject(error);
501
+ }
502
+ throw error;
398
503
  }
399
- // Resolve all pending promises for this request
400
- for (const p of matching.pending) {
401
- p.resolve(true);
504
+ finally {
505
+ // Always clean up the request entry
506
+ this.activeRequests.delete(params.requestID);
402
507
  }
403
- this.activeRequests.delete(params.requestID);
404
508
  }
405
509
  /**
406
510
  * Denies a previously requested grouped permission.
@@ -2482,22 +2586,16 @@ class WalletPermissionsManager {
2482
2586
  * 7) BRC-100 WALLET INTERFACE FORWARDING WITH PERMISSION CHECKS
2483
2587
  * --------------------------------------------------------------------- */
2484
2588
  async createAction(args, originator) {
2485
- var _a;
2486
- // 1) Identify unique P-modules involved (one per schemeID)
2589
+ // 1) Identify unique P-modules involved (one per schemeID) from both baskets and labels
2487
2590
  const pModulesByScheme = new Map();
2488
2591
  const nonPBaskets = [];
2592
+ // Check baskets for p modules
2489
2593
  if (args.outputs) {
2490
2594
  for (const out of args.outputs) {
2491
2595
  if (out.basket) {
2492
2596
  if (out.basket.startsWith('p ')) {
2493
2597
  const schemeID = out.basket.split(' ')[1];
2494
- if (!pModulesByScheme.has(schemeID)) {
2495
- const module = (_a = this.config.permissionModules) === null || _a === void 0 ? void 0 : _a[schemeID];
2496
- if (!module) {
2497
- throw new Error(`Unsupported P-basket scheme: p ${schemeID}`);
2498
- }
2499
- pModulesByScheme.set(schemeID, module);
2500
- }
2598
+ this.addPModuleByScheme(schemeID, 'basket', pModulesByScheme);
2501
2599
  }
2502
2600
  else {
2503
2601
  // Track non-P baskets for normal permission checks
@@ -2506,6 +2604,8 @@ class WalletPermissionsManager {
2506
2604
  }
2507
2605
  }
2508
2606
  }
2607
+ // Check labels for p modules
2608
+ const nonPLabels = this.splitLabelsByPermissionModule(args.labels, pModulesByScheme);
2509
2609
  // 2) Check permissions for non-P baskets
2510
2610
  for (const basket of nonPBaskets) {
2511
2611
  await this.ensureBasketAccess({
@@ -2515,15 +2615,14 @@ class WalletPermissionsManager {
2515
2615
  usageType: 'insertion'
2516
2616
  });
2517
2617
  }
2518
- if (args.labels) {
2519
- for (const lbl of args.labels) {
2520
- await this.ensureLabelAccess({
2521
- originator: originator,
2522
- label: lbl,
2523
- reason: args.description,
2524
- usageType: 'apply'
2525
- });
2526
- }
2618
+ // 3) Check permissions for non-P labels
2619
+ for (const lbl of nonPLabels) {
2620
+ await this.ensureLabelAccess({
2621
+ originator: originator,
2622
+ label: lbl,
2623
+ reason: args.description,
2624
+ usageType: 'apply'
2625
+ });
2527
2626
  }
2528
2627
  /**
2529
2628
  * 4) Force signAndProcess=false unless the originator is admin and explicitly sets it to true.
@@ -2701,74 +2800,132 @@ class WalletPermissionsManager {
2701
2800
  }
2702
2801
  async listActions(...args) {
2703
2802
  const [requestArgs, originator] = args;
2704
- // for each label, ensure label access
2705
- if (requestArgs.labels) {
2706
- for (const lbl of requestArgs.labels) {
2707
- await this.ensureLabelAccess({
2708
- originator: originator,
2709
- label: lbl,
2710
- reason: 'listActions',
2711
- usageType: 'list'
2803
+ // 1) Identify unique P-modules involved (one per schemeID, preserving label order)
2804
+ const pModulesByScheme = new Map();
2805
+ const nonPLabels = this.splitLabelsByPermissionModule(requestArgs.labels, pModulesByScheme);
2806
+ // 2) Check permissions for non-P labels
2807
+ for (const lbl of nonPLabels) {
2808
+ await this.ensureLabelAccess({
2809
+ originator: originator,
2810
+ label: lbl,
2811
+ reason: 'listActions',
2812
+ usageType: 'list'
2813
+ });
2814
+ }
2815
+ // 3) Call underlying wallet, with P-module transformations if needed
2816
+ let results;
2817
+ if (pModulesByScheme.size > 0) {
2818
+ // P-modules are involved - chain transformations
2819
+ const pModules = Array.from(pModulesByScheme.values());
2820
+ // Chain onRequest calls through all modules in order
2821
+ let transformedArgs = requestArgs;
2822
+ for (const module of pModules) {
2823
+ const transformed = await module.onRequest({
2824
+ method: 'listActions',
2825
+ args: transformedArgs,
2826
+ originator: originator
2712
2827
  });
2828
+ transformedArgs = transformed.args;
2713
2829
  }
2714
- }
2715
- const results = await this.underlying.listActions(...args);
2716
- // Transparently decrypt transaction metadata, if configured to do so.
2717
- if (results.actions) {
2718
- for (let i = 0; i < results.actions.length; i++) {
2719
- if (results.actions[i].description) {
2720
- results.actions[i].description = await this.maybeDecryptMetadata(results.actions[i].description);
2721
- }
2722
- if (results.actions[i].inputs) {
2723
- for (let j = 0; j < results.actions[i].inputs.length; j++) {
2724
- if (results.actions[i].inputs[j].inputDescription) {
2725
- results.actions[i].inputs[j].inputDescription = await this.maybeDecryptMetadata(results.actions[i].inputs[j].inputDescription);
2726
- }
2727
- }
2728
- }
2729
- if (results.actions[i].outputs) {
2730
- for (let j = 0; j < results.actions[i].outputs.length; j++) {
2731
- if (results.actions[i].outputs[j].outputDescription) {
2732
- results.actions[i].outputs[j].outputDescription = await this.maybeDecryptMetadata(results.actions[i].outputs[j].outputDescription);
2733
- }
2734
- if (results.actions[i].outputs[j].customInstructions) {
2735
- results.actions[i].outputs[j].customInstructions = await this.maybeDecryptMetadata(results.actions[i].outputs[j].customInstructions);
2736
- }
2737
- }
2738
- }
2830
+ // Call underlying wallet with transformed args
2831
+ results = await this.underlying.listActions(transformedArgs, originator);
2832
+ // Chain onResponse calls in reverse order
2833
+ for (let i = pModules.length - 1; i >= 0; i--) {
2834
+ results = await pModules[i].onResponse(results, {
2835
+ method: 'listActions',
2836
+ originator: originator
2837
+ });
2739
2838
  }
2740
2839
  }
2741
- return results;
2840
+ else {
2841
+ // No P-modules - call underlying wallet directly
2842
+ results = await this.underlying.listActions(...args);
2843
+ }
2844
+ // 4) Transparently decrypt transaction metadata, if configured to do so.
2845
+ return await this.decryptListActionsMetadata(results);
2742
2846
  }
2743
2847
  async internalizeAction(...args) {
2848
+ var _a;
2744
2849
  const [requestArgs, originator] = args;
2745
- // If the transaction is inserting outputs into baskets, we also ensure basket permission
2850
+ // 1) Identify unique P-modules involved (one per schemeID) from both baskets and labels
2851
+ const pModulesByScheme = new Map();
2852
+ const nonPBaskets = [];
2853
+ // Check baskets for p modules
2746
2854
  for (const outIndex in requestArgs.outputs) {
2747
2855
  const out = requestArgs.outputs[outIndex];
2748
2856
  if (out.protocol === 'basket insertion') {
2749
- // Delegate to permission module if needed
2750
- const pModuleResult = await this.delegateToPModuleIfNeeded(out.insertionRemittance.basket, 'internalizeAction', requestArgs, originator, async (transformedArgs) => {
2751
- if (out.insertionRemittance.customInstructions) {
2752
- ;
2753
- transformedArgs.outputs[outIndex].insertionRemittance.customInstructions =
2754
- await this.maybeEncryptMetadata(out.insertionRemittance.customInstructions);
2755
- }
2756
- return await this.underlying.internalizeAction(transformedArgs, originator);
2757
- });
2758
- if (pModuleResult !== null) {
2759
- return pModuleResult;
2857
+ const basket = out.insertionRemittance.basket;
2858
+ if (basket.startsWith('p ')) {
2859
+ const schemeID = basket.split(' ')[1];
2860
+ this.addPModuleByScheme(schemeID, 'basket', pModulesByScheme);
2760
2861
  }
2761
- await this.ensureBasketAccess({
2762
- originator: originator,
2763
- basket: out.insertionRemittance.basket,
2764
- reason: requestArgs.description,
2765
- usageType: 'insertion'
2862
+ else {
2863
+ // Track non-P baskets for normal permission checks
2864
+ nonPBaskets.push({
2865
+ outIndex,
2866
+ basket,
2867
+ customInstructions: out.insertionRemittance.customInstructions
2868
+ });
2869
+ }
2870
+ }
2871
+ }
2872
+ // Check labels for p modules
2873
+ const nonPLabels = this.splitLabelsByPermissionModule(requestArgs.labels, pModulesByScheme);
2874
+ // 2) Check permissions for non-P baskets
2875
+ for (const { outIndex, basket, customInstructions } of nonPBaskets) {
2876
+ await this.ensureBasketAccess({
2877
+ originator: originator,
2878
+ basket,
2879
+ reason: requestArgs.description,
2880
+ usageType: 'insertion'
2881
+ });
2882
+ if (customInstructions) {
2883
+ requestArgs.outputs[outIndex].insertionRemittance.customInstructions =
2884
+ await this.maybeEncryptMetadata(customInstructions);
2885
+ }
2886
+ }
2887
+ // 3) Check permissions for non-P labels
2888
+ for (const lbl of nonPLabels) {
2889
+ await this.ensureLabelAccess({
2890
+ originator: originator,
2891
+ label: lbl,
2892
+ reason: requestArgs.description,
2893
+ usageType: 'apply'
2894
+ });
2895
+ }
2896
+ // 4) Call underlying wallet, with P-module transformations if needed
2897
+ if (pModulesByScheme.size > 0) {
2898
+ // P-modules are involved - chain transformations
2899
+ const pModules = Array.from(pModulesByScheme.values());
2900
+ // Chain onRequest calls through all modules in order
2901
+ let transformedArgs = requestArgs;
2902
+ for (const module of pModules) {
2903
+ const transformed = await module.onRequest({
2904
+ method: 'internalizeAction',
2905
+ args: transformedArgs,
2906
+ originator: originator
2766
2907
  });
2767
- if (out.insertionRemittance.customInstructions) {
2768
- requestArgs.outputs[outIndex].insertionRemittance.customInstructions = await this.maybeEncryptMetadata(out.insertionRemittance.customInstructions);
2908
+ transformedArgs = transformed.args;
2909
+ }
2910
+ // Encrypt custom instructions for p basket outputs
2911
+ for (const outIndex in transformedArgs.outputs) {
2912
+ const out = transformedArgs.outputs[outIndex];
2913
+ if (out.protocol === 'basket insertion' && ((_a = out.insertionRemittance) === null || _a === void 0 ? void 0 : _a.customInstructions)) {
2914
+ out.insertionRemittance.customInstructions = await this.maybeEncryptMetadata(out.insertionRemittance.customInstructions);
2769
2915
  }
2770
2916
  }
2917
+ // Call underlying wallet with transformed args
2918
+ let results = await this.underlying.internalizeAction(transformedArgs, originator);
2919
+ // Chain onResponse calls in reverse order
2920
+ for (let i = pModules.length - 1; i >= 0; i--) {
2921
+ results = await pModules[i].onResponse(results, {
2922
+ method: 'internalizeAction',
2923
+ originator: originator
2924
+ });
2925
+ }
2926
+ return results;
2771
2927
  }
2928
+ // No P-modules - call underlying wallet directly
2772
2929
  return this.underlying.internalizeAction(...args);
2773
2930
  }
2774
2931
  async listOutputs(...args) {
@@ -3153,6 +3310,7 @@ class WalletPermissionsManager {
3153
3310
  * Checks if the given label is admin-reserved per BRC-100 rules:
3154
3311
  *
3155
3312
  * - Must not start with `admin` (admin-reserved)
3313
+ * - Must not start with `p ` (permissioned labels requiring a permission module)
3156
3314
  *
3157
3315
  * If it violates these rules and the caller is not admin, we consider it "admin-only."
3158
3316
  */
@@ -3160,6 +3318,9 @@ class WalletPermissionsManager {
3160
3318
  if (label.startsWith('admin')) {
3161
3319
  return true;
3162
3320
  }
3321
+ if (label.startsWith('p ')) {
3322
+ return true;
3323
+ }
3163
3324
  return false;
3164
3325
  }
3165
3326
  /**