Skip to content

Commit 2bd5396

Browse files
committed
Add delegation support to web wallet
1 parent 252f035 commit 2bd5396

2 files changed

Lines changed: 220 additions & 1 deletion

File tree

packages/web/src/index.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,24 @@ import {createCredentialProvider} from '@docknetwork/wallet-sdk-core/src/credent
2828
import {createDIDProvider} from '@docknetwork/wallet-sdk-core/src/did-provider';
2929
import {createMessageProvider} from '@docknetwork/wallet-sdk-core/src/message-provider';
3030
import {createVerificationController} from '@docknetwork/wallet-sdk-core/src/verification-controller';
31+
import {
32+
createDelegationOffer,
33+
createOOBInvitation,
34+
acceptDelegationOffer,
35+
handleMessage,
36+
decodeMessage,
37+
} from '@docknetwork/wallet-sdk-core/src/delegation/delegation-offer';
38+
import {
39+
issueCredential,
40+
delegateCredential,
41+
} from '@docknetwork/wallet-sdk-core/src/delegation/delegation-issuance';
42+
import {getDelegationDetails} from '@docknetwork/wallet-sdk-core/src/delegation/delegation-policy';
43+
import {getDelegationChain} from '@docknetwork/wallet-sdk-core/src/delegation/delegation-chain';
44+
import {
45+
revokeDelegatableCredential,
46+
unrevokeDelegatableCredential,
47+
isDelegatableCredentialRevoked,
48+
} from '@docknetwork/wallet-sdk-core/src/delegation/delegation-revocation';
3149
import {blockchainService} from '@docknetwork/wallet-sdk-wasm/src/services/blockchain';
3250
import {
3351
checkPasskeySupport,
@@ -637,6 +655,78 @@ async function initialize({
637655
},
638656
};
639657
},
658+
659+
/**
660+
* Routes incoming DIDComm delegation messages (offers, credential
661+
* requests, issuance, acks) to the delegation handlers automatically.
662+
*
663+
* Without this, delegation messages must be dispatched manually via
664+
* `handleMessage`. Call once after initialization to participate in
665+
* delegation flows as issuer and/or holder. Listen for the
666+
* `delegationOfferReceived` and `delegatedCredentialReceived` events on
667+
* `wallet.eventManager` to react in the UI.
668+
*
669+
* @returns {Function} Unsubscribe function that stops routing.
670+
*
671+
* @example
672+
* const stop = wallet.enableDelegation();
673+
* wallet.wallet.eventManager.addListener('delegationOfferReceived', offer => {
674+
* // prompt the user, then accept
675+
* wallet.acceptDelegationOffer(offer);
676+
* });
677+
*/
678+
enableDelegation: () =>
679+
messageProvider.addMessageListener(message =>
680+
handleMessage(message, {wallet, messageProvider}),
681+
),
682+
683+
/**
684+
* Creates a delegation offer for a role and persists it on the wallet.
685+
* Pass the returned offer to `createOOBInvitation` to produce a shareable
686+
* URL. Works for both root issuance (no `credentialId`) and re-delegation
687+
* (pass the delegatable credential's `credentialId`).
688+
*
689+
* @async
690+
* @param {Object} params - see core createDelegationOffer; `wallet` is bound automatically
691+
* @returns {Promise<Object>} The delegation offer
692+
*/
693+
createDelegationOffer: params =>
694+
createDelegationOffer({wallet, ...params}),
695+
696+
/**
697+
* Accepts a received delegation offer, sending a credential request back
698+
* to the issuer.
699+
*
700+
* @async
701+
* @param {Object} delegationOffer - The offer from a `delegationOfferReceived` event
702+
* @returns {Promise<void>}
703+
*/
704+
acceptDelegationOffer: delegationOffer =>
705+
acceptDelegationOffer({delegationOffer, wallet, messageProvider}),
706+
707+
/**
708+
* Resolves the full delegation details for a stored delegatable credential:
709+
* policy, role, remaining delegation depth, chain, and the roles the holder
710+
* may re-delegate to.
711+
*
712+
* @async
713+
* @param {Object} credential - A stored delegatable credential
714+
* @returns {Promise<Object>} Delegation details
715+
*/
716+
getDelegationDetails: credential =>
717+
getDelegationDetails(credential, wallet),
718+
719+
/**
720+
* Manually dispatch a DIDComm delegation message (e.g. an OOB invitation
721+
* URL scanned from a QR code) to the delegation handlers. Prefer
722+
* `enableDelegation()` for automatic routing of relayed messages.
723+
*
724+
* @async
725+
* @param {string|Object} message - OOB URL string or DIDComm message object
726+
* @returns {Promise<void>}
727+
*/
728+
handleMessage: message =>
729+
handleMessage(message, {wallet, messageProvider}),
640730
};
641731

642732
if (passkeyMnemonic) {
@@ -735,6 +825,18 @@ export {
735825
enrollUserWithPasskey,
736826
authenticateWithPasskey,
737827
initializeCloudWalletWithPasskey,
828+
createDelegationOffer,
829+
createOOBInvitation,
830+
acceptDelegationOffer,
831+
handleMessage,
832+
decodeMessage,
833+
issueCredential,
834+
delegateCredential,
835+
getDelegationDetails,
836+
getDelegationChain,
837+
revokeDelegatableCredential,
838+
unrevokeDelegatableCredential,
839+
isDelegatableCredentialRevoked,
738840
};
739841

740842
export default {
@@ -759,4 +861,16 @@ export default {
759861
enrollUserWithPasskey,
760862
authenticateWithPasskey,
761863
initializeCloudWalletWithPasskey,
864+
createDelegationOffer,
865+
createOOBInvitation,
866+
acceptDelegationOffer,
867+
handleMessage,
868+
decodeMessage,
869+
issueCredential,
870+
delegateCredential,
871+
getDelegationDetails,
872+
getDelegationChain,
873+
revokeDelegatableCredential,
874+
unrevokeDelegatableCredential,
875+
isDelegatableCredentialRevoked,
762876
};

packages/web/src/index.test.js

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ jest.mock('@docknetwork/wallet-sdk-core/src/did-provider', () => ({
7575
jest.mock('@docknetwork/wallet-sdk-core/src/message-provider', () => ({
7676
createMessageProvider: jest.fn().mockImplementation(() => {
7777
mockCallOrder.push('createMessageProvider');
78-
return {mockMessageProvider: true};
78+
return {
79+
mockMessageProvider: true,
80+
addMessageListener: jest.fn().mockReturnValue(() => {}),
81+
};
7982
}),
8083
}));
8184

@@ -87,6 +90,36 @@ jest.mock('@docknetwork/wallet-sdk-wasm/src/services/blockchain', () => ({
8790
blockchainService: {ensureBlockchainReady: jest.fn()},
8891
}));
8992

93+
jest.mock(
94+
'@docknetwork/wallet-sdk-core/src/delegation/delegation-offer',
95+
() => ({
96+
createDelegationOffer: jest.fn().mockResolvedValue({id: 'offer-1'}),
97+
createOOBInvitation: jest.fn(),
98+
acceptDelegationOffer: jest.fn().mockResolvedValue(undefined),
99+
handleMessage: jest.fn().mockResolvedValue(undefined),
100+
decodeMessage: jest.fn(),
101+
}),
102+
);
103+
jest.mock(
104+
'@docknetwork/wallet-sdk-core/src/delegation/delegation-issuance',
105+
() => ({issueCredential: jest.fn(), delegateCredential: jest.fn()}),
106+
);
107+
jest.mock(
108+
'@docknetwork/wallet-sdk-core/src/delegation/delegation-policy',
109+
() => ({getDelegationDetails: jest.fn().mockResolvedValue({role: {}})}),
110+
);
111+
jest.mock('@docknetwork/wallet-sdk-core/src/delegation/delegation-chain', () => ({
112+
getDelegationChain: jest.fn(),
113+
}));
114+
jest.mock(
115+
'@docknetwork/wallet-sdk-core/src/delegation/delegation-revocation',
116+
() => ({
117+
revokeDelegatableCredential: jest.fn(),
118+
unrevokeDelegatableCredential: jest.fn(),
119+
isDelegatableCredentialRevoked: jest.fn(),
120+
}),
121+
);
122+
90123
const {
91124
initializeCloudWallet,
92125
} = require('@docknetwork/wallet-sdk-core/src/cloud-wallet');
@@ -390,4 +423,76 @@ describe('WalletSDK initialize', () => {
390423
);
391424
});
392425
});
426+
427+
describe('delegation', () => {
428+
const {
429+
createDelegationOffer,
430+
acceptDelegationOffer,
431+
handleMessage,
432+
} = require('@docknetwork/wallet-sdk-core/src/delegation/delegation-offer');
433+
const {
434+
getDelegationDetails,
435+
} = require('@docknetwork/wallet-sdk-core/src/delegation/delegation-policy');
436+
const config = {
437+
edvUrl: 'https://edv.example.com',
438+
edvAuthKey: 'auth-key',
439+
masterKey: new Uint8Array([1, 2, 3]),
440+
networkId: 'testnet',
441+
};
442+
443+
it('re-exports delegation functions', () => {
444+
[
445+
'createDelegationOffer',
446+
'createOOBInvitation',
447+
'acceptDelegationOffer',
448+
'handleMessage',
449+
'decodeMessage',
450+
'issueCredential',
451+
'delegateCredential',
452+
'getDelegationDetails',
453+
'getDelegationChain',
454+
'revokeDelegatableCredential',
455+
'unrevokeDelegatableCredential',
456+
'isDelegatableCredentialRevoked',
457+
].forEach(name => expect(typeof WalletSDK[name]).toBe('function'));
458+
});
459+
460+
it('enableDelegation registers a message listener that routes to handleMessage', async () => {
461+
const wallet = await WalletSDK.initialize(config);
462+
const stop = wallet.enableDelegation();
463+
464+
expect(wallet.messageProvider.addMessageListener).toHaveBeenCalledTimes(1);
465+
// Invoke the registered listener and confirm it delegates to handleMessage.
466+
const listener =
467+
wallet.messageProvider.addMessageListener.mock.calls[0][0];
468+
await listener('some-oob-url');
469+
expect(handleMessage).toHaveBeenCalledWith('some-oob-url', {
470+
wallet: wallet.wallet,
471+
messageProvider: wallet.messageProvider,
472+
});
473+
expect(typeof stop).toBe('function');
474+
});
475+
476+
it('binds wallet/messageProvider to the delegation methods', async () => {
477+
const wallet = await WalletSDK.initialize(config);
478+
479+
await wallet.createDelegationOffer({issuerDID: 'did:x', delegationRole: 'r'});
480+
expect(createDelegationOffer).toHaveBeenCalledWith(
481+
expect.objectContaining({wallet: wallet.wallet, issuerDID: 'did:x'}),
482+
);
483+
484+
await wallet.acceptDelegationOffer({id: 'offer-1'});
485+
expect(acceptDelegationOffer).toHaveBeenCalledWith({
486+
delegationOffer: {id: 'offer-1'},
487+
wallet: wallet.wallet,
488+
messageProvider: wallet.messageProvider,
489+
});
490+
491+
await wallet.getDelegationDetails({id: 'cred-1'});
492+
expect(getDelegationDetails).toHaveBeenCalledWith(
493+
{id: 'cred-1'},
494+
wallet.wallet,
495+
);
496+
});
497+
});
393498
});

0 commit comments

Comments
 (0)