Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions packages/web/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ import {createCredentialProvider} from '@docknetwork/wallet-sdk-core/src/credent
import {createDIDProvider} from '@docknetwork/wallet-sdk-core/src/did-provider';
import {createMessageProvider} from '@docknetwork/wallet-sdk-core/src/message-provider';
import {createVerificationController} from '@docknetwork/wallet-sdk-core/src/verification-controller';
import {
createDelegationOffer,
createOOBInvitation,
acceptDelegationOffer,
handleMessage,
decodeMessage,
} from '@docknetwork/wallet-sdk-core/src/delegation/delegation-offer';
import {
issueCredential,
delegateCredential,
} from '@docknetwork/wallet-sdk-core/src/delegation/delegation-issuance';
import {getDelegationDetails} from '@docknetwork/wallet-sdk-core/src/delegation/delegation-policy';
import {getDelegationChain} from '@docknetwork/wallet-sdk-core/src/delegation/delegation-chain';
import {
revokeDelegatableCredential,
unrevokeDelegatableCredential,
isDelegatableCredentialRevoked,
} from '@docknetwork/wallet-sdk-core/src/delegation/delegation-revocation';
import {blockchainService} from '@docknetwork/wallet-sdk-wasm/src/services/blockchain';
import {
checkPasskeySupport,
Expand Down Expand Up @@ -637,6 +655,112 @@ async function initialize({
},
};
},

/**
* Routes incoming DIDComm delegation messages (offers, credential
* requests, issuance, acks) to the delegation handlers automatically.
*
* Without this, delegation messages must be dispatched manually via
* `handleMessage`. Call once after initialization to participate in
* delegation flows as issuer and/or holder.
*
* Pass optional callbacks to react in the UI without reaching into the
* core wallet's event manager:
* - `onOfferReceived(offer)` — a delegation offer arrived (accept it with
* `acceptDelegationOffer(offer)`)
* - `onCredentialReceived(payload)` — a delegated credential was issued
* and stored
*
* @param {Object} [handlers]
* @param {Function} [handlers.onOfferReceived]
* @param {Function} [handlers.onCredentialReceived]
* @returns {Function} Unsubscribe function that stops routing and removes
* the event listeners.
*
* @example
* const stop = wallet.enableDelegation({
* onOfferReceived: offer => wallet.acceptDelegationOffer(offer),
* onCredentialReceived: ({credentials}) => refreshUI(credentials),
* });
*/
enableDelegation: ({onOfferReceived, onCredentialReceived} = {}) => {
const removeMessageListener = messageProvider.addMessageListener(
message => handleMessage(message, {wallet, messageProvider}),
);

const subscriptions = [];
if (onOfferReceived) {
wallet.eventManager.addListener(
'delegationOfferReceived',
onOfferReceived,
);
subscriptions.push(['delegationOfferReceived', onOfferReceived]);
}
if (onCredentialReceived) {
wallet.eventManager.addListener(
'delegatedCredentialReceived',
onCredentialReceived,
);
subscriptions.push([
'delegatedCredentialReceived',
onCredentialReceived,
]);
}

return () => {
removeMessageListener();
subscriptions.forEach(([event, handler]) =>
wallet.eventManager.removeListener(event, handler),
);
};
},

/**
* Creates a delegation offer for a role and persists it on the wallet.
* Pass the returned offer to `createOOBInvitation` to produce a shareable
* URL. Works for both root issuance (no `credentialId`) and re-delegation
* (pass the delegatable credential's `credentialId`).
*
* @async
* @param {Object} params - see core createDelegationOffer; `wallet` is bound automatically
* @returns {Promise<Object>} The delegation offer
*/
createDelegationOffer: (params = {}) =>
createDelegationOffer({wallet, ...params}),

/**
* Accepts a received delegation offer, sending a credential request back
* to the issuer.
*
* @async
* @param {Object} delegationOffer - The offer from a `delegationOfferReceived` event
* @returns {Promise<void>}
*/
acceptDelegationOffer: delegationOffer =>
acceptDelegationOffer({delegationOffer, wallet, messageProvider}),

/**
* Resolves the full delegation details for a stored delegatable credential:
* policy, role, remaining delegation depth, chain, and the roles the holder
* may re-delegate to.
*
* @async
* @param {Object} credential - A stored delegatable credential
* @returns {Promise<Object>} Delegation details
*/
getDelegationDetails: credential =>
getDelegationDetails(credential, wallet),

/**
* Manually dispatch a DIDComm delegation message (e.g. an OOB invitation
* URL scanned from a QR code) to the delegation handlers. Prefer
* `enableDelegation()` for automatic routing of relayed messages.
*
* @async
* @param {string|Object} message - OOB URL string or DIDComm message object
* @returns {Promise<void>}
*/
handleMessage: message => handleMessage(message, {wallet, messageProvider}),
};

if (passkeyMnemonic) {
Expand Down Expand Up @@ -735,6 +859,18 @@ export {
enrollUserWithPasskey,
authenticateWithPasskey,
initializeCloudWalletWithPasskey,
createDelegationOffer,
createOOBInvitation,
acceptDelegationOffer,
handleMessage,
decodeMessage,
issueCredential,
delegateCredential,
getDelegationDetails,
getDelegationChain,
revokeDelegatableCredential,
unrevokeDelegatableCredential,
isDelegatableCredentialRevoked,
};

export default {
Expand All @@ -759,4 +895,16 @@ export default {
enrollUserWithPasskey,
authenticateWithPasskey,
initializeCloudWalletWithPasskey,
createDelegationOffer,
createOOBInvitation,
acceptDelegationOffer,
handleMessage,
decodeMessage,
issueCredential,
delegateCredential,
getDelegationDetails,
getDelegationChain,
revokeDelegatableCredential,
unrevokeDelegatableCredential,
isDelegatableCredentialRevoked,
};
157 changes: 155 additions & 2 deletions packages/web/src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ jest.mock('./passkey', () => ({
jest.mock('@docknetwork/wallet-sdk-core/src/wallet', () => ({
createWallet: jest.fn().mockImplementation(async () => {
mockCallOrder.push('createWallet');
return {mockWallet: true};
return {
mockWallet: true,
eventManager: {addListener: jest.fn(), removeListener: jest.fn()},
};
}),
}));

Expand All @@ -75,7 +78,10 @@ jest.mock('@docknetwork/wallet-sdk-core/src/did-provider', () => ({
jest.mock('@docknetwork/wallet-sdk-core/src/message-provider', () => ({
createMessageProvider: jest.fn().mockImplementation(() => {
mockCallOrder.push('createMessageProvider');
return {mockMessageProvider: true};
return {
mockMessageProvider: true,
addMessageListener: jest.fn().mockReturnValue(() => {}),
};
}),
}));

Expand All @@ -87,6 +93,39 @@ jest.mock('@docknetwork/wallet-sdk-wasm/src/services/blockchain', () => ({
blockchainService: {ensureBlockchainReady: jest.fn()},
}));

jest.mock(
'@docknetwork/wallet-sdk-core/src/delegation/delegation-offer',
() => ({
createDelegationOffer: jest.fn().mockResolvedValue({id: 'offer-1'}),
createOOBInvitation: jest.fn(),
acceptDelegationOffer: jest.fn().mockResolvedValue(undefined),
handleMessage: jest.fn().mockResolvedValue(undefined),
decodeMessage: jest.fn(),
}),
);
jest.mock(
'@docknetwork/wallet-sdk-core/src/delegation/delegation-issuance',
() => ({issueCredential: jest.fn(), delegateCredential: jest.fn()}),
);
jest.mock(
'@docknetwork/wallet-sdk-core/src/delegation/delegation-policy',
() => ({getDelegationDetails: jest.fn().mockResolvedValue({role: {}})}),
);
jest.mock(
'@docknetwork/wallet-sdk-core/src/delegation/delegation-chain',
() => ({
getDelegationChain: jest.fn(),
}),
);
jest.mock(
'@docknetwork/wallet-sdk-core/src/delegation/delegation-revocation',
() => ({
revokeDelegatableCredential: jest.fn(),
unrevokeDelegatableCredential: jest.fn(),
isDelegatableCredentialRevoked: jest.fn(),
}),
);

const {
initializeCloudWallet,
} = require('@docknetwork/wallet-sdk-core/src/cloud-wallet');
Expand Down Expand Up @@ -390,4 +429,118 @@ describe('WalletSDK initialize', () => {
);
});
});

describe('delegation', () => {
const {
createDelegationOffer,
acceptDelegationOffer,
handleMessage,
} = require('@docknetwork/wallet-sdk-core/src/delegation/delegation-offer');
const {
getDelegationDetails,
} = require('@docknetwork/wallet-sdk-core/src/delegation/delegation-policy');
const config = {
edvUrl: 'https://edv.example.com',
edvAuthKey: 'auth-key',
masterKey: new Uint8Array([1, 2, 3]),
networkId: 'testnet',
};

it('re-exports delegation functions', () => {
[
'createDelegationOffer',
'createOOBInvitation',
'acceptDelegationOffer',
'handleMessage',
'decodeMessage',
'issueCredential',
'delegateCredential',
'getDelegationDetails',
'getDelegationChain',
'revokeDelegatableCredential',
'unrevokeDelegatableCredential',
'isDelegatableCredentialRevoked',
].forEach(name => expect(typeof WalletSDK[name]).toBe('function'));
});

it('enableDelegation registers a message listener that routes to handleMessage', async () => {
const wallet = await WalletSDK.initialize(config);
const stop = wallet.enableDelegation();

expect(wallet.messageProvider.addMessageListener).toHaveBeenCalledTimes(
1,
);
// Invoke the registered listener and confirm it delegates to handleMessage.
const listener =
wallet.messageProvider.addMessageListener.mock.calls[0][0];
await listener('some-oob-url');
expect(handleMessage).toHaveBeenCalledWith('some-oob-url', {
wallet: wallet.wallet,
messageProvider: wallet.messageProvider,
});
expect(typeof stop).toBe('function');
});

it('enableDelegation subscribes callbacks to wallet events and unsubscribes on stop', async () => {
const wallet = await WalletSDK.initialize(config);
const onOfferReceived = jest.fn();
const onCredentialReceived = jest.fn();

const removeMessageListener = jest.fn();
wallet.messageProvider.addMessageListener.mockReturnValueOnce(
removeMessageListener,
);

const stop = wallet.enableDelegation({
onOfferReceived,
onCredentialReceived,
});

expect(wallet.wallet.eventManager.addListener).toHaveBeenCalledWith(
'delegationOfferReceived',
onOfferReceived,
);
expect(wallet.wallet.eventManager.addListener).toHaveBeenCalledWith(
'delegatedCredentialReceived',
onCredentialReceived,
);

stop();

expect(removeMessageListener).toHaveBeenCalledTimes(1);
expect(wallet.wallet.eventManager.removeListener).toHaveBeenCalledWith(
'delegationOfferReceived',
onOfferReceived,
);
expect(wallet.wallet.eventManager.removeListener).toHaveBeenCalledWith(
'delegatedCredentialReceived',
onCredentialReceived,
);
});

it('binds wallet/messageProvider to the delegation methods', async () => {
const wallet = await WalletSDK.initialize(config);

await wallet.createDelegationOffer({
issuerDID: 'did:x',
delegationRole: 'r',
});
expect(createDelegationOffer).toHaveBeenCalledWith(
expect.objectContaining({wallet: wallet.wallet, issuerDID: 'did:x'}),
);

await wallet.acceptDelegationOffer({id: 'offer-1'});
expect(acceptDelegationOffer).toHaveBeenCalledWith({
delegationOffer: {id: 'offer-1'},
wallet: wallet.wallet,
messageProvider: wallet.messageProvider,
});

await wallet.getDelegationDetails({id: 'cred-1'});
expect(getDelegationDetails).toHaveBeenCalledWith(
{id: 'cred-1'},
wallet.wallet,
);
});
});
});
Loading