@bsv/wallet-toolbox-client 1.7.18 → 1.7.20

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.
@@ -77,7 +77,8 @@ class WalletPermissionsManager {
77
77
  onBasketAccessRequested: [],
78
78
  onCertificateAccessRequested: [],
79
79
  onSpendingAuthorizationRequested: [],
80
- onGroupedPermissionRequested: []
80
+ onGroupedPermissionRequested: [],
81
+ onCounterpartyPermissionRequested: []
81
82
  };
82
83
  /**
83
84
  * We queue parallel requests for the same resource so that only one
@@ -93,6 +94,9 @@ class WalletPermissionsManager {
93
94
  /** Cache recently confirmed permissions to avoid repeated lookups. */
94
95
  this.permissionCache = new Map();
95
96
  this.recentGrants = new Map();
97
+ this.manifestCache = new Map();
98
+ this.manifestFetchInProgress = new Map();
99
+ this.pactEstablishedCache = new Map();
96
100
  this.underlying = underlyingWallet;
97
101
  this.adminOriginator = this.normalizeOriginator(adminOriginator) || adminOriginator;
98
102
  // Default all config options to true unless specified
@@ -316,9 +320,8 @@ class WalletPermissionsManager {
316
320
  const { originator, permissions: requestedPermissions, displayOriginator } = originalRequest;
317
321
  const originLookupValues = this.buildOriginatorLookupValues(displayOriginator, originator);
318
322
  // --- Validation: Ensure granted permissions are a subset of what was requested ---
319
- if (params.granted.spendingAuthorization &&
320
- !deepEqual(params.granted.spendingAuthorization, requestedPermissions.spendingAuthorization)) {
321
- throw new Error('Granted spending authorization does not match the original request.');
323
+ if (params.granted.spendingAuthorization && !requestedPermissions.spendingAuthorization) {
324
+ throw new Error('Granted spending authorization was not part of the original request.');
322
325
  }
323
326
  if ((_a = params.granted.protocolPermissions) === null || _a === void 0 ? void 0 : _a.some(g => { var _a; return !((_a = requestedPermissions.protocolPermissions) === null || _a === void 0 ? void 0 : _a.find(r => deepEqual(r, g))); })) {
324
327
  throw new Error('Granted protocol permissions are not a subset of the original request.');
@@ -331,62 +334,67 @@ class WalletPermissionsManager {
331
334
  }
332
335
  // --- End Validation ---
333
336
  const expiry = params.expiry || 0; // default: never expires
337
+ const toCreate = [];
338
+ const toRenew = [];
334
339
  if (params.granted.spendingAuthorization) {
335
- await this.createPermissionOnChain({
336
- type: 'spending',
340
+ toCreate.push({
341
+ request: {
342
+ type: 'spending',
343
+ originator,
344
+ spending: { satoshis: params.granted.spendingAuthorization.amount },
345
+ reason: params.granted.spendingAuthorization.description
346
+ },
347
+ expiry: 0,
348
+ amount: params.granted.spendingAuthorization.amount
349
+ });
350
+ }
351
+ const grantedProtocols = params.granted.protocolPermissions || [];
352
+ const protocolTokens = await this.mapWithConcurrency(grantedProtocols, 8, async (p) => {
353
+ const token = await this.findProtocolToken(originator, false, p.protocolID, p.counterparty || 'self', true, originLookupValues);
354
+ return { p, token };
355
+ });
356
+ for (const { p, token } of protocolTokens) {
357
+ const request = {
358
+ type: 'protocol',
337
359
  originator,
338
- spending: { satoshis: params.granted.spendingAuthorization.amount },
339
- reason: params.granted.spendingAuthorization.description
340
- }, 0, // No expiry for spending tokens
341
- params.granted.spendingAuthorization.amount);
342
- }
343
- for (const p of params.granted.protocolPermissions || []) {
344
- const token = await this.findProtocolToken(originator, false, // No privileged protocols allowed in groups for added security.
345
- p.protocolID, p.counterparty || 'self', true, originLookupValues);
360
+ privileged: false,
361
+ protocolID: p.protocolID,
362
+ counterparty: p.counterparty || 'self',
363
+ reason: p.description
364
+ };
346
365
  if (token) {
347
- const request = {
348
- type: 'protocol',
349
- originator,
350
- privileged: false, // No privileged protocols allowed in groups for added security.
351
- protocolID: p.protocolID,
352
- counterparty: p.counterparty || 'self',
353
- reason: p.description
354
- };
355
- await this.renewPermissionOnChain(token, request, expiry);
356
- this.markRecentGrant(request);
366
+ toRenew.push({ oldToken: token, request, expiry });
357
367
  }
358
368
  else {
359
- const request = {
360
- type: 'protocol',
361
- originator,
362
- privileged: false, // No privileged protocols allowed in groups for added security.
363
- protocolID: p.protocolID,
364
- counterparty: p.counterparty || 'self',
365
- reason: p.description
366
- };
367
- await this.createPermissionOnChain(request, expiry);
368
- this.markRecentGrant(request);
369
+ toCreate.push({ request, expiry });
369
370
  }
370
371
  }
371
372
  for (const b of params.granted.basketAccess || []) {
372
- const request = { type: 'basket', originator, basket: b.basket, reason: b.description };
373
- await this.createPermissionOnChain(request, expiry);
374
- this.markRecentGrant(request);
373
+ toCreate.push({
374
+ request: { type: 'basket', originator, basket: b.basket, reason: b.description },
375
+ expiry
376
+ });
375
377
  }
376
378
  for (const c of params.granted.certificateAccess || []) {
377
- const request = {
378
- type: 'certificate',
379
- originator,
380
- privileged: false, // No certificates on the privileged identity are allowed as part of groups.
381
- certificate: {
382
- verifier: c.verifierPublicKey,
383
- certType: c.type,
384
- fields: c.fields
379
+ toCreate.push({
380
+ request: {
381
+ type: 'certificate',
382
+ originator,
383
+ privileged: false,
384
+ certificate: {
385
+ verifier: c.verifierPublicKey,
386
+ certType: c.type,
387
+ fields: c.fields
388
+ },
389
+ reason: c.description
385
390
  },
386
- reason: c.description
387
- };
388
- await this.createPermissionOnChain(request, expiry);
389
- this.markRecentGrant(request);
391
+ expiry
392
+ });
393
+ }
394
+ const created = await this.createPermissionTokensBestEffort(toCreate);
395
+ const renewed = await this.renewPermissionTokensBestEffort(toRenew);
396
+ for (const req of [...created, ...renewed]) {
397
+ this.markRecentGrant(req);
390
398
  }
391
399
  // Resolve all pending promises for this request
392
400
  for (const p of matching.pending) {
@@ -410,6 +418,74 @@ class WalletPermissionsManager {
410
418
  }
411
419
  this.activeRequests.delete(requestID);
412
420
  }
421
+ async dismissGroupedPermission(requestID) {
422
+ const matching = this.activeRequests.get(requestID);
423
+ if (!matching) {
424
+ throw new Error('Request ID not found.');
425
+ }
426
+ for (const p of matching.pending) {
427
+ p.resolve(true);
428
+ }
429
+ this.activeRequests.delete(requestID);
430
+ }
431
+ async grantCounterpartyPermission(params) {
432
+ var _a;
433
+ const matching = this.activeRequests.get(params.requestID);
434
+ if (!matching) {
435
+ throw new Error('Request ID not found.');
436
+ }
437
+ const originalRequest = matching.request;
438
+ const { originator, counterparty, permissions: requestedPermissions, displayOriginator } = originalRequest;
439
+ const originLookupValues = this.buildOriginatorLookupValues(displayOriginator, originator);
440
+ if ((_a = params.granted.protocols) === null || _a === void 0 ? void 0 : _a.some(g => !requestedPermissions.protocols.find(r => deepEqual(r, g)))) {
441
+ throw new Error('Granted protocol permissions are not a subset of the original request.');
442
+ }
443
+ const expiry = params.expiry || 0;
444
+ const toCreate = [];
445
+ const toRenew = [];
446
+ const grantedProtocols = params.granted.protocols || [];
447
+ const protocolTokens = await this.mapWithConcurrency(grantedProtocols, 8, async (p) => {
448
+ const token = await this.findProtocolToken(originator, false, p.protocolID, counterparty, true, originLookupValues);
449
+ return { p, token };
450
+ });
451
+ for (const { p, token } of protocolTokens) {
452
+ const request = {
453
+ type: 'protocol',
454
+ originator,
455
+ privileged: false,
456
+ protocolID: p.protocolID,
457
+ counterparty,
458
+ reason: p.description
459
+ };
460
+ if (token) {
461
+ toRenew.push({ oldToken: token, request, expiry });
462
+ }
463
+ else {
464
+ toCreate.push({ request, expiry });
465
+ }
466
+ }
467
+ const created = await this.createPermissionTokensBestEffort(toCreate);
468
+ const renewed = await this.renewPermissionTokensBestEffort(toRenew);
469
+ for (const req of [...created, ...renewed]) {
470
+ this.markRecentGrant(req);
471
+ }
472
+ for (const p of matching.pending) {
473
+ p.resolve(true);
474
+ }
475
+ this.activeRequests.delete(params.requestID);
476
+ }
477
+ async denyCounterpartyPermission(requestID) {
478
+ const matching = this.activeRequests.get(requestID);
479
+ if (!matching) {
480
+ throw new Error('Request ID not found.');
481
+ }
482
+ const err = new Error('The user has denied the request for permission.');
483
+ err.code = 'ERR_PERMISSION_DENIED';
484
+ for (const p of matching.pending) {
485
+ p.reject(err);
486
+ }
487
+ this.activeRequests.delete(requestID);
488
+ }
413
489
  /* ---------------------------------------------------------------------
414
490
  * 3) THE "ENSURE" METHODS: CHECK IF PERMISSION EXISTS, OTHERWISE PROMPT
415
491
  * --------------------------------------------------------------------- */
@@ -728,6 +804,469 @@ class WalletPermissionsManager {
728
804
  usageType: 'generic'
729
805
  });
730
806
  }
807
+ validateCounterpartyPermissions(raw) {
808
+ if (!raw || !Array.isArray(raw.protocols) || raw.protocols.length === 0)
809
+ return null;
810
+ const validProtocols = raw.protocols.filter((p) => {
811
+ return (Array.isArray(p === null || p === void 0 ? void 0 : p.protocolID) &&
812
+ p.protocolID[0] === 2 &&
813
+ typeof p.protocolID[1] === 'string' &&
814
+ typeof (p === null || p === void 0 ? void 0 : p.description) === 'string');
815
+ });
816
+ if (validProtocols.length === 0)
817
+ return null;
818
+ return {
819
+ description: typeof raw.description === 'string' ? raw.description : undefined,
820
+ protocols: validProtocols
821
+ };
822
+ }
823
+ async fetchManifestPermissions(originator) {
824
+ const cached = this.manifestCache.get(originator);
825
+ if (cached && Date.now() - cached.fetchedAt < WalletPermissionsManager.MANIFEST_CACHE_TTL_MS) {
826
+ return {
827
+ groupPermissions: cached.groupPermissions,
828
+ counterpartyPermissions: cached.counterpartyPermissions
829
+ };
830
+ }
831
+ const inProgress = this.manifestFetchInProgress.get(originator);
832
+ if (inProgress) {
833
+ return inProgress;
834
+ }
835
+ const fetchPromise = (async () => {
836
+ var _a, _b;
837
+ try {
838
+ const proto = originator.startsWith('localhost:') ? 'http' : 'https';
839
+ const response = await fetch(`${proto}://${originator}/manifest.json`);
840
+ if (response.ok) {
841
+ const manifest = await response.json();
842
+ const groupPermissions = ((_a = manifest === null || manifest === void 0 ? void 0 : manifest.babbage) === null || _a === void 0 ? void 0 : _a.groupPermissions) || null;
843
+ const counterpartyPermissions = this.validateCounterpartyPermissions((_b = manifest === null || manifest === void 0 ? void 0 : manifest.babbage) === null || _b === void 0 ? void 0 : _b.counterpartyPermissions);
844
+ this.manifestCache.set(originator, { groupPermissions, counterpartyPermissions, fetchedAt: Date.now() });
845
+ return { groupPermissions, counterpartyPermissions };
846
+ }
847
+ }
848
+ catch (e) { }
849
+ const result = { groupPermissions: null, counterpartyPermissions: null };
850
+ this.manifestCache.set(originator, { ...result, fetchedAt: Date.now() });
851
+ return result;
852
+ })();
853
+ this.manifestFetchInProgress.set(originator, fetchPromise);
854
+ try {
855
+ return await fetchPromise;
856
+ }
857
+ finally {
858
+ this.manifestFetchInProgress.delete(originator);
859
+ }
860
+ }
861
+ async fetchManifestGroupPermissions(originator) {
862
+ const { groupPermissions } = await this.fetchManifestPermissions(originator);
863
+ return groupPermissions;
864
+ }
865
+ async filterAlreadyGrantedPermissions(originator, groupPermissions) {
866
+ const permissionsToRequest = {
867
+ description: groupPermissions.description,
868
+ protocolPermissions: [],
869
+ basketAccess: [],
870
+ certificateAccess: []
871
+ };
872
+ if (groupPermissions.spendingAuthorization) {
873
+ const hasAuth = await this.hasSpendingAuthorization({
874
+ originator,
875
+ satoshis: groupPermissions.spendingAuthorization.amount
876
+ });
877
+ if (!hasAuth) {
878
+ permissionsToRequest.spendingAuthorization = groupPermissions.spendingAuthorization;
879
+ }
880
+ }
881
+ for (const p of groupPermissions.protocolPermissions || []) {
882
+ const hasPerm = await this.hasProtocolPermission({
883
+ originator,
884
+ privileged: false,
885
+ protocolID: p.protocolID,
886
+ counterparty: p.counterparty || 'self'
887
+ });
888
+ if (!hasPerm) {
889
+ permissionsToRequest.protocolPermissions.push(p);
890
+ }
891
+ }
892
+ for (const b of groupPermissions.basketAccess || []) {
893
+ const hasAccess = await this.hasBasketAccess({
894
+ originator,
895
+ basket: b.basket
896
+ });
897
+ if (!hasAccess) {
898
+ permissionsToRequest.basketAccess.push(b);
899
+ }
900
+ }
901
+ for (const c of groupPermissions.certificateAccess || []) {
902
+ const hasAccess = await this.hasCertificateAccess({
903
+ originator,
904
+ privileged: false,
905
+ verifier: c.verifierPublicKey,
906
+ certType: c.type,
907
+ fields: c.fields
908
+ });
909
+ if (!hasAccess) {
910
+ permissionsToRequest.certificateAccess.push(c);
911
+ }
912
+ }
913
+ return permissionsToRequest;
914
+ }
915
+ hasAnyPermissionsToRequest(permissions) {
916
+ var _a, _b, _c, _d, _e, _f;
917
+ return !!(permissions.spendingAuthorization ||
918
+ ((_b = (_a = permissions.protocolPermissions) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0 ||
919
+ ((_d = (_c = permissions.basketAccess) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) > 0 ||
920
+ ((_f = (_e = permissions.certificateAccess) === null || _e === void 0 ? void 0 : _e.length) !== null && _f !== void 0 ? _f : 0) > 0);
921
+ }
922
+ hasGroupedPermissionRequestedHandlers() {
923
+ const handlers = this.callbacks.onGroupedPermissionRequested || [];
924
+ return handlers.some(h => typeof h === 'function');
925
+ }
926
+ hasCounterpartyPermissionRequestedHandlers() {
927
+ const handlers = this.callbacks.onCounterpartyPermissionRequested || [];
928
+ return handlers.some(h => typeof h === 'function');
929
+ }
930
+ async hasPactEstablished(originator, counterparty) {
931
+ var _a;
932
+ if (counterparty === 'self' || counterparty === 'anyone') {
933
+ return true;
934
+ }
935
+ const cacheKey = `${originator}:${counterparty}`;
936
+ if (this.pactEstablishedCache.has(cacheKey)) {
937
+ return true;
938
+ }
939
+ const { counterpartyPermissions } = await this.fetchManifestPermissions(originator);
940
+ if (!((_a = counterpartyPermissions === null || counterpartyPermissions === void 0 ? void 0 : counterpartyPermissions.protocols) === null || _a === void 0 ? void 0 : _a.length)) {
941
+ return true;
942
+ }
943
+ const firstProtocol = counterpartyPermissions.protocols[0];
944
+ const hasToken = await this.hasProtocolPermission({
945
+ originator,
946
+ privileged: false,
947
+ protocolID: firstProtocol.protocolID,
948
+ counterparty
949
+ });
950
+ if (hasToken) {
951
+ this.pactEstablishedCache.set(cacheKey, Date.now());
952
+ return true;
953
+ }
954
+ return false;
955
+ }
956
+ markPactEstablished(originator, counterparty) {
957
+ const cacheKey = `${originator}:${counterparty}`;
958
+ this.pactEstablishedCache.set(cacheKey, Date.now());
959
+ }
960
+ async maybeRequestPact(currentRequest) {
961
+ var _a;
962
+ if (!this.config.seekGroupedPermission) {
963
+ return null;
964
+ }
965
+ if (!this.hasCounterpartyPermissionRequestedHandlers()) {
966
+ return null;
967
+ }
968
+ if (currentRequest.type !== 'protocol') {
969
+ return null;
970
+ }
971
+ if (currentRequest.privileged) {
972
+ return null;
973
+ }
974
+ const [level] = currentRequest.protocolID;
975
+ if (level !== 2) {
976
+ return null;
977
+ }
978
+ const originator = currentRequest.originator;
979
+ const counterparty = currentRequest.counterparty;
980
+ if (!counterparty || counterparty === 'self' || counterparty === 'anyone') {
981
+ return null;
982
+ }
983
+ if (!/^[0-9a-fA-F]{66}$/.test(counterparty)) {
984
+ return null;
985
+ }
986
+ if (await this.hasPactEstablished(originator, counterparty)) {
987
+ return null;
988
+ }
989
+ const { counterpartyPermissions } = await this.fetchManifestPermissions(originator);
990
+ if (!((_a = counterpartyPermissions === null || counterpartyPermissions === void 0 ? void 0 : counterpartyPermissions.protocols) === null || _a === void 0 ? void 0 : _a.length)) {
991
+ return null;
992
+ }
993
+ const protocolsToRequest = [];
994
+ for (const p of counterpartyPermissions.protocols) {
995
+ const hasPerm = await this.hasProtocolPermission({
996
+ originator,
997
+ privileged: false,
998
+ protocolID: p.protocolID,
999
+ counterparty
1000
+ });
1001
+ if (!hasPerm) {
1002
+ protocolsToRequest.push(p);
1003
+ }
1004
+ }
1005
+ if (protocolsToRequest.length === 0) {
1006
+ this.markPactEstablished(originator, counterparty);
1007
+ return null;
1008
+ }
1009
+ const permissionsToRequest = {
1010
+ description: counterpartyPermissions.description,
1011
+ protocols: protocolsToRequest
1012
+ };
1013
+ const key = `pact:${originator}:${counterparty}`;
1014
+ const existing = this.activeRequests.get(key);
1015
+ if (existing) {
1016
+ const existingRequest = existing.request;
1017
+ for (const p of permissionsToRequest.protocols) {
1018
+ if (!existingRequest.permissions.protocols.find(x => deepEqual(x, p))) {
1019
+ existingRequest.permissions.protocols.push(p);
1020
+ }
1021
+ }
1022
+ await new Promise((resolve, reject) => {
1023
+ existing.pending.push({ resolve, reject });
1024
+ });
1025
+ }
1026
+ else {
1027
+ await new Promise(async (resolve, reject) => {
1028
+ this.activeRequests.set(key, {
1029
+ request: {
1030
+ originator,
1031
+ counterparty,
1032
+ permissions: permissionsToRequest,
1033
+ displayOriginator: currentRequest.displayOriginator
1034
+ },
1035
+ pending: [{ resolve, reject }]
1036
+ });
1037
+ await this.callEvent('onCounterpartyPermissionRequested', {
1038
+ requestID: key,
1039
+ originator,
1040
+ counterparty,
1041
+ permissions: permissionsToRequest
1042
+ });
1043
+ });
1044
+ }
1045
+ this.markPactEstablished(originator, counterparty);
1046
+ const satisfied = await this.hasProtocolPermission({
1047
+ originator,
1048
+ privileged: false,
1049
+ protocolID: currentRequest.protocolID,
1050
+ counterparty
1051
+ });
1052
+ return satisfied ? true : null;
1053
+ }
1054
+ async maybeRequestPeerGroupedLevel2ProtocolPermissions(currentRequest) {
1055
+ var _a, _b;
1056
+ if (!this.config.seekGroupedPermission) {
1057
+ return null;
1058
+ }
1059
+ if (!this.hasGroupedPermissionRequestedHandlers()) {
1060
+ return null;
1061
+ }
1062
+ if (currentRequest.type !== 'protocol') {
1063
+ return null;
1064
+ }
1065
+ const [level] = currentRequest.protocolID;
1066
+ if (level !== 2) {
1067
+ return null;
1068
+ }
1069
+ const originator = currentRequest.originator;
1070
+ const privileged = (_a = currentRequest.privileged) !== null && _a !== void 0 ? _a : false;
1071
+ const counterparty = (_b = currentRequest.counterparty) !== null && _b !== void 0 ? _b : 'self';
1072
+ const groupPermissions = await this.fetchManifestGroupPermissions(originator);
1073
+ if (!groupPermissions) {
1074
+ return null;
1075
+ }
1076
+ const normalizeManifestCounterparty = (cp) => {
1077
+ if (cp === '')
1078
+ return counterparty;
1079
+ return cp !== null && cp !== void 0 ? cp : 'self';
1080
+ };
1081
+ const manifestLevel2ForThisPeer = (groupPermissions.protocolPermissions || [])
1082
+ .filter(p => { var _a, _b; return ((_b = (_a = p.protocolID) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 0) === 2; })
1083
+ .map(p => ({
1084
+ protocolID: p.protocolID,
1085
+ counterparty: normalizeManifestCounterparty(p.counterparty),
1086
+ description: p.description
1087
+ }))
1088
+ .filter(p => p.counterparty === counterparty);
1089
+ const isCurrentRequestInManifest = manifestLevel2ForThisPeer.some(p => deepEqual(p.protocolID, currentRequest.protocolID));
1090
+ if (!isCurrentRequestInManifest) {
1091
+ return null;
1092
+ }
1093
+ const permissionsToRequest = {
1094
+ protocolPermissions: []
1095
+ };
1096
+ for (const p of manifestLevel2ForThisPeer) {
1097
+ const hasPerm = await this.hasProtocolPermission({
1098
+ originator,
1099
+ privileged,
1100
+ protocolID: p.protocolID,
1101
+ counterparty: p.counterparty
1102
+ });
1103
+ if (!hasPerm) {
1104
+ permissionsToRequest.protocolPermissions.push({
1105
+ protocolID: p.protocolID,
1106
+ counterparty: p.counterparty,
1107
+ description: p.description
1108
+ });
1109
+ }
1110
+ }
1111
+ if (!this.hasAnyPermissionsToRequest(permissionsToRequest)) {
1112
+ return null;
1113
+ }
1114
+ const key = `group-peer:${originator}:${privileged}:${counterparty}`;
1115
+ const existing = this.activeRequests.get(key);
1116
+ if (existing) {
1117
+ const existingRequest = existing.request;
1118
+ if (!existingRequest.permissions.protocolPermissions) {
1119
+ existingRequest.permissions.protocolPermissions = [];
1120
+ }
1121
+ for (const p of permissionsToRequest.protocolPermissions || []) {
1122
+ if (!existingRequest.permissions.protocolPermissions.find(x => deepEqual(x, p))) {
1123
+ existingRequest.permissions.protocolPermissions.push(p);
1124
+ }
1125
+ }
1126
+ await new Promise((resolve, reject) => {
1127
+ existing.pending.push({ resolve, reject });
1128
+ });
1129
+ }
1130
+ else {
1131
+ await new Promise(async (resolve, reject) => {
1132
+ const permissions = permissionsToRequest;
1133
+ this.activeRequests.set(key, {
1134
+ request: {
1135
+ originator,
1136
+ permissions,
1137
+ displayOriginator: currentRequest.displayOriginator
1138
+ },
1139
+ pending: [{ resolve, reject }]
1140
+ });
1141
+ await this.callEvent('onGroupedPermissionRequested', {
1142
+ requestID: key,
1143
+ originator,
1144
+ permissions
1145
+ });
1146
+ });
1147
+ }
1148
+ const satisfied = await this.checkSpecificPermissionAfterGroupFlow(currentRequest);
1149
+ return satisfied ? true : null;
1150
+ }
1151
+ async checkSpecificPermissionAfterGroupFlow(request) {
1152
+ var _a, _b, _c;
1153
+ switch (request.type) {
1154
+ case 'protocol':
1155
+ return await this.hasProtocolPermission({
1156
+ originator: request.originator,
1157
+ privileged: (_a = request.privileged) !== null && _a !== void 0 ? _a : false,
1158
+ protocolID: request.protocolID,
1159
+ counterparty: (_b = request.counterparty) !== null && _b !== void 0 ? _b : 'self'
1160
+ });
1161
+ case 'basket':
1162
+ return await this.hasBasketAccess({
1163
+ originator: request.originator,
1164
+ basket: request.basket
1165
+ });
1166
+ case 'certificate':
1167
+ return await this.hasCertificateAccess({
1168
+ originator: request.originator,
1169
+ privileged: (_c = request.privileged) !== null && _c !== void 0 ? _c : false,
1170
+ verifier: request.certificate.verifier,
1171
+ certType: request.certificate.certType,
1172
+ fields: request.certificate.fields
1173
+ });
1174
+ case 'spending':
1175
+ return await this.hasSpendingAuthorization({
1176
+ originator: request.originator,
1177
+ satoshis: request.spending.satoshis
1178
+ });
1179
+ default:
1180
+ return false;
1181
+ }
1182
+ }
1183
+ isRequestIncludedInGroupPermissions(request, groupPermissions) {
1184
+ var _a, _b, _c, _d;
1185
+ switch (request.type) {
1186
+ case 'protocol': {
1187
+ if (request.privileged)
1188
+ return false;
1189
+ const pid = request.protocolID;
1190
+ if (!pid)
1191
+ return false;
1192
+ const cp = (_a = request.counterparty) !== null && _a !== void 0 ? _a : 'self';
1193
+ return !!((_b = groupPermissions.protocolPermissions) === null || _b === void 0 ? void 0 : _b.some(p => {
1194
+ var _a;
1195
+ const manifestCp = p.counterparty === '' ? cp : ((_a = p.counterparty) !== null && _a !== void 0 ? _a : 'self');
1196
+ return deepEqual(p.protocolID, pid) && manifestCp === cp;
1197
+ }));
1198
+ }
1199
+ case 'basket': {
1200
+ const basket = request.basket;
1201
+ if (!basket)
1202
+ return false;
1203
+ return !!((_c = groupPermissions.basketAccess) === null || _c === void 0 ? void 0 : _c.some(b => b.basket === basket));
1204
+ }
1205
+ case 'certificate': {
1206
+ if (request.privileged)
1207
+ return false;
1208
+ const cert = request.certificate;
1209
+ if (!cert)
1210
+ return false;
1211
+ return !!((_d = groupPermissions.certificateAccess) === null || _d === void 0 ? void 0 : _d.some(c => {
1212
+ const fieldsA = new Set(c.fields || []);
1213
+ const fieldsB = new Set(cert.fields || []);
1214
+ if (fieldsA.size !== fieldsB.size)
1215
+ return false;
1216
+ for (const f of fieldsA)
1217
+ if (!fieldsB.has(f))
1218
+ return false;
1219
+ return c.type === cert.certType && c.verifierPublicKey === cert.verifier;
1220
+ }));
1221
+ }
1222
+ case 'spending':
1223
+ return !!groupPermissions.spendingAuthorization;
1224
+ default:
1225
+ return false;
1226
+ }
1227
+ }
1228
+ async maybeRequestGroupedPermissions(currentRequest) {
1229
+ if (!this.config.seekGroupedPermission) {
1230
+ return null;
1231
+ }
1232
+ const originator = currentRequest.originator;
1233
+ const groupPermissions = await this.fetchManifestGroupPermissions(originator);
1234
+ if (!groupPermissions) {
1235
+ return null;
1236
+ }
1237
+ if (!this.isRequestIncludedInGroupPermissions(currentRequest, groupPermissions)) {
1238
+ return null;
1239
+ }
1240
+ const permissionsToRequest = await this.filterAlreadyGrantedPermissions(originator, groupPermissions);
1241
+ if (!this.hasAnyPermissionsToRequest(permissionsToRequest)) {
1242
+ return null;
1243
+ }
1244
+ const key = `group:${originator}`;
1245
+ if (this.activeRequests.has(key)) {
1246
+ await new Promise((resolve, reject) => {
1247
+ this.activeRequests.get(key).pending.push({ resolve, reject });
1248
+ });
1249
+ }
1250
+ else {
1251
+ await new Promise(async (resolve, reject) => {
1252
+ this.activeRequests.set(key, {
1253
+ request: {
1254
+ originator,
1255
+ permissions: permissionsToRequest,
1256
+ displayOriginator: currentRequest.displayOriginator
1257
+ },
1258
+ pending: [{ resolve, reject }]
1259
+ });
1260
+ await this.callEvent('onGroupedPermissionRequested', {
1261
+ requestID: key,
1262
+ originator,
1263
+ permissions: permissionsToRequest
1264
+ });
1265
+ });
1266
+ }
1267
+ const satisfied = await this.checkSpecificPermissionAfterGroupFlow(currentRequest);
1268
+ return satisfied ? true : null;
1269
+ }
731
1270
  /**
732
1271
  * A central method that triggers the permission request flow.
733
1272
  * - It checks if there's already an active request for the same key
@@ -736,13 +1275,25 @@ class WalletPermissionsManager {
736
1275
  * and return a promise that resolves once permission is granted or rejects if denied.
737
1276
  */
738
1277
  async requestPermissionFlow(r) {
739
- var _a;
1278
+ var _a, _b, _c;
740
1279
  const normalizedOriginator = this.normalizeOriginator(r.originator) || r.originator;
741
1280
  const preparedRequest = {
742
1281
  ...r,
743
1282
  originator: normalizedOriginator,
744
- displayOriginator: (_a = r.displayOriginator) !== null && _a !== void 0 ? _a : r.originator
1283
+ displayOriginator: (_c = (_a = r.displayOriginator) !== null && _a !== void 0 ? _a : (_b = r.previousToken) === null || _b === void 0 ? void 0 : _b.rawOriginator) !== null && _c !== void 0 ? _c : r.originator
745
1284
  };
1285
+ const pactResult = await this.maybeRequestPact(preparedRequest);
1286
+ if (pactResult !== null) {
1287
+ return pactResult;
1288
+ }
1289
+ const peerGroupResult = await this.maybeRequestPeerGroupedLevel2ProtocolPermissions(preparedRequest);
1290
+ if (peerGroupResult !== null) {
1291
+ return peerGroupResult;
1292
+ }
1293
+ const groupResult = await this.maybeRequestGroupedPermissions(preparedRequest);
1294
+ if (groupResult !== null) {
1295
+ return groupResult;
1296
+ }
746
1297
  const key = this.buildRequestKey(preparedRequest);
747
1298
  // If there's already a queue for the same resource, we piggyback on it
748
1299
  const existingQueue = this.activeRequests.get(key);
@@ -1196,10 +1747,128 @@ class WalletPermissionsManager {
1196
1747
  }
1197
1748
  ],
1198
1749
  options: {
1199
- acceptDelayedBroadcast: false
1750
+ acceptDelayedBroadcast: true
1200
1751
  }
1201
1752
  }, this.adminOriginator);
1202
1753
  }
1754
+ async mapWithConcurrency(items, concurrency, fn) {
1755
+ if (!items.length)
1756
+ return [];
1757
+ const results = new Array(items.length);
1758
+ let i = 0;
1759
+ const worker = async () => {
1760
+ while (true) {
1761
+ const idx = i++;
1762
+ if (idx >= items.length)
1763
+ return;
1764
+ results[idx] = await fn(items[idx]);
1765
+ }
1766
+ };
1767
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
1768
+ return results;
1769
+ }
1770
+ async runBestEffortBatches(items, chunkSize, runChunk) {
1771
+ if (!items.length)
1772
+ return [];
1773
+ const out = [];
1774
+ for (let i = 0; i < items.length; i += chunkSize) {
1775
+ const chunk = items.slice(i, i + chunkSize);
1776
+ out.push(...(await this.runBestEffortChunk(chunk, runChunk)));
1777
+ }
1778
+ return out;
1779
+ }
1780
+ async runBestEffortChunk(chunk, runChunk) {
1781
+ try {
1782
+ return await runChunk(chunk);
1783
+ }
1784
+ catch (e) {
1785
+ if (chunk.length <= 1) {
1786
+ console.error('Permission batch failed:', e);
1787
+ return [];
1788
+ }
1789
+ const mid = Math.ceil(chunk.length / 2);
1790
+ const left = await this.runBestEffortChunk(chunk.slice(0, mid), runChunk);
1791
+ const right = await this.runBestEffortChunk(chunk.slice(mid), runChunk);
1792
+ return [...left, ...right];
1793
+ }
1794
+ }
1795
+ async buildPermissionOutput(r, expiry, amount) {
1796
+ const normalizedOriginator = this.normalizeOriginator(r.originator) || r.originator;
1797
+ r.originator = normalizedOriginator;
1798
+ const basketName = BASKET_MAP[r.type];
1799
+ if (!basketName) {
1800
+ throw new Error(`Unsupported permission type: ${r.type}`);
1801
+ }
1802
+ const fields = await this.buildPushdropFields(r, expiry, amount);
1803
+ const script = await new sdk_1.PushDrop(this.underlying).lock(fields, WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL, '1', 'self', true, true);
1804
+ const tags = this.buildTagsForRequest(r);
1805
+ return {
1806
+ request: r,
1807
+ output: {
1808
+ lockingScript: script.toHex(),
1809
+ satoshis: 1,
1810
+ outputDescription: `${r.type} permission token`,
1811
+ basket: basketName,
1812
+ tags
1813
+ }
1814
+ };
1815
+ }
1816
+ async createPermissionTokensBestEffort(items) {
1817
+ const CHUNK = 25;
1818
+ return this.runBestEffortBatches(items, CHUNK, async (chunk) => {
1819
+ const built = await this.mapWithConcurrency(chunk, 8, c => this.buildPermissionOutput(c.request, c.expiry, c.amount));
1820
+ await this.createAction({
1821
+ description: `Grant ${built.length} permissions`,
1822
+ outputs: built.map(b => b.output),
1823
+ options: { acceptDelayedBroadcast: true }
1824
+ }, this.adminOriginator);
1825
+ return built.map(b => b.request);
1826
+ });
1827
+ }
1828
+ async renewPermissionTokensBestEffort(items) {
1829
+ const CHUNK = 15;
1830
+ return this.runBestEffortBatches(items, CHUNK, async (chunk) => {
1831
+ const built = await this.mapWithConcurrency(chunk, 8, c => this.buildPermissionOutput(c.request, c.expiry, c.amount));
1832
+ const inputBeef = new sdk_1.Beef();
1833
+ for (const c of chunk) {
1834
+ inputBeef.mergeBeef(sdk_1.Beef.fromBinary(c.oldToken.tx));
1835
+ }
1836
+ const { signableTransaction } = await this.createAction({
1837
+ description: `Renew ${chunk.length} permissions`,
1838
+ inputBEEF: inputBeef.toBinary(),
1839
+ inputs: chunk.map((c, i) => ({
1840
+ outpoint: `${c.oldToken.txid}.${c.oldToken.outputIndex}`,
1841
+ unlockingScriptLength: 73,
1842
+ inputDescription: `Consume old permission token #${i + 1}`
1843
+ })),
1844
+ outputs: built.map(b => b.output),
1845
+ options: {
1846
+ acceptDelayedBroadcast: true,
1847
+ randomizeOutputs: false,
1848
+ signAndProcess: false
1849
+ }
1850
+ }, this.adminOriginator);
1851
+ if (!(signableTransaction === null || signableTransaction === void 0 ? void 0 : signableTransaction.reference) || !signableTransaction.tx) {
1852
+ throw new Error('Failed to create signable transaction');
1853
+ }
1854
+ const partialTx = sdk_1.Transaction.fromAtomicBEEF(signableTransaction.tx);
1855
+ const pushdrop = new sdk_1.PushDrop(this.underlying);
1856
+ const spends = {};
1857
+ for (let i = 0; i < chunk.length; i++) {
1858
+ const token = chunk[i].oldToken;
1859
+ const unlocker = pushdrop.unlock(WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL, '1', 'self', 'all', false, 1, sdk_1.LockingScript.fromHex(token.outputScript));
1860
+ const unlockingScript = await unlocker.sign(partialTx, i);
1861
+ spends[i] = { unlockingScript: unlockingScript.toHex() };
1862
+ }
1863
+ const { txid } = await this.underlying.signAction({
1864
+ reference: signableTransaction.reference,
1865
+ spends
1866
+ });
1867
+ if (!txid)
1868
+ throw new Error('Failed to finalize renewal transaction');
1869
+ return built.map(b => b.request);
1870
+ });
1871
+ }
1203
1872
  async coalescePermissionTokens(oldTokens, newScript, opts) {
1204
1873
  var _a;
1205
1874
  if (!(oldTokens === null || oldTokens === void 0 ? void 0 : oldTokens.length))
@@ -1230,7 +1899,7 @@ class WalletPermissionsManager {
1230
1899
  }
1231
1900
  ],
1232
1901
  options: {
1233
- acceptDelayedBroadcast: false,
1902
+ acceptDelayedBroadcast: true,
1234
1903
  randomizeOutputs: false,
1235
1904
  signAndProcess: false
1236
1905
  }
@@ -1309,7 +1978,7 @@ class WalletPermissionsManager {
1309
1978
  }
1310
1979
  ],
1311
1980
  options: {
1312
- acceptDelayedBroadcast: false
1981
+ acceptDelayedBroadcast: true
1313
1982
  }
1314
1983
  }, this.adminOriginator);
