@sphereon/ssi-sdk.vc-status-list-issuer
Version:
Sphereon SSI-SDK plugin for Status List issuance
324 lines (295 loc) • 13.4 kB
text/typescript
import { DataSources } from '@sphereon/ssi-sdk.agent-config'
import {
createNewStatusList,
CreateNewStatusListArgs,
CredentialWithStatusSupport,
GetStatusListArgs,
IAddStatusToCredentialArgs,
IAddStatusToSdJwtCredentialArgs,
IRequiredContext,
IRequiredPlugins,
IStatusListPlugin,
StatusListResult,
} from '@sphereon/ssi-sdk.vc-status-list'
import { getDriver, IStatusListDriver } from '@sphereon/ssi-sdk.vc-status-list-issuer-drivers'
import { Loggers, OrPromise } from '@sphereon/ssi-types'
import { IAgentContext, IAgentPlugin, IKeyManager } from '@veramo/core'
import { createStatusList, handleCredentialStatus, handleSdJwtCredentialStatus } from '../functions'
import { StatusListInstance } from '../types'
import { SdJwtVcPayload } from '@sd-jwt/sd-jwt-vc'
import { DataSource } from 'typeorm'
const logger = Loggers.DEFAULT.get('sphereon:ssi-sdk:vc-status-list')
export class StatusListPlugin implements IAgentPlugin {
private readonly instances: Array<StatusListInstance> = new Array<StatusListInstance>()
private readonly defaultStatusListId?: string
private readonly allDataSources: DataSources
private initialized: Boolean = false
readonly methods: IStatusListPlugin = {
slAddStatusToCredential: this.slAddStatusToCredential.bind(this),
slAddStatusToSdJwtCredential: this.slAddStatusToSdJwtCredential.bind(this),
slCreateStatusList: this.slCreateStatusList.bind(this),
slGetStatusList: this.slGetStatusList.bind(this),
slImportStatusLists: this.slImportStatusLists.bind(this),
}
constructor(opts: {
instances?: Array<StatusListInstance>
defaultStatusListId?: string
allDataSources?: DataSources
autoCreateInstances?: boolean
}) {
this.instances = opts.instances ?? []
// TODO: Do we only want the instances configured, or do we also want to look them up from the DB
const instanceId = opts.defaultStatusListId ?? opts.instances?.[0]?.id
if (!instanceId) {
logger.warning(
`Could not deduce the default instance id from the status lists, so no default status list will be used. Please configure the default instance id in the agent configuration.`,
)
}
this.defaultStatusListId = instanceId
this.allDataSources = opts.allDataSources ?? DataSources.singleInstance()
}
private async init() {
if (!this.initialized) {
for (const dbName of this.allDataSources.getDbNames()) {
const driver = await getDriver({ dbName, dataSources: this.allDataSources })
const statusLists = await driver.getStatusLists()
const instances = await Promise.all(
statusLists.map((statusList) => {
const dataSource = this.allDataSources.getDbConnection(dbName)
return this.createStatusListInstance(statusList, dataSource, driver)
}),
)
this.instances.push(...instances)
}
this.initialized = true
}
}
private async slImportStatusLists(
imports: Array<CreateNewStatusListArgs>,
context: IAgentContext<IRequiredPlugins & IStatusListPlugin & IKeyManager>,
): Promise<boolean> {
await this.init()
for (const instanceImport of imports) {
const dataSource = this.allDataSources.getDbConnection(instanceImport.dbName ?? 'default')
await createStatusList(instanceImport, dataSource, context)
}
return true
}
private createStatusListInstance(statusList: StatusListResult, dataSource: OrPromise<DataSource>, driver: IStatusListDriver) {
return {
id: statusList.id,
correlationId: statusList.correlationId,
issuer: typeof statusList.issuer === 'string' ? statusList.issuer : statusList.issuer.id,
dataSource: dataSource,
driverOptions: driver.getOptions(),
driverType: driver.getType(),
} satisfies StatusListInstance
}
private async getDriverForStatusListOption(effectiveStatusListId: string, correlationId?: string) {
let instance
if (correlationId && correlationId.trim() !== '') {
instance = this.instances.find((inst) => inst.correlationId === correlationId)
} else {
instance = this.instances.find((inst) => inst.id === effectiveStatusListId)
}
if (!instance) {
throw Error(`Status list with identifier ${correlationId ?? effectiveStatusListId} is not managed by the status list plugin`)
}
if (!instance.dataSource && !instance.driverOptions?.dbName) {
throw Error(`Either a datasource or dbName needs to be supplied`)
}
const dataSource = instance.dataSource ? await instance.dataSource : await this.allDataSources.getDbConnection(instance.driverOptions!.dbName!)
const driver =
correlationId && correlationId.trim() !== ''
? await getDriver({ dataSource, correlationId })
: await getDriver({ dataSource, id: effectiveStatusListId })
return driver
}
private async slGetStatusList(args: GetStatusListArgs): Promise<StatusListResult> {
await this.init()
const sl = this.instances.find((instance) => instance.id === args.id || instance.correlationId === args.correlationId)
const dataSource = await this.selectDatasource({ dbName: args.dbName, dataSource: args.dataSource ?? sl?.dataSource, instance: sl })
const driver = await getDriver({
id: args.id ?? sl?.id,
correlationId: args.correlationId ?? sl?.correlationId,
dataSource,
})
return await driver.getStatusList()
}
private async selectDatasource(args: { dbName?: string; dataSource?: OrPromise<DataSource>; instance?: StatusListInstance }) {
const dbName = args.dbName ?? this.allDataSources.getDbNames()[0]
if (args?.dataSource) {
return args.dataSource
} else if (args.instance?.dataSource) {
return args.instance.dataSource
} else if (args.dbName) {
return await this.allDataSources.getDbConnection(dbName)
} else {
return await this.allDataSources.getDbConnection(dbName)
}
}
private async slCreateStatusList(
args: CreateNewStatusListArgs,
context: IAgentContext<IRequiredPlugins & IStatusListPlugin & IKeyManager>,
): Promise<StatusListResult> {
await this.init()
const sl = await createNewStatusList(args, context)
const dataSource = await this.selectDatasource({ dbName: args.dbName, dataSource: args.dataSource })
const driver = await getDriver({
id: sl.id,
correlationId: sl.correlationId,
dataSource,
})
let statusListDetails: StatusListResult | undefined = undefined
try {
statusListDetails = await this.slGetStatusList(args)
} catch (e) {
// That is fine if there is no status list yet
}
if (statusListDetails && this.instances.find((instance) => instance.id === args.id || instance.correlationId === args.correlationId)) {
return Promise.reject(Error(`Status list with id ${args.id} or correlation id ${args.correlationId} already exists`))
} else {
statusListDetails = await driver.createStatusList({
statusListCredential: sl.statusListCredential,
correlationId: sl.correlationId,
})
this.instances.push(this.createStatusListInstance(statusListDetails, dataSource, driver))
}
return statusListDetails
}
/**
* Adds status information to a credential by either:
* 1. Using existing status ID from the credential if present
* 2. Using provided status list options
* 3. Falling back to the default status list ID
*
* @param args Contains credential and status options
* @param context Required agent context
* @returns Credential with added status information
*/
private async slAddStatusToCredential(args: IAddStatusToCredentialArgs, context: IRequiredContext): Promise<CredentialWithStatusSupport> {
const { credential, ...rest } = args
logger.debug(`Adding status to credential ${credential.id ?? 'without ID'}`)
await this.init()
const credentialStatus = credential.credentialStatus
if (credentialStatus && (!rest.statusLists || rest.statusLists.length == 0)) {
let existingStatusId: string | undefined
if (Array.isArray(credentialStatus)) {
// This was implemented with VCDM v2.0 support, but the rest of the SDK is not ready for that, so ICredential.credentialStatus's array union is disabled for now
for (const stat of credentialStatus) {
if (stat.id && stat.id.trim() !== '') {
existingStatusId = stat.id.split('#')[0]
break
}
}
} else if (credentialStatus.id && credentialStatus.id.trim() !== '') {
existingStatusId = credentialStatus.id.split('#')[0]
}
if (existingStatusId) {
logger.debug(`Using existing status ID ${existingStatusId} for credential ${credential.id ?? 'without ID'}`)
const instance = this.instances.find((inst) => inst.id === existingStatusId)
if (!instance) {
throw Error(`Status list with id ${existingStatusId} is not managed by the status list plugin`)
}
if (!instance.dataSource && !instance.driverOptions?.dbName) {
throw Error(`Either a datasource or dbName needs to be supplied`)
}
const credentialId = credential.id ?? rest.credentialId
const dataSource = instance.dataSource
? await instance.dataSource
: await this.allDataSources.getDbConnection(instance.driverOptions!.dbName!)
const driver = await getDriver({ dataSource, id: existingStatusId })
await handleCredentialStatus(credential, {
...rest,
credentialId,
statusLists: [{ statusListId: existingStatusId }],
driver,
})
return credential
}
}
const statusListOpts = rest.statusLists?.length ? rest.statusLists : []
logger.debug(`Adding new status using ${statusListOpts.length} status list option(s)`)
const credentialId = credential.id ?? rest.credentialId
for (const opt of statusListOpts) {
const effectiveStatusListId = opt.statusListId ?? this.defaultStatusListId
if (!effectiveStatusListId) {
return Promise.reject(
Error(
`No status list ID provided and no default status list ID configured. Please provide a status list ID or configure a default status list ID in the agent configuration.`,
),
)
}
const driver = await this.getDriverForStatusListOption(effectiveStatusListId, opt.statusListCorrelationId)
await handleCredentialStatus(credential, {
...rest,
credentialId,
statusLists: [
{
...opt,
statusListId: effectiveStatusListId,
},
],
driver,
})
}
logger.debug(`Successfully added status information to credential ${credential.id ?? 'without ID'}`)
return credential
}
/**
* Adds status information to an SD-JWT credential by either:
* 1. Using existing status URI from the credential if present
* 2. Using provided status list options
* 3. Falling back to the default status list ID
*
* @param args Contains SD-JWT credential and status options
* @param context Required agent context
* @returns SD-JWT credential with added status information
*/
private async slAddStatusToSdJwtCredential(args: IAddStatusToSdJwtCredentialArgs, context: IRequiredContext): Promise<SdJwtVcPayload> {
const { credential, ...rest } = args
logger.debug(`Adding status to SD-JWT credential`)
await this.init()
const credentialStatus = credential.status
if (credentialStatus && (!rest.statusLists || rest.statusLists.length == 0)) {
let existingStatusUri: string | undefined
if (credentialStatus.status_list && credentialStatus.status_list.uri && credentialStatus.status_list.uri.trim() !== '') {
existingStatusUri = credentialStatus.status_list.uri
}
if (existingStatusUri) {
logger.debug(`Using existing status URI ${existingStatusUri} for SD-JWT credential`)
const driver = await this.getDriverForStatusListOption(existingStatusUri)
await handleSdJwtCredentialStatus(credential, {
...rest,
statusLists: [{ ...rest.statusLists, statusListId: existingStatusUri }],
driver,
})
return credential
}
}
const statusListOpts = rest.statusLists?.length ? rest.statusLists : []
logger.info(`Adding new status using status list options with ID ${statusListOpts[0].statusListId ?? this.defaultStatusListId}`)
const firstOpt = statusListOpts[0]
const effectiveStatusListId = firstOpt.statusListId ?? this.defaultStatusListId
if (!effectiveStatusListId) {
return Promise.reject(
Error(
`No status list ID provided and no default status list ID configured. Please provide a status list ID or configure a default status list ID in the agent configuration.`,
),
)
}
const driver = await this.getDriverForStatusListOption(effectiveStatusListId, firstOpt.statusListCorrelationId)
await handleSdJwtCredentialStatus(credential, {
...rest,
statusLists: [
{
...firstOpt,
statusListId: effectiveStatusListId,
},
],
driver,
})
logger.debug(`Successfully added status information to SD-JWT credential`)
return credential
}
}