@bsv/wallet-toolbox-client 2.0.0-beta.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/src/Wallet.d.ts +1 -1
- package/out/src/Wallet.d.ts.map +1 -1
- package/out/src/Wallet.js +23 -7
- package/out/src/Wallet.js.map +1 -1
- package/out/src/WalletPermissionsManager.d.ts +84 -1
- package/out/src/WalletPermissionsManager.d.ts.map +1 -1
- package/out/src/WalletPermissionsManager.js +1045 -214
- package/out/src/WalletPermissionsManager.js.map +1 -1
- package/out/src/sdk/WalletServices.interfaces.d.ts.map +1 -1
- package/out/src/sdk/WalletStorage.interfaces.d.ts +3 -3
- package/out/src/sdk/WalletStorage.interfaces.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Ingest/BulkIngestorWhatsOnChainCdn.d.ts +1 -1
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageBase.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageBase.js +4 -1
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageBase.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/util/blockHeaderUtilities.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/util/blockHeaderUtilities.js +1 -2
- package/out/src/services/chaintracker/chaintracks/util/blockHeaderUtilities.js.map +1 -1
- package/out/src/services/chaintracker/chaintracks/util/validBulkHeaderFilesByFileHash.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/util/validBulkHeaderFilesByFileHash.js +12 -0
- package/out/src/services/chaintracker/chaintracks/util/validBulkHeaderFilesByFileHash.js.map +1 -1
- package/out/src/storage/StorageProvider.d.ts +16 -2
- package/out/src/storage/StorageProvider.d.ts.map +1 -1
- package/out/src/storage/StorageProvider.js +33 -4
- package/out/src/storage/StorageProvider.js.map +1 -1
- package/out/src/storage/methods/ListOutputsSpecOp.d.ts +15 -1
- package/out/src/storage/methods/ListOutputsSpecOp.d.ts.map +1 -1
- package/out/src/storage/methods/ListOutputsSpecOp.js +52 -2
- package/out/src/storage/methods/ListOutputsSpecOp.js.map +1 -1
- package/out/src/storage/methods/getBeefForTransaction.js +1 -1
- package/out/src/storage/methods/getBeefForTransaction.js.map +1 -1
- package/out/src/storage/methods/internalizeAction.d.ts.map +1 -1
- package/out/src/storage/methods/internalizeAction.js +2 -2
- package/out/src/storage/methods/internalizeAction.js.map +1 -1
- package/out/src/storage/methods/listOutputsIdb.d.ts.map +1 -1
- package/out/src/storage/methods/listOutputsIdb.js +10 -17
- package/out/src/storage/methods/listOutputsIdb.js.map +1 -1
- package/out/src/wab-client/WABClient.d.ts +65 -0
- package/out/src/wab-client/WABClient.d.ts.map +1 -1
- package/out/src/wab-client/WABClient.js +107 -0
- package/out/src/wab-client/WABClient.js.map +1 -1
- package/package.json +2 -2
- package/out/src/sdk/validationHelpers.d.ts +0 -303
- package/out/src/sdk/validationHelpers.d.ts.map +0 -1
- package/out/src/sdk/validationHelpers.js +0 -632
- package/out/src/sdk/validationHelpers.js.map +0 -1
- package/out/src/utility/ReaderUint8Array.d.ts +0 -28
- package/out/src/utility/ReaderUint8Array.d.ts.map +0 -1
- package/out/src/utility/ReaderUint8Array.js +0 -166
- package/out/src/utility/ReaderUint8Array.js.map +0 -1
|
@@ -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
|
|
@@ -117,6 +121,7 @@ class WalletPermissionsManager {
|
|
|
117
121
|
seekSpendingPermissions: true,
|
|
118
122
|
seekGroupedPermission: true,
|
|
119
123
|
differentiatePrivilegedOperations: true,
|
|
124
|
+
whitelistedCounterparties: {},
|
|
120
125
|
...config // override with user-specified config
|
|
121
126
|
};
|
|
122
127
|
}
|
|
@@ -159,6 +164,68 @@ class WalletPermissionsManager {
|
|
|
159
164
|
originator
|
|
160
165
|
});
|
|
161
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Adds a permission module for the given schemeID if needed, throwing if unsupported.
|
|
169
|
+
*/
|
|
170
|
+
addPModuleByScheme(schemeID, kind, pModulesByScheme) {
|
|
171
|
+
var _a;
|
|
172
|
+
if (pModulesByScheme.has(schemeID))
|
|
173
|
+
return;
|
|
174
|
+
const module = (_a = this.config.permissionModules) === null || _a === void 0 ? void 0 : _a[schemeID];
|
|
175
|
+
if (!module) {
|
|
176
|
+
throw new Error(`Unsupported P-${kind} scheme: p ${schemeID}`);
|
|
177
|
+
}
|
|
178
|
+
pModulesByScheme.set(schemeID, module);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Splits labels into P and non-P lists, registering any P-modules encountered.
|
|
182
|
+
*
|
|
183
|
+
* P-labels follow BRC-111 format: `p <moduleId> <payload>`
|
|
184
|
+
* - Must start with "p " (lowercase p + space)
|
|
185
|
+
* - Module ID must be at least 1 character with no spaces
|
|
186
|
+
* - Single space separates module ID from payload
|
|
187
|
+
* - Payload must be at least 1 character
|
|
188
|
+
*
|
|
189
|
+
* @example Valid: "p btms token123", "p invoicing invoice 2026-02-02"
|
|
190
|
+
* @example Invalid: "p btms" (no payload), "p btms " (empty payload), "p data" (empty moduleId)
|
|
191
|
+
*
|
|
192
|
+
* @param labels - Array of label strings to process
|
|
193
|
+
* @param pModulesByScheme - Map to populate with discovered p-modules
|
|
194
|
+
* @returns Array of non-P labels for normal permission checks
|
|
195
|
+
* @throws Error if p-label format is invalid or module is unsupported
|
|
196
|
+
*/
|
|
197
|
+
splitLabelsByPermissionModule(labels, pModulesByScheme) {
|
|
198
|
+
const nonPLabels = [];
|
|
199
|
+
if (!labels)
|
|
200
|
+
return nonPLabels;
|
|
201
|
+
for (const label of labels) {
|
|
202
|
+
if (label.startsWith('p ')) {
|
|
203
|
+
// Remove "p " prefix to get "moduleId payload"
|
|
204
|
+
const remainder = label.slice(2);
|
|
205
|
+
// Find the space that separates moduleId from payload
|
|
206
|
+
const separatorIndex = remainder.indexOf(' ');
|
|
207
|
+
// Validate: must have a space (separatorIndex > 0) and payload after it
|
|
208
|
+
// separatorIndex <= 0 means no space found or moduleId is empty
|
|
209
|
+
// separatorIndex === remainder.length - 1 means space is last char (no payload)
|
|
210
|
+
if (separatorIndex <= 0 || separatorIndex === remainder.length - 1) {
|
|
211
|
+
throw new Error(`Invalid P-label format: ${label}`);
|
|
212
|
+
}
|
|
213
|
+
// Reject double spaces after moduleId (payload can't start with space)
|
|
214
|
+
if (remainder[separatorIndex + 1] === ' ') {
|
|
215
|
+
throw new Error(`Invalid P-label format: ${label}`);
|
|
216
|
+
}
|
|
217
|
+
// Extract moduleId (substring before first space)
|
|
218
|
+
const schemeID = remainder.slice(0, separatorIndex);
|
|
219
|
+
// Register the module (throws if unsupported)
|
|
220
|
+
this.addPModuleByScheme(schemeID, 'label', pModulesByScheme);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
// Regular label - add to list for normal permission checks
|
|
224
|
+
nonPLabels.push(label);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return nonPLabels;
|
|
228
|
+
}
|
|
162
229
|
/**
|
|
163
230
|
* Decrypts custom instructions in listOutputs results if encryption is configured.
|
|
164
231
|
*/
|
|
@@ -172,6 +239,36 @@ class WalletPermissionsManager {
|
|
|
172
239
|
}
|
|
173
240
|
return results;
|
|
174
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Decrypts metadata in listActions results if encryption is configured.
|
|
244
|
+
*/
|
|
245
|
+
async decryptListActionsMetadata(results) {
|
|
246
|
+
if (results.actions) {
|
|
247
|
+
for (let i = 0; i < results.actions.length; i++) {
|
|
248
|
+
if (results.actions[i].description) {
|
|
249
|
+
results.actions[i].description = await this.maybeDecryptMetadata(results.actions[i].description);
|
|
250
|
+
}
|
|
251
|
+
if (results.actions[i].inputs) {
|
|
252
|
+
for (let j = 0; j < results.actions[i].inputs.length; j++) {
|
|
253
|
+
if (results.actions[i].inputs[j].inputDescription) {
|
|
254
|
+
results.actions[i].inputs[j].inputDescription = await this.maybeDecryptMetadata(results.actions[i].inputs[j].inputDescription);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (results.actions[i].outputs) {
|
|
259
|
+
for (let j = 0; j < results.actions[i].outputs.length; j++) {
|
|
260
|
+
if (results.actions[i].outputs[j].outputDescription) {
|
|
261
|
+
results.actions[i].outputs[j].outputDescription = await this.maybeDecryptMetadata(results.actions[i].outputs[j].outputDescription);
|
|
262
|
+
}
|
|
263
|
+
if (results.actions[i].outputs[j].customInstructions) {
|
|
264
|
+
results.actions[i].outputs[j].customInstructions = await this.maybeDecryptMetadata(results.actions[i].outputs[j].customInstructions);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return results;
|
|
271
|
+
}
|
|
175
272
|
/* ---------------------------------------------------------------------
|
|
176
273
|
* 1) PUBLIC API FOR REGISTERING CALLBACKS (UI PROMPTS, LOGGING, ETC.)
|
|
177
274
|
* --------------------------------------------------------------------- */
|
|
@@ -312,93 +409,177 @@ class WalletPermissionsManager {
|
|
|
312
409
|
if (!matching) {
|
|
313
410
|
throw new Error('Request ID not found.');
|
|
314
411
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
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
|
-
throw new Error('Granted protocol permissions are not a subset of the original request.');
|
|
325
|
-
}
|
|
326
|
-
if ((_b = params.granted.basketAccess) === null || _b === void 0 ? void 0 : _b.some(g => { var _a; return !((_a = requestedPermissions.basketAccess) === null || _a === void 0 ? void 0 : _a.find(r => deepEqual(r, g))); })) {
|
|
327
|
-
throw new Error('Granted basket access permissions are not a subset of the original request.');
|
|
328
|
-
}
|
|
329
|
-
if ((_c = params.granted.certificateAccess) === null || _c === void 0 ? void 0 : _c.some(g => { var _a; return !((_a = requestedPermissions.certificateAccess) === null || _a === void 0 ? void 0 : _a.find(r => deepEqual(r, g))); })) {
|
|
330
|
-
throw new Error('Granted certificate access permissions are not a subset of the original request.');
|
|
331
|
-
}
|
|
332
|
-
// --- End Validation ---
|
|
333
|
-
const expiry = params.expiry || 0; // default: never expires
|
|
334
|
-
if (params.granted.spendingAuthorization) {
|
|
335
|
-
await this.createPermissionOnChain({
|
|
336
|
-
type: 'spending',
|
|
337
|
-
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);
|
|
346
|
-
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);
|
|
412
|
+
try {
|
|
413
|
+
const originalRequest = matching.request;
|
|
414
|
+
const { originator, permissions: requestedPermissions, displayOriginator } = originalRequest;
|
|
415
|
+
const originLookupValues = this.buildOriginatorLookupValues(displayOriginator, originator);
|
|
416
|
+
// --- Validation: Ensure granted permissions are a subset of what was requested ---
|
|
417
|
+
if (params.granted.spendingAuthorization && !requestedPermissions.spendingAuthorization) {
|
|
418
|
+
throw new Error('Granted spending authorization was not part of the original request.');
|
|
357
419
|
}
|
|
358
|
-
|
|
420
|
+
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))); })) {
|
|
421
|
+
throw new Error('Granted protocol permissions are not a subset of the original request.');
|
|
422
|
+
}
|
|
423
|
+
if ((_b = params.granted.basketAccess) === null || _b === void 0 ? void 0 : _b.some(g => { var _a; return !((_a = requestedPermissions.basketAccess) === null || _a === void 0 ? void 0 : _a.find(r => deepEqual(r, g))); })) {
|
|
424
|
+
throw new Error('Granted basket access permissions are not a subset of the original request.');
|
|
425
|
+
}
|
|
426
|
+
if ((_c = params.granted.certificateAccess) === null || _c === void 0 ? void 0 : _c.some(g => { var _a; return !((_a = requestedPermissions.certificateAccess) === null || _a === void 0 ? void 0 : _a.find(r => deepEqual(r, g))); })) {
|
|
427
|
+
throw new Error('Granted certificate access permissions are not a subset of the original request.');
|
|
428
|
+
}
|
|
429
|
+
// --- End Validation ---
|
|
430
|
+
const expiry = params.expiry || 0; // default: never expires
|
|
431
|
+
const toCreate = [];
|
|
432
|
+
const toRenew = [];
|
|
433
|
+
if (params.granted.spendingAuthorization) {
|
|
434
|
+
toCreate.push({
|
|
435
|
+
request: {
|
|
436
|
+
type: 'spending',
|
|
437
|
+
originator,
|
|
438
|
+
spending: { satoshis: params.granted.spendingAuthorization.amount },
|
|
439
|
+
reason: params.granted.spendingAuthorization.description
|
|
440
|
+
},
|
|
441
|
+
expiry: 0,
|
|
442
|
+
amount: params.granted.spendingAuthorization.amount
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
const grantedProtocols = params.granted.protocolPermissions || [];
|
|
446
|
+
const protocolTokens = await this.mapWithConcurrency(grantedProtocols, 8, async (p) => {
|
|
447
|
+
const token = await this.findProtocolToken(originator, false, p.protocolID, p.counterparty || 'self', true, originLookupValues);
|
|
448
|
+
return { p, token };
|
|
449
|
+
});
|
|
450
|
+
for (const { p, token } of protocolTokens) {
|
|
359
451
|
const request = {
|
|
360
452
|
type: 'protocol',
|
|
361
453
|
originator,
|
|
362
|
-
privileged: false,
|
|
454
|
+
privileged: false,
|
|
363
455
|
protocolID: p.protocolID,
|
|
364
456
|
counterparty: p.counterparty || 'self',
|
|
365
457
|
reason: p.description
|
|
366
458
|
};
|
|
367
|
-
|
|
368
|
-
|
|
459
|
+
if (token) {
|
|
460
|
+
toRenew.push({ oldToken: token, request, expiry });
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
toCreate.push({ request, expiry });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
for (const b of params.granted.basketAccess || []) {
|
|
467
|
+
toCreate.push({
|
|
468
|
+
request: { type: 'basket', originator, basket: b.basket, reason: b.description },
|
|
469
|
+
expiry
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
for (const c of params.granted.certificateAccess || []) {
|
|
473
|
+
toCreate.push({
|
|
474
|
+
request: {
|
|
475
|
+
type: 'certificate',
|
|
476
|
+
originator,
|
|
477
|
+
privileged: false,
|
|
478
|
+
certificate: {
|
|
479
|
+
verifier: c.verifierPublicKey,
|
|
480
|
+
certType: c.type,
|
|
481
|
+
fields: c.fields
|
|
482
|
+
},
|
|
483
|
+
reason: c.description
|
|
484
|
+
},
|
|
485
|
+
expiry
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
const created = await this.createPermissionTokensBestEffort(toCreate);
|
|
489
|
+
const renewed = await this.renewPermissionTokensBestEffort(toRenew);
|
|
490
|
+
for (const req of [...created, ...renewed]) {
|
|
491
|
+
this.markRecentGrant(req);
|
|
492
|
+
}
|
|
493
|
+
// Success - resolve all pending promises for this request
|
|
494
|
+
for (const p of matching.pending) {
|
|
495
|
+
p.resolve(true);
|
|
369
496
|
}
|
|
370
497
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
498
|
+
catch (error) {
|
|
499
|
+
// Failure - reject all pending promises so callers don't hang forever
|
|
500
|
+
for (const p of matching.pending) {
|
|
501
|
+
p.reject(error);
|
|
502
|
+
}
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
finally {
|
|
506
|
+
// Always clean up the request entry
|
|
507
|
+
this.activeRequests.delete(params.requestID);
|
|
375
508
|
}
|
|
376
|
-
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Denies a previously requested grouped permission.
|
|
512
|
+
* @param requestID The ID of the request being denied.
|
|
513
|
+
*/
|
|
514
|
+
async denyGroupedPermission(requestID) {
|
|
515
|
+
const matching = this.activeRequests.get(requestID);
|
|
516
|
+
if (!matching) {
|
|
517
|
+
throw new Error('Request ID not found.');
|
|
518
|
+
}
|
|
519
|
+
const err = new Error('The user has denied the request for permission.');
|
|
520
|
+
err.code = 'ERR_PERMISSION_DENIED';
|
|
521
|
+
for (const p of matching.pending) {
|
|
522
|
+
p.reject(err);
|
|
523
|
+
}
|
|
524
|
+
this.activeRequests.delete(requestID);
|
|
525
|
+
}
|
|
526
|
+
async dismissGroupedPermission(requestID) {
|
|
527
|
+
const matching = this.activeRequests.get(requestID);
|
|
528
|
+
if (!matching) {
|
|
529
|
+
throw new Error('Request ID not found.');
|
|
530
|
+
}
|
|
531
|
+
for (const p of matching.pending) {
|
|
532
|
+
p.resolve(true);
|
|
533
|
+
}
|
|
534
|
+
this.activeRequests.delete(requestID);
|
|
535
|
+
}
|
|
536
|
+
async grantCounterpartyPermission(params) {
|
|
537
|
+
var _a;
|
|
538
|
+
const matching = this.activeRequests.get(params.requestID);
|
|
539
|
+
if (!matching) {
|
|
540
|
+
throw new Error('Request ID not found.');
|
|
541
|
+
}
|
|
542
|
+
const originalRequest = matching.request;
|
|
543
|
+
const { originator, counterparty, permissions: requestedPermissions, displayOriginator } = originalRequest;
|
|
544
|
+
const originLookupValues = this.buildOriginatorLookupValues(displayOriginator, originator);
|
|
545
|
+
if ((_a = params.granted.protocols) === null || _a === void 0 ? void 0 : _a.some(g => !requestedPermissions.protocols.find(r => deepEqual(r, g)))) {
|
|
546
|
+
throw new Error('Granted protocol permissions are not a subset of the original request.');
|
|
547
|
+
}
|
|
548
|
+
const expiry = params.expiry || 0;
|
|
549
|
+
const toCreate = [];
|
|
550
|
+
const toRenew = [];
|
|
551
|
+
const grantedProtocols = params.granted.protocols || [];
|
|
552
|
+
const protocolTokens = await this.mapWithConcurrency(grantedProtocols, 8, async (p) => {
|
|
553
|
+
const token = await this.findProtocolToken(originator, false, p.protocolID, counterparty, true, originLookupValues);
|
|
554
|
+
return { p, token };
|
|
555
|
+
});
|
|
556
|
+
for (const { p, token } of protocolTokens) {
|
|
377
557
|
const request = {
|
|
378
|
-
type: '
|
|
558
|
+
type: 'protocol',
|
|
379
559
|
originator,
|
|
380
|
-
privileged: false,
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
fields: c.fields
|
|
385
|
-
},
|
|
386
|
-
reason: c.description
|
|
560
|
+
privileged: false,
|
|
561
|
+
protocolID: p.protocolID,
|
|
562
|
+
counterparty,
|
|
563
|
+
reason: p.description
|
|
387
564
|
};
|
|
388
|
-
|
|
389
|
-
|
|
565
|
+
if (token) {
|
|
566
|
+
toRenew.push({ oldToken: token, request, expiry });
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
toCreate.push({ request, expiry });
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const created = await this.createPermissionTokensBestEffort(toCreate);
|
|
573
|
+
const renewed = await this.renewPermissionTokensBestEffort(toRenew);
|
|
574
|
+
for (const req of [...created, ...renewed]) {
|
|
575
|
+
this.markRecentGrant(req);
|
|
390
576
|
}
|
|
391
|
-
// Resolve all pending promises for this request
|
|
392
577
|
for (const p of matching.pending) {
|
|
393
578
|
p.resolve(true);
|
|
394
579
|
}
|
|
395
580
|
this.activeRequests.delete(params.requestID);
|
|
396
581
|
}
|
|
397
|
-
|
|
398
|
-
* Denies a previously requested grouped permission.
|
|
399
|
-
* @param requestID The ID of the request being denied.
|
|
400
|
-
*/
|
|
401
|
-
async denyGroupedPermission(requestID) {
|
|
582
|
+
async denyCounterpartyPermission(requestID) {
|
|
402
583
|
const matching = this.activeRequests.get(requestID);
|
|
403
584
|
if (!matching) {
|
|
404
585
|
throw new Error('Request ID not found.');
|
|
@@ -453,6 +634,9 @@ class WalletPermissionsManager {
|
|
|
453
634
|
if (!this.config.differentiatePrivilegedOperations) {
|
|
454
635
|
privileged = false;
|
|
455
636
|
}
|
|
637
|
+
if (!privileged && this.isWhitelistedCounterpartyProtocol(counterparty, protocolID)) {
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
456
640
|
const cacheKey = this.buildRequestKey({
|
|
457
641
|
type: 'protocol',
|
|
458
642
|
originator,
|
|
@@ -728,6 +912,469 @@ class WalletPermissionsManager {
|
|
|
728
912
|
usageType: 'generic'
|
|
729
913
|
});
|
|
730
914
|
}
|
|
915
|
+
validateCounterpartyPermissions(raw) {
|
|
916
|
+
if (!raw || !Array.isArray(raw.protocols) || raw.protocols.length === 0)
|
|
917
|
+
return null;
|
|
918
|
+
const validProtocols = raw.protocols.filter((p) => {
|
|
919
|
+
return (Array.isArray(p === null || p === void 0 ? void 0 : p.protocolID) &&
|
|
920
|
+
p.protocolID[0] === 2 &&
|
|
921
|
+
typeof p.protocolID[1] === 'string' &&
|
|
922
|
+
typeof (p === null || p === void 0 ? void 0 : p.description) === 'string');
|
|
923
|
+
});
|
|
924
|
+
if (validProtocols.length === 0)
|
|
925
|
+
return null;
|
|
926
|
+
return {
|
|
927
|
+
description: typeof raw.description === 'string' ? raw.description : undefined,
|
|
928
|
+
protocols: validProtocols
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
async fetchManifestPermissions(originator) {
|
|
932
|
+
const cached = this.manifestCache.get(originator);
|
|
933
|
+
if (cached && Date.now() - cached.fetchedAt < WalletPermissionsManager.MANIFEST_CACHE_TTL_MS) {
|
|
934
|
+
return {
|
|
935
|
+
groupPermissions: cached.groupPermissions,
|
|
936
|
+
counterpartyPermissions: cached.counterpartyPermissions
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
const inProgress = this.manifestFetchInProgress.get(originator);
|
|
940
|
+
if (inProgress) {
|
|
941
|
+
return inProgress;
|
|
942
|
+
}
|
|
943
|
+
const fetchPromise = (async () => {
|
|
944
|
+
var _a, _b;
|
|
945
|
+
try {
|
|
946
|
+
const proto = originator.startsWith('localhost:') ? 'http' : 'https';
|
|
947
|
+
const response = await fetch(`${proto}://${originator}/manifest.json`);
|
|
948
|
+
if (response.ok) {
|
|
949
|
+
const manifest = await response.json();
|
|
950
|
+
const groupPermissions = ((_a = manifest === null || manifest === void 0 ? void 0 : manifest.babbage) === null || _a === void 0 ? void 0 : _a.groupPermissions) || null;
|
|
951
|
+
const counterpartyPermissions = this.validateCounterpartyPermissions((_b = manifest === null || manifest === void 0 ? void 0 : manifest.babbage) === null || _b === void 0 ? void 0 : _b.counterpartyPermissions);
|
|
952
|
+
this.manifestCache.set(originator, { groupPermissions, counterpartyPermissions, fetchedAt: Date.now() });
|
|
953
|
+
return { groupPermissions, counterpartyPermissions };
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
catch (e) { }
|
|
957
|
+
const result = { groupPermissions: null, counterpartyPermissions: null };
|
|
958
|
+
this.manifestCache.set(originator, { ...result, fetchedAt: Date.now() });
|
|
959
|
+
return result;
|
|
960
|
+
})();
|
|
961
|
+
this.manifestFetchInProgress.set(originator, fetchPromise);
|
|
962
|
+
try {
|
|
963
|
+
return await fetchPromise;
|
|
964
|
+
}
|
|
965
|
+
finally {
|
|
966
|
+
this.manifestFetchInProgress.delete(originator);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
async fetchManifestGroupPermissions(originator) {
|
|
970
|
+
const { groupPermissions } = await this.fetchManifestPermissions(originator);
|
|
971
|
+
return groupPermissions;
|
|
972
|
+
}
|
|
973
|
+
async filterAlreadyGrantedPermissions(originator, groupPermissions) {
|
|
974
|
+
const permissionsToRequest = {
|
|
975
|
+
description: groupPermissions.description,
|
|
976
|
+
protocolPermissions: [],
|
|
977
|
+
basketAccess: [],
|
|
978
|
+
certificateAccess: []
|
|
979
|
+
};
|
|
980
|
+
if (groupPermissions.spendingAuthorization) {
|
|
981
|
+
const hasAuth = await this.hasSpendingAuthorization({
|
|
982
|
+
originator,
|
|
983
|
+
satoshis: groupPermissions.spendingAuthorization.amount
|
|
984
|
+
});
|
|
985
|
+
if (!hasAuth) {
|
|
986
|
+
permissionsToRequest.spendingAuthorization = groupPermissions.spendingAuthorization;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
for (const p of groupPermissions.protocolPermissions || []) {
|
|
990
|
+
const hasPerm = await this.hasProtocolPermission({
|
|
991
|
+
originator,
|
|
992
|
+
privileged: false,
|
|
993
|
+
protocolID: p.protocolID,
|
|
994
|
+
counterparty: p.counterparty || 'self'
|
|
995
|
+
});
|
|
996
|
+
if (!hasPerm) {
|
|
997
|
+
permissionsToRequest.protocolPermissions.push(p);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
for (const b of groupPermissions.basketAccess || []) {
|
|
1001
|
+
const hasAccess = await this.hasBasketAccess({
|
|
1002
|
+
originator,
|
|
1003
|
+
basket: b.basket
|
|
1004
|
+
});
|
|
1005
|
+
if (!hasAccess) {
|
|
1006
|
+
permissionsToRequest.basketAccess.push(b);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
for (const c of groupPermissions.certificateAccess || []) {
|
|
1010
|
+
const hasAccess = await this.hasCertificateAccess({
|
|
1011
|
+
originator,
|
|
1012
|
+
privileged: false,
|
|
1013
|
+
verifier: c.verifierPublicKey,
|
|
1014
|
+
certType: c.type,
|
|
1015
|
+
fields: c.fields
|
|
1016
|
+
});
|
|
1017
|
+
if (!hasAccess) {
|
|
1018
|
+
permissionsToRequest.certificateAccess.push(c);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return permissionsToRequest;
|
|
1022
|
+
}
|
|
1023
|
+
hasAnyPermissionsToRequest(permissions) {
|
|
1024
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1025
|
+
return !!(permissions.spendingAuthorization ||
|
|
1026
|
+
((_b = (_a = permissions.protocolPermissions) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0 ||
|
|
1027
|
+
((_d = (_c = permissions.basketAccess) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) > 0 ||
|
|
1028
|
+
((_f = (_e = permissions.certificateAccess) === null || _e === void 0 ? void 0 : _e.length) !== null && _f !== void 0 ? _f : 0) > 0);
|
|
1029
|
+
}
|
|
1030
|
+
hasGroupedPermissionRequestedHandlers() {
|
|
1031
|
+
const handlers = this.callbacks.onGroupedPermissionRequested || [];
|
|
1032
|
+
return handlers.some(h => typeof h === 'function');
|
|
1033
|
+
}
|
|
1034
|
+
hasCounterpartyPermissionRequestedHandlers() {
|
|
1035
|
+
const handlers = this.callbacks.onCounterpartyPermissionRequested || [];
|
|
1036
|
+
return handlers.some(h => typeof h === 'function');
|
|
1037
|
+
}
|
|
1038
|
+
async hasPactEstablished(originator, counterparty) {
|
|
1039
|
+
var _a;
|
|
1040
|
+
if (counterparty === 'self' || counterparty === 'anyone') {
|
|
1041
|
+
return true;
|
|
1042
|
+
}
|
|
1043
|
+
const cacheKey = `${originator}:${counterparty}`;
|
|
1044
|
+
if (this.pactEstablishedCache.has(cacheKey)) {
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
const { counterpartyPermissions } = await this.fetchManifestPermissions(originator);
|
|
1048
|
+
if (!((_a = counterpartyPermissions === null || counterpartyPermissions === void 0 ? void 0 : counterpartyPermissions.protocols) === null || _a === void 0 ? void 0 : _a.length)) {
|
|
1049
|
+
return true;
|
|
1050
|
+
}
|
|
1051
|
+
const firstProtocol = counterpartyPermissions.protocols[0];
|
|
1052
|
+
const hasToken = await this.hasProtocolPermission({
|
|
1053
|
+
originator,
|
|
1054
|
+
privileged: false,
|
|
1055
|
+
protocolID: firstProtocol.protocolID,
|
|
1056
|
+
counterparty
|
|
1057
|
+
});
|
|
1058
|
+
if (hasToken) {
|
|
1059
|
+
this.pactEstablishedCache.set(cacheKey, Date.now());
|
|
1060
|
+
return true;
|
|
1061
|
+
}
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
markPactEstablished(originator, counterparty) {
|
|
1065
|
+
const cacheKey = `${originator}:${counterparty}`;
|
|
1066
|
+
this.pactEstablishedCache.set(cacheKey, Date.now());
|
|
1067
|
+
}
|
|
1068
|
+
async maybeRequestPact(currentRequest) {
|
|
1069
|
+
var _a;
|
|
1070
|
+
if (!this.config.seekGroupedPermission) {
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
if (!this.hasCounterpartyPermissionRequestedHandlers()) {
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
if (currentRequest.type !== 'protocol') {
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
if (currentRequest.privileged) {
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
const [level] = currentRequest.protocolID;
|
|
1083
|
+
if (level !== 2) {
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
const originator = currentRequest.originator;
|
|
1087
|
+
const counterparty = currentRequest.counterparty;
|
|
1088
|
+
if (!counterparty || counterparty === 'self' || counterparty === 'anyone') {
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
if (!/^[0-9a-fA-F]{66}$/.test(counterparty)) {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
if (await this.hasPactEstablished(originator, counterparty)) {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
const { counterpartyPermissions } = await this.fetchManifestPermissions(originator);
|
|
1098
|
+
if (!((_a = counterpartyPermissions === null || counterpartyPermissions === void 0 ? void 0 : counterpartyPermissions.protocols) === null || _a === void 0 ? void 0 : _a.length)) {
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
const protocolsToRequest = [];
|
|
1102
|
+
for (const p of counterpartyPermissions.protocols) {
|
|
1103
|
+
const hasPerm = await this.hasProtocolPermission({
|
|
1104
|
+
originator,
|
|
1105
|
+
privileged: false,
|
|
1106
|
+
protocolID: p.protocolID,
|
|
1107
|
+
counterparty
|
|
1108
|
+
});
|
|
1109
|
+
if (!hasPerm) {
|
|
1110
|
+
protocolsToRequest.push(p);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (protocolsToRequest.length === 0) {
|
|
1114
|
+
this.markPactEstablished(originator, counterparty);
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
const permissionsToRequest = {
|
|
1118
|
+
description: counterpartyPermissions.description,
|
|
1119
|
+
protocols: protocolsToRequest
|
|
1120
|
+
};
|
|
1121
|
+
const key = `pact:${originator}:${counterparty}`;
|
|
1122
|
+
const existing = this.activeRequests.get(key);
|
|
1123
|
+
if (existing) {
|
|
1124
|
+
const existingRequest = existing.request;
|
|
1125
|
+
for (const p of permissionsToRequest.protocols) {
|
|
1126
|
+
if (!existingRequest.permissions.protocols.find(x => deepEqual(x, p))) {
|
|
1127
|
+
existingRequest.permissions.protocols.push(p);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
await new Promise((resolve, reject) => {
|
|
1131
|
+
existing.pending.push({ resolve, reject });
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
await new Promise(async (resolve, reject) => {
|
|
1136
|
+
this.activeRequests.set(key, {
|
|
1137
|
+
request: {
|
|
1138
|
+
originator,
|
|
1139
|
+
counterparty,
|
|
1140
|
+
permissions: permissionsToRequest,
|
|
1141
|
+
displayOriginator: currentRequest.displayOriginator
|
|
1142
|
+
},
|
|
1143
|
+
pending: [{ resolve, reject }]
|
|
1144
|
+
});
|
|
1145
|
+
await this.callEvent('onCounterpartyPermissionRequested', {
|
|
1146
|
+
requestID: key,
|
|
1147
|
+
originator,
|
|
1148
|
+
counterparty,
|
|
1149
|
+
permissions: permissionsToRequest
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
this.markPactEstablished(originator, counterparty);
|
|
1154
|
+
const satisfied = await this.hasProtocolPermission({
|
|
1155
|
+
originator,
|
|
1156
|
+
privileged: false,
|
|
1157
|
+
protocolID: currentRequest.protocolID,
|
|
1158
|
+
counterparty
|
|
1159
|
+
});
|
|
1160
|
+
return satisfied ? true : null;
|
|
1161
|
+
}
|
|
1162
|
+
async maybeRequestPeerGroupedLevel2ProtocolPermissions(currentRequest) {
|
|
1163
|
+
var _a, _b;
|
|
1164
|
+
if (!this.config.seekGroupedPermission) {
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
if (!this.hasGroupedPermissionRequestedHandlers()) {
|
|
1168
|
+
return null;
|
|
1169
|
+
}
|
|
1170
|
+
if (currentRequest.type !== 'protocol') {
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
const [level] = currentRequest.protocolID;
|
|
1174
|
+
if (level !== 2) {
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
const originator = currentRequest.originator;
|
|
1178
|
+
const privileged = (_a = currentRequest.privileged) !== null && _a !== void 0 ? _a : false;
|
|
1179
|
+
const counterparty = (_b = currentRequest.counterparty) !== null && _b !== void 0 ? _b : 'self';
|
|
1180
|
+
const groupPermissions = await this.fetchManifestGroupPermissions(originator);
|
|
1181
|
+
if (!groupPermissions) {
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
const normalizeManifestCounterparty = (cp) => {
|
|
1185
|
+
if (cp === '')
|
|
1186
|
+
return counterparty;
|
|
1187
|
+
return cp !== null && cp !== void 0 ? cp : 'self';
|
|
1188
|
+
};
|
|
1189
|
+
const manifestLevel2ForThisPeer = (groupPermissions.protocolPermissions || [])
|
|
1190
|
+
.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; })
|
|
1191
|
+
.map(p => ({
|
|
1192
|
+
protocolID: p.protocolID,
|
|
1193
|
+
counterparty: normalizeManifestCounterparty(p.counterparty),
|
|
1194
|
+
description: p.description
|
|
1195
|
+
}))
|
|
1196
|
+
.filter(p => p.counterparty === counterparty);
|
|
1197
|
+
const isCurrentRequestInManifest = manifestLevel2ForThisPeer.some(p => deepEqual(p.protocolID, currentRequest.protocolID));
|
|
1198
|
+
if (!isCurrentRequestInManifest) {
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
const permissionsToRequest = {
|
|
1202
|
+
protocolPermissions: []
|
|
1203
|
+
};
|
|
1204
|
+
for (const p of manifestLevel2ForThisPeer) {
|
|
1205
|
+
const hasPerm = await this.hasProtocolPermission({
|
|
1206
|
+
originator,
|
|
1207
|
+
privileged,
|
|
1208
|
+
protocolID: p.protocolID,
|
|
1209
|
+
counterparty: p.counterparty
|
|
1210
|
+
});
|
|
1211
|
+
if (!hasPerm) {
|
|
1212
|
+
permissionsToRequest.protocolPermissions.push({
|
|
1213
|
+
protocolID: p.protocolID,
|
|
1214
|
+
counterparty: p.counterparty,
|
|
1215
|
+
description: p.description
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
if (!this.hasAnyPermissionsToRequest(permissionsToRequest)) {
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
const key = `group-peer:${originator}:${privileged}:${counterparty}`;
|
|
1223
|
+
const existing = this.activeRequests.get(key);
|
|
1224
|
+
if (existing) {
|
|
1225
|
+
const existingRequest = existing.request;
|
|
1226
|
+
if (!existingRequest.permissions.protocolPermissions) {
|
|
1227
|
+
existingRequest.permissions.protocolPermissions = [];
|
|
1228
|
+
}
|
|
1229
|
+
for (const p of permissionsToRequest.protocolPermissions || []) {
|
|
1230
|
+
if (!existingRequest.permissions.protocolPermissions.find(x => deepEqual(x, p))) {
|
|
1231
|
+
existingRequest.permissions.protocolPermissions.push(p);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
await new Promise((resolve, reject) => {
|
|
1235
|
+
existing.pending.push({ resolve, reject });
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
await new Promise(async (resolve, reject) => {
|
|
1240
|
+
const permissions = permissionsToRequest;
|
|
1241
|
+
this.activeRequests.set(key, {
|
|
1242
|
+
request: {
|
|
1243
|
+
originator,
|
|
1244
|
+
permissions,
|
|
1245
|
+
displayOriginator: currentRequest.displayOriginator
|
|
1246
|
+
},
|
|
1247
|
+
pending: [{ resolve, reject }]
|
|
1248
|
+
});
|
|
1249
|
+
await this.callEvent('onGroupedPermissionRequested', {
|
|
1250
|
+
requestID: key,
|
|
1251
|
+
originator,
|
|
1252
|
+
permissions
|
|
1253
|
+
});
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
const satisfied = await this.checkSpecificPermissionAfterGroupFlow(currentRequest);
|
|
1257
|
+
return satisfied ? true : null;
|
|
1258
|
+
}
|
|
1259
|
+
async checkSpecificPermissionAfterGroupFlow(request) {
|
|
1260
|
+
var _a, _b, _c;
|
|
1261
|
+
switch (request.type) {
|
|
1262
|
+
case 'protocol':
|
|
1263
|
+
return await this.hasProtocolPermission({
|
|
1264
|
+
originator: request.originator,
|
|
1265
|
+
privileged: (_a = request.privileged) !== null && _a !== void 0 ? _a : false,
|
|
1266
|
+
protocolID: request.protocolID,
|
|
1267
|
+
counterparty: (_b = request.counterparty) !== null && _b !== void 0 ? _b : 'self'
|
|
1268
|
+
});
|
|
1269
|
+
case 'basket':
|
|
1270
|
+
return await this.hasBasketAccess({
|
|
1271
|
+
originator: request.originator,
|
|
1272
|
+
basket: request.basket
|
|
1273
|
+
});
|
|
1274
|
+
case 'certificate':
|
|
1275
|
+
return await this.hasCertificateAccess({
|
|
1276
|
+
originator: request.originator,
|
|
1277
|
+
privileged: (_c = request.privileged) !== null && _c !== void 0 ? _c : false,
|
|
1278
|
+
verifier: request.certificate.verifier,
|
|
1279
|
+
certType: request.certificate.certType,
|
|
1280
|
+
fields: request.certificate.fields
|
|
1281
|
+
});
|
|
1282
|
+
case 'spending':
|
|
1283
|
+
return await this.hasSpendingAuthorization({
|
|
1284
|
+
originator: request.originator,
|
|
1285
|
+
satoshis: request.spending.satoshis
|
|
1286
|
+
});
|
|
1287
|
+
default:
|
|
1288
|
+
return false;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
isRequestIncludedInGroupPermissions(request, groupPermissions) {
|
|
1292
|
+
var _a, _b, _c, _d;
|
|
1293
|
+
switch (request.type) {
|
|
1294
|
+
case 'protocol': {
|
|
1295
|
+
if (request.privileged)
|
|
1296
|
+
return false;
|
|
1297
|
+
const pid = request.protocolID;
|
|
1298
|
+
if (!pid)
|
|
1299
|
+
return false;
|
|
1300
|
+
const cp = (_a = request.counterparty) !== null && _a !== void 0 ? _a : 'self';
|
|
1301
|
+
return !!((_b = groupPermissions.protocolPermissions) === null || _b === void 0 ? void 0 : _b.some(p => {
|
|
1302
|
+
var _a;
|
|
1303
|
+
const manifestCp = p.counterparty === '' ? cp : ((_a = p.counterparty) !== null && _a !== void 0 ? _a : 'self');
|
|
1304
|
+
return deepEqual(p.protocolID, pid) && manifestCp === cp;
|
|
1305
|
+
}));
|
|
1306
|
+
}
|
|
1307
|
+
case 'basket': {
|
|
1308
|
+
const basket = request.basket;
|
|
1309
|
+
if (!basket)
|
|
1310
|
+
return false;
|
|
1311
|
+
return !!((_c = groupPermissions.basketAccess) === null || _c === void 0 ? void 0 : _c.some(b => b.basket === basket));
|
|
1312
|
+
}
|
|
1313
|
+
case 'certificate': {
|
|
1314
|
+
if (request.privileged)
|
|
1315
|
+
return false;
|
|
1316
|
+
const cert = request.certificate;
|
|
1317
|
+
if (!cert)
|
|
1318
|
+
return false;
|
|
1319
|
+
return !!((_d = groupPermissions.certificateAccess) === null || _d === void 0 ? void 0 : _d.some(c => {
|
|
1320
|
+
const fieldsA = new Set(c.fields || []);
|
|
1321
|
+
const fieldsB = new Set(cert.fields || []);
|
|
1322
|
+
if (fieldsA.size !== fieldsB.size)
|
|
1323
|
+
return false;
|
|
1324
|
+
for (const f of fieldsA)
|
|
1325
|
+
if (!fieldsB.has(f))
|
|
1326
|
+
return false;
|
|
1327
|
+
return c.type === cert.certType && c.verifierPublicKey === cert.verifier;
|
|
1328
|
+
}));
|
|
1329
|
+
}
|
|
1330
|
+
case 'spending':
|
|
1331
|
+
return !!groupPermissions.spendingAuthorization;
|
|
1332
|
+
default:
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
async maybeRequestGroupedPermissions(currentRequest) {
|
|
1337
|
+
if (!this.config.seekGroupedPermission) {
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
const originator = currentRequest.originator;
|
|
1341
|
+
const groupPermissions = await this.fetchManifestGroupPermissions(originator);
|
|
1342
|
+
if (!groupPermissions) {
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
if (!this.isRequestIncludedInGroupPermissions(currentRequest, groupPermissions)) {
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
const permissionsToRequest = await this.filterAlreadyGrantedPermissions(originator, groupPermissions);
|
|
1349
|
+
if (!this.hasAnyPermissionsToRequest(permissionsToRequest)) {
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
const key = `group:${originator}`;
|
|
1353
|
+
if (this.activeRequests.has(key)) {
|
|
1354
|
+
await new Promise((resolve, reject) => {
|
|
1355
|
+
this.activeRequests.get(key).pending.push({ resolve, reject });
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
else {
|
|
1359
|
+
await new Promise(async (resolve, reject) => {
|
|
1360
|
+
this.activeRequests.set(key, {
|
|
1361
|
+
request: {
|
|
1362
|
+
originator,
|
|
1363
|
+
permissions: permissionsToRequest,
|
|
1364
|
+
displayOriginator: currentRequest.displayOriginator
|
|
1365
|
+
},
|
|
1366
|
+
pending: [{ resolve, reject }]
|
|
1367
|
+
});
|
|
1368
|
+
await this.callEvent('onGroupedPermissionRequested', {
|
|
1369
|
+
requestID: key,
|
|
1370
|
+
originator,
|
|
1371
|
+
permissions: permissionsToRequest
|
|
1372
|
+
});
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
const satisfied = await this.checkSpecificPermissionAfterGroupFlow(currentRequest);
|
|
1376
|
+
return satisfied ? true : null;
|
|
1377
|
+
}
|
|
731
1378
|
/**
|
|
732
1379
|
* A central method that triggers the permission request flow.
|
|
733
1380
|
* - It checks if there's already an active request for the same key
|
|
@@ -736,13 +1383,25 @@ class WalletPermissionsManager {
|
|
|
736
1383
|
* and return a promise that resolves once permission is granted or rejects if denied.
|
|
737
1384
|
*/
|
|
738
1385
|
async requestPermissionFlow(r) {
|
|
739
|
-
var _a;
|
|
1386
|
+
var _a, _b, _c;
|
|
740
1387
|
const normalizedOriginator = this.normalizeOriginator(r.originator) || r.originator;
|
|
741
1388
|
const preparedRequest = {
|
|
742
1389
|
...r,
|
|
743
1390
|
originator: normalizedOriginator,
|
|
744
|
-
displayOriginator: (_a = r.displayOriginator) !== null && _a !== void 0 ? _a : r.originator
|
|
1391
|
+
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
1392
|
};
|
|
1393
|
+
const pactResult = await this.maybeRequestPact(preparedRequest);
|
|
1394
|
+
if (pactResult !== null) {
|
|
1395
|
+
return pactResult;
|
|
1396
|
+
}
|
|
1397
|
+
const peerGroupResult = await this.maybeRequestPeerGroupedLevel2ProtocolPermissions(preparedRequest);
|
|
1398
|
+
if (peerGroupResult !== null) {
|
|
1399
|
+
return peerGroupResult;
|
|
1400
|
+
}
|
|
1401
|
+
const groupResult = await this.maybeRequestGroupedPermissions(preparedRequest);
|
|
1402
|
+
if (groupResult !== null) {
|
|
1403
|
+
return groupResult;
|
|
1404
|
+
}
|
|
746
1405
|
const key = this.buildRequestKey(preparedRequest);
|
|
747
1406
|
// If there's already a queue for the same resource, we piggyback on it
|
|
748
1407
|
const existingQueue = this.activeRequests.get(key);
|
|
@@ -1196,10 +1855,128 @@ class WalletPermissionsManager {
|
|
|
1196
1855
|
}
|
|
1197
1856
|
],
|
|
1198
1857
|
options: {
|
|
1199
|
-
acceptDelayedBroadcast:
|
|
1858
|
+
acceptDelayedBroadcast: true
|
|
1200
1859
|
}
|
|
1201
1860
|
}, this.adminOriginator);
|
|
1202
1861
|
}
|
|
1862
|
+
async mapWithConcurrency(items, concurrency, fn) {
|
|
1863
|
+
if (!items.length)
|
|
1864
|
+
return [];
|
|
1865
|
+
const results = new Array(items.length);
|
|
1866
|
+
let i = 0;
|
|
1867
|
+
const worker = async () => {
|
|
1868
|
+
while (true) {
|
|
1869
|
+
const idx = i++;
|
|
1870
|
+
if (idx >= items.length)
|
|
1871
|
+
return;
|
|
1872
|
+
results[idx] = await fn(items[idx]);
|
|
1873
|
+
}
|
|
1874
|
+
};
|
|
1875
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
1876
|
+
return results;
|
|
1877
|
+
}
|
|
1878
|
+
async runBestEffortBatches(items, chunkSize, runChunk) {
|
|
1879
|
+
if (!items.length)
|
|
1880
|
+
return [];
|
|
1881
|
+
const out = [];
|
|
1882
|
+
for (let i = 0; i < items.length; i += chunkSize) {
|
|
1883
|
+
const chunk = items.slice(i, i + chunkSize);
|
|
1884
|
+
out.push(...(await this.runBestEffortChunk(chunk, runChunk)));
|
|
1885
|
+
}
|
|
1886
|
+
return out;
|
|
1887
|
+
}
|
|
1888
|
+
async runBestEffortChunk(chunk, runChunk) {
|
|
1889
|
+
try {
|
|
1890
|
+
return await runChunk(chunk);
|
|
1891
|
+
}
|
|
1892
|
+
catch (e) {
|
|
1893
|
+
if (chunk.length <= 1) {
|
|
1894
|
+
console.error('Permission batch failed:', e);
|
|
1895
|
+
return [];
|
|
1896
|
+
}
|
|
1897
|
+
const mid = Math.ceil(chunk.length / 2);
|
|
1898
|
+
const left = await this.runBestEffortChunk(chunk.slice(0, mid), runChunk);
|
|
1899
|
+
const right = await this.runBestEffortChunk(chunk.slice(mid), runChunk);
|
|
1900
|
+
return [...left, ...right];
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
async buildPermissionOutput(r, expiry, amount) {
|
|
1904
|
+
const normalizedOriginator = this.normalizeOriginator(r.originator) || r.originator;
|
|
1905
|
+
r.originator = normalizedOriginator;
|
|
1906
|
+
const basketName = BASKET_MAP[r.type];
|
|
1907
|
+
if (!basketName) {
|
|
1908
|
+
throw new Error(`Unsupported permission type: ${r.type}`);
|
|
1909
|
+
}
|
|
1910
|
+
const fields = await this.buildPushdropFields(r, expiry, amount);
|
|
1911
|
+
const script = await new sdk_1.PushDrop(this.underlying).lock(fields, WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL, '1', 'self', true, true);
|
|
1912
|
+
const tags = this.buildTagsForRequest(r);
|
|
1913
|
+
return {
|
|
1914
|
+
request: r,
|
|
1915
|
+
output: {
|
|
1916
|
+
lockingScript: script.toHex(),
|
|
1917
|
+
satoshis: 1,
|
|
1918
|
+
outputDescription: `${r.type} permission token`,
|
|
1919
|
+
basket: basketName,
|
|
1920
|
+
tags
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
async createPermissionTokensBestEffort(items) {
|
|
1925
|
+
const CHUNK = 25;
|
|
1926
|
+
return this.runBestEffortBatches(items, CHUNK, async (chunk) => {
|
|
1927
|
+
const built = await this.mapWithConcurrency(chunk, 8, c => this.buildPermissionOutput(c.request, c.expiry, c.amount));
|
|
1928
|
+
await this.createAction({
|
|
1929
|
+
description: `Grant ${built.length} permissions`,
|
|
1930
|
+
outputs: built.map(b => b.output),
|
|
1931
|
+
options: { acceptDelayedBroadcast: true }
|
|
1932
|
+
}, this.adminOriginator);
|
|
1933
|
+
return built.map(b => b.request);
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
async renewPermissionTokensBestEffort(items) {
|
|
1937
|
+
const CHUNK = 15;
|
|
1938
|
+
return this.runBestEffortBatches(items, CHUNK, async (chunk) => {
|
|
1939
|
+
const built = await this.mapWithConcurrency(chunk, 8, c => this.buildPermissionOutput(c.request, c.expiry, c.amount));
|
|
1940
|
+
const inputBeef = new sdk_1.Beef();
|
|
1941
|
+
for (const c of chunk) {
|
|
1942
|
+
inputBeef.mergeBeef(sdk_1.Beef.fromBinary(c.oldToken.tx));
|
|
1943
|
+
}
|
|
1944
|
+
const { signableTransaction } = await this.createAction({
|
|
1945
|
+
description: `Renew ${chunk.length} permissions`,
|
|
1946
|
+
inputBEEF: inputBeef.toBinary(),
|
|
1947
|
+
inputs: chunk.map((c, i) => ({
|
|
1948
|
+
outpoint: `${c.oldToken.txid}.${c.oldToken.outputIndex}`,
|
|
1949
|
+
unlockingScriptLength: 73,
|
|
1950
|
+
inputDescription: `Consume old permission token #${i + 1}`
|
|
1951
|
+
})),
|
|
1952
|
+
outputs: built.map(b => b.output),
|
|
1953
|
+
options: {
|
|
1954
|
+
acceptDelayedBroadcast: true,
|
|
1955
|
+
randomizeOutputs: false,
|
|
1956
|
+
signAndProcess: false
|
|
1957
|
+
}
|
|
1958
|
+
}, this.adminOriginator);
|
|
1959
|
+
if (!(signableTransaction === null || signableTransaction === void 0 ? void 0 : signableTransaction.reference) || !signableTransaction.tx) {
|
|
1960
|
+
throw new Error('Failed to create signable transaction');
|
|
1961
|
+
}
|
|
1962
|
+
const partialTx = sdk_1.Transaction.fromAtomicBEEF(signableTransaction.tx);
|
|
1963
|
+
const pushdrop = new sdk_1.PushDrop(this.underlying);
|
|
1964
|
+
const spends = {};
|
|
1965
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
1966
|
+
const token = chunk[i].oldToken;
|
|
1967
|
+
const unlocker = pushdrop.unlock(WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL, '1', 'self', 'all', false, 1, sdk_1.LockingScript.fromHex(token.outputScript));
|
|
1968
|
+
const unlockingScript = await unlocker.sign(partialTx, i);
|
|
1969
|
+
spends[i] = { unlockingScript: unlockingScript.toHex() };
|
|
1970
|
+
}
|
|
1971
|
+
const { txid } = await this.underlying.signAction({
|
|
1972
|
+
reference: signableTransaction.reference,
|
|
1973
|
+
spends
|
|
1974
|
+
});
|
|
1975
|
+
if (!txid)
|
|
1976
|
+
throw new Error('Failed to finalize renewal transaction');
|
|
1977
|
+
return built.map(b => b.request);
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1203
1980
|
async coalescePermissionTokens(oldTokens, newScript, opts) {
|
|
1204
1981
|
var _a;
|
|
1205
1982
|
if (!(oldTokens === null || oldTokens === void 0 ? void 0 : oldTokens.length))
|
|
@@ -1230,7 +2007,7 @@ class WalletPermissionsManager {
|
|
|
1230
2007
|
}
|
|
1231
2008
|
],
|
|
1232
2009
|
options: {
|
|
1233
|
-
acceptDelayedBroadcast:
|
|
2010
|
+
acceptDelayedBroadcast: true,
|
|
1234
2011
|
randomizeOutputs: false,
|
|
1235
2012
|
signAndProcess: false
|
|
1236
2013
|
}
|
|
@@ -1309,7 +2086,7 @@ class WalletPermissionsManager {
|
|
|
1309
2086
|
}
|
|
1310
2087
|
],
|
|
1311
2088
|
options: {
|
|
1312
|
-
acceptDelayedBroadcast:
|
|
2089
|
+
acceptDelayedBroadcast: true
|
|
1313
2090
|
}
|
|
1314
2091
|
}, this.adminOriginator);
|
|
1315
2092
|
const tx = sdk_1.Transaction.fromBEEF(signableTransaction.tx);
|
|
@@ -1445,7 +2222,7 @@ class WalletPermissionsManager {
|
|
|
1445
2222
|
tags,
|
|
1446
2223
|
tagQueryMode: 'all',
|
|
1447
2224
|
include: 'entire transactions',
|
|
1448
|
-
limit:
|
|
2225
|
+
limit: 10000
|
|
1449
2226
|
}, this.adminOriginator);
|
|
1450
2227
|
for (const out of result.outputs) {
|
|
1451
2228
|
if (seen.has(out.outpoint))
|
|
@@ -1750,16 +2527,60 @@ class WalletPermissionsManager {
|
|
|
1750
2527
|
}
|
|
1751
2528
|
],
|
|
1752
2529
|
options: {
|
|
1753
|
-
acceptDelayedBroadcast:
|
|
2530
|
+
acceptDelayedBroadcast: true
|
|
1754
2531
|
}
|
|
1755
2532
|
}, this.adminOriginator);
|
|
1756
2533
|
const tx = sdk_1.Transaction.fromBEEF(signableTransaction.tx);
|
|
2534
|
+
const normalizeTxid = (txid) => (txid !== null && txid !== void 0 ? txid : '').toLowerCase();
|
|
2535
|
+
const reverseHexTxid = (txid) => {
|
|
2536
|
+
const hex = normalizeTxid(txid);
|
|
2537
|
+
if (!/^[0-9a-f]{64}$/.test(hex))
|
|
2538
|
+
return hex;
|
|
2539
|
+
const bytes = hex.match(/../g);
|
|
2540
|
+
return bytes ? bytes.reverse().join('') : hex;
|
|
2541
|
+
};
|
|
2542
|
+
const matchesOutpointString = (outpoint) => {
|
|
2543
|
+
const dot = outpoint.lastIndexOf('.');
|
|
2544
|
+
const colon = outpoint.lastIndexOf(':');
|
|
2545
|
+
const sep = dot > colon ? dot : colon;
|
|
2546
|
+
if (sep === -1)
|
|
2547
|
+
return false;
|
|
2548
|
+
const txidPart = outpoint.slice(0, sep);
|
|
2549
|
+
const indexPart = outpoint.slice(sep + 1);
|
|
2550
|
+
const vout = Number(indexPart);
|
|
2551
|
+
if (!Number.isFinite(vout))
|
|
2552
|
+
return false;
|
|
2553
|
+
return normalizeTxid(txidPart) === normalizeTxid(oldToken.txid) && vout === oldToken.outputIndex;
|
|
2554
|
+
};
|
|
2555
|
+
let permInputIndex = tx.inputs.findIndex((input) => {
|
|
2556
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
2557
|
+
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;
|
|
2558
|
+
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;
|
|
2559
|
+
if (typeof txidCandidate === 'string' && typeof voutCandidate === 'number') {
|
|
2560
|
+
const cand = normalizeTxid(txidCandidate);
|
|
2561
|
+
const target = normalizeTxid(oldToken.txid);
|
|
2562
|
+
if (cand === target && voutCandidate === oldToken.outputIndex)
|
|
2563
|
+
return true;
|
|
2564
|
+
if (cand === reverseHexTxid(oldToken.txid) && voutCandidate === oldToken.outputIndex)
|
|
2565
|
+
return true;
|
|
2566
|
+
}
|
|
2567
|
+
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;
|
|
2568
|
+
if (typeof outpointCandidate === 'string' && matchesOutpointString(outpointCandidate))
|
|
2569
|
+
return true;
|
|
2570
|
+
return false;
|
|
2571
|
+
});
|
|
2572
|
+
if (permInputIndex === -1 && tx.inputs.length === 1) {
|
|
2573
|
+
permInputIndex = 0;
|
|
2574
|
+
}
|
|
2575
|
+
if (permInputIndex === -1) {
|
|
2576
|
+
throw new Error('Unable to locate permission token input for revocation.');
|
|
2577
|
+
}
|
|
1757
2578
|
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,
|
|
2579
|
+
const unlockingScript = await unlocker.sign(tx, permInputIndex);
|
|
1759
2580
|
await this.underlying.signAction({
|
|
1760
2581
|
reference: signableTransaction.reference,
|
|
1761
2582
|
spends: {
|
|
1762
|
-
|
|
2583
|
+
[permInputIndex]: {
|
|
1763
2584
|
unlockingScript: unlockingScript.toHex()
|
|
1764
2585
|
}
|
|
1765
2586
|
}
|
|
@@ -1769,22 +2590,16 @@ class WalletPermissionsManager {
|
|
|
1769
2590
|
* 7) BRC-100 WALLET INTERFACE FORWARDING WITH PERMISSION CHECKS
|
|
1770
2591
|
* --------------------------------------------------------------------- */
|
|
1771
2592
|
async createAction(args, originator) {
|
|
1772
|
-
|
|
1773
|
-
// 1) Identify unique P-modules involved (one per schemeID)
|
|
2593
|
+
// 1) Identify unique P-modules involved (one per schemeID) from both baskets and labels
|
|
1774
2594
|
const pModulesByScheme = new Map();
|
|
1775
2595
|
const nonPBaskets = [];
|
|
2596
|
+
// Check baskets for p modules
|
|
1776
2597
|
if (args.outputs) {
|
|
1777
2598
|
for (const out of args.outputs) {
|
|
1778
2599
|
if (out.basket) {
|
|
1779
2600
|
if (out.basket.startsWith('p ')) {
|
|
1780
2601
|
const schemeID = out.basket.split(' ')[1];
|
|
1781
|
-
|
|
1782
|
-
const module = (_a = this.config.permissionModules) === null || _a === void 0 ? void 0 : _a[schemeID];
|
|
1783
|
-
if (!module) {
|
|
1784
|
-
throw new Error(`Unsupported P-basket scheme: p ${schemeID}`);
|
|
1785
|
-
}
|
|
1786
|
-
pModulesByScheme.set(schemeID, module);
|
|
1787
|
-
}
|
|
2602
|
+
this.addPModuleByScheme(schemeID, 'basket', pModulesByScheme);
|
|
1788
2603
|
}
|
|
1789
2604
|
else {
|
|
1790
2605
|
// Track non-P baskets for normal permission checks
|
|
@@ -1793,6 +2608,8 @@ class WalletPermissionsManager {
|
|
|
1793
2608
|
}
|
|
1794
2609
|
}
|
|
1795
2610
|
}
|
|
2611
|
+
// Check labels for p modules
|
|
2612
|
+
const nonPLabels = this.splitLabelsByPermissionModule(args.labels, pModulesByScheme);
|
|
1796
2613
|
// 2) Check permissions for non-P baskets
|
|
1797
2614
|
for (const basket of nonPBaskets) {
|
|
1798
2615
|
await this.ensureBasketAccess({
|
|
@@ -1802,15 +2619,14 @@ class WalletPermissionsManager {
|
|
|
1802
2619
|
usageType: 'insertion'
|
|
1803
2620
|
});
|
|
1804
2621
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
}
|
|
2622
|
+
// 3) Check permissions for non-P labels
|
|
2623
|
+
for (const lbl of nonPLabels) {
|
|
2624
|
+
await this.ensureLabelAccess({
|
|
2625
|
+
originator: originator,
|
|
2626
|
+
label: lbl,
|
|
2627
|
+
reason: args.description,
|
|
2628
|
+
usageType: 'apply'
|
|
2629
|
+
});
|
|
1814
2630
|
}
|
|
1815
2631
|
/**
|
|
1816
2632
|
* 4) Force signAndProcess=false unless the originator is admin and explicitly sets it to true.
|
|
@@ -1988,74 +2804,132 @@ class WalletPermissionsManager {
|
|
|
1988
2804
|
}
|
|
1989
2805
|
async listActions(...args) {
|
|
1990
2806
|
const [requestArgs, originator] = args;
|
|
1991
|
-
//
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2807
|
+
// 1) Identify unique P-modules involved (one per schemeID, preserving label order)
|
|
2808
|
+
const pModulesByScheme = new Map();
|
|
2809
|
+
const nonPLabels = this.splitLabelsByPermissionModule(requestArgs.labels, pModulesByScheme);
|
|
2810
|
+
// 2) Check permissions for non-P labels
|
|
2811
|
+
for (const lbl of nonPLabels) {
|
|
2812
|
+
await this.ensureLabelAccess({
|
|
2813
|
+
originator: originator,
|
|
2814
|
+
label: lbl,
|
|
2815
|
+
reason: 'listActions',
|
|
2816
|
+
usageType: 'list'
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
// 3) Call underlying wallet, with P-module transformations if needed
|
|
2820
|
+
let results;
|
|
2821
|
+
if (pModulesByScheme.size > 0) {
|
|
2822
|
+
// P-modules are involved - chain transformations
|
|
2823
|
+
const pModules = Array.from(pModulesByScheme.values());
|
|
2824
|
+
// Chain onRequest calls through all modules in order
|
|
2825
|
+
let transformedArgs = requestArgs;
|
|
2826
|
+
for (const module of pModules) {
|
|
2827
|
+
const transformed = await module.onRequest({
|
|
2828
|
+
method: 'listActions',
|
|
2829
|
+
args: transformedArgs,
|
|
2830
|
+
originator: originator
|
|
1999
2831
|
});
|
|
2832
|
+
transformedArgs = transformed.args;
|
|
2000
2833
|
}
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
}
|
|
2009
|
-
if (results.actions[i].inputs) {
|
|
2010
|
-
for (let j = 0; j < results.actions[i].inputs.length; j++) {
|
|
2011
|
-
if (results.actions[i].inputs[j].inputDescription) {
|
|
2012
|
-
results.actions[i].inputs[j].inputDescription = await this.maybeDecryptMetadata(results.actions[i].inputs[j].inputDescription);
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
if (results.actions[i].outputs) {
|
|
2017
|
-
for (let j = 0; j < results.actions[i].outputs.length; j++) {
|
|
2018
|
-
if (results.actions[i].outputs[j].outputDescription) {
|
|
2019
|
-
results.actions[i].outputs[j].outputDescription = await this.maybeDecryptMetadata(results.actions[i].outputs[j].outputDescription);
|
|
2020
|
-
}
|
|
2021
|
-
if (results.actions[i].outputs[j].customInstructions) {
|
|
2022
|
-
results.actions[i].outputs[j].customInstructions = await this.maybeDecryptMetadata(results.actions[i].outputs[j].customInstructions);
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2834
|
+
// Call underlying wallet with transformed args
|
|
2835
|
+
results = await this.underlying.listActions(transformedArgs, originator);
|
|
2836
|
+
// Chain onResponse calls in reverse order
|
|
2837
|
+
for (let i = pModules.length - 1; i >= 0; i--) {
|
|
2838
|
+
results = await pModules[i].onResponse(results, {
|
|
2839
|
+
method: 'listActions',
|
|
2840
|
+
originator: originator
|
|
2841
|
+
});
|
|
2026
2842
|
}
|
|
2027
2843
|
}
|
|
2028
|
-
|
|
2844
|
+
else {
|
|
2845
|
+
// No P-modules - call underlying wallet directly
|
|
2846
|
+
results = await this.underlying.listActions(...args);
|
|
2847
|
+
}
|
|
2848
|
+
// 4) Transparently decrypt transaction metadata, if configured to do so.
|
|
2849
|
+
return await this.decryptListActionsMetadata(results);
|
|
2029
2850
|
}
|
|
2030
2851
|
async internalizeAction(...args) {
|
|
2852
|
+
var _a;
|
|
2031
2853
|
const [requestArgs, originator] = args;
|
|
2032
|
-
//
|
|
2854
|
+
// 1) Identify unique P-modules involved (one per schemeID) from both baskets and labels
|
|
2855
|
+
const pModulesByScheme = new Map();
|
|
2856
|
+
const nonPBaskets = [];
|
|
2857
|
+
// Check baskets for p modules
|
|
2033
2858
|
for (const outIndex in requestArgs.outputs) {
|
|
2034
2859
|
const out = requestArgs.outputs[outIndex];
|
|
2035
2860
|
if (out.protocol === 'basket insertion') {
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
transformedArgs.outputs[outIndex].insertionRemittance.customInstructions =
|
|
2041
|
-
await this.maybeEncryptMetadata(out.insertionRemittance.customInstructions);
|
|
2042
|
-
}
|
|
2043
|
-
return await this.underlying.internalizeAction(transformedArgs, originator);
|
|
2044
|
-
});
|
|
2045
|
-
if (pModuleResult !== null) {
|
|
2046
|
-
return pModuleResult;
|
|
2861
|
+
const basket = out.insertionRemittance.basket;
|
|
2862
|
+
if (basket.startsWith('p ')) {
|
|
2863
|
+
const schemeID = basket.split(' ')[1];
|
|
2864
|
+
this.addPModuleByScheme(schemeID, 'basket', pModulesByScheme);
|
|
2047
2865
|
}
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2866
|
+
else {
|
|
2867
|
+
// Track non-P baskets for normal permission checks
|
|
2868
|
+
nonPBaskets.push({
|
|
2869
|
+
outIndex,
|
|
2870
|
+
basket,
|
|
2871
|
+
customInstructions: out.insertionRemittance.customInstructions
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
// Check labels for p modules
|
|
2877
|
+
const nonPLabels = this.splitLabelsByPermissionModule(requestArgs.labels, pModulesByScheme);
|
|
2878
|
+
// 2) Check permissions for non-P baskets
|
|
2879
|
+
for (const { outIndex, basket, customInstructions } of nonPBaskets) {
|
|
2880
|
+
await this.ensureBasketAccess({
|
|
2881
|
+
originator: originator,
|
|
2882
|
+
basket,
|
|
2883
|
+
reason: requestArgs.description,
|
|
2884
|
+
usageType: 'insertion'
|
|
2885
|
+
});
|
|
2886
|
+
if (customInstructions) {
|
|
2887
|
+
requestArgs.outputs[outIndex].insertionRemittance.customInstructions =
|
|
2888
|
+
await this.maybeEncryptMetadata(customInstructions);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
// 3) Check permissions for non-P labels
|
|
2892
|
+
for (const lbl of nonPLabels) {
|
|
2893
|
+
await this.ensureLabelAccess({
|
|
2894
|
+
originator: originator,
|
|
2895
|
+
label: lbl,
|
|
2896
|
+
reason: requestArgs.description,
|
|
2897
|
+
usageType: 'apply'
|
|
2898
|
+
});
|
|
2899
|
+
}
|
|
2900
|
+
// 4) Call underlying wallet, with P-module transformations if needed
|
|
2901
|
+
if (pModulesByScheme.size > 0) {
|
|
2902
|
+
// P-modules are involved - chain transformations
|
|
2903
|
+
const pModules = Array.from(pModulesByScheme.values());
|
|
2904
|
+
// Chain onRequest calls through all modules in order
|
|
2905
|
+
let transformedArgs = requestArgs;
|
|
2906
|
+
for (const module of pModules) {
|
|
2907
|
+
const transformed = await module.onRequest({
|
|
2908
|
+
method: 'internalizeAction',
|
|
2909
|
+
args: transformedArgs,
|
|
2910
|
+
originator: originator
|
|
2053
2911
|
});
|
|
2054
|
-
|
|
2055
|
-
|
|
2912
|
+
transformedArgs = transformed.args;
|
|
2913
|
+
}
|
|
2914
|
+
// Encrypt custom instructions for p basket outputs
|
|
2915
|
+
for (const outIndex in transformedArgs.outputs) {
|
|
2916
|
+
const out = transformedArgs.outputs[outIndex];
|
|
2917
|
+
if (out.protocol === 'basket insertion' && ((_a = out.insertionRemittance) === null || _a === void 0 ? void 0 : _a.customInstructions)) {
|
|
2918
|
+
out.insertionRemittance.customInstructions = await this.maybeEncryptMetadata(out.insertionRemittance.customInstructions);
|
|
2056
2919
|
}
|
|
2057
2920
|
}
|
|
2921
|
+
// Call underlying wallet with transformed args
|
|
2922
|
+
let results = await this.underlying.internalizeAction(transformedArgs, originator);
|
|
2923
|
+
// Chain onResponse calls in reverse order
|
|
2924
|
+
for (let i = pModules.length - 1; i >= 0; i--) {
|
|
2925
|
+
results = await pModules[i].onResponse(results, {
|
|
2926
|
+
method: 'internalizeAction',
|
|
2927
|
+
originator: originator
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
return results;
|
|
2058
2931
|
}
|
|
2932
|
+
// No P-modules - call underlying wallet directly
|
|
2059
2933
|
return this.underlying.internalizeAction(...args);
|
|
2060
2934
|
}
|
|
2061
2935
|
async listOutputs(...args) {
|
|
@@ -2358,80 +3232,17 @@ class WalletPermissionsManager {
|
|
|
2358
3232
|
return this.underlying.isAuthenticated(...args);
|
|
2359
3233
|
}
|
|
2360
3234
|
async waitForAuthentication(...args) {
|
|
2361
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
2362
3235
|
let [_, originator] = args;
|
|
2363
3236
|
if (this.config.seekGroupedPermission && originator) {
|
|
2364
3237
|
const { normalized: normalizedOriginator } = this.prepareOriginator(originator);
|
|
2365
3238
|
originator = normalizedOriginator;
|
|
2366
3239
|
// 1. Fetch manifest.json from the originator
|
|
2367
|
-
|
|
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
|
-
}
|
|
3240
|
+
const groupPermissions = await this.fetchManifestGroupPermissions(originator);
|
|
2381
3241
|
if (groupPermissions) {
|
|
2382
3242
|
// 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
|
-
}
|
|
3243
|
+
const permissionsToRequest = await this.filterAlreadyGrantedPermissions(originator, groupPermissions);
|
|
2429
3244
|
// 3. If any permissions are left to request, start the flow
|
|
2430
|
-
|
|
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) {
|
|
3245
|
+
if (this.hasAnyPermissionsToRequest(permissionsToRequest)) {
|
|
2435
3246
|
const key = `group:${originator}`;
|
|
2436
3247
|
if (this.activeRequests.has(key)) {
|
|
2437
3248
|
// Another call is already waiting, piggyback on it
|
|
@@ -2503,6 +3314,7 @@ class WalletPermissionsManager {
|
|
|
2503
3314
|
* Checks if the given label is admin-reserved per BRC-100 rules:
|
|
2504
3315
|
*
|
|
2505
3316
|
* - Must not start with `admin` (admin-reserved)
|
|
3317
|
+
* - Must not start with `p ` (permissioned labels requiring a permission module)
|
|
2506
3318
|
*
|
|
2507
3319
|
* If it violates these rules and the caller is not admin, we consider it "admin-only."
|
|
2508
3320
|
*/
|
|
@@ -2510,6 +3322,9 @@ class WalletPermissionsManager {
|
|
|
2510
3322
|
if (label.startsWith('admin')) {
|
|
2511
3323
|
return true;
|
|
2512
3324
|
}
|
|
3325
|
+
if (label.startsWith('p ')) {
|
|
3326
|
+
return true;
|
|
3327
|
+
}
|
|
2513
3328
|
return false;
|
|
2514
3329
|
}
|
|
2515
3330
|
/**
|
|
@@ -2598,6 +3413,21 @@ class WalletPermissionsManager {
|
|
|
2598
3413
|
return trimmed.toLowerCase();
|
|
2599
3414
|
}
|
|
2600
3415
|
}
|
|
3416
|
+
isWhitelistedCounterpartyProtocol(counterparty, protocolID) {
|
|
3417
|
+
var _a;
|
|
3418
|
+
const whitelist = this.config.whitelistedCounterparties;
|
|
3419
|
+
if (!whitelist)
|
|
3420
|
+
return false;
|
|
3421
|
+
if (!counterparty || counterparty === 'self' || counterparty === 'anyone')
|
|
3422
|
+
return false;
|
|
3423
|
+
const protocols = whitelist[counterparty] || whitelist[counterparty.toLowerCase()] || whitelist[counterparty.toUpperCase()];
|
|
3424
|
+
if (!protocols || protocols.length === 0)
|
|
3425
|
+
return false;
|
|
3426
|
+
const protoName = ((_a = protocolID === null || protocolID === void 0 ? void 0 : protocolID[1]) !== null && _a !== void 0 ? _a : '').toString().toLowerCase();
|
|
3427
|
+
if (!protoName)
|
|
3428
|
+
return false;
|
|
3429
|
+
return protocols.some(p => (p !== null && p !== void 0 ? p : '').toString().toLowerCase() === protoName);
|
|
3430
|
+
}
|
|
2601
3431
|
/**
|
|
2602
3432
|
* Produces a normalized originator value along with the set of legacy
|
|
2603
3433
|
* representations that should be considered when searching for existing
|
|
@@ -2646,6 +3476,7 @@ class WalletPermissionsManager {
|
|
|
2646
3476
|
}
|
|
2647
3477
|
}
|
|
2648
3478
|
exports.WalletPermissionsManager = WalletPermissionsManager;
|
|
3479
|
+
WalletPermissionsManager.MANIFEST_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
2649
3480
|
/** How long a cached permission remains valid (5 minutes). */
|
|
2650
3481
|
WalletPermissionsManager.CACHE_TTL_MS = 5 * 60 * 1000;
|
|
2651
3482
|
/** Window during which freshly granted permissions are auto-allowed (except spending). */
|