1315
1984
  const tx = sdk_1.Transaction.fromBEEF(signableTransaction.tx);
@@ -1445,7 +2114,7 @@ class WalletPermissionsManager {
1445
2114
  tags,
1446
2115
  tagQueryMode: 'all',
1447
2116
  include: 'entire transactions',
1448
- limit: 100
2117
+ limit: 10000
1449
2118
  }, this.adminOriginator);
1450
2119
  for (const out of result.outputs) {
1451
2120
  if (seen.has(out.outpoint))
@@ -1750,16 +2419,60 @@ class WalletPermissionsManager {
1750
2419
  }
1751
2420
  ],
1752
2421
  options: {
1753
- acceptDelayedBroadcast: false
2422
+ acceptDelayedBroadcast: true
1754
2423
  }
1755
2424
  }, this.adminOriginator);
1756
2425
  const tx = sdk_1.Transaction.fromBEEF(signableTransaction.tx);
2426
+ const normalizeTxid = (txid) => (txid !== null && txid !== void 0 ? txid : '').toLowerCase();
2427
+ const reverseHexTxid = (txid) => {
2428
+ const hex = normalizeTxid(txid);
2429
+ if (!/^[0-9a-f]{64}$/.test(hex))
2430
+ return hex;
2431
+ const bytes = hex.match(/../g);
2432
+ return bytes ? bytes.reverse().join('') : hex;
2433
+ };
2434
+ const matchesOutpointString = (outpoint) => {
2435
+ const dot = outpoint.lastIndexOf('.');
2436
+ const colon = outpoint.lastIndexOf(':');
2437
+ const sep = dot > colon ? dot : colon;
2438
+ if (sep === -1)
2439
+ return false;
2440
+ const txidPart = outpoint.slice(0, sep);
2441
+ const indexPart = outpoint.slice(sep + 1);
2442
+ const vout = Number(indexPart);
2443
+ if (!Number.isFinite(vout))
2444
+ return false;
2445
+ return normalizeTxid(txidPart) === normalizeTxid(oldToken.txid) && vout === oldToken.outputIndex;
2446
+ };
2447
+ let permInputIndex = tx.inputs.findIndex((input) => {
2448
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
2449
+ const txidCandidate = (_g = (_f = (_e = (_d = (_c = (_b = (_a = input === null || input === void 0 ? void 0 : input.sourceTXID) !== null && _a !== void 0 ? _a : input === null || input === void 0 ? void 0 : input.sourceTxid) !== null && _b !== void 0 ? _b : input === null || input === void 0 ? void 0 : input.sourceTxId) !== null && _c !== void 0 ? _c : input === null || input === void 0 ? void 0 : input.prevTxId) !== null && _d !== void 0 ? _d : input === null || input === void 0 ? void 0 : input.prevTxid) !== null && _e !== void 0 ? _e : input === null || input === void 0 ? void 0 : input.prevTXID) !== null && _f !== void 0 ? _f : input === null || input === void 0 ? void 0 : input.txid) !== null && _g !== void 0 ? _g : input === null || input === void 0 ? void 0 : input.txID;
2450
+ const voutCandidate = (_l = (_k = (_j = (_h = input === null || input === void 0 ? void 0 : input.sourceOutputIndex) !== null && _h !== void 0 ? _h : input === null || input === void 0 ? void 0 : input.sourceOutput) !== null && _j !== void 0 ? _j : input === null || input === void 0 ? void 0 : input.outputIndex) !== null && _k !== void 0 ? _k : input === null || input === void 0 ? void 0 : input.vout) !== null && _l !== void 0 ? _l : input === null || input === void 0 ? void 0 : input.prevOutIndex;
2451
+ if (typeof txidCandidate === 'string' && typeof voutCandidate === 'number') {
2452
+ const cand = normalizeTxid(txidCandidate);
2453
+ const target = normalizeTxid(oldToken.txid);
2454
+ if (cand === target && voutCandidate === oldToken.outputIndex)
2455
+ return true;
2456
+ if (cand === reverseHexTxid(oldToken.txid) && voutCandidate === oldToken.outputIndex)
2457
+ return true;
2458
+ }
2459
+ const outpointCandidate = (_o = (_m = input === null || input === void 0 ? void 0 : input.outpoint) !== null && _m !== void 0 ? _m : input === null || input === void 0 ? void 0 : input.sourceOutpoint) !== null && _o !== void 0 ? _o : input === null || input === void 0 ? void 0 : input.prevOutpoint;
2460
+ if (typeof outpointCandidate === 'string' && matchesOutpointString(outpointCandidate))
2461
+ return true;
2462
+ return false;
2463
+ });
2464
+ if (permInputIndex === -1 && tx.inputs.length === 1) {
2465
+ permInputIndex = 0;
2466
+ }
2467
+ if (permInputIndex === -1) {
2468
+ throw new Error('Unable to locate permission token input for revocation.');
2469
+ }
1757
2470
  const unlocker = new sdk_1.PushDrop(this.underlying).unlock(WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL, '1', 'self', 'all', false, 1, sdk_1.LockingScript.fromHex(oldToken.outputScript));
