@sphereon/ssi-sdk.linked-vp 0.34.1-feature.SSISDK.82.linkedVP.325

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.
@@ -0,0 +1,183 @@
1
+ {
2
+ "ILinkedVPManager": {
3
+ "components": {
4
+ "schemas": {
5
+ "GeneratePresentationArgs": {
6
+ "type": "object",
7
+ "properties": {
8
+ "linkedVpId": {
9
+ "type": "string"
10
+ }
11
+ },
12
+ "required": [
13
+ "linkedVpId"
14
+ ],
15
+ "additionalProperties": false
16
+ },
17
+ "LinkedVPPresentation": {
18
+ "anyOf": [
19
+ {
20
+ "type": "string"
21
+ },
22
+ {
23
+ "$ref": "#/components/schemas/Record<string,any>"
24
+ }
25
+ ]
26
+ },
27
+ "Record<string,any>": {
28
+ "type": "object"
29
+ },
30
+ "GetServiceEntriesArgs": {
31
+ "type": "object",
32
+ "properties": {
33
+ "tenantId": {
34
+ "type": "string"
35
+ }
36
+ },
37
+ "additionalProperties": false
38
+ },
39
+ "LinkedVPServiceEntry": {
40
+ "type": "object",
41
+ "properties": {
42
+ "id": {
43
+ "type": "string"
44
+ },
45
+ "type": {
46
+ "type": "string",
47
+ "const": "LinkedVerifiablePresentation"
48
+ },
49
+ "serviceEndpoint": {
50
+ "type": "string"
51
+ }
52
+ },
53
+ "required": [
54
+ "id",
55
+ "type",
56
+ "serviceEndpoint"
57
+ ],
58
+ "additionalProperties": false
59
+ },
60
+ "HasLinkedVPEntryArgs": {
61
+ "type": "object",
62
+ "properties": {
63
+ "linkedVpId": {
64
+ "type": "string"
65
+ }
66
+ },
67
+ "required": [
68
+ "linkedVpId"
69
+ ],
70
+ "additionalProperties": false
71
+ },
72
+ "PublishCredentialArgs": {
73
+ "type": "object",
74
+ "properties": {
75
+ "digitalCredentialId": {
76
+ "type": "string"
77
+ },
78
+ "linkedVpId": {
79
+ "type": "string"
80
+ },
81
+ "tenantId": {
82
+ "type": "string"
83
+ }
84
+ },
85
+ "required": [
86
+ "digitalCredentialId"
87
+ ],
88
+ "additionalProperties": false
89
+ },
90
+ "LinkedVPEntry": {
91
+ "type": "object",
92
+ "properties": {
93
+ "id": {
94
+ "type": "string"
95
+ },
96
+ "linkedVpId": {
97
+ "type": "string"
98
+ },
99
+ "tenantId": {
100
+ "type": "string"
101
+ },
102
+ "linkedVpFrom": {
103
+ "type": "string",
104
+ "format": "date-time"
105
+ },
106
+ "createdAt": {
107
+ "type": "string",
108
+ "format": "date-time"
109
+ }
110
+ },
111
+ "required": [
112
+ "id",
113
+ "linkedVpId",
114
+ "createdAt"
115
+ ],
116
+ "additionalProperties": false
117
+ },
118
+ "UnpublishCredentialArgs": {
119
+ "type": "object",
120
+ "properties": {
121
+ "linkedVpId": {
122
+ "type": "string"
123
+ }
124
+ },
125
+ "required": [
126
+ "linkedVpId"
127
+ ],
128
+ "additionalProperties": false
129
+ }
130
+ },
131
+ "methods": {
132
+ "lvpGeneratePresentation": {
133
+ "description": "Generate and return a Verifiable Presentation for a published LinkedVP This is the main endpoint handler for GET /linked-vp/",
134
+ "arguments": {
135
+ "$ref": "#/components/schemas/GeneratePresentationArgs"
136
+ },
137
+ "returnType": {
138
+ "$ref": "#/components/schemas/LinkedVPPresentation"
139
+ }
140
+ },
141
+ "lvpGetServiceEntries": {
142
+ "description": "Get LinkedVP service entries for a DID to be added to a DID Document This is useful when generating DID Documents with toDidDocument",
143
+ "arguments": {
144
+ "$ref": "#/components/schemas/GetServiceEntriesArgs"
145
+ },
146
+ "returnType": {
147
+ "type": "array",
148
+ "items": {
149
+ "$ref": "#/components/schemas/LinkedVPServiceEntry"
150
+ }
151
+ }
152
+ },
153
+ "lvpHasEntry": {
154
+ "description": "Check if a LinkedVP entry exists by linkedVpId",
155
+ "arguments": {
156
+ "$ref": "#/components/schemas/HasLinkedVPEntryArgs"
157
+ },
158
+ "returnType": {
159
+ "type": "boolean"
160
+ }
161
+ },
162
+ "lvpPublishCredential": {
163
+ "description": "Publish a credential as a LinkedVP by adding it to the holder's DID Document",
164
+ "arguments": {
165
+ "$ref": "#/components/schemas/PublishCredentialArgs"
166
+ },
167
+ "returnType": {
168
+ "$ref": "#/components/schemas/LinkedVPEntry"
169
+ }
170
+ },
171
+ "lvpUnpublishCredential": {
172
+ "description": "Unpublish a credential by removing its LinkedVP entry from the DID Document",
173
+ "arguments": {
174
+ "$ref": "#/components/schemas/UnpublishCredentialArgs"
175
+ },
176
+ "returnType": {
177
+ "type": "boolean"
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
@@ -0,0 +1,95 @@
1
+ import { IdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution'
2
+ import { SphereonKeyManager } from '@sphereon/ssi-sdk-ext.key-manager'
3
+ import { SphereonKeyManagementSystem } from '@sphereon/ssi-sdk-ext.kms-local'
4
+ import { CredentialStore } from '@sphereon/ssi-sdk.credential-store'
5
+ import { VcdmCredentialPlugin } from '@sphereon/ssi-sdk.credential-vcdm'
6
+ import { CredentialProviderJWT } from '@sphereon/ssi-sdk.credential-vcdm1-jwt-provider'
7
+
8
+ import { DigitalCredentialEntity, DigitalCredentialStore } from '@sphereon/ssi-sdk.data-store'
9
+
10
+ import { Agent } from '@veramo/core'
11
+ import { DIDManager, MemoryDIDStore } from '@veramo/did-manager'
12
+ import { WebDIDProvider } from '@veramo/did-provider-web'
13
+ import { MemoryKeyStore, MemoryPrivateKeyStore } from '@veramo/key-manager'
14
+ import { DataSource } from 'typeorm'
15
+ import { describe } from 'vitest'
16
+ import { LinkedVPManager } from '../agent/LinkedVPManager'
17
+ import linkedVPManagerAgentLogic from './shared/linkedVPManagerAgentLogic'
18
+
19
+ let agent: any
20
+
21
+ const setup = async () => {
22
+ const db = new DataSource({
23
+ type: 'sqlite',
24
+ database: ':memory:',
25
+ synchronize: true,
26
+ entities: [DigitalCredentialEntity],
27
+ })
28
+ await db.initialize()
29
+
30
+ const digitalStore = new DigitalCredentialStore(db)
31
+ const jwt = new CredentialProviderJWT()
32
+
33
+ const plugins = [
34
+ new CredentialStore({ store: digitalStore }),
35
+
36
+ new SphereonKeyManager({
37
+ store: new MemoryKeyStore(),
38
+ kms: {
39
+ local: new SphereonKeyManagementSystem(new MemoryPrivateKeyStore()),
40
+ },
41
+ }),
42
+
43
+ new DIDManager({
44
+ store: new MemoryDIDStore(),
45
+ defaultProvider: 'did:web',
46
+ providers: {
47
+ [`did:web`]: new WebDIDProvider({
48
+ defaultKms: 'local',
49
+ }),
50
+ },
51
+ }),
52
+
53
+ new IdentifierResolution(),
54
+
55
+ new LinkedVPManager({
56
+ holderDids: {
57
+ default: 'did:web:example.com',
58
+ tenant1: 'did:web:example.com:tenants:tenant1',
59
+ },
60
+ }),
61
+
62
+ new VcdmCredentialPlugin({ issuers: [jwt] }),
63
+ ]
64
+
65
+ agent = new Agent({
66
+ plugins,
67
+ })
68
+
69
+ // Create tenant DIDs
70
+ await agent.didManagerImport({
71
+ did: 'did:web:example.com:tenants:tenant1',
72
+ provider: 'did:web',
73
+ keys: [
74
+ {
75
+ kid: 'key-1',
76
+ type: 'Secp256r1',
77
+ privateKeyHex:
78
+ '078c0f0eaa6510fab9f4f2cf8657b32811c53d7d98869fd0d5bd08a7ba34376b8adfdd44784dea407e088ff2437d5e2123e685a26dca91efceb7a9f4dfd81848',
79
+ publicKeyHex: '8adfdd44784dea407e088ff2437d5e2123e685a26dca91efceb7a9f4dfd81848',
80
+ kms: 'local',
81
+ },
82
+ ],
83
+ })
84
+
85
+ return true
86
+ }
87
+
88
+ const tearDown = async () => true
89
+ const getAgent = () => agent
90
+
91
+ const testContext = { getAgent, setup, tearDown, isRestTest: false }
92
+
93
+ describe('LinkedVP Manager Local integration tests', () => {
94
+ linkedVPManagerAgentLogic(testContext)
95
+ })
@@ -0,0 +1,170 @@
1
+ import { CredentialRole } from '@sphereon/ssi-types'
2
+ import { TAgent } from '@veramo/core'
3
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
4
+ import { ILinkedVPManager } from '../../index'
5
+
6
+ type ConfiguredAgent = TAgent<ILinkedVPManager>
7
+
8
+ const holderDid = 'did:web:example.com'
9
+ const tenantId = 'tenant1'
10
+ const holderDidWithTenant = 'did:web:example.com:tenants:tenant1'
11
+
12
+ function createMockVC(typeSuffix: string) {
13
+ return {
14
+ '@context': ['https://www.w3.org/2018/credentials/v1'],
15
+ type: ['VerifiableCredential', typeSuffix],
16
+ issuer: 'did:web:issuer.com',
17
+ issuanceDate: new Date().toISOString(),
18
+ credentialSubject: {
19
+ id: holderDid,
20
+ value: `value-${Math.random().toString(36).slice(2)}`,
21
+ },
22
+ }
23
+ }
24
+
25
+ async function createTestCredential(agent: ConfiguredAgent, tenantId: string) {
26
+ const mockVC = createMockVC('TestCredential')
27
+
28
+ const created = await agent.crsAddCredential({
29
+ credential: {
30
+ credentialRole: CredentialRole.HOLDER,
31
+ rawDocument: JSON.stringify(mockVC),
32
+ issuerCorrelationType: 'DID' as any,
33
+ issuerCorrelationId: 'did:web:issuer.com',
34
+ kmsKeyRef: 'mock-key-ref',
35
+ identifierMethod: 'did:web',
36
+ tenantId,
37
+ },
38
+ })
39
+
40
+ return created.id
41
+ }
42
+
43
+ export default (testContext: {
44
+ getAgent: () => ConfiguredAgent
45
+ setup: () => Promise<boolean>
46
+ tearDown: () => Promise<boolean>
47
+ isRestTest: boolean
48
+ }): void => {
49
+ describe('LinkedVP Manager Agent Plugin', (): void => {
50
+ let agent: ConfiguredAgent
51
+
52
+ beforeAll(async (): Promise<void> => {
53
+ await testContext.setup()
54
+ agent = testContext.getAgent()
55
+ })
56
+
57
+ afterAll(testContext.tearDown)
58
+
59
+ it('should publish credential with auto-generated linkedVpId INCLUDING tenant suffix', async () => {
60
+ const credentialId = await createTestCredential(agent, tenantId)
61
+
62
+ const result = await agent.lvpPublishCredential({
63
+ digitalCredentialId: credentialId,
64
+ })
65
+
66
+ expect(result.linkedVpId).toMatch(/^lvp-\d+-[a-z0-9]+@tenant1$/)
67
+ })
68
+
69
+ it('should publish credential with custom linkedVpId AND append tenantId', async () => {
70
+ const credentialId = await createTestCredential(agent, tenantId)
71
+ const customLinkedVpId = 'my-custom-lvp-id'
72
+
73
+ const result = await agent.lvpPublishCredential({
74
+ digitalCredentialId: credentialId,
75
+ linkedVpId: customLinkedVpId,
76
+ })
77
+
78
+ expect(result.linkedVpId).toBe('my-custom-lvp-id@tenant1')
79
+ })
80
+
81
+ it('should fail to publish already published credential', async () => {
82
+ const credentialId = await createTestCredential(agent, tenantId)
83
+ const linkedVpId = 'already-published'
84
+
85
+ await agent.lvpPublishCredential({
86
+ digitalCredentialId: credentialId,
87
+ linkedVpId,
88
+ })
89
+
90
+ await expect(
91
+ agent.lvpPublishCredential({
92
+ digitalCredentialId: credentialId,
93
+ linkedVpId: 'different-id',
94
+ }),
95
+ ).rejects.toThrow(/already published/)
96
+ })
97
+
98
+ it('should fail to publish duplicate linkedVpId (after tenant appending)', async () => {
99
+ const cred1 = await createTestCredential(agent, tenantId)
100
+ const cred2 = await createTestCredential(agent, tenantId)
101
+ const duplicateId = 'duplicate-lvp-id'
102
+
103
+ await agent.lvpPublishCredential({ digitalCredentialId: cred1, linkedVpId: duplicateId })
104
+
105
+ await expect(
106
+ agent.lvpPublishCredential({
107
+ digitalCredentialId: cred2,
108
+ linkedVpId: duplicateId,
109
+ }),
110
+ ).rejects.toThrow(/already exists/)
111
+ })
112
+
113
+ it('should unpublish a credential', async () => {
114
+ const credentialId = await createTestCredential(agent, tenantId)
115
+ const linkedVpId = 'to-be-unpublished'
116
+
117
+ await agent.lvpPublishCredential({
118
+ digitalCredentialId: credentialId,
119
+ linkedVpId,
120
+ })
121
+
122
+ const result = await agent.lvpUnpublishCredential({
123
+ linkedVpId: `${linkedVpId}@tenant1`,
124
+ })
125
+
126
+ expect(result).toBe(true)
127
+ })
128
+
129
+ it('should get service entries for default tenant (no tenantId param)', async () => {
130
+ const id1 = await createTestCredential(agent, tenantId)
131
+ const id2 = await createTestCredential(agent, tenantId)
132
+
133
+ await agent.lvpPublishCredential({ digitalCredentialId: id1, linkedVpId: 'service1' })
134
+ await agent.lvpPublishCredential({ digitalCredentialId: id2, linkedVpId: 'service2' })
135
+
136
+ const entries = await agent.lvpGetServiceEntries({})
137
+
138
+ expect(entries).toEqual(
139
+ expect.arrayContaining([
140
+ {
141
+ id: `${holderDidWithTenant}#service1@tenant1`,
142
+ type: 'LinkedVerifiablePresentation',
143
+ serviceEndpoint: `https://example.com/tenants/tenant1/linked-vp/service1@tenant1`,
144
+ },
145
+ {
146
+ id: `${holderDidWithTenant}#service2@tenant1`,
147
+ type: 'LinkedVerifiablePresentation',
148
+ serviceEndpoint: `https://example.com/tenants/tenant1/linked-vp/service2@tenant1`,
149
+ },
150
+ ]),
151
+ )
152
+ })
153
+
154
+ /* TODO, did:web may be complicated in a test
155
+ it('should generate presentation for published credentials', async () => {
156
+ const credentialId = await createTestCredential(agent, tenantId)
157
+ const baseId = 'presentation-test'
158
+
159
+ await agent.lvpPublishCredential({
160
+ digitalCredentialId: credentialId,
161
+ linkedVpId: baseId,
162
+ })
163
+
164
+ const fullLinkedVpId = `${baseId}@tenant1`
165
+
166
+ const vp = await agent.lvpGeneratePresentation({ linkedVpId: fullLinkedVpId })
167
+ expect(vp).toBeDefined()
168
+ })*/
169
+ })
170
+ }
@@ -0,0 +1,221 @@
1
+ import { DigitalCredential } from '@sphereon/ssi-sdk.data-store-types'
2
+ import { IAgentPlugin } from '@veramo/core'
3
+ import { IsNull, Not } from 'typeorm'
4
+ import { schema } from '../index'
5
+ import { createLinkedVPPresentation } from '../services/LinkedVPService'
6
+ import {
7
+ GeneratePresentationArgs,
8
+ GetServiceEntriesArgs,
9
+ HasLinkedVPEntryArgs,
10
+ ILinkedVPManager,
11
+ LinkedVPEntry,
12
+ LinkedVPPresentation,
13
+ LinkedVPServiceEntry,
14
+ PublishCredentialArgs,
15
+ RequiredContext,
16
+ UnpublishCredentialArgs,
17
+ } from '../types'
18
+
19
+ // Exposing the methods here for any REST implementation
20
+ export const linkedVPManagerMethods: Array<string> = [
21
+ 'lvpPublishCredential',
22
+ 'lvpUnpublishCredential',
23
+ 'lvpHasEntry',
24
+ 'lvpGetServiceEntries',
25
+ 'lvpGeneratePresentation',
26
+ ]
27
+
28
+ /**
29
+ * {@inheritDoc ILinkedVPManager}
30
+ */
31
+ export class LinkedVPManager implements IAgentPlugin {
32
+ readonly schema = schema.ILinkedVPManager
33
+ readonly methods: ILinkedVPManager = {
34
+ lvpPublishCredential: this.lvpPublishCredential.bind(this),
35
+ lvpUnpublishCredential: this.lvpUnpublishCredential.bind(this),
36
+ lvpHasEntry: this.lvpHasEntry.bind(this),
37
+ lvpGetServiceEntries: this.lvpGetServiceEntries.bind(this),
38
+ lvpGeneratePresentation: this.lvpGeneratePresentation.bind(this),
39
+ }
40
+
41
+ private readonly holderDids: Record<string, string>
42
+
43
+ constructor(options: { holderDids: Record<string, string> }) {
44
+ this.holderDids = options.holderDids
45
+ }
46
+
47
+ private async lvpPublishCredential(args: PublishCredentialArgs, context: RequiredContext): Promise<LinkedVPEntry> {
48
+ const { digitalCredentialId } = args
49
+
50
+ const credential: DigitalCredential = await context.agent.crsGetCredential({ id: digitalCredentialId })
51
+
52
+ if (credential.linkedVpId) {
53
+ return Promise.reject(new Error(`Credential ${digitalCredentialId} is already published with linkedVpId ${credential.linkedVpId}`))
54
+ }
55
+
56
+ const linkedVpId = this.buildLinkedVpId(args.linkedVpId, credential.tenantId)
57
+
58
+ await this.ensureLinkedVpIdUnique(linkedVpId, context, credential.tenantId)
59
+
60
+ const publishedAt = new Date()
61
+ await context.agent.crsUpdateCredential({
62
+ id: digitalCredentialId,
63
+ linkedVpId,
64
+ linkedVpFrom: publishedAt,
65
+ })
66
+
67
+ return {
68
+ id: credential.id,
69
+ linkedVpId,
70
+ tenantId: credential.tenantId,
71
+ linkedVpFrom: publishedAt,
72
+ createdAt: credential.createdAt,
73
+ }
74
+ }
75
+
76
+ private async lvpUnpublishCredential(args: UnpublishCredentialArgs, context: RequiredContext): Promise<boolean> {
77
+ const { linkedVpId } = args
78
+
79
+ // Find credential by linkedVpId and tenantId
80
+ const credentials = await context.agent.crsGetCredentials({
81
+ filter: [{ linkedVpId }],
82
+ })
83
+ if (credentials.length === 0) {
84
+ return Promise.reject(Error(`No credential found with linkedVpId ${linkedVpId}`))
85
+ }
86
+
87
+ const credential = credentials[0]
88
+ await context.agent.crsUpdateCredential({
89
+ id: credential.id,
90
+ linkedVpId: undefined,
91
+ linkedVpFrom: undefined,
92
+ })
93
+
94
+ return true
95
+ }
96
+
97
+ private async lvpHasEntry(args: HasLinkedVPEntryArgs, context: RequiredContext): Promise<boolean> {
98
+ const { linkedVpId } = args
99
+
100
+ try {
101
+ const credentials = await context.agent.crsGetCredentials({
102
+ filter: [{ linkedVpId }],
103
+ })
104
+ return credentials.length > 0
105
+ } catch (error) {
106
+ return false
107
+ }
108
+ }
109
+
110
+ private async lvpGetServiceEntries(args: GetServiceEntriesArgs, context: RequiredContext): Promise<Array<LinkedVPServiceEntry>> {
111
+ const { tenantId } = args
112
+
113
+ // Get all published credentials (credentials with linkedVpId set)
114
+ const filter: any = { linkedVpId: Not(IsNull()) }
115
+ if (tenantId) {
116
+ filter.tenantId = tenantId
117
+ }
118
+
119
+ const credentials = await context.agent.crsGetCredentials({
120
+ filter: [filter],
121
+ })
122
+
123
+ return credentials
124
+ .filter((cred) => cred.linkedVpId !== undefined && cred.linkedVpId !== null)
125
+ .map((cred) => {
126
+ const holderDidForEntry = this.getHolderDid(cred.tenantId)
127
+ return this.credentialToServiceEntry(cred, holderDidForEntry)
128
+ })
129
+ }
130
+
131
+ private async lvpGeneratePresentation(args: GeneratePresentationArgs, context: RequiredContext): Promise<LinkedVPPresentation> {
132
+ const { linkedVpId } = args
133
+ const tenantId = this.parseTenantFromLinkedVpId(linkedVpId)
134
+ const holderDid = this.getHolderDid(tenantId)
135
+
136
+ const uniqueCredentials = await context.agent.crsGetUniqueCredentials({
137
+ filter: [
138
+ {
139
+ linkedVpId: args.linkedVpId,
140
+ ...(tenantId && { tenantId }),
141
+ },
142
+ ],
143
+ })
144
+ if (uniqueCredentials.length === 0) {
145
+ return Promise.reject(Error(`No published credentials found for linkedVpId ${linkedVpId}`))
146
+ }
147
+
148
+ // Generate the Verifiable Presentation with all published credentials
149
+ return createLinkedVPPresentation(holderDid, uniqueCredentials, context.agent)
150
+ }
151
+
152
+ private getHolderDid(tenantId: string | undefined) {
153
+ const holderDid = this.holderDids[tenantId ?? 'default']
154
+ if (!holderDid) {
155
+ throw Error(`No holder did supplied for tenant ${tenantId ?? 'default'}`)
156
+ }
157
+ return holderDid
158
+ }
159
+
160
+ private parseTenantFromLinkedVpId(linkedVpId: string): string | undefined {
161
+ const idx = linkedVpId.lastIndexOf('@')
162
+ return idx === -1 ? undefined : linkedVpId.substring(idx + 1)
163
+ }
164
+
165
+ private generateLinkedVpId(): string {
166
+ return `lvp-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`
167
+ }
168
+
169
+ private async ensureLinkedVpIdUnique(linkedVpId: string, context: RequiredContext, tenantId?: string): Promise<void> {
170
+ const credentials = await context.agent.crsGetCredentials({
171
+ filter: [{ linkedVpId, ...(tenantId && { tenantId }) }],
172
+ })
173
+
174
+ if (credentials.length > 0) {
175
+ throw new Error(`LinkedVP ID ${linkedVpId} already exists${tenantId ? ` for tenant ${tenantId}` : ''}`)
176
+ }
177
+ }
178
+
179
+ private buildLinkedVpId(linkedVpId: string | undefined, tenantId: string | undefined) {
180
+ let finalLinkedVpId = linkedVpId || this.generateLinkedVpId()
181
+
182
+ // Append tenantId if provided and not already present
183
+ if (tenantId && tenantId !== '' && !finalLinkedVpId.includes('@')) {
184
+ finalLinkedVpId = `${finalLinkedVpId}@${tenantId}`
185
+ }
186
+ return finalLinkedVpId
187
+ }
188
+
189
+ private getBaseUrlFromDid(holderDid: string): string {
190
+ if (!holderDid.startsWith('did:web:')) {
191
+ throw new Error(`Invalid DID: ${holderDid}, must be did:web`)
192
+ }
193
+
194
+ const withoutPrefix = holderDid.replace('did:web:', '') // example.com:tenants:tenant1
195
+ const parts = withoutPrefix.split(':')
196
+ const domain = parts.shift()! // example.com
197
+ const path = parts.join('/') // tenants/tenant1
198
+
199
+ return path
200
+ ? `https://${domain}/${path}` // https://example.com/tenants/tenant1
201
+ : `https://${domain}` // https://example.com
202
+ }
203
+
204
+ private buildServiceEndpoint(holderDid: string, linkedVpId: string): string {
205
+ const baseUrl = this.getBaseUrlFromDid(holderDid)
206
+ const cleanBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl
207
+ return `${cleanBaseUrl}/linked-vp/${linkedVpId}`
208
+ }
209
+
210
+ private credentialToServiceEntry(credential: DigitalCredential, holderDid: string): LinkedVPServiceEntry {
211
+ if (!credential.linkedVpId) {
212
+ throw new Error(`Credential ${credential.id} does not have a linkedVpId`)
213
+ }
214
+
215
+ return {
216
+ id: `${holderDid}#${credential.linkedVpId}`,
217
+ type: 'LinkedVerifiablePresentation',
218
+ serviceEndpoint: this.buildServiceEndpoint(holderDid, credential.linkedVpId),
219
+ }
220
+ }
221
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @public
3
+ */
4
+ import schema from '../plugin.schema.json'
5
+ export { schema }
6
+ export { LinkedVPManager, linkedVPManagerMethods } from './agent/LinkedVPManager'
7
+ export * from './types/ILinkedVPManager'