@sphereon/ssi-sdk.linked-vp 0.34.1-feature.SSISDK.82.linkedVP.328 → 0.36.1-feature.SSISDK.70.integrate.digidentity.55
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/dist/index.cjs +146 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -5
- package/dist/index.d.ts +6 -5
- package/dist/index.js +146 -105
- package/dist/index.js.map +1 -1
- package/package.json +15 -15
- package/src/__tests__/shared/linkedVPManagerAgentLogic.ts +16 -6
- package/src/agent/LinkedVPManager.ts +49 -23
- package/src/services/LinkedVPService.ts +143 -100
- package/src/types/ILinkedVPManager.ts +6 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DigitalCredential } from '@sphereon/ssi-sdk.data-store-types'
|
|
2
|
+
import { type IVerifiableCredential } from '@sphereon/ssi-types'
|
|
2
3
|
import { IAgentPlugin } from '@veramo/core'
|
|
3
4
|
import { IsNull, Not } from 'typeorm'
|
|
4
5
|
import { schema } from '../index'
|
|
@@ -38,12 +39,6 @@ export class LinkedVPManager implements IAgentPlugin {
|
|
|
38
39
|
lvpGeneratePresentation: this.lvpGeneratePresentation.bind(this),
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
private readonly holderDids: Record<string, string>
|
|
42
|
-
|
|
43
|
-
constructor(options: { holderDids: Record<string, string> }) {
|
|
44
|
-
this.holderDids = options.holderDids
|
|
45
|
-
}
|
|
46
|
-
|
|
47
42
|
private async lvpPublishCredential(args: PublishCredentialArgs, context: RequiredContext): Promise<LinkedVPEntry> {
|
|
48
43
|
const { digitalCredentialId } = args
|
|
49
44
|
|
|
@@ -57,18 +52,20 @@ export class LinkedVPManager implements IAgentPlugin {
|
|
|
57
52
|
|
|
58
53
|
await this.ensureLinkedVpIdUnique(linkedVpId, context, credential.tenantId)
|
|
59
54
|
|
|
60
|
-
const
|
|
55
|
+
const publishAt = args.linkedVpFrom ?? new Date()
|
|
61
56
|
await context.agent.crsUpdateCredential({
|
|
62
57
|
id: digitalCredentialId,
|
|
63
58
|
linkedVpId,
|
|
64
|
-
linkedVpFrom:
|
|
59
|
+
linkedVpFrom: publishAt,
|
|
60
|
+
linkedVpUntil: args.linkedVpUntil,
|
|
65
61
|
})
|
|
66
62
|
|
|
67
63
|
return {
|
|
68
64
|
id: credential.id,
|
|
69
65
|
linkedVpId,
|
|
70
66
|
tenantId: credential.tenantId,
|
|
71
|
-
linkedVpFrom:
|
|
67
|
+
linkedVpFrom: publishAt,
|
|
68
|
+
linkedVpUntil: args.linkedVpUntil,
|
|
72
69
|
createdAt: credential.createdAt,
|
|
73
70
|
}
|
|
74
71
|
}
|
|
@@ -108,35 +105,38 @@ export class LinkedVPManager implements IAgentPlugin {
|
|
|
108
105
|
}
|
|
109
106
|
|
|
110
107
|
private async lvpGetServiceEntries(args: GetServiceEntriesArgs, context: RequiredContext): Promise<Array<LinkedVPServiceEntry>> {
|
|
111
|
-
const { tenantId } = args
|
|
108
|
+
const { tenantId, subjectDid } = args
|
|
112
109
|
|
|
113
110
|
// Get all published credentials (credentials with linkedVpId set)
|
|
114
111
|
const filter: any = { linkedVpId: Not(IsNull()) }
|
|
115
112
|
if (tenantId) {
|
|
116
113
|
filter.tenantId = tenantId
|
|
117
114
|
}
|
|
115
|
+
if (subjectDid) {
|
|
116
|
+
filter.subjectCorrelationId = subjectDid
|
|
117
|
+
}
|
|
118
118
|
|
|
119
119
|
const credentials = await context.agent.crsGetCredentials({
|
|
120
120
|
filter: [filter],
|
|
121
121
|
})
|
|
122
122
|
|
|
123
123
|
return credentials
|
|
124
|
-
.
|
|
125
|
-
|
|
126
|
-
const holderDidForEntry = this.getHolderDid(
|
|
127
|
-
return this.credentialToServiceEntry(cred, holderDidForEntry)
|
|
124
|
+
.flatMap((cred) => {
|
|
125
|
+
const uniformDocument = JSON.parse(cred.uniformDocument) as IVerifiableCredential
|
|
126
|
+
const holderDidForEntry = this.getHolderDid(uniformDocument)
|
|
127
|
+
return holderDidForEntry && holderDidForEntry.startsWith('did:web') ? [this.credentialToServiceEntry(cred, holderDidForEntry)] : []
|
|
128
128
|
})
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
private async lvpGeneratePresentation(args: GeneratePresentationArgs, context: RequiredContext): Promise<LinkedVPPresentation> {
|
|
132
132
|
const { linkedVpId } = args
|
|
133
133
|
const tenantId = this.parseTenantFromLinkedVpId(linkedVpId)
|
|
134
|
-
const holderDid = this.getHolderDid(tenantId)
|
|
135
134
|
|
|
136
135
|
const uniqueCredentials = await context.agent.crsGetUniqueCredentials({
|
|
137
136
|
filter: [
|
|
138
137
|
{
|
|
139
138
|
linkedVpId: args.linkedVpId,
|
|
139
|
+
linkedVpUntil: args.linkedVpUntil,
|
|
140
140
|
...(tenantId && { tenantId }),
|
|
141
141
|
},
|
|
142
142
|
],
|
|
@@ -148,16 +148,37 @@ export class LinkedVPManager implements IAgentPlugin {
|
|
|
148
148
|
return Promise.reject(Error(`Multiple credentials found for linkedVpId ${linkedVpId}`))
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
const uniqueDigitalCredential = uniqueCredentials[0]
|
|
152
|
+
if (!uniqueDigitalCredential.uniformVerifiableCredential) {
|
|
153
|
+
return Promise.reject(Error(`uniformVerifiableCredential could not be found for credential ${uniqueDigitalCredential.digitalCredential.id}`))
|
|
154
|
+
}
|
|
155
|
+
const holderDid = this.getHolderDid(uniqueDigitalCredential.uniformVerifiableCredential)
|
|
156
|
+
if (!holderDid) {
|
|
157
|
+
return Promise.reject(Error(`Could not extract the holder did:web from cnf nor the credentialSubject id`))
|
|
158
|
+
}
|
|
159
|
+
|
|
151
160
|
// Generate the Verifiable Presentation with all published credentials
|
|
152
|
-
return createLinkedVPPresentation(holderDid,
|
|
161
|
+
return createLinkedVPPresentation(holderDid, uniqueDigitalCredential, context.agent)
|
|
153
162
|
}
|
|
154
163
|
|
|
155
|
-
private getHolderDid(
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
164
|
+
private getHolderDid(uniformDocument: IVerifiableCredential): string | undefined {
|
|
165
|
+
// Determine holder DID for identifier resolution
|
|
166
|
+
if ('cnf' in uniformDocument && 'jwk' in uniformDocument.cnf && 'kid' in uniformDocument.cnf.jwk) {
|
|
167
|
+
return uniformDocument.cnf.jwk.kid.split('#')[0]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if ('credentialSubject' in uniformDocument) {
|
|
171
|
+
const credentialSubject = Array.isArray(uniformDocument.credentialSubject)
|
|
172
|
+
? uniformDocument.credentialSubject[0]
|
|
173
|
+
: uniformDocument.credentialSubject
|
|
174
|
+
if ('id' in credentialSubject && credentialSubject.id) {
|
|
175
|
+
if (credentialSubject.id.startsWith('did:web')) {
|
|
176
|
+
return credentialSubject.id
|
|
177
|
+
}
|
|
178
|
+
}
|
|
159
179
|
}
|
|
160
|
-
|
|
180
|
+
|
|
181
|
+
return undefined
|
|
161
182
|
}
|
|
162
183
|
|
|
163
184
|
private parseTenantFromLinkedVpId(linkedVpId: string): string | undefined {
|
|
@@ -182,8 +203,13 @@ export class LinkedVPManager implements IAgentPlugin {
|
|
|
182
203
|
private buildLinkedVpId(linkedVpId: string | undefined, tenantId: string | undefined) {
|
|
183
204
|
let finalLinkedVpId = linkedVpId || this.generateLinkedVpId()
|
|
184
205
|
|
|
185
|
-
//
|
|
186
|
-
if (
|
|
206
|
+
// Validate that user-provided ID doesn't contain @ char reserved for tenant id separator
|
|
207
|
+
if (linkedVpId && linkedVpId.includes('@')) {
|
|
208
|
+
throw new Error(`LinkedVP ID cannot contain '@' character as it is reserved for tenant separation`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Append tenantId if provided
|
|
212
|
+
if (tenantId && tenantId !== '') {
|
|
187
213
|
finalLinkedVpId = `${finalLinkedVpId}@${tenantId}`
|
|
188
214
|
}
|
|
189
215
|
return finalLinkedVpId
|
|
@@ -14,6 +14,37 @@ import { LinkedVPPresentation, LOGGER_NAMESPACE, RequiredContext } from '../type
|
|
|
14
14
|
const logger = Loggers.DEFAULT.get(LOGGER_NAMESPACE)
|
|
15
15
|
const CLOCK_SKEW = 120 // TODO make adjustable?
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Creates a Verifiable Presentation for LinkedVP publishing
|
|
19
|
+
* Contains multiple credentials in a single JWT VP
|
|
20
|
+
* No nonce or audience since this is for publishing, not responding to verification
|
|
21
|
+
*/
|
|
22
|
+
export async function createLinkedVPPresentation(
|
|
23
|
+
holderDid: string,
|
|
24
|
+
credential: UniqueDigitalCredential,
|
|
25
|
+
agent: RequiredContext['agent'],
|
|
26
|
+
): Promise<LinkedVPPresentation> {
|
|
27
|
+
logger.debug(`Creating LinkedVP presentation for ${holderDid} of credential ${credential.id}`)
|
|
28
|
+
|
|
29
|
+
const originalCredential = extractOriginalCredential(credential)
|
|
30
|
+
const documentFormat = CredentialMapper.detectDocumentType(originalCredential)
|
|
31
|
+
|
|
32
|
+
switch (documentFormat) {
|
|
33
|
+
case DocumentFormat.SD_JWT_VC: {
|
|
34
|
+
return createSdJwtPresentation(originalCredential, agent)
|
|
35
|
+
}
|
|
36
|
+
case DocumentFormat.JSONLD: {
|
|
37
|
+
return createJsonLdPresentation(holderDid, originalCredential, agent)
|
|
38
|
+
}
|
|
39
|
+
case DocumentFormat.MSO_MDOC: {
|
|
40
|
+
return createMdocPresentation(originalCredential)
|
|
41
|
+
}
|
|
42
|
+
default: {
|
|
43
|
+
return createJwtPresentation(holderDid, originalCredential, agent)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
17
48
|
/**
|
|
18
49
|
* Extracts the original credential from various wrapper types
|
|
19
50
|
*/
|
|
@@ -40,111 +71,123 @@ function extractOriginalCredential(
|
|
|
40
71
|
}
|
|
41
72
|
|
|
42
73
|
/**
|
|
43
|
-
* Creates
|
|
44
|
-
* Contains multiple credentials in a single JWT VP
|
|
45
|
-
* No nonce or audience since this is for publishing, not responding to verification
|
|
74
|
+
* Creates an SD-JWT presentation with KB-JWT
|
|
46
75
|
*/
|
|
47
|
-
|
|
76
|
+
async function createSdJwtPresentation(
|
|
77
|
+
originalCredential: OriginalVerifiableCredential,
|
|
78
|
+
agent: RequiredContext['agent'],
|
|
79
|
+
): Promise<LinkedVPPresentation> {
|
|
80
|
+
// SD-JWT with KB-JWT
|
|
81
|
+
const decodedSdJwt = await CredentialMapper.decodeSdJwtVcAsync(
|
|
82
|
+
typeof originalCredential === 'string' ? originalCredential : (originalCredential as SdJwtDecodedVerifiableCredential).compactSdJwtVc,
|
|
83
|
+
defaultGenerateDigest,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const hashAlg = decodedSdJwt.signedPayload._sd_alg ?? 'sha-256'
|
|
87
|
+
const sdHash = calculateSdHash(decodedSdJwt.compactSdJwtVc, hashAlg, defaultGenerateDigest)
|
|
88
|
+
const kbJwtPayload: PartialSdJwtKbJwt['payload'] = {
|
|
89
|
+
iat: Math.floor(Date.now() / 1000 - CLOCK_SKEW),
|
|
90
|
+
sd_hash: sdHash,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const presentationResult = await agent.createSdJwtPresentation({
|
|
94
|
+
presentation: decodedSdJwt.compactSdJwtVc,
|
|
95
|
+
kb: {
|
|
96
|
+
payload: kbJwtPayload as any, // FIXME? (typescript seems impossible)
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
documentFormat: DocumentFormat.SD_JWT_VC,
|
|
102
|
+
presentationPayload: presentationResult.presentation,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Creates a JSON-LD presentation with proof
|
|
108
|
+
*/
|
|
109
|
+
async function createJsonLdPresentation(
|
|
48
110
|
holderDid: string,
|
|
49
|
-
|
|
111
|
+
originalCredential: OriginalVerifiableCredential,
|
|
50
112
|
agent: RequiredContext['agent'],
|
|
51
113
|
): Promise<LinkedVPPresentation> {
|
|
52
|
-
|
|
114
|
+
// JSON-LD VC - create JSON-LD VP with challenge and domain in proof
|
|
115
|
+
const vcObject = typeof originalCredential === 'string' ? JSON.parse(originalCredential) : originalCredential
|
|
116
|
+
|
|
117
|
+
const vpObject = {
|
|
118
|
+
'@context': ['https://www.w3.org/2018/credentials/v1'],
|
|
119
|
+
type: ['VerifiablePresentation'],
|
|
120
|
+
verifiableCredential: [vcObject],
|
|
121
|
+
holder: holderDid,
|
|
122
|
+
}
|
|
53
123
|
|
|
54
124
|
const identifier = await agent.identifierManagedGet({ identifier: holderDid })
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const vcJwt = typeof originalCredential === 'string' ? originalCredential : JSON.stringify(originalCredential)
|
|
123
|
-
|
|
124
|
-
// Create VP JWT using agent method
|
|
125
|
-
const vpPayload = {
|
|
126
|
-
iss: holderDid,
|
|
127
|
-
vp: {
|
|
128
|
-
'@context': ['https://www.w3.org/2018/credentials/v1'],
|
|
129
|
-
type: ['VerifiablePresentation'],
|
|
130
|
-
holder: holderDid,
|
|
131
|
-
verifiableCredential: [vcJwt],
|
|
132
|
-
},
|
|
133
|
-
iat: Math.floor(Date.now() / 1000 - CLOCK_SKEW),
|
|
134
|
-
exp: Math.floor(Date.now() / 1000 + 600 + CLOCK_SKEW), // 10 minutes
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Use the agent's JWT creation capability
|
|
138
|
-
const vpJwt = await agent.createVerifiablePresentation({
|
|
139
|
-
presentation: vpPayload.vp,
|
|
140
|
-
proofFormat: 'jwt',
|
|
141
|
-
keyRef: identifier.kmsKeyRef || identifier.kid,
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
documentFormat,
|
|
146
|
-
presentationPayload: (vpJwt.proof && 'jwt' in vpJwt.proof && vpJwt.proof.jwt) || vpJwt,
|
|
147
|
-
}
|
|
148
|
-
}
|
|
125
|
+
|
|
126
|
+
// Create JSON-LD VP with proof
|
|
127
|
+
const verifiablePresentationSP = await agent.createVerifiablePresentation({
|
|
128
|
+
presentation: vpObject,
|
|
129
|
+
proofFormat: 'lds',
|
|
130
|
+
keyRef: identifier.kmsKeyRef || identifier.kid,
|
|
131
|
+
})
|
|
132
|
+
return {
|
|
133
|
+
documentFormat: DocumentFormat.JSONLD,
|
|
134
|
+
presentationPayload: verifiablePresentationSP,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Creates an ISO mdoc presentation (basic support)
|
|
140
|
+
*/
|
|
141
|
+
async function createMdocPresentation(originalCredential: OriginalVerifiableCredential): Promise<LinkedVPPresentation> {
|
|
142
|
+
// ISO mdoc - create mdoc VP token
|
|
143
|
+
// This is a placeholder implementation
|
|
144
|
+
// Full implementation would require:
|
|
145
|
+
// 1. Decode the mdoc using CredentialMapper or mdoc utilities
|
|
146
|
+
// 2. Build proper mdoc VP token with session transcript
|
|
147
|
+
// 3. Include nonce/audience in the session transcript
|
|
148
|
+
logger.warning('mso_mdoc format has basic support - production use requires proper mdoc VP token implementation')
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
documentFormat: DocumentFormat.MSO_MDOC,
|
|
152
|
+
presentationPayload: originalCredential,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Creates a JWT presentation
|
|
158
|
+
*/
|
|
159
|
+
async function createJwtPresentation(
|
|
160
|
+
holderDid: string,
|
|
161
|
+
originalCredential: OriginalVerifiableCredential,
|
|
162
|
+
agent: RequiredContext['agent'],
|
|
163
|
+
): Promise<LinkedVPPresentation> {
|
|
164
|
+
// JWT VC - create JWT VP with nonce and aud in payload
|
|
165
|
+
const vcJwt = typeof originalCredential === 'string' ? originalCredential : JSON.stringify(originalCredential)
|
|
166
|
+
|
|
167
|
+
const identifier = await agent.identifierManagedGet({ identifier: holderDid })
|
|
168
|
+
|
|
169
|
+
// Create VP JWT using agent method
|
|
170
|
+
const vpPayload = {
|
|
171
|
+
iss: holderDid,
|
|
172
|
+
vp: {
|
|
173
|
+
'@context': ['https://www.w3.org/2018/credentials/v1'],
|
|
174
|
+
type: ['VerifiablePresentation'],
|
|
175
|
+
holder: holderDid,
|
|
176
|
+
verifiableCredential: [vcJwt],
|
|
177
|
+
},
|
|
178
|
+
iat: Math.floor(Date.now() / 1000 - CLOCK_SKEW),
|
|
179
|
+
exp: Math.floor(Date.now() / 1000 + 600 + CLOCK_SKEW), // 10 minutes
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Use the agent's JWT creation capability
|
|
183
|
+
const vpJwt = await agent.createVerifiablePresentation({
|
|
184
|
+
presentation: vpPayload.vp,
|
|
185
|
+
proofFormat: 'jwt',
|
|
186
|
+
keyRef: identifier.kmsKeyRef || identifier.kid,
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
documentFormat: DocumentFormat.JWT,
|
|
191
|
+
presentationPayload: (vpJwt.proof && 'jwt' in vpJwt.proof && vpJwt.proof.jwt) || vpJwt,
|
|
149
192
|
}
|
|
150
193
|
}
|
|
@@ -55,6 +55,8 @@ export interface ILinkedVPManager extends IPluginMethodMap {
|
|
|
55
55
|
export type PublishCredentialArgs = {
|
|
56
56
|
digitalCredentialId: string
|
|
57
57
|
linkedVpId?: string // Optional: if not provided, will be auto-generated
|
|
58
|
+
linkedVpFrom?: Date
|
|
59
|
+
linkedVpUntil?: Date
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
export type UnpublishCredentialArgs = {
|
|
@@ -66,18 +68,21 @@ export type HasLinkedVPEntryArgs = {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
export type GetServiceEntriesArgs = {
|
|
71
|
+
subjectDid?: string
|
|
69
72
|
tenantId?: string
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
export type GeneratePresentationArgs = {
|
|
73
76
|
linkedVpId: string
|
|
77
|
+
linkedVpUntil?: Date
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
export type LinkedVPEntry = {
|
|
77
81
|
id: string
|
|
78
82
|
linkedVpId: string
|
|
79
|
-
tenantId?: string
|
|
80
83
|
linkedVpFrom?: Date
|
|
84
|
+
linkedVpUntil?: Date
|
|
85
|
+
tenantId?: string
|
|
81
86
|
createdAt: Date
|
|
82
87
|
}
|
|
83
88
|
|