1758
- const unlockingScript = await unlocker.sign(tx, 0);
2471
+ const unlockingScript = await unlocker.sign(tx, permInputIndex);
1759
2472
  await this.underlying.signAction({
1760
2473
  reference: signableTransaction.reference,
1761
2474
  spends: {
1762
- 0: {
2475
+ [permInputIndex]: {
1763
2476
  unlockingScript: unlockingScript.toHex()
1764
2477
  }
1765
2478
  }
@@ -2358,80 +3071,17 @@ class WalletPermissionsManager {
2358
3071
  return this.underlying.isAuthenticated(...args);
2359
3072
  }
2360
3073
  async waitForAuthentication(...args) {
2361
- var _a, _b, _c, _d, _e, _f, _g;
2362
3074
  let [_, originator] = args;
2363
3075
  if (this.config.seekGroupedPermission && originator) {
2364
3076
  const { normalized: normalizedOriginator } = this.prepareOriginator(originator);
2365
3077
  originator = normalizedOriginator;
2366
3078
  // 1. Fetch manifest.json from the originator
2367
- let groupPermissions;
2368
- try {
2369
- const proto = originator.startsWith('localhost:') ? 'http' : 'https';
2370
- const response = await fetch(`${proto}://${originator}/manifest.json`);
2371
- if (response.ok) {
2372
- const manifest = await response.json();
2373
- if ((_a = manifest === null || manifest === void 0 ? void 0 : manifest.babbage) === null || _a === void 0 ? void 0 : _a.groupPermissions) {
2374
- groupPermissions = manifest.babbage.groupPermissions;
2375
- }
2376
- }
2377
- }
2378
- catch (e) {
2379
- // Ignore fetch/parse errors, just proceed without group permissions.
2380
- }
3079
+ const groupPermissions = await this.fetchManifestGroupPermissions(originator);
2381
3080
  if (groupPermissions) {
2382
3081
  // 2. Filter out already-granted permissions
2383
- const permissionsToRequest = {
2384
- protocolPermissions: [],
2385
- basketAccess: [],
2386
- certificateAccess: []
2387
- };
2388
- if (groupPermissions.spendingAuthorization) {
2389
- const hasAuth = await this.hasSpendingAuthorization({
2390
- originator,
2391
- satoshis: groupPermissions.spendingAuthorization.amount
2392
- });
2393
- if (!hasAuth) {
2394
- permissionsToRequest.spendingAuthorization = groupPermissions.spendingAuthorization;
2395
- }
2396
- }
2397
- for (const p of groupPermissions.protocolPermissions || []) {
2398
- const hasPerm = await this.hasProtocolPermission({
2399
- originator,
2400
- privileged: false, // Privilege is never allowed here
2401
- protocolID: p.protocolID,
2402
- counterparty: p.counterparty || 'self'
2403
- });
2404
- if (!hasPerm) {
2405
- permissionsToRequest.protocolPermissions.push(p);
2406
- }
2407
- }
2408
- for (const b of groupPermissions.basketAccess || []) {
2409
- const hasAccess = await this.hasBasketAccess({
2410
- originator,
2411
- basket: b.basket
2412
- });
2413
- if (!hasAccess) {
2414
- permissionsToRequest.basketAccess.push(b);
2415
- }
2416
- }
2417
- for (const c of groupPermissions.certificateAccess || []) {
2418
- const hasAccess = await this.hasCertificateAccess({
2419
- originator,
2420
- privileged: false, // Privilege is never allowed here for security
2421
- verifier: c.verifierPublicKey,
2422
- certType: c.type,
2423
- fields: c.fields
2424
- });
2425
- if (!hasAccess) {
2426
- permissionsToRequest.certificateAccess.push(c);
2427
- }
2428
- }
3082
+ const permissionsToRequest = await this.filterAlreadyGrantedPermissions(originator, groupPermissions);
2429
3083
  // 3. If any permissions are left to request, start the flow
2430
- const hasRequests = permissionsToRequest.spendingAuthorization ||
2431
- ((_c = (_b = permissionsToRequest.protocolPermissions) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) > 0 ||
2432
- ((_e = (_d = permissionsToRequest.basketAccess) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0) > 0 ||
2433
- ((_g = (_f = permissionsToRequest.certificateAccess) === null || _f === void 0 ? void 0 : _f.length) !== null && _g !== void 0 ? _g : 0) > 0;
2434
- if (hasRequests) {
3084
+ if (this.hasAnyPermissionsToRequest(permissionsToRequest)) {
2435
3085
  const key = `group:${originator}`;
2436
3086
  if (this.activeRequests.has(key)) {
2437
3087
  // Another call is already waiting, piggyback on it
@@ -2646,6 +3296,7 @@ class WalletPermissionsManager {
2646
3296
  }
2647
3297
  }
2648
3298
  exports.WalletPermissionsManager = WalletPermissionsManager;
3299
+ WalletPermissionsManager.MANIFEST_CACHE_TTL_MS = 5 * 60 * 1000;
2649
3300
  /** How long a cached permission remains valid (5 minutes). */
2650
3301
  WalletPermissionsManager.CACHE_TTL_MS = 5 * 60 * 1000;
2651
3302
  /** Window during which freshly granted permissions are auto-allowed (except spending). */