@bsv/wallet-toolbox 1.6.40 → 1.6.41

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 (34) hide show
  1. package/docs/client.md +67 -29
  2. package/docs/services.md +1 -1
  3. package/docs/storage.md +34 -8
  4. package/docs/wallet.md +67 -29
  5. package/mobile/out/src/WalletPermissionsManager.d.ts +29 -0
  6. package/mobile/out/src/WalletPermissionsManager.d.ts.map +1 -1
  7. package/mobile/out/src/WalletPermissionsManager.js +581 -371
  8. package/mobile/out/src/WalletPermissionsManager.js.map +1 -1
  9. package/mobile/out/src/sdk/WalletError.d.ts.map +1 -1
  10. package/mobile/out/src/sdk/WalletError.js +3 -1
  11. package/mobile/out/src/sdk/WalletError.js.map +1 -1
  12. package/mobile/out/src/storage/WalletStorageManager.d.ts.map +1 -1
  13. package/mobile/out/src/storage/WalletStorageManager.js +3 -1
  14. package/mobile/out/src/storage/WalletStorageManager.js.map +1 -1
  15. package/mobile/package-lock.json +6 -6
  16. package/mobile/package.json +2 -2
  17. package/out/src/WalletPermissionsManager.d.ts +29 -0
  18. package/out/src/WalletPermissionsManager.d.ts.map +1 -1
  19. package/out/src/WalletPermissionsManager.js +581 -371
  20. package/out/src/WalletPermissionsManager.js.map +1 -1
  21. package/out/src/__tests/WalletPermissionsManager.tokens.test.js +1 -80
  22. package/out/src/__tests/WalletPermissionsManager.tokens.test.js.map +1 -1
  23. package/out/src/sdk/WalletError.d.ts.map +1 -1
  24. package/out/src/sdk/WalletError.js +3 -1
  25. package/out/src/sdk/WalletError.js.map +1 -1
  26. package/out/src/storage/WalletStorageManager.d.ts.map +1 -1
  27. package/out/src/storage/WalletStorageManager.js +3 -1
  28. package/out/src/storage/WalletStorageManager.js.map +1 -1
  29. package/out/tsconfig.all.tsbuildinfo +1 -1
  30. package/package.json +2 -2
  31. package/src/WalletPermissionsManager.ts +674 -444
  32. package/src/__tests/WalletPermissionsManager.tokens.test.ts +1 -83
  33. package/src/sdk/WalletError.ts +9 -3
  34. package/src/storage/WalletStorageManager.ts +7 -5
@@ -93,8 +93,9 @@ class WalletPermissionsManager {
93
93
  this.activeRequests = new Map();
94
94
  /** Cache recently confirmed permissions to avoid repeated lookups. */
95
95
  this.permissionCache = new Map();
96
+ this.recentGrants = new Map();
96
97
  this.underlying = underlyingWallet;
97
- this.adminOriginator = adminOriginator;
98
+ this.adminOriginator = this.normalizeOriginator(adminOriginator) || adminOriginator;
98
99
  // Default all config options to true unless specified
99
100
  this.config = {
100
101
  seekProtocolPermissionsForSigning: true,
@@ -226,6 +227,7 @@ class WalletPermissionsManager {
226
227
  const expiry = params.expiry || Math.floor(Date.now() / 1000) + 3600 * 24 * 30;
227
228
  const key = this.buildRequestKey(matching.request);
228
229
  this.cachePermission(key, expiry);
230
+ this.markRecentGrant(matching.request);
229
231
  }
230
232
  }
231
233
  /**
@@ -259,7 +261,8 @@ class WalletPermissionsManager {
259
261
  throw new Error('Request ID not found.');
260
262
  }
261
263
  const originalRequest = matching.request;
262
- const { originator, permissions: requestedPermissions } = originalRequest;
264
+ const { originator, permissions: requestedPermissions, displayOriginator } = originalRequest;
265
+ const originLookupValues = this.buildOriginatorLookupValues(displayOriginator, originator);
263
266
  // --- Validation: Ensure granted permissions are a subset of what was requested ---
264
267
  if (params.granted.spendingAuthorization &&
265
268
  !deepEqual(params.granted.spendingAuthorization, requestedPermissions.spendingAuthorization)) {
@@ -287,33 +290,39 @@ class WalletPermissionsManager {
287
290
  }
288
291
  for (const p of params.granted.protocolPermissions || []) {
289
292
  const token = await this.findProtocolToken(originator, false, // No privileged protocols allowed in groups for added security.
290
- p.protocolID, p.counterparty || 'self', true);
293
+ p.protocolID, p.counterparty || 'self', true, originLookupValues);
291
294
  if (token) {
292
- await this.renewPermissionOnChain(token, {
295
+ const request = {
293
296
  type: 'protocol',
294
297
  originator,
295
298
  privileged: false, // No privileged protocols allowed in groups for added security.
296
299
  protocolID: p.protocolID,
297
300
  counterparty: p.counterparty || 'self',
298
301
  reason: p.description
299
- }, expiry);
302
+ };
303
+ await this.renewPermissionOnChain(token, request, expiry);
304
+ this.markRecentGrant(request);
300
305
  }
301
306
  else {
302
- await this.createPermissionOnChain({
307
+ const request = {
303
308
  type: 'protocol',
304
309
  originator,
305
310
  privileged: false, // No privileged protocols allowed in groups for added security.
306
311
  protocolID: p.protocolID,
307
312
  counterparty: p.counterparty || 'self',
308
313
  reason: p.description
309
- }, expiry);
314
+ };
315
+ await this.createPermissionOnChain(request, expiry);
316
+ this.markRecentGrant(request);
310
317
  }
311
318
  }
312
319
  for (const b of params.granted.basketAccess || []) {
313
- await this.createPermissionOnChain({ type: 'basket', originator, basket: b.basket, reason: b.description }, expiry);
320
+ const request = { type: 'basket', originator, basket: b.basket, reason: b.description };
321
+ await this.createPermissionOnChain(request, expiry);
322
+ this.markRecentGrant(request);
314
323
  }
315
324
  for (const c of params.granted.certificateAccess || []) {
316
- await this.createPermissionOnChain({
325
+ const request = {
317
326
  type: 'certificate',
318
327
  originator,
319
328
  privileged: false, // No certificates on the privileged identity are allowed as part of groups.
@@ -323,7 +332,9 @@ class WalletPermissionsManager {
323
332
  fields: c.fields
324
333
  },
325
334
  reason: c.description
326
- }, expiry);
335
+ };
336
+ await this.createPermissionOnChain(request, expiry);
337
+ this.markRecentGrant(request);
327
338
  }
328
339
  // Resolve all pending promises for this request
329
340
  for (const p of matching.pending) {
@@ -355,6 +366,8 @@ class WalletPermissionsManager {
355
366
  * If no valid (unexpired) permission token is found, triggers a permission request flow.
356
367
  */
357
368
  async ensureProtocolPermission({ originator, privileged, protocolID, counterparty, reason, seekPermission = true, usageType }) {
369
+ const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator);
370
+ originator = normalizedOriginator;
358
371
  // 1) adminOriginator can do anything
359
372
  if (this.isAdminOriginator(originator))
360
373
  return true;
@@ -398,9 +411,12 @@ class WalletPermissionsManager {
398
411
  if (this.isPermissionCached(cacheKey)) {
399
412
  return true;
400
413
  }
414
+ if (this.isRecentlyGranted(cacheKey)) {
415
+ return true;
416
+ }
401
417
  // 4) Attempt to find a valid token in the internal basket
402
418
  const token = await this.findProtocolToken(originator, privileged, protocolID, counterparty,
403
- /*includeExpired=*/ true);
419
+ /*includeExpired=*/ true, lookupValues);
404
420
  if (token) {
405
421
  if (!this.isTokenExpired(token.expiry)) {
406
422
  // valid and unexpired
@@ -446,6 +462,8 @@ class WalletPermissionsManager {
446
462
  * If not, triggers a permission request flow.
447
463
  */
448
464
  async ensureBasketAccess({ originator, basket, reason, seekPermission = true, usageType }) {
465
+ const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator);
466
+ originator = normalizedOriginator;
449
467
  if (this.isAdminOriginator(originator))
450
468
  return true;
451
469
  if (this.isAdminBasket(basket)) {
@@ -461,7 +479,10 @@ class WalletPermissionsManager {
461
479
  if (this.isPermissionCached(cacheKey)) {
462
480
  return true;
463
481
  }
464
- const token = await this.findBasketToken(originator, basket, true);
482
+ if (this.isRecentlyGranted(cacheKey)) {
483
+ return true;
484
+ }
485
+ const token = await this.findBasketToken(originator, basket, true, lookupValues);
465
486
  if (token) {
466
487
  if (!this.isTokenExpired(token.expiry)) {
467
488
  this.cachePermission(cacheKey, token.expiry);
@@ -501,6 +522,8 @@ class WalletPermissionsManager {
501
522
  * This is relevant when revealing certificate fields in DCAP contexts.
502
523
  */
503
524
  async ensureCertificateAccess({ originator, privileged, verifier, certType, fields, reason, seekPermission = true, usageType }) {
525
+ const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator);
526
+ originator = normalizedOriginator;
504
527
  if (this.isAdminOriginator(originator))
505
528
  return true;
506
529
  if (usageType === 'disclosure' && !this.config.seekCertificateDisclosurePermissions) {
@@ -518,8 +541,11 @@ class WalletPermissionsManager {
518
541
  if (this.isPermissionCached(cacheKey)) {
519
542
  return true;
520
543
  }
544
+ if (this.isRecentlyGranted(cacheKey)) {
545
+ return true;
546
+ }
521
547
  const token = await this.findCertificateToken(originator, privileged, verifier, certType, fields,
522
- /*includeExpired=*/ true);
548
+ /*includeExpired=*/ true, lookupValues);
523
549
  if (token) {
524
550
  if (!this.isTokenExpired(token.expiry)) {
525
551
  this.cachePermission(cacheKey, token.expiry);
@@ -560,6 +586,8 @@ class WalletPermissionsManager {
560
586
  * If the existing token limit is insufficient, attempts to renew. If no token, attempts to create one.
561
587
  */
562
588
  async ensureSpendingAuthorization({ originator, satoshis, lineItems, reason, seekPermission = true }) {
589
+ const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator);
590
+ originator = normalizedOriginator;
563
591
  if (this.isAdminOriginator(originator))
564
592
  return true;
565
593
  if (!this.config.seekSpendingPermissions) {
@@ -570,7 +598,7 @@ class WalletPermissionsManager {
570
598
  if (this.isPermissionCached(cacheKey)) {
571
599
  return true;
572
600
  }
573
- const token = await this.findSpendingToken(originator);
601
+ const token = await this.findSpendingToken(originator, lookupValues);
574
602
  if (token === null || token === void 0 ? void 0 : token.authorizedAmount) {
575
603
  // Check how much has been spent so far
576
604
  const spentSoFar = await this.querySpentSince(token);
@@ -612,6 +640,8 @@ class WalletPermissionsManager {
612
640
  * If no valid (unexpired) permission token is found, triggers a permission request flow.
613
641
  */
614
642
  async ensureLabelAccess({ originator, label, reason, seekPermission = true, usageType }) {
643
+ const { normalized: normalizedOriginator } = this.prepareOriginator(originator);
644
+ originator = normalizedOriginator;
615
645
  // 1) adminOriginator can do anything
616
646
  if (this.isAdminOriginator(originator))
617
647
  return true;
@@ -654,7 +684,14 @@ class WalletPermissionsManager {
654
684
  * and return a promise that resolves once permission is granted or rejects if denied.
655
685
  */
656
686
  async requestPermissionFlow(r) {
657
- const key = this.buildRequestKey(r);
687
+ var _a;
688
+ const normalizedOriginator = this.normalizeOriginator(r.originator) || r.originator;
689
+ const preparedRequest = {
690
+ ...r,
691
+ originator: normalizedOriginator,
692
+ displayOriginator: (_a = r.displayOriginator) !== null && _a !== void 0 ? _a : r.originator
693
+ };
694
+ const key = this.buildRequestKey(preparedRequest);
658
695
  // If there's already a queue for the same resource, we piggyback on it
659
696
  const existingQueue = this.activeRequests.get(key);
660
697
  if (existingQueue && existingQueue.pending.length > 0) {
@@ -666,32 +703,32 @@ class WalletPermissionsManager {
666
703
  // Return a promise that resolves or rejects once the user grants/denies
667
704
  return new Promise(async (resolve, reject) => {
668
705
  this.activeRequests.set(key, {
669
- request: r,
706
+ request: preparedRequest,
670
707
  pending: [{ resolve, reject }]
671
708
  });
672
709
  // Fire the relevant onXXXRequested event (which one depends on r.type)
673
- switch (r.type) {
710
+ switch (preparedRequest.type) {
674
711
  case 'protocol':
675
712
  await this.callEvent('onProtocolPermissionRequested', {
676
- ...r,
713
+ ...preparedRequest,
677
714
  requestID: key
678
715
  });
679
716
  break;
680
717
  case 'basket':
681
718
  await this.callEvent('onBasketAccessRequested', {
682
- ...r,
719
+ ...preparedRequest,
683
720
  requestID: key
684
721
  });
685
722
  break;
686
723
  case 'certificate':
687
724
  await this.callEvent('onCertificateAccessRequested', {
688
- ...r,
725
+ ...preparedRequest,
689
726
  requestID: key
690
727
  });
691
728
  break;
692
729
  case 'spending':
693
730
  await this.callEvent('onSpendingAuthorizationRequested', {
694
- ...r,
731
+ ...preparedRequest,
695
732
  requestID: key
696
733
  });
697
734
  break;
@@ -761,250 +798,284 @@ class WalletPermissionsManager {
761
798
  return expiry > 0 && expiry < now;
762
799
  }
763
800
  /** Looks for a DPACP permission token matching origin/domain, privileged, protocol, cpty. */
764
- async findProtocolToken(originator, privileged, protocolID, counterparty, includeExpired) {
801
+ async findProtocolToken(originator, privileged, protocolID, counterparty, includeExpired, originatorLookupValues) {
765
802
  const [secLevel, protoName] = protocolID;
766
- const tags = [
767
- `originator ${originator}`,
768
- `privileged ${!!privileged}`,
769
- `protocolName ${protoName}`,
770
- `protocolSecurityLevel ${secLevel}`
771
- ];
772
- if (secLevel === 2) {
773
- tags.push(`counterparty ${counterparty}`);
774
- }
775
- const result = await this.underlying.listOutputs({
776
- basket: BASKET_MAP.protocol,
777
- tags,
778
- tagQueryMode: 'all',
779
- include: 'entire transactions'
780
- }, this.adminOriginator);
781
- for (const out of result.outputs) {
782
- const [txid, outputIndexStr] = out.outpoint.split('.');
783
- const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
784
- const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript);
785
- if (!dec || !dec.fields || dec.fields.length < 6)
786
- continue;
787
- const domainRaw = dec.fields[0];
788
- const expiryRaw = dec.fields[1];
789
- const privRaw = dec.fields[2];
790
- const secLevelRaw = dec.fields[3];
791
- const protoNameRaw = dec.fields[4];
792
- const counterpartyRaw = dec.fields[5];
793
- const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
794
- const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10);
795
- const privDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true';
796
- const secLevelDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(secLevelRaw)), 10);
797
- const protoNameDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(protoNameRaw));
798
- const cptyDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(counterpartyRaw));
799
- if (domainDecoded !== originator ||
800
- privDecoded !== !!privileged ||
801
- secLevelDecoded !== secLevel ||
802
- protoNameDecoded !== protoName ||
803
- (secLevelDecoded === 2 && cptyDecoded !== counterparty)) {
804
- continue;
803
+ const originsToTry = (originatorLookupValues === null || originatorLookupValues === void 0 ? void 0 : originatorLookupValues.length) ? originatorLookupValues : [originator];
804
+ for (const originTag of originsToTry) {
805
+ const tags = [
806
+ `originator ${originTag}`,
807
+ `privileged ${!!privileged}`,
808
+ `protocolName ${protoName}`,
809
+ `protocolSecurityLevel ${secLevel}`
810
+ ];
811
+ if (secLevel === 2) {
812
+ tags.push(`counterparty ${counterparty}`);
805
813
  }
806
- if (!includeExpired && this.isTokenExpired(expiryDecoded)) {
807
- continue;
814
+ const result = await this.underlying.listOutputs({
815
+ basket: BASKET_MAP.protocol,
816
+ tags,
817
+ tagQueryMode: 'all',
818
+ include: 'entire transactions'
819
+ }, this.adminOriginator);
820
+ for (const out of result.outputs) {
821
+ const [txid, outputIndexStr] = out.outpoint.split('.');
822
+ const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
823
+ const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript);
824
+ if (!dec || !dec.fields || dec.fields.length < 6)
825
+ continue;
826
+ const domainRaw = dec.fields[0];
827
+ const expiryRaw = dec.fields[1];
828
+ const privRaw = dec.fields[2];
829
+ const secLevelRaw = dec.fields[3];
830
+ const protoNameRaw = dec.fields[4];
831
+ const counterpartyRaw = dec.fields[5];
832
+ const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
833
+ const normalizedDomain = this.normalizeOriginator(domainDecoded);
834
+ if (normalizedDomain !== originator) {
835
+ continue;
836
+ }
837
+ const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10);
838
+ const privDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true';
839
+ const secLevelDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(secLevelRaw)), 10);
840
+ const protoNameDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(protoNameRaw));
841
+ const cptyDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(counterpartyRaw));
842
+ if (privDecoded !== !!privileged ||
843
+ secLevelDecoded !== secLevel ||
844
+ protoNameDecoded !== protoName ||
845
+ (secLevelDecoded === 2 && cptyDecoded !== counterparty)) {
846
+ continue;
847
+ }
848
+ if (!includeExpired && this.isTokenExpired(expiryDecoded)) {
849
+ continue;
850
+ }
851
+ return {
852
+ tx: tx.toBEEF(),
853
+ txid: out.outpoint.split('.')[0],
854
+ outputIndex: parseInt(out.outpoint.split('.')[1], 10),
855
+ outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
856
+ satoshis: out.satoshis,
857
+ originator,
858
+ rawOriginator: domainDecoded,
859
+ privileged,
860
+ protocol: protoName,
861
+ securityLevel: secLevel,
862
+ expiry: expiryDecoded,
863
+ counterparty: cptyDecoded
864
+ };
808
865
  }
809
- return {
810
- tx: tx.toBEEF(),
811
- txid: out.outpoint.split('.')[0],
812
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
813
- outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
814
- satoshis: out.satoshis,
815
- originator,
816
- privileged,
817
- protocol: protoName,
818
- securityLevel: secLevel,
819
- expiry: expiryDecoded,
820
- counterparty: cptyDecoded
821
- };
822
866
  }
823
867
  return undefined;
824
868
  }
825
869
  /** Finds ALL DPACP permission tokens matching origin/domain, privileged, protocol, cpty. Never filters by expiry. */
826
- async findAllProtocolTokens(originator, privileged, protocolID, counterparty) {
870
+ async findAllProtocolTokens(originator, privileged, protocolID, counterparty, originatorLookupValues) {
827
871
  const [secLevel, protoName] = protocolID;
828
- const tags = [
829
- `originator ${originator}`,
830
- `privileged ${!!privileged}`,
831
- `protocolName ${protoName}`,
832
- `protocolSecurityLevel ${secLevel}`
833
- ];
834
- if (secLevel === 2) {
835
- tags.push(`counterparty ${counterparty}`);
836
- }
837
- const result = await this.underlying.listOutputs({
838
- basket: BASKET_MAP.protocol,
839
- tags,
840
- tagQueryMode: 'all',
841
- include: 'entire transactions'
842
- }, this.adminOriginator);
872
+ const originsToTry = (originatorLookupValues === null || originatorLookupValues === void 0 ? void 0 : originatorLookupValues.length) ? originatorLookupValues : [originator];
843
873
  const matches = [];
844
- for (const out of result.outputs) {
845
- const [txid, outputIndexStr] = out.outpoint.split('.');
846
- const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
847
- const vout = Number(outputIndexStr);
848
- const dec = sdk_1.PushDrop.decode(tx.outputs[vout].lockingScript);
849
- if (!dec || !dec.fields || dec.fields.length < 6)
850
- continue;
851
- const domainRaw = dec.fields[0];
852
- const expiryRaw = dec.fields[1];
853
- const privRaw = dec.fields[2];
854
- const secLevelRaw = dec.fields[3];
855
- const protoNameRaw = dec.fields[4];
856
- const counterpartyRaw = dec.fields[5];
857
- // Decrypt all fields
858
- const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
859
- const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10);
860
- const privDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true';
861
- const secLevelDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(secLevelRaw)), 10);
862
- const protoNameDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(protoNameRaw));
863
- const cptyDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(counterpartyRaw));
864
- // Strict attribute match; NO expiry filtering
865
- if (domainDecoded !== originator ||
866
- privDecoded !== !!privileged ||
867
- secLevelDecoded !== secLevel ||
868
- protoNameDecoded !== protoName ||
869
- (secLevelDecoded === 2 && cptyDecoded !== counterparty)) {
870
- continue;
874
+ const seen = new Set();
875
+ for (const originTag of originsToTry) {
876
+ const tags = [
877
+ `originator ${originTag}`,
878
+ `privileged ${!!privileged}`,
879
+ `protocolName ${protoName}`,
880
+ `protocolSecurityLevel ${secLevel}`
881
+ ];
882
+ if (secLevel === 2) {
883
+ tags.push(`counterparty ${counterparty}`);
884
+ }
885
+ const result = await this.underlying.listOutputs({
886
+ basket: BASKET_MAP.protocol,
887
+ tags,
888
+ tagQueryMode: 'all',
889
+ include: 'entire transactions'
890
+ }, this.adminOriginator);
891
+ for (const out of result.outputs) {
892
+ if (seen.has(out.outpoint))
893
+ continue;
894
+ const [txid, outputIndexStr] = out.outpoint.split('.');
895
+ const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
896
+ const vout = Number(outputIndexStr);
897
+ const dec = sdk_1.PushDrop.decode(tx.outputs[vout].lockingScript);
898
+ if (!dec || !dec.fields || dec.fields.length < 6)
899
+ continue;
900
+ const domainRaw = dec.fields[0];
901
+ const expiryRaw = dec.fields[1];
902
+ const privRaw = dec.fields[2];
903
+ const secLevelRaw = dec.fields[3];
904
+ const protoNameRaw = dec.fields[4];
905
+ const counterpartyRaw = dec.fields[5];
906
+ const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
907
+ const normalizedDomain = this.normalizeOriginator(domainDecoded);
908
+ if (normalizedDomain !== originator) {
909
+ continue;
910
+ }
911
+ const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10);
912
+ const privDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true';
913
+ const secLevelDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(secLevelRaw)), 10);
914
+ const protoNameDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(protoNameRaw));
915
+ const cptyDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(counterpartyRaw));
916
+ if (privDecoded !== !!privileged ||
917
+ secLevelDecoded !== secLevel ||
918
+ protoNameDecoded !== protoName ||
919
+ (secLevelDecoded === 2 && cptyDecoded !== counterparty)) {
920
+ continue;
921
+ }
922
+ seen.add(out.outpoint);
923
+ matches.push({
924
+ tx: tx.toBEEF(),
925
+ txid,
926
+ outputIndex: vout,
927
+ outputScript: tx.outputs[vout].lockingScript.toHex(),
928
+ satoshis: out.satoshis,
929
+ originator,
930
+ rawOriginator: domainDecoded,
931
+ privileged,
932
+ protocol: protoName,
933
+ securityLevel: secLevel,
934
+ expiry: expiryDecoded,
935
+ counterparty: cptyDecoded
936
+ });
871
937
  }
872
- matches.push({
873
- tx: tx.toBEEF(),
874
- txid,
875
- outputIndex: vout,
876
- outputScript: tx.outputs[vout].lockingScript.toHex(),
877
- satoshis: out.satoshis,
878
- originator,
879
- privileged,
880
- protocol: protoName,
881
- securityLevel: secLevel,
882
- expiry: expiryDecoded,
883
- counterparty: cptyDecoded
884
- });
885
938
  }
886
939
  return matches;
887
940
  }
888
941
  /** Looks for a DBAP token matching (originator, basket). */
889
- async findBasketToken(originator, basket, includeExpired) {
890
- const result = await this.underlying.listOutputs({
891
- basket: BASKET_MAP.basket,
892
- tags: [`originator ${originator}`, `basket ${basket}`],
893
- tagQueryMode: 'all',
894
- include: 'entire transactions'
895
- }, this.adminOriginator);
896
- for (const out of result.outputs) {
897
- const [txid, outputIndexStr] = out.outpoint.split('.');
898
- const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
899
- const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript);
900
- if (!(dec === null || dec === void 0 ? void 0 : dec.fields) || dec.fields.length < 3)
901
- continue;
902
- const domainRaw = dec.fields[0];
903
- const expiryRaw = dec.fields[1];
904
- const basketRaw = dec.fields[2];
905
- const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
906
- const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10);
907
- const basketDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(basketRaw));
908
- if (domainDecoded !== originator || basketDecoded !== basket)
909
- continue;
910
- if (!includeExpired && this.isTokenExpired(expiryDecoded))
911
- continue;
912
- return {
913
- tx: tx.toBEEF(),
914
- txid: out.outpoint.split('.')[0],
915
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
916
- outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
917
- satoshis: out.satoshis,
918
- originator,
919
- basketName: basketDecoded,
920
- expiry: expiryDecoded
921
- };
942
+ async findBasketToken(originator, basket, includeExpired, originatorLookupValues) {
943
+ const originsToTry = (originatorLookupValues === null || originatorLookupValues === void 0 ? void 0 : originatorLookupValues.length) ? originatorLookupValues : [originator];
944
+ for (const originTag of originsToTry) {
945
+ const result = await this.underlying.listOutputs({
946
+ basket: BASKET_MAP.basket,
947
+ tags: [`originator ${originTag}`, `basket ${basket}`],
948
+ tagQueryMode: 'all',
949
+ include: 'entire transactions'
950
+ }, this.adminOriginator);
951
+ for (const out of result.outputs) {
952
+ const [txid, outputIndexStr] = out.outpoint.split('.');
953
+ const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
954
+ const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript);
955
+ if (!(dec === null || dec === void 0 ? void 0 : dec.fields) || dec.fields.length < 3)
956
+ continue;
957
+ const domainRaw = dec.fields[0];
958
+ const expiryRaw = dec.fields[1];
959
+ const basketRaw = dec.fields[2];
960
+ const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
961
+ const normalizedDomain = this.normalizeOriginator(domainDecoded);
962
+ if (normalizedDomain !== originator) {
963
+ continue;
964
+ }
965
+ const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10);
966
+ const basketDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(basketRaw));
967
+ if (basketDecoded !== basket)
968
+ continue;
969
+ if (!includeExpired && this.isTokenExpired(expiryDecoded))
970
+ continue;
971
+ return {
972
+ tx: tx.toBEEF(),
973
+ txid: out.outpoint.split('.')[0],
974
+ outputIndex: parseInt(out.outpoint.split('.')[1], 10),
975
+ outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
976
+ satoshis: out.satoshis,
977
+ originator,
978
+ rawOriginator: domainDecoded,
979
+ basketName: basketDecoded,
980
+ expiry: expiryDecoded
981
+ };
982
+ }
922
983
  }
923
984
  return undefined;
924
985
  }
925
986
  /** Looks for a DCAP token matching (origin, privileged, verifier, certType, fields subset). */
926
- async findCertificateToken(originator, privileged, verifier, certType, fields, includeExpired) {
927
- const result = await this.underlying.listOutputs({
928
- basket: BASKET_MAP.certificate,
929
- tags: [`originator ${originator}`, `privileged ${!!privileged}`, `type ${certType}`, `verifier ${verifier}`],
930
- tagQueryMode: 'all',
931
- include: 'entire transactions'
932
- }, this.adminOriginator);
933
- for (const out of result.outputs) {
934
- const [txid, outputIndexStr] = out.outpoint.split('.');
935
- const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
936
- const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript);
937
- if (!(dec === null || dec === void 0 ? void 0 : dec.fields) || dec.fields.length < 6)
938
- continue;
939
- const [domainRaw, expiryRaw, privRaw, typeRaw, fieldsRaw, verifierRaw] = dec.fields;
940
- const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
941
- const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10);
942
- const privDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true';
943
- const typeDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(typeRaw));
944
- const verifierDec = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(verifierRaw));
945
- const fieldsJson = await this.decryptPermissionTokenField(fieldsRaw);
946
- const allFields = JSON.parse(sdk_1.Utils.toUTF8(fieldsJson));
947
- if (domainDecoded !== originator ||
948
- privDecoded !== !!privileged ||
949
- typeDecoded !== certType ||
950
- verifierDec !== verifier) {
951
- continue;
952
- }
953
- // Check if 'fields' is a subset of 'allFields'
954
- const setAll = new Set(allFields);
955
- if (fields.some(f => !setAll.has(f))) {
956
- continue;
957
- }
958
- if (!includeExpired && this.isTokenExpired(expiryDecoded)) {
959
- continue;
987
+ async findCertificateToken(originator, privileged, verifier, certType, fields, includeExpired, originatorLookupValues) {
988
+ const originsToTry = (originatorLookupValues === null || originatorLookupValues === void 0 ? void 0 : originatorLookupValues.length) ? originatorLookupValues : [originator];
989
+ for (const originTag of originsToTry) {
990
+ const result = await this.underlying.listOutputs({
991
+ basket: BASKET_MAP.certificate,
992
+ tags: [`originator ${originTag}`, `privileged ${!!privileged}`, `type ${certType}`, `verifier ${verifier}`],
993
+ tagQueryMode: 'all',
994
+ include: 'entire transactions'
995
+ }, this.adminOriginator);
996
+ for (const out of result.outputs) {
997
+ const [txid, outputIndexStr] = out.outpoint.split('.');
998
+ const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
999
+ const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript);
1000
+ if (!(dec === null || dec === void 0 ? void 0 : dec.fields) || dec.fields.length < 6)
1001
+ continue;
1002
+ const [domainRaw, expiryRaw, privRaw, typeRaw, fieldsRaw, verifierRaw] = dec.fields;
1003
+ const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
1004
+ const normalizedDomain = this.normalizeOriginator(domainDecoded);
1005
+ if (normalizedDomain !== originator) {
1006
+ continue;
1007
+ }
1008
+ const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10);
1009
+ const privDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true';
1010
+ const typeDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(typeRaw));
1011
+ const verifierDec = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(verifierRaw));
1012
+ const fieldsJson = await this.decryptPermissionTokenField(fieldsRaw);
1013
+ const allFields = JSON.parse(sdk_1.Utils.toUTF8(fieldsJson));
1014
+ if (privDecoded !== !!privileged || typeDecoded !== certType || verifierDec !== verifier) {
1015
+ continue;
1016
+ }
1017
+ // Check if 'fields' is a subset of 'allFields'
1018
+ const setAll = new Set(allFields);
1019
+ if (fields.some(f => !setAll.has(f))) {
1020
+ continue;
1021
+ }
1022
+ if (!includeExpired && this.isTokenExpired(expiryDecoded)) {
1023
+ continue;
1024
+ }
1025
+ return {
1026
+ tx: tx.toBEEF(),
1027
+ txid: out.outpoint.split('.')[0],
1028
+ outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1029
+ outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1030
+ satoshis: out.satoshis,
1031
+ originator,
1032
+ rawOriginator: domainDecoded,
1033
+ privileged,
1034
+ verifier: verifierDec,
1035
+ certType: typeDecoded,
1036
+ certFields: allFields,
1037
+ expiry: expiryDecoded
1038
+ };
960
1039
  }
961
- return {
962
- tx: tx.toBEEF(),
963
- txid: out.outpoint.split('.')[0],
964
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
965
- outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
966
- satoshis: out.satoshis,
967
- originator,
968
- privileged,
969
- verifier: verifierDec,
970
- certType: typeDecoded,
971
- certFields: allFields,
972
- expiry: expiryDecoded
973
- };
974
1040
  }
975
1041
  return undefined;
976
1042
  }
977
1043
  /** Looks for a DSAP token matching origin, returning the first one found. */
978
- async findSpendingToken(originator) {
979
- const result = await this.underlying.listOutputs({
980
- basket: BASKET_MAP.spending,
981
- tags: [`originator ${originator}`],
982
- tagQueryMode: 'all',
983
- include: 'entire transactions'
984
- }, this.adminOriginator);
985
- for (const out of result.outputs) {
986
- const [txid, outputIndexStr] = out.outpoint.split('.');
987
- const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
988
- const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript);
989
- if (!(dec === null || dec === void 0 ? void 0 : dec.fields) || dec.fields.length < 2)
990
- continue;
991
- const domainRaw = dec.fields[0];
992
- const amtRaw = dec.fields[1];
993
- const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
994
- if (domainDecoded !== originator)
995
- continue;
996
- const amtDecodedStr = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(amtRaw));
997
- const authorizedAmount = parseInt(amtDecodedStr, 10);
998
- return {
999
- tx: tx.toBEEF(),
1000
- txid: out.outpoint.split('.')[0],
1001
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1002
- outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1003
- satoshis: out.satoshis,
1004
- originator,
1005
- authorizedAmount,
1006
- expiry: 0 // Not time-limited, monthly authorization
1007
- };
1044
+ async findSpendingToken(originator, originatorLookupValues) {
1045
+ const originsToTry = (originatorLookupValues === null || originatorLookupValues === void 0 ? void 0 : originatorLookupValues.length) ? originatorLookupValues : [originator];
1046
+ for (const originTag of originsToTry) {
1047
+ const result = await this.underlying.listOutputs({
1048
+ basket: BASKET_MAP.spending,
1049
+ tags: [`originator ${originTag}`],
1050
+ tagQueryMode: 'all',
1051
+ include: 'entire transactions'
1052
+ }, this.adminOriginator);
1053
+ for (const out of result.outputs) {
1054
+ const [txid, outputIndexStr] = out.outpoint.split('.');
1055
+ const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
1056
+ const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript);
1057
+ if (!(dec === null || dec === void 0 ? void 0 : dec.fields) || dec.fields.length < 2)
1058
+ continue;
1059
+ const domainRaw = dec.fields[0];
1060
+ const amtRaw = dec.fields[1];
1061
+ const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
1062
+ const normalizedDomain = this.normalizeOriginator(domainDecoded);
1063
+ if (normalizedDomain !== originator)
1064
+ continue;
1065
+ const amtDecodedStr = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(amtRaw));
1066
+ const authorizedAmount = parseInt(amtDecodedStr, 10);
1067
+ return {
1068
+ tx: tx.toBEEF(),
1069
+ txid: out.outpoint.split('.')[0],
1070
+ outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1071
+ outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1072
+ satoshis: out.satoshis,
1073
+ originator,
1074
+ rawOriginator: domainDecoded,
1075
+ authorizedAmount,
1076
+ expiry: 0 // Not time-limited, monthly authorization
1077
+ };
1078
+ }
1008
1079
  }
1009
1080
  return undefined;
1010
1081
  }
@@ -1023,11 +1094,16 @@ class WalletPermissionsManager {
1023
1094
  * Returns spending for an originator in the current calendar month.
1024
1095
  */
1025
1096
  async querySpentSince(token) {
1026
- const { actions } = await this.underlying.listActions({
1027
- labels: [`admin originator ${token.originator}`, `admin month ${this.getCurrentMonthYearUTC()}`],
1028
- labelQueryMode: 'all'
1029
- }, this.adminOriginator);
1030
- return actions.reduce((a, e) => a + e.satoshis, 0);
1097
+ const labelOrigins = this.buildOriginatorLookupValues(token.rawOriginator, token.originator);
1098
+ let total = 0;
1099
+ for (const labelOrigin of labelOrigins) {
1100
+ const { actions } = await this.underlying.listActions({
1101
+ labels: [`admin originator ${labelOrigin}`, `admin month ${this.getCurrentMonthYearUTC()}`],
1102
+ labelQueryMode: 'all'
1103
+ }, this.adminOriginator);
1104
+ total += actions.reduce((a, e) => a + e.satoshis, 0);
1105
+ }
1106
+ return total;
1031
1107
  }
1032
1108
  /* ---------------------------------------------------------------------
1033
1109
  * 5) CREATE / RENEW / REVOKE PERMISSION TOKENS ON CHAIN
@@ -1042,6 +1118,8 @@ class WalletPermissionsManager {
1042
1118
  * @param amount For DSAP, the authorized spending limit
1043
1119
  */
1044
1120
  async createPermissionOnChain(r, expiry, amount) {
1121
+ const normalizedOriginator = this.normalizeOriginator(r.originator) || r.originator;
1122
+ r.originator = normalizedOriginator;
1045
1123
  const basketName = BASKET_MAP[r.type];
1046
1124
  if (!basketName)
1047
1125
  return;
@@ -1131,13 +1209,14 @@ class WalletPermissionsManager {
1131
1209
  * @param newAmount For DSAP, the new authorized amount
1132
1210
  */
1133
1211
  async renewPermissionOnChain(oldToken, r, newExpiry, newAmount) {
1212
+ r.originator = this.normalizeOriginator(r.originator) || r.originator;
1134
1213
  // 1) build new fields
1135
1214
  const newFields = await this.buildPushdropFields(r, newExpiry, newAmount);
1136
1215
  // 2) new script
1137
1216
  const newScript = await new sdk_1.PushDrop(this.underlying).lock(newFields, WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL, '1', 'self', true, true);
1138
1217
  const tags = this.buildTagsForRequest(r);
1139
1218
  // Check if there are multiple old tokens for the same parameters (shouldn't usually happen)
1140
- const oldTokens = await this.findAllProtocolTokens(oldToken.originator, oldToken.privileged, [oldToken.securityLevel, oldToken.protocol], oldToken.counterparty);
1219
+ const oldTokens = await this.findAllProtocolTokens(oldToken.originator, oldToken.privileged, [oldToken.securityLevel, oldToken.protocol], oldToken.counterparty, this.buildOriginatorLookupValues(oldToken.rawOriginator, oldToken.originator));
1141
1220
  // If so, coalesce them into a single token first, to avoid bloat
1142
1221
  if (oldTokens.length > 1) {
1143
1222
  const txid = await this.coalescePermissionTokens(oldTokens, newScript, {
@@ -1145,7 +1224,6 @@ class WalletPermissionsManager {
1145
1224
  basket: BASKET_MAP[r.type],
1146
1225
  description: `Coalesce ${r.type} permission tokens`
1147
1226
  });
1148
- console.log('Coalesced permission tokens:', txid);
1149
1227
  }
1150
1228
  else {
1151
1229
  // Otherwise, just proceed with the single-token renewal
@@ -1245,7 +1323,9 @@ class WalletPermissionsManager {
1245
1323
  tags.push(`privileged ${!!r.privileged}`);
1246
1324
  tags.push(`protocolName ${r.protocolID[1]}`);
1247
1325
  tags.push(`protocolSecurityLevel ${r.protocolID[0]}`);
1248
- tags.push(`counterparty ${r.counterparty}`);
1326
+ if (r.protocolID[0] === 2) {
1327
+ tags.push(`counterparty ${r.counterparty}`);
1328
+ }
1249
1329
  break;
1250
1330
  }
1251
1331
  case 'basket': {
@@ -1279,56 +1359,70 @@ class WalletPermissionsManager {
1279
1359
  */
1280
1360
  async listProtocolPermissions({ originator, privileged, protocolName, protocolSecurityLevel, counterparty } = {}) {
1281
1361
  const basketName = BASKET_MAP.protocol;
1282
- const tags = [];
1283
- if (originator) {
1284
- tags.push(`originator ${originator}`);
1285
- }
1362
+ const baseTags = [];
1286
1363
  if (privileged !== undefined) {
1287
- tags.push(`privileged ${!!privileged}`);
1364
+ baseTags.push(`privileged ${!!privileged}`);
1288
1365
  }
1289
1366
  if (protocolName) {
1290
- tags.push(`protocolName ${protocolName}`);
1367
+ baseTags.push(`protocolName ${protocolName}`);
1291
1368
  }
1292
1369
  if (protocolSecurityLevel !== undefined) {
1293
- tags.push(`protocolSecurityLevel ${protocolSecurityLevel}`);
1370
+ baseTags.push(`protocolSecurityLevel ${protocolSecurityLevel}`);
1294
1371
  }
1295
1372
  if (counterparty) {
1296
- tags.push(`counterparty ${counterparty}`);
1373
+ baseTags.push(`counterparty ${counterparty}`);
1297
1374
  }
1298
- const result = await this.underlying.listOutputs({
1299
- basket: basketName,
1300
- tags,
1301
- tagQueryMode: 'all',
1302
- include: 'entire transactions',
1303
- limit: 100
1304
- }, this.adminOriginator);
1375
+ const originFilter = originator ? this.prepareOriginator(originator) : undefined;
1376
+ const originVariants = originFilter ? originFilter.lookupValues : [undefined];
1377
+ const seen = new Set();
1305
1378
  const tokens = [];
1306
- for (const out of result.outputs) {
1307
- const [txid, outputIndexStr] = out.outpoint.split('.');
1308
- const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
1309
- const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript);
1310
- if (!(dec === null || dec === void 0 ? void 0 : dec.fields) || dec.fields.length < 6)
1311
- continue;
1312
- const [domainRaw, expiryRaw, privRaw, secRaw, protoRaw, cptyRaw] = dec.fields;
1313
- const domainDec = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
1314
- const expiryDec = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10);
1315
- const privDec = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true';
1316
- const secDec = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(secRaw)), 10);
1317
- const protoDec = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(protoRaw));
1318
- const cptyDec = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(cptyRaw));
1319
- tokens.push({
1320
- tx: tx.toBEEF(),
1321
- txid: out.outpoint.split('.')[0],
1322
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1323
- outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1324
- satoshis: out.satoshis,
1325
- originator: domainDec,
1326
- expiry: expiryDec,
1327
- privileged: privDec,
1328
- securityLevel: secDec,
1329
- protocol: protoDec,
1330
- counterparty: cptyDec
1331
- });
1379
+ for (const originTag of originVariants) {
1380
+ const tags = [...baseTags];
1381
+ if (originTag) {
1382
+ tags.push(`originator ${originTag}`);
1383
+ }
1384
+ const result = await this.underlying.listOutputs({
1385
+ basket: basketName,
1386
+ tags,
1387
+ tagQueryMode: 'all',
1388
+ include: 'entire transactions',
1389
+ limit: 100
1390
+ }, this.adminOriginator);
1391
+ for (const out of result.outputs) {
1392
+ if (seen.has(out.outpoint))
1393
+ continue;
1394
+ const [txid, outputIndexStr] = out.outpoint.split('.');
1395
+ const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
1396
+ const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript);
1397
+ if (!(dec === null || dec === void 0 ? void 0 : dec.fields) || dec.fields.length < 6)
1398
+ continue;
1399
+ const [domainRaw, expiryRaw, privRaw, secRaw, protoRaw, cptyRaw] = dec.fields;
1400
+ const domainDec = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
1401
+ const normalizedDomain = this.normalizeOriginator(domainDec);
1402
+ if (originFilter && normalizedDomain !== originFilter.normalized) {
1403
+ continue;
1404
+ }
1405
+ const expiryDec = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10);
1406
+ const privDec = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true';
1407
+ const secDec = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(secRaw)), 10);
1408
+ const protoDec = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(protoRaw));
1409
+ const cptyDec = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(cptyRaw));
1410
+ seen.add(out.outpoint);
1411
+ tokens.push({
1412
+ tx: tx.toBEEF(),
1413
+ txid: out.outpoint.split('.')[0],
1414
+ outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1415
+ outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1416
+ satoshis: out.satoshis,
1417
+ originator: normalizedDomain,
1418
+ rawOriginator: domainDec,
1419
+ expiry: expiryDec,
1420
+ privileged: privDec,
1421
+ securityLevel: secDec,
1422
+ protocol: protoDec,
1423
+ counterparty: cptyDec
1424
+ });
1425
+ }
1332
1426
  }
1333
1427
  return tokens;
1334
1428
  }
@@ -1358,41 +1452,55 @@ class WalletPermissionsManager {
1358
1452
  */
1359
1453
  async listBasketAccess(params = {}) {
1360
1454
  const basketName = BASKET_MAP.basket;
1361
- const tags = [];
1362
- if (params.originator) {
1363
- tags.push(`originator ${params.originator}`);
1364
- }
1455
+ const baseTags = [];
1365
1456
  if (params.basket) {
1366
- tags.push(`basket ${params.basket}`);
1457
+ baseTags.push(`basket ${params.basket}`);
1367
1458
  }
1368
- const result = await this.underlying.listOutputs({
1369
- basket: basketName,
1370
- tags,
1371
- tagQueryMode: 'all',
1372
- include: 'entire transactions',
1373
- limit: 10000
1374
- }, this.adminOriginator);
1459
+ const originFilter = params.originator ? this.prepareOriginator(params.originator) : undefined;
1460
+ const originVariants = originFilter ? originFilter.lookupValues : [undefined];
1461
+ const seen = new Set();
1375
1462
  const tokens = [];
1376
- for (const out of result.outputs) {
1377
- const [txid, outputIndexStr] = out.outpoint.split('.');
1378
- const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
1379
- const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript);
1380
- if (!(dec === null || dec === void 0 ? void 0 : dec.fields) || dec.fields.length < 3)
1381
- continue;
1382
- const [domainRaw, expiryRaw, basketRaw] = dec.fields;
1383
- const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
1384
- const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10);
1385
- const basketDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(basketRaw));
1386
- tokens.push({
1387
- tx: tx.toBEEF(),
1388
- txid: out.outpoint.split('.')[0],
1389
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1390
- satoshis: out.satoshis,
1391
- outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1392
- originator: domainDecoded,
1393
- basketName: basketDecoded,
1394
- expiry: expiryDecoded
1395
- });
1463
+ for (const originTag of originVariants) {
1464
+ const tags = [...baseTags];
1465
+ if (originTag) {
1466
+ tags.push(`originator ${originTag}`);
1467
+ }
1468
+ const result = await this.underlying.listOutputs({
1469
+ basket: basketName,
1470
+ tags,
1471
+ tagQueryMode: 'all',
1472
+ include: 'entire transactions',
1473
+ limit: 10000
1474
+ }, this.adminOriginator);
1475
+ for (const out of result.outputs) {
1476
+ if (seen.has(out.outpoint))
1477
+ continue;
1478
+ const [txid, outputIndexStr] = out.outpoint.split('.');
1479
+ const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
1480
+ const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript);
1481
+ if (!(dec === null || dec === void 0 ? void 0 : dec.fields) || dec.fields.length < 3)
1482
+ continue;
1483
+ const [domainRaw, expiryRaw, basketRaw] = dec.fields;
1484
+ const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
1485
+ const normalizedDomain = this.normalizeOriginator(domainDecoded);
1486
+ if (originFilter && normalizedDomain !== originFilter.normalized) {
1487
+ continue;
1488
+ }
1489
+ const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10);
1490
+ const basketDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(basketRaw));
1491
+ seen.add(out.outpoint);
1492
+ tokens.push({
1493
+ tx: tx.toBEEF(),
1494
+ txid: out.outpoint.split('.')[0],
1495
+ outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1496
+ satoshis: out.satoshis,
1497
+ outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1498
+ originator: normalizedDomain,
1499
+ rawOriginator: domainDecoded,
1500
+ basketName: basketDecoded,
1501
+ expiry: expiryDecoded
1502
+ });
1503
+ }
1396
1504
  }
1397
1505
  return tokens;
1398
1506
  }
@@ -1480,54 +1588,68 @@ class WalletPermissionsManager {
1480
1588
  */
1481
1589
  async listCertificateAccess(params = {}) {
1482
1590
  const basketName = BASKET_MAP.certificate;
1483
- const tags = [];
1484
- if (params.originator) {
1485
- tags.push(`originator ${params.originator}`);
1486
- }
1591
+ const baseTags = [];
1487
1592
  if (params.privileged !== undefined) {
1488
- tags.push(`privileged ${!!params.privileged}`);
1593
+ baseTags.push(`privileged ${!!params.privileged}`);
1489
1594
  }
1490
1595
  if (params.certType) {
1491
- tags.push(`type ${params.certType}`);
1596
+ baseTags.push(`type ${params.certType}`);
1492
1597
  }
1493
1598
  if (params.verifier) {
1494
- tags.push(`verifier ${params.verifier}`);
1599
+ baseTags.push(`verifier ${params.verifier}`);
1495
1600
  }
1496
- const result = await this.underlying.listOutputs({
1497
- basket: basketName,
1498
- tags,
1499
- tagQueryMode: 'all',
1500
- include: 'entire transactions',
1501
- limit: 10000
1502
- }, this.adminOriginator);
1601
+ const originFilter = params.originator ? this.prepareOriginator(params.originator) : undefined;
1602
+ const originVariants = originFilter ? originFilter.lookupValues : [undefined];
1603
+ const seen = new Set();
1503
1604
  const tokens = [];
1504
- for (const out of result.outputs) {
1505
- const [txid, outputIndexStr] = out.outpoint.split('.');
1506
- const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
1507
- const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript);
1508
- if (!(dec === null || dec === void 0 ? void 0 : dec.fields) || dec.fields.length < 6)
1509
- continue;
1510
- const [domainRaw, expiryRaw, privRaw, typeRaw, fieldsRaw, verifierRaw] = dec.fields;
1511
- const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
1512
- const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10);
1513
- const privDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true';
1514
- const typeDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(typeRaw));
1515
- const verifierDec = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(verifierRaw));
1516
- const fieldsJson = await this.decryptPermissionTokenField(fieldsRaw);
1517
- const allFields = JSON.parse(sdk_1.Utils.toUTF8(fieldsJson));
1518
- tokens.push({
1519
- tx: tx.toBEEF(),
1520
- txid: out.outpoint.split('.')[0],
1521
- outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1522
- satoshis: out.satoshis,
1523
- outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1524
- originator: domainDecoded,
1525
- privileged: privDecoded,
1526
- certType: typeDecoded,
1527
- certFields: allFields,
1528
- verifier: verifierDec,
1529
- expiry: expiryDecoded
1530
- });
1605
+ for (const originTag of originVariants) {
1606
+ const tags = [...baseTags];
1607
+ if (originTag) {
1608
+ tags.push(`originator ${originTag}`);
1609
+ }
1610
+ const result = await this.underlying.listOutputs({
1611
+ basket: basketName,
1612
+ tags,
1613
+ tagQueryMode: 'all',
1614
+ include: 'entire transactions',
1615
+ limit: 10000
1616
+ }, this.adminOriginator);
1617
+ for (const out of result.outputs) {
1618
+ if (seen.has(out.outpoint))
1619
+ continue;
1620
+ const [txid, outputIndexStr] = out.outpoint.split('.');
1621
+ const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid);
1622
+ const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript);
1623
+ if (!(dec === null || dec === void 0 ? void 0 : dec.fields) || dec.fields.length < 6)
1624
+ continue;
1625
+ const [domainRaw, expiryRaw, privRaw, typeRaw, fieldsRaw, verifierRaw] = dec.fields;
1626
+ const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw));
1627
+ const normalizedDomain = this.normalizeOriginator(domainDecoded);
1628
+ if (originFilter && normalizedDomain !== originFilter.normalized) {
1629
+ continue;
1630
+ }
1631
+ const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10);
1632
+ const privDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true';
1633
+ const typeDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(typeRaw));
1634
+ const verifierDec = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(verifierRaw));
1635
+ const fieldsJson = await this.decryptPermissionTokenField(fieldsRaw);
1636
+ const allFields = JSON.parse(sdk_1.Utils.toUTF8(fieldsJson));
1637
+ seen.add(out.outpoint);
1638
+ tokens.push({
1639
+ tx: tx.toBEEF(),
1640
+ txid: out.outpoint.split('.')[0],
1641
+ outputIndex: parseInt(out.outpoint.split('.')[1], 10),
1642
+ satoshis: out.satoshis,
1643
+ outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(),
1644
+ originator: normalizedDomain,
1645
+ rawOriginator: domainDecoded,
1646
+ privileged: privDecoded,
1647
+ certType: typeDecoded,
1648
+ certFields: allFields,
1649
+ verifier: verifierDec,
1650
+ expiry: expiryDecoded
1651
+ });
1652
+ }
1531
1653
  }
1532
1654
  return tokens;
1533
1655
  }
@@ -2063,8 +2185,10 @@ class WalletPermissionsManager {
2063
2185
  }
2064
2186
  async waitForAuthentication(...args) {
2065
2187
  var _a, _b, _c, _d, _e, _f, _g;
2066
- const [_, originator] = args;
2188
+ let [_, originator] = args;
2067
2189
  if (this.config.seekGroupedPermission && originator) {
2190
+ const { normalized: normalizedOriginator } = this.prepareOriginator(originator);
2191
+ originator = normalizedOriginator;
2068
2192
  // 1. Fetch manifest.json from the originator
2069
2193
  let groupPermissions;
2070
2194
  try {
@@ -2146,7 +2270,7 @@ class WalletPermissionsManager {
2146
2270
  try {
2147
2271
  await new Promise(async (resolve, reject) => {
2148
2272
  this.activeRequests.set(key, {
2149
- request: { originator, permissions: permissionsToRequest },
2273
+ request: { originator: originator, permissions: permissionsToRequest },
2150
2274
  pending: [{ resolve, reject }]
2151
2275
  });
2152
2276
  await this.callEvent('onGroupedPermissionRequested', {
@@ -2184,7 +2308,7 @@ class WalletPermissionsManager {
2184
2308
  * --------------------------------------------------------------------- */
2185
2309
  /** Returns true if the specified origin is the admin originator. */
2186
2310
  isAdminOriginator(originator) {
2187
- return originator === this.adminOriginator;
2311
+ return this.normalizeOriginator(originator) === this.adminOriginator;
2188
2312
  }
2189
2313
  /**
2190
2314
  * Checks if the given protocol is admin-reserved per BRC-100 rules:
@@ -2252,27 +2376,113 @@ class WalletPermissionsManager {
2252
2376
  cachePermission(key, expiry) {
2253
2377
  this.permissionCache.set(key, { expiry, cachedAt: Date.now() });
2254
2378
  }
2379
+ /** Records that a non-spending permission was just granted so we can skip re-prompting briefly. */
2380
+ markRecentGrant(request) {
2381
+ if (request.type === 'spending')
2382
+ return;
2383
+ const key = this.buildRequestKey(request);
2384
+ if (!key)
2385
+ return;
2386
+ this.recentGrants.set(key, Date.now() + WalletPermissionsManager.RECENT_GRANT_COVER_MS);
2387
+ }
2388
+ /** Returns true if we are inside the short "cover window" immediately after granting permission. */
2389
+ isRecentlyGranted(key) {
2390
+ const expiry = this.recentGrants.get(key);
2391
+ if (!expiry)
2392
+ return false;
2393
+ if (Date.now() > expiry) {
2394
+ this.recentGrants.delete(key);
2395
+ return false;
2396
+ }
2397
+ return true;
2398
+ }
2399
+ /** Normalizes and canonicalizes originator domains (e.g., lowercase + drop default ports). */
2400
+ normalizeOriginator(originator) {
2401
+ if (!originator)
2402
+ return '';
2403
+ const trimmed = originator.trim();
2404
+ if (!trimmed) {
2405
+ return '';
2406
+ }
2407
+ try {
2408
+ const hasScheme = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(trimmed);
2409
+ const candidate = hasScheme ? trimmed : `https://${trimmed}`;
2410
+ const url = new URL(candidate);
2411
+ if (!url.hostname) {
2412
+ return trimmed.toLowerCase();
2413
+ }
2414
+ const hostname = url.hostname.toLowerCase();
2415
+ const needsBrackets = hostname.includes(':');
2416
+ const baseHost = needsBrackets ? `[${hostname}]` : hostname;
2417
+ const port = url.port;
2418
+ const defaultPort = WalletPermissionsManager.DEFAULT_PORTS[url.protocol];
2419
+ if (port && defaultPort && port === defaultPort) {
2420
+ return baseHost;
2421
+ }
2422
+ return port ? `${baseHost}:${port}` : baseHost;
2423
+ }
2424
+ catch (_a) {
2425
+ // Fall back to a conservative lowercase trim if URL parsing fails.
2426
+ return trimmed.toLowerCase();
2427
+ }
2428
+ }
2429
+ /**
2430
+ * Produces a normalized originator value along with the set of legacy
2431
+ * representations that should be considered when searching for existing
2432
+ * permission tokens (for backwards compatibility).
2433
+ */
2434
+ prepareOriginator(originator) {
2435
+ const trimmed = originator === null || originator === void 0 ? void 0 : originator.trim();
2436
+ if (!trimmed) {
2437
+ throw new Error('Originator is required for permission checks.');
2438
+ }
2439
+ const normalized = this.normalizeOriginator(trimmed) || trimmed.toLowerCase();
2440
+ const lookupValues = Array.from(new Set([trimmed, normalized])).filter(Boolean);
2441
+ return { normalized, lookupValues };
2442
+ }
2443
+ /**
2444
+ * Builds a unique list of originator variants that should be searched when
2445
+ * looking up on-chain tokens (e.g., legacy raw + normalized forms).
2446
+ */
2447
+ buildOriginatorLookupValues(...origins) {
2448
+ const variants = new Set();
2449
+ for (const origin of origins) {
2450
+ const trimmed = origin === null || origin === void 0 ? void 0 : origin.trim();
2451
+ if (trimmed) {
2452
+ variants.add(trimmed);
2453
+ }
2454
+ }
2455
+ return Array.from(variants);
2456
+ }
2255
2457
  /**
2256
2458
  * Builds a "map key" string so that identical requests (e.g. "protocol:domain:true:protoName:counterparty")
2257
2459
  * do not produce multiple user prompts.
2258
2460
  */
2259
2461
  buildRequestKey(r) {
2260
2462
  var _a, _b, _c, _d, _e;
2463
+ const normalizedOriginator = this.normalizeOriginator(r.originator);
2261
2464
  switch (r.type) {
2262
2465
  case 'protocol':
2263
- return `proto:${r.originator}:${!!r.privileged}:${(_a = r.protocolID) === null || _a === void 0 ? void 0 : _a.join(',')}:${r.counterparty}`;
2466
+ return `proto:${normalizedOriginator}:${!!r.privileged}:${(_a = r.protocolID) === null || _a === void 0 ? void 0 : _a.join(',')}:${r.counterparty}`;
2264
2467
  case 'basket':
2265
- return `basket:${r.originator}:${r.basket}`;
2468
+ return `basket:${normalizedOriginator}:${r.basket}`;
2266
2469
  case 'certificate':
2267
- return `cert:${r.originator}:${!!r.privileged}:${(_b = r.certificate) === null || _b === void 0 ? void 0 : _b.verifier}:${(_c = r.certificate) === null || _c === void 0 ? void 0 : _c.certType}:${(_d = r.certificate) === null || _d === void 0 ? void 0 : _d.fields.join('|')}`;
2470
+ return `cert:${normalizedOriginator}:${!!r.privileged}:${(_b = r.certificate) === null || _b === void 0 ? void 0 : _b.verifier}:${(_c = r.certificate) === null || _c === void 0 ? void 0 : _c.certType}:${(_d = r.certificate) === null || _d === void 0 ? void 0 : _d.fields.join('|')}`;
2268
2471
  case 'spending':
2269
- return `spend:${r.originator}:${(_e = r.spending) === null || _e === void 0 ? void 0 : _e.satoshis}`;
2472
+ return `spend:${normalizedOriginator}:${(_e = r.spending) === null || _e === void 0 ? void 0 : _e.satoshis}`;
2270
2473
  }
2271
2474
  }
2272
2475
  }
2273
2476
  exports.WalletPermissionsManager = WalletPermissionsManager;
2274
2477
  /** How long a cached permission remains valid (5 minutes). */
2275
2478
  WalletPermissionsManager.CACHE_TTL_MS = 5 * 60 * 1000;
2479
+ /** Window during which freshly granted permissions are auto-allowed (except spending). */
2480
+ WalletPermissionsManager.RECENT_GRANT_COVER_MS = 15 * 1000;
2481
+ /** Default ports used when normalizing originator values. */
2482
+ WalletPermissionsManager.DEFAULT_PORTS = {
2483
+ 'http:': '80',
2484
+ 'https:': '443'
2485
+ };
2276
2486
  /* ---------------------------------------------------------------------
2277
2487
  * 4) SEARCH / DECODE / DECRYPT ON-CHAIN TOKENS (PushDrop Scripts)
2278
2488
  * --------------------------------------------------------------------- */