@docknetwork/wallet-sdk-core 1.7.7-alpha.0 → 1.9.0
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/lib/cloud-wallet.d.ts +79 -3
- package/lib/cloud-wallet.d.ts.map +1 -1
- package/lib/cloud-wallet.js +147 -14
- package/lib/cloud-wallet.js.map +1 -1
- package/lib/credential-provider.d.ts.map +1 -1
- package/lib/credential-provider.js +10 -4
- package/lib/credential-provider.js.map +1 -1
- package/lib/delegation/delegation-chain.d.ts +8 -0
- package/lib/delegation/delegation-chain.d.ts.map +1 -0
- package/lib/delegation/delegation-chain.js +33 -0
- package/lib/delegation/delegation-chain.js.map +1 -0
- package/lib/delegation/delegation-fixtures.d.ts +69 -0
- package/lib/delegation/delegation-fixtures.d.ts.map +1 -0
- package/lib/delegation/delegation-fixtures.js +553 -0
- package/lib/delegation/delegation-fixtures.js.map +1 -0
- package/lib/delegation/delegation-issuance.d.ts +19 -0
- package/lib/delegation/delegation-issuance.d.ts.map +1 -0
- package/lib/delegation/delegation-issuance.js +60 -0
- package/lib/delegation/delegation-issuance.js.map +1 -0
- package/lib/delegation/delegation-offer.d.ts +84 -0
- package/lib/delegation/delegation-offer.d.ts.map +1 -0
- package/lib/delegation/delegation-offer.js +349 -0
- package/lib/delegation/delegation-offer.js.map +1 -0
- package/lib/delegation/delegation-policy-validation.d.ts +28 -0
- package/lib/delegation/delegation-policy-validation.d.ts.map +1 -0
- package/lib/delegation/delegation-policy-validation.js +170 -0
- package/lib/delegation/delegation-policy-validation.js.map +1 -0
- package/lib/delegation/delegation-policy.d.ts +21 -0
- package/lib/delegation/delegation-policy.d.ts.map +1 -0
- package/lib/delegation/delegation-policy.js +73 -0
- package/lib/delegation/delegation-policy.js.map +1 -0
- package/lib/delegation/delegation-tree.d.ts +17 -0
- package/lib/delegation/delegation-tree.d.ts.map +1 -0
- package/lib/delegation/delegation-tree.js +58 -0
- package/lib/delegation/delegation-tree.js.map +1 -0
- package/lib/delegation/delegation-types.d.ts +56 -0
- package/lib/delegation/delegation-types.d.ts.map +1 -0
- package/lib/delegation/delegation-types.js +3 -0
- package/lib/delegation/delegation-types.js.map +1 -0
- package/lib/delegation/delegation-utils.d.ts +3 -0
- package/lib/delegation/delegation-utils.d.ts.map +1 -0
- package/lib/delegation/delegation-utils.js +10 -0
- package/lib/delegation/delegation-utils.js.map +1 -0
- package/lib/did-provider.d.ts +2 -1
- package/lib/did-provider.d.ts.map +1 -1
- package/lib/did-provider.js +11 -7
- package/lib/did-provider.js.map +1 -1
- package/lib/message-provider.js +1 -1
- package/lib/message-provider.js.map +1 -1
- package/lib/verification-controller.d.ts +30 -11
- package/lib/verification-controller.d.ts.map +1 -1
- package/lib/verification-controller.js +372 -68
- package/lib/verification-controller.js.map +1 -1
- package/package.json +3 -3
- package/src/cloud-wallet.test.js +369 -0
- package/src/cloud-wallet.ts +206 -18
- package/src/credential-provider.ts +13 -4
- package/src/delegation/delegation-chain.test.ts +64 -0
- package/src/delegation/delegation-chain.ts +34 -0
- package/src/delegation/delegation-fixtures.ts +552 -0
- package/src/delegation/delegation-issuance.ts +92 -0
- package/src/delegation/delegation-offer.ts +488 -0
- package/src/delegation/delegation-policy-validation.test.ts +237 -0
- package/src/delegation/delegation-policy-validation.ts +281 -0
- package/src/delegation/delegation-policy.ts +100 -0
- package/src/delegation/delegation-tree.test.ts +110 -0
- package/src/delegation/delegation-tree.ts +60 -0
- package/src/delegation/delegation-types.ts +65 -0
- package/src/delegation/delegation-utils.ts +10 -0
- package/src/did-provider.ts +10 -6
- package/src/globals.d.ts +6 -0
- package/src/message-provider.ts +1 -1
- package/src/verification-controller.test.ts +23 -0
- package/src/verification-controller.ts +534 -82
- package/tsconfig.build.json +2 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import {DelegationPolicy} from './delegation-types';
|
|
3
|
+
import {isDelegatableCredential} from './delegation-utils';
|
|
4
|
+
import {buildDelegationPolicyAttributes} from './delegation-policy';
|
|
5
|
+
import {delegationService} from '@docknetwork/wallet-sdk-wasm/src/services/delegation';
|
|
6
|
+
import {v4 as uuidv4} from 'uuid';
|
|
7
|
+
import {getAllDIDs, getDIDKeyPair} from '../did-provider';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Issue a delegatable credential
|
|
11
|
+
*
|
|
12
|
+
* @param credentialData
|
|
13
|
+
* @param issuerKey
|
|
14
|
+
* @param delegationPolicy
|
|
15
|
+
* @param roleId
|
|
16
|
+
* @param rootCredentialId
|
|
17
|
+
*/
|
|
18
|
+
export async function issueCredential(
|
|
19
|
+
credentialData,
|
|
20
|
+
issuerKey,
|
|
21
|
+
delegationPolicy: DelegationPolicy,
|
|
22
|
+
roleId,
|
|
23
|
+
rootCredentialId?,
|
|
24
|
+
) {
|
|
25
|
+
assert(
|
|
26
|
+
isDelegatableCredential(credentialData),
|
|
27
|
+
'Credential is not delegatable',
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const recipientRole = delegationPolicy.ruleset.roles.find(
|
|
31
|
+
role => role.roleId === roleId,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
assert(recipientRole, `Role ${roleId} not found in ruleset`);
|
|
35
|
+
|
|
36
|
+
const credentialId = credentialData.id || `urn:uuid:${uuidv4()}`;
|
|
37
|
+
const credential = await delegationService.issueCredential(issuerKey, {
|
|
38
|
+
...credentialData,
|
|
39
|
+
...(await buildDelegationPolicyAttributes(delegationPolicy)),
|
|
40
|
+
id: credentialId,
|
|
41
|
+
roleId,
|
|
42
|
+
rootCredentialId: rootCredentialId || credentialId,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return credential;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function delegateCredential({
|
|
49
|
+
credential,
|
|
50
|
+
wallet,
|
|
51
|
+
delegationPolicy,
|
|
52
|
+
roleId,
|
|
53
|
+
delegatorDID,
|
|
54
|
+
}: {
|
|
55
|
+
credential: any;
|
|
56
|
+
wallet: any;
|
|
57
|
+
delegationPolicy: DelegationPolicy;
|
|
58
|
+
roleId: string;
|
|
59
|
+
delegatorDID: string;
|
|
60
|
+
}) {
|
|
61
|
+
assert(isDelegatableCredential(credential), 'Credential is not delegatable');
|
|
62
|
+
assert(!!delegatorDID, 'delegatorDID is required');
|
|
63
|
+
|
|
64
|
+
const allDIDs = await getAllDIDs({wallet});
|
|
65
|
+
const issuerDID = allDIDs.find(d => d.didDocument.id === delegatorDID);
|
|
66
|
+
assert(!!issuerDID, `delegatorDID ${delegatorDID} not found in wallet`);
|
|
67
|
+
|
|
68
|
+
const keyPair = await getDIDKeyPair(wallet, issuerDID);
|
|
69
|
+
|
|
70
|
+
const credentialData = {
|
|
71
|
+
...(await buildDelegationPolicyAttributes(delegationPolicy)),
|
|
72
|
+
'@context': credential['@context'],
|
|
73
|
+
id: `urn:uuid:${uuidv4()}`,
|
|
74
|
+
roleId: roleId,
|
|
75
|
+
rootCredentialId: credential.rootCredentialId || credential.id,
|
|
76
|
+
type: credential.type,
|
|
77
|
+
issuer: {
|
|
78
|
+
id: issuerDID.didDocument.id,
|
|
79
|
+
name: issuerDID.name,
|
|
80
|
+
},
|
|
81
|
+
issuanceDate: new Date().toISOString(),
|
|
82
|
+
credentialSubject: credential.credentialSubject,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return issueCredential(
|
|
86
|
+
credentialData,
|
|
87
|
+
keyPair,
|
|
88
|
+
delegationPolicy,
|
|
89
|
+
roleId,
|
|
90
|
+
credential.rootCredentialId || credential.id,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import {v4 as uuid} from 'uuid';
|
|
3
|
+
import {logger} from '@docknetwork/wallet-sdk-data-store/src/logger';
|
|
4
|
+
import {getAllDIDs, getDefaultDID} from '../did-provider';
|
|
5
|
+
import {delegateCredential} from './delegation-issuance';
|
|
6
|
+
import {getDelegationChain} from './delegation-chain';
|
|
7
|
+
import {getDelegationDetails} from './delegation-policy';
|
|
8
|
+
import {isDelegatableCredential} from './delegation-utils';
|
|
9
|
+
import {
|
|
10
|
+
assertPolicyConformsToParent,
|
|
11
|
+
validateDelegationPolicy,
|
|
12
|
+
} from './delegation-policy-validation';
|
|
13
|
+
|
|
14
|
+
const GOAL_CODE = 'dock.offer-delegation';
|
|
15
|
+
const OOB_INVITATION = 'https://didcomm.org/out-of-band/2.0/invitation';
|
|
16
|
+
const REQUEST_CREDENTIAL =
|
|
17
|
+
'https://didcomm.org/issue-credential/3.0/request-credential';
|
|
18
|
+
const ISSUE_CREDENTIAL =
|
|
19
|
+
'https://didcomm.org/issue-credential/3.0/issue-credential';
|
|
20
|
+
const ACK = 'https://didcomm.org/issue-credential/3.0/ack';
|
|
21
|
+
|
|
22
|
+
function base64urlEncode(input: string): string {
|
|
23
|
+
return Buffer.from(input, 'utf8')
|
|
24
|
+
.toString('base64')
|
|
25
|
+
.replace(/\+/g, '-')
|
|
26
|
+
.replace(/\//g, '_')
|
|
27
|
+
.replace(/=+$/, '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function base64urlDecode(input: string): string {
|
|
31
|
+
const padded = input.replace(/-/g, '+').replace(/_/g, '/');
|
|
32
|
+
const padLen = (4 - (padded.length % 4)) % 4;
|
|
33
|
+
return Buffer.from(padded + '='.repeat(padLen), 'base64').toString('utf8');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pickDID(value: string | string[] | undefined): string | undefined {
|
|
37
|
+
if (!value) return undefined;
|
|
38
|
+
return Array.isArray(value) ? value[0] : value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type DelegationOffer = {
|
|
42
|
+
id: string;
|
|
43
|
+
messageId?: string;
|
|
44
|
+
issuerDID?: string;
|
|
45
|
+
status: 'sent' | 'requested' | 'accepted' | 'rejected';
|
|
46
|
+
expiresAt?: string;
|
|
47
|
+
[key: string]: any;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type DelegationOfferPreview = {
|
|
51
|
+
id: string;
|
|
52
|
+
issuerDID: string;
|
|
53
|
+
issuerName?: string;
|
|
54
|
+
role: string;
|
|
55
|
+
createdAt: string;
|
|
56
|
+
expiresAt?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const DEFAULT_OFFER_EXPIRATION_MS = 24 * 60 * 60 * 1000;
|
|
60
|
+
|
|
61
|
+
export async function createDelegationOffer({
|
|
62
|
+
wallet,
|
|
63
|
+
issuerDID,
|
|
64
|
+
delegationPolicy,
|
|
65
|
+
delegationRole,
|
|
66
|
+
credentialId,
|
|
67
|
+
expiresInMs = DEFAULT_OFFER_EXPIRATION_MS,
|
|
68
|
+
}: {
|
|
69
|
+
wallet: any;
|
|
70
|
+
issuerDID: string;
|
|
71
|
+
delegationPolicy: any;
|
|
72
|
+
delegationRole: string;
|
|
73
|
+
credentialId?: string;
|
|
74
|
+
expiresInMs?: number;
|
|
75
|
+
}) {
|
|
76
|
+
validateDelegationPolicy(delegationPolicy);
|
|
77
|
+
assert(
|
|
78
|
+
delegationPolicy.ruleset.roles.some(r => r.roleId === delegationRole),
|
|
79
|
+
`delegationRole "${delegationRole}" not found in delegationPolicy.ruleset.roles`,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (credentialId) {
|
|
83
|
+
const parentCredential = await wallet.getDocumentById(credentialId);
|
|
84
|
+
if (parentCredential) {
|
|
85
|
+
assert(
|
|
86
|
+
isDelegatableCredential(parentCredential),
|
|
87
|
+
`Credential ${credentialId} is not delegatable`,
|
|
88
|
+
);
|
|
89
|
+
const parentDetails = await getDelegationDetails(parentCredential, wallet);
|
|
90
|
+
if (parentDetails.delegationPolicy) {
|
|
91
|
+
assertPolicyConformsToParent(delegationPolicy, parentDetails.delegationPolicy, {
|
|
92
|
+
delegationRole,
|
|
93
|
+
remainingDepth: parentDetails.remainingDelegationDepth,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const dids = await getAllDIDs({wallet});
|
|
100
|
+
const issuer = dids.find(d => d.didDocument.id === issuerDID);
|
|
101
|
+
const issuerName = issuer?.name;
|
|
102
|
+
|
|
103
|
+
const offerId = uuid();
|
|
104
|
+
const sentAt = new Date();
|
|
105
|
+
const delegationOffer = {
|
|
106
|
+
id: offerId,
|
|
107
|
+
credentialId: credentialId,
|
|
108
|
+
issuerDID,
|
|
109
|
+
issuerName,
|
|
110
|
+
issuer: {
|
|
111
|
+
did: issuerDID,
|
|
112
|
+
},
|
|
113
|
+
to: undefined,
|
|
114
|
+
delegationPolicy,
|
|
115
|
+
delegationRole,
|
|
116
|
+
capabilities: [],
|
|
117
|
+
attributes: [],
|
|
118
|
+
delegationConstraints: {},
|
|
119
|
+
sentAt: sentAt.toISOString(),
|
|
120
|
+
expiresAt: new Date(sentAt.getTime() + expiresInMs).toISOString(),
|
|
121
|
+
updatedAt: null,
|
|
122
|
+
status: 'sent',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Persist on the issuer side so DELEGATION_REQUEST_HANDLER can look it up
|
|
126
|
+
// when the holder replies with a credential request.
|
|
127
|
+
await wallet.addDocument({
|
|
128
|
+
type: 'DelegationOffer',
|
|
129
|
+
...delegationOffer,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return delegationOffer;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// OOB invitation (issuer → holder via QR/link)
|
|
136
|
+
export function createOOBInvitation(
|
|
137
|
+
issuerDID,
|
|
138
|
+
delegationOffer,
|
|
139
|
+
{goal, issuerName}: {goal: string; issuerName?: string},
|
|
140
|
+
) {
|
|
141
|
+
assert(!!goal, 'goal is required');
|
|
142
|
+
|
|
143
|
+
const finalIssuerName = issuerName ?? delegationOffer.issuerName;
|
|
144
|
+
|
|
145
|
+
const preview: DelegationOfferPreview = {
|
|
146
|
+
id: delegationOffer.id,
|
|
147
|
+
issuerDID: issuerDID,
|
|
148
|
+
issuerName: finalIssuerName,
|
|
149
|
+
role: delegationOffer.delegationRole,
|
|
150
|
+
createdAt: delegationOffer.sentAt,
|
|
151
|
+
expiresAt: delegationOffer.expiresAt,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const delegationOfferMessage = {
|
|
155
|
+
type: OOB_INVITATION,
|
|
156
|
+
id: delegationOffer.id,
|
|
157
|
+
from: issuerDID,
|
|
158
|
+
body: {
|
|
159
|
+
goal_code: GOAL_CODE,
|
|
160
|
+
goal,
|
|
161
|
+
offer_id: delegationOffer.id,
|
|
162
|
+
},
|
|
163
|
+
attachments: [
|
|
164
|
+
{
|
|
165
|
+
id: delegationOffer.id,
|
|
166
|
+
media_type: 'application/json',
|
|
167
|
+
data: {json: preview},
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const offerUrl =
|
|
173
|
+
'didcomm://?_oob=' +
|
|
174
|
+
base64urlEncode(JSON.stringify(delegationOfferMessage));
|
|
175
|
+
|
|
176
|
+
return offerUrl;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Decode an OOB invitation URL into a DIDComm message object.
|
|
180
|
+
// Returns the input unchanged if it's already an object.
|
|
181
|
+
export function decodeMessage(message) {
|
|
182
|
+
if (typeof message !== 'string') {
|
|
183
|
+
return message;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const oobPrefix = 'didcomm://?_oob=';
|
|
187
|
+
if (!message.startsWith(oobPrefix)) {
|
|
188
|
+
logger.debug('decodeMessage: unrecognized URL scheme, skipping');
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const encoded = message.slice(oobPrefix.length);
|
|
193
|
+
try {
|
|
194
|
+
return JSON.parse(base64urlDecode(encoded));
|
|
195
|
+
} catch (err) {
|
|
196
|
+
logger.error(`decodeMessage: failed to decode OOB payload: ${err}`);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function acceptDelegationOffer({
|
|
202
|
+
delegationOffer,
|
|
203
|
+
wallet,
|
|
204
|
+
messageProvider,
|
|
205
|
+
}: {
|
|
206
|
+
delegationOffer: DelegationOffer;
|
|
207
|
+
wallet: any;
|
|
208
|
+
messageProvider: any;
|
|
209
|
+
}) {
|
|
210
|
+
const issuerDID = delegationOffer.issuerDID;
|
|
211
|
+
const holderDID = await getDefaultDID({wallet});
|
|
212
|
+
const dids = await getAllDIDs({wallet});
|
|
213
|
+
const holderName = dids.find(d => d.didDocument.id === holderDID)?.name;
|
|
214
|
+
|
|
215
|
+
const requestCredentialMessage = {
|
|
216
|
+
type: REQUEST_CREDENTIAL,
|
|
217
|
+
pthid: delegationOffer.messageId, // parent thread = the OOB invitation
|
|
218
|
+
from: holderDID,
|
|
219
|
+
to: issuerDID,
|
|
220
|
+
body: {
|
|
221
|
+
goal_code: GOAL_CODE,
|
|
222
|
+
sender_profile: {name: holderName},
|
|
223
|
+
offer_id: delegationOffer.id,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
await messageProvider.sendMessage(requestCredentialMessage);
|
|
228
|
+
|
|
229
|
+
// Mirror issuer-side bookkeeping: mark the holder's stored offer as accepted.
|
|
230
|
+
const storedOffer = await wallet.getDocumentById(delegationOffer.id);
|
|
231
|
+
if (storedOffer) {
|
|
232
|
+
await wallet.updateDocument({
|
|
233
|
+
...storedOffer,
|
|
234
|
+
status: 'requested',
|
|
235
|
+
updatedAt: new Date().toISOString(),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Delegation message handlers
|
|
241
|
+
export const INVITATION_HANDLER = {
|
|
242
|
+
check: function (message) {
|
|
243
|
+
return (
|
|
244
|
+
message.type === OOB_INVITATION && message.body?.goal_code === GOAL_CODE
|
|
245
|
+
);
|
|
246
|
+
},
|
|
247
|
+
handle: async function (message, {wallet}) {
|
|
248
|
+
const offerAttachment = message.attachments?.[0]?.data?.json ?? {};
|
|
249
|
+
const delegationOffer: DelegationOffer = {
|
|
250
|
+
...offerAttachment,
|
|
251
|
+
id: message.body.offer_id,
|
|
252
|
+
messageId: message.id,
|
|
253
|
+
issuerDID: message.from,
|
|
254
|
+
status: 'sent',
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
await wallet.addDocument({
|
|
258
|
+
type: 'DelegationOffer',
|
|
259
|
+
...delegationOffer,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
logger.debug(
|
|
263
|
+
`INVITATION_HANDLER: emitting delegationOfferReceived for offer ${delegationOffer.id}`,
|
|
264
|
+
);
|
|
265
|
+
wallet.eventManager.emit('delegationOfferReceived', delegationOffer);
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const DELEGATION_REQUEST_HANDLER = {
|
|
270
|
+
check: function (message) {
|
|
271
|
+
return (
|
|
272
|
+
message.type === REQUEST_CREDENTIAL &&
|
|
273
|
+
message.body?.goal_code === GOAL_CODE
|
|
274
|
+
);
|
|
275
|
+
},
|
|
276
|
+
handle: async function (message, {wallet, messageProvider}) {
|
|
277
|
+
const offerId = message.body.offer_id;
|
|
278
|
+
const delegationOffer = await wallet.getDocumentById(offerId);
|
|
279
|
+
if (!delegationOffer) {
|
|
280
|
+
logger.debug(
|
|
281
|
+
`DELEGATION_REQUEST_HANDLER: no matching delegation offer found for request ${offerId}`,
|
|
282
|
+
);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Authorization checks: only the targeted holder (if any) may accept,
|
|
287
|
+
// and the offer must still be in the 'sent' state to prevent replay.
|
|
288
|
+
if (delegationOffer.status !== 'sent') {
|
|
289
|
+
logger.warn(
|
|
290
|
+
`DELEGATION_REQUEST_HANDLER: rejecting request for offer ${offerId} — already ${delegationOffer.status}`,
|
|
291
|
+
);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (delegationOffer.to) {
|
|
296
|
+
const targets = Array.isArray(delegationOffer.to)
|
|
297
|
+
? delegationOffer.to
|
|
298
|
+
: [delegationOffer.to];
|
|
299
|
+
if (!targets.includes(message.from)) {
|
|
300
|
+
logger.warn(
|
|
301
|
+
`DELEGATION_REQUEST_HANDLER: rejecting request for offer ${offerId} — sender does not match offer.to`,
|
|
302
|
+
);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (
|
|
308
|
+
delegationOffer.expiresAt &&
|
|
309
|
+
new Date(delegationOffer.expiresAt) < new Date()
|
|
310
|
+
) {
|
|
311
|
+
logger.debug(
|
|
312
|
+
`DELEGATION_REQUEST_HANDLER: rejecting expired offer ${offerId}`,
|
|
313
|
+
);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const parentCredential = await wallet.getDocumentById(
|
|
318
|
+
delegationOffer.credentialId,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
validateDelegationPolicy(delegationOffer.delegationPolicy);
|
|
323
|
+
if (parentCredential && isDelegatableCredential(parentCredential)) {
|
|
324
|
+
const parentDetails = await getDelegationDetails(parentCredential, wallet);
|
|
325
|
+
if (parentDetails.delegationPolicy) {
|
|
326
|
+
assertPolicyConformsToParent(
|
|
327
|
+
delegationOffer.delegationPolicy,
|
|
328
|
+
parentDetails.delegationPolicy,
|
|
329
|
+
{
|
|
330
|
+
delegationRole: delegationOffer.delegationRole,
|
|
331
|
+
remainingDepth: parentDetails.remainingDelegationDepth,
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} catch (err: any) {
|
|
337
|
+
logger.warn(
|
|
338
|
+
`DELEGATION_REQUEST_HANDLER: rejecting offer ${offerId} — policy validation failed: ${err.message}`,
|
|
339
|
+
);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const holderDID = message.from;
|
|
344
|
+
|
|
345
|
+
delegationOffer.status = 'accepted';
|
|
346
|
+
delegationOffer.holderDID = holderDID;
|
|
347
|
+
delegationOffer.updatedAt = new Date().toISOString();
|
|
348
|
+
|
|
349
|
+
await wallet.updateDocument(delegationOffer);
|
|
350
|
+
|
|
351
|
+
const issuerDID = pickDID(message.to);
|
|
352
|
+
|
|
353
|
+
const delegatedCredential = await delegateCredential({
|
|
354
|
+
credential: parentCredential,
|
|
355
|
+
wallet,
|
|
356
|
+
delegationPolicy: delegationOffer.delegationPolicy,
|
|
357
|
+
roleId: delegationOffer.delegationRole,
|
|
358
|
+
delegatorDID: delegationOffer.issuerDID || issuerDID,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const delegationChain = await getDelegationChain(parentCredential, wallet);
|
|
362
|
+
|
|
363
|
+
await messageProvider.sendMessage({
|
|
364
|
+
type: ISSUE_CREDENTIAL,
|
|
365
|
+
from: issuerDID,
|
|
366
|
+
to: holderDID,
|
|
367
|
+
message: {
|
|
368
|
+
goal_code: GOAL_CODE,
|
|
369
|
+
delegationOfferId: delegationOffer.id,
|
|
370
|
+
credentials: [delegatedCredential],
|
|
371
|
+
delegationChain,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
export const ISSUE_CREDENTIAL_HANDLER = {
|
|
378
|
+
check: function (message) {
|
|
379
|
+
return (
|
|
380
|
+
message.type === ISSUE_CREDENTIAL &&
|
|
381
|
+
message.body?.goal_code === GOAL_CODE
|
|
382
|
+
);
|
|
383
|
+
},
|
|
384
|
+
handle: async function (message, {wallet, messageProvider}) {
|
|
385
|
+
const offerId = message.body.delegationOfferId;
|
|
386
|
+
|
|
387
|
+
if (!offerId) {
|
|
388
|
+
logger.debug(
|
|
389
|
+
'ISSUE_CREDENTIAL_HANDLER: missing delegationOfferId in message body',
|
|
390
|
+
);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const storedOffer = await wallet.getDocumentById(offerId);
|
|
395
|
+
if (!storedOffer) {
|
|
396
|
+
logger.debug(
|
|
397
|
+
`ISSUE_CREDENTIAL_HANDLER: no stored offer found for ${offerId}`,
|
|
398
|
+
);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// SECURITY: only accept credentials from the DID that originally made the offer
|
|
403
|
+
if (message.from !== storedOffer.issuerDID) {
|
|
404
|
+
logger.debug(
|
|
405
|
+
`ISSUE_CREDENTIAL_HANDLER: rejecting credential for offer ${offerId} — sender ${message.from} does not match stored issuerDID ${storedOffer.issuerDID}`,
|
|
406
|
+
);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const credentials = message.body.credentials ?? [];
|
|
411
|
+
const delegationChain = message.body.delegationChain ?? [];
|
|
412
|
+
|
|
413
|
+
if (credentials.length === 0) {
|
|
414
|
+
logger.debug(
|
|
415
|
+
`ISSUE_CREDENTIAL_HANDLER: no credentials in message for offer ${offerId}`,
|
|
416
|
+
);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
for (const credential of credentials) {
|
|
421
|
+
await wallet.addDocument(credential);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
for (const ancestor of delegationChain) {
|
|
425
|
+
const existing = await wallet.getDocumentById(ancestor.id);
|
|
426
|
+
if (!existing) {
|
|
427
|
+
await wallet.addDocument(ancestor);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
await wallet.updateDocument({
|
|
432
|
+
...storedOffer,
|
|
433
|
+
status: 'accepted',
|
|
434
|
+
updatedAt: new Date().toISOString(),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const holderDID = pickDID(message.to);
|
|
438
|
+
const issuerDID = message.from;
|
|
439
|
+
|
|
440
|
+
await messageProvider.sendMessage({
|
|
441
|
+
type: ACK,
|
|
442
|
+
from: holderDID,
|
|
443
|
+
to: issuerDID,
|
|
444
|
+
pthid: message.id,
|
|
445
|
+
body: {
|
|
446
|
+
goal_code: GOAL_CODE,
|
|
447
|
+
delegationOfferId: offerId,
|
|
448
|
+
status: 'OK',
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
wallet.eventManager.emit('delegatedCredentialReceived', {
|
|
453
|
+
delegationOfferId: offerId,
|
|
454
|
+
credentials,
|
|
455
|
+
delegationChain,
|
|
456
|
+
});
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
export const messageHandlers = [
|
|
461
|
+
INVITATION_HANDLER,
|
|
462
|
+
DELEGATION_REQUEST_HANDLER,
|
|
463
|
+
ISSUE_CREDENTIAL_HANDLER,
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
export async function handleMessage(
|
|
467
|
+
message,
|
|
468
|
+
context: {
|
|
469
|
+
wallet;
|
|
470
|
+
messageProvider;
|
|
471
|
+
},
|
|
472
|
+
) {
|
|
473
|
+
const decoded = decodeMessage(message);
|
|
474
|
+
if (!decoded) {
|
|
475
|
+
logger.debug('handleMessage: message could not be decoded, skipping');
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const handler = messageHandlers.find(h => h.check(decoded));
|
|
480
|
+
if (!handler) {
|
|
481
|
+
logger.debug(
|
|
482
|
+
`handleMessage: no handler matched message type ${decoded.type}`,
|
|
483
|
+
);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return handler.handle(decoded, context);
|
|
488
|
+
}
|