@bsv/wallet-toolbox 1.6.40 → 1.6.42

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