UNPKG

@sphereon/ssi-sdk.vc-status-list-issuer

Version:

Sphereon SSI-SDK plugin for Status List issuance

289 lines (277 loc) 11.6 kB
import { IAddStatusListEntryArgs, IStatusListEntryEntity } from '@sphereon/ssi-sdk.data-store' import { CreateNewStatusListArgs, CredentialWithStatusSupport, IIssueCredentialStatusOpts, IRequiredPlugins, IStatusListPlugin, StatusListResult, } from '@sphereon/ssi-sdk.vc-status-list' import { getDriver, IStatusListDriver } from '@sphereon/ssi-sdk.vc-status-list-issuer-drivers' import debug from 'debug' import { SdJwtVcPayload } from '@sd-jwt/sd-jwt-vc' import { Loggers, OrPromise } from '@sphereon/ssi-types' import { IAgentContext } from '@veramo/core' import { StatusListOpts } from '@sphereon/oid4vci-common' import { DataSource } from 'typeorm' const logger = Loggers.DEFAULT.get('sphereon:ssi-sdk:vc-status-list-issuer') /** * Retrieves a status list driver and its associated status list in one call. */ async function getDriverAndStatusList( statusListId: string, opts?: IIssueCredentialStatusOpts & { driver?: IStatusListDriver }, ): Promise<{ slDriver: IStatusListDriver; statusList: Pick<StatusListResult, 'id' | 'correlationId'> }> { const slDriver = opts?.driver ?? (await getDriver({ id: statusListId, dataSource: opts?.dataSource })) const statusList = await slDriver.statusListStore.getStatusList({ id: statusListId }) return { slDriver, statusList } } /** * Extracts status list options from a regular credential. */ function getCredentialStatusListOpts(credential: CredentialWithStatusSupport, opts?: IIssueCredentialStatusOpts): Array<StatusListOpts> { // Start with any options provided via the opts parameter const statusListOpts: StatusListOpts[] = [...(opts?.statusLists ?? [])] // If none were provided, try to derive them from the credential's credentialStatus if (statusListOpts.length === 0 && credential.credentialStatus) { if (Array.isArray(credential.credentialStatus)) { for (const credStatus of credential.credentialStatus) { if (credStatus.statusListCredential) { statusListOpts.push({ statusListId: credStatus.statusListCredential, statusListIndex: credStatus.statusListIndex, statusListCorrelationId: credStatus.statusListCorrelationId, statusEntryCorrelationId: credStatus.statusEntryCorrelationId, }) } } } else if (credential.credentialStatus.statusListCredential) { statusListOpts.push({ statusListId: credential.credentialStatus.statusListCredential, statusListIndex: credential.credentialStatus.statusListIndex, statusListCorrelationId: credential.credentialStatus.statusListCorrelationId, statusEntryCorrelationId: credential.credentialStatus.statusEntryCorrelationId, }) } } return statusListOpts } /** * Extracts status list options from an SD‑JWT credential. */ function getSdJwtStatusListOpts(credential: SdJwtVcPayload, opts?: IIssueCredentialStatusOpts): StatusListOpts[] { const statusListOpts: StatusListOpts[] = [...(opts?.statusLists ?? [])] if (statusListOpts.length === 0 && credential.status?.status_list) { statusListOpts.push({ statusListId: credential.status.status_list.uri, statusListIndex: credential.status.status_list.idx, }) } return statusListOpts } /** * Processes a status list entry for a given credential. */ async function processStatusListEntry(params: { statusListId: string statusList: Pick<StatusListResult, 'id' | 'correlationId'> credentialId?: string currentIndex: number opts?: IIssueCredentialStatusOpts slDriver: IStatusListDriver debugCredentialInfo: string useIndexCondition: (index: number) => boolean checkCredentialIdMismatch?: (existingEntry: IStatusListEntryEntity, credentialId: string, index: number) => void statusEntryCorrelationId?: string }): Promise<{ statusListIndex: number; updateResult: any }> { let existingEntry: IStatusListEntryEntity | undefined = undefined // Search whether there is an existing status list entry for this credential first if (params.credentialId) { existingEntry = await params.slDriver.getStatusListEntryByCredentialId({ statusListId: params.statusList.id, credentialId: params.credentialId, errorOnNotFound: false, }) if (existingEntry) { debug(`Existing statusList entry and index ${existingEntry.statusListIndex} found for ${params.debugCredentialInfo}. Will reuse the index`) } } let statusListIndex = existingEntry?.statusListIndex ?? params.currentIndex if (params.useIndexCondition(statusListIndex)) { existingEntry = await params.slDriver.getStatusListEntryByIndex({ statusListId: params.statusList.id, statusListIndex, errorOnNotFound: false, }) logger.debug( `${!existingEntry && 'no'} existing statusList entry and index ${existingEntry?.statusListIndex} for ${params.debugCredentialInfo}. Will reuse the index`, ) if ( existingEntry && params.credentialId && existingEntry.credentialId && existingEntry.credentialId !== params.credentialId && params.checkCredentialIdMismatch ) { params.checkCredentialIdMismatch(existingEntry, params.credentialId, statusListIndex) } } else { debug(`Will generate a new random statusListIndex since the credential did not contain a statusListIndex for ${params.debugCredentialInfo}...`) statusListIndex = await params.slDriver.getRandomNewStatusListIndex({ correlationId: params.statusList.correlationId, }) debug(`Random statusListIndex ${statusListIndex} assigned for ${params.debugCredentialInfo}`) } const updateArgs: IAddStatusListEntryArgs = { statusListId: params.statusListId, statusListIndex, correlationId: params.statusEntryCorrelationId, value: params.opts?.value ?? '0', } if (params.credentialId) { updateArgs.credentialId = params.credentialId } const updateResult = await params.slDriver.updateStatusListEntry(updateArgs) return { statusListIndex, updateResult } } export const createStatusList = async ( args: CreateNewStatusListArgs, dataSource: OrPromise<DataSource>, context: IAgentContext<IRequiredPlugins & IStatusListPlugin>, ): Promise<StatusListResult> => { let statusList: StatusListResult try { statusList = await context.agent.slGetStatusList({ ...(args.id && { id: args.id }), ...(args.correlationId && { correlationId: args.correlationId }), ...(args.dbName && { dbName: args.dbName }), dataSource: await dataSource, }) } catch (e) { const id = args.id const correlationId = args.correlationId if (!id || !correlationId) { return Promise.reject(Error(`No correlation id and id provided for status list`)) } statusList = await context.agent.slCreateStatusList(args) } return statusList } /** * Adds status information to a credential using status list options from either: * - The provided options * - Existing credential status information * * The function updates each status list entry and modifies the credential's status. * * @param credential The credential to update with status information * @param credentialStatusOpts Options for status handling and driver configuration */ export const handleCredentialStatus = async ( credential: CredentialWithStatusSupport, credentialStatusOpts?: IIssueCredentialStatusOpts & { driver?: IStatusListDriver }, ): Promise<void> => { logger.debug(`Starting status update for credential ${credential.id ?? 'without ID'}`) const statusListOpts = getCredentialStatusListOpts(credential, credentialStatusOpts) if (statusListOpts.length === 0) { logger.debug('No status list options found, skipping update') return } const credentialId = credential.id ?? credentialStatusOpts?.credentialId for (const statusListOpt of statusListOpts) { const statusListId = statusListOpt.statusListId if (!statusListId) { logger.debug('Skipping status list option without ID') continue } logger.debug(`Processing status list ${statusListId} for credential ${credentialId ?? 'without ID'}`) const { slDriver, statusList } = await getDriverAndStatusList(statusListId, credentialStatusOpts) const currentIndex = statusListOpt.statusListIndex ?? 0 const { updateResult } = await processStatusListEntry({ statusListId, statusList, credentialId, currentIndex, statusEntryCorrelationId: statusListOpt.statusEntryCorrelationId, opts: credentialStatusOpts, slDriver, debugCredentialInfo: `credential with id ${credentialId} and statusListId ${statusListId}`, useIndexCondition: (index) => Boolean(index), checkCredentialIdMismatch: (existingEntry, credentialId, index) => { throw Error( `A credential with new id (${credentialId}) is issued, but its id does not match a registered statusListEntry id ${existingEntry.credentialId} for index ${index}`, ) }, }) if (!credential.credentialStatus || Array.isArray(credential.credentialStatus)) { credential.credentialStatus = { id: `${statusListId}`, type: 'StatusList2021Entry', statusPurpose: 'revocation', statusListCredential: statusListId, ...updateResult.credentialStatus, } } } logger.debug(`Completed status updates for credential ${credentialId ?? 'without ID'}`) } /** * Adds status information to an SD‑JWT credential using status list options from either: * - The provided options * - Existing credential status information * * Updates the credential's status field with status list URI and index. * * @param credential The SD‑JWT credential to update * @param credentialStatusOpts Options for status handling and driver configuration * @throws Error if no status list options are available */ export const handleSdJwtCredentialStatus = async ( credential: SdJwtVcPayload, credentialStatusOpts?: IIssueCredentialStatusOpts & { driver?: IStatusListDriver }, ): Promise<void> => { logger.debug('Starting status update for SD‑JWT credential') const statusListOpts = getSdJwtStatusListOpts(credential, credentialStatusOpts) if (statusListOpts.length === 0) { throw Error('No status list options available from credential or options') } for (const statusListOpt of statusListOpts) { const statusListId = statusListOpt.statusListId if (!statusListId) { logger.debug('Skipping status list option without ID') continue } logger.info(`Processing status list ${statusListId}`) const { slDriver, statusList } = await getDriverAndStatusList(statusListId, credentialStatusOpts) const currentIndex = statusListOpt.statusListIndex ?? 0 const { statusListIndex } = await processStatusListEntry({ statusListId, statusList, currentIndex, statusEntryCorrelationId: statusListOpt.statusEntryCorrelationId, opts: credentialStatusOpts, slDriver, debugCredentialInfo: `credential with statusListId ${statusListId}`, useIndexCondition: (index) => index > 0, }) if (!credential.status) { credential.status = { status_list: { uri: statusListId, idx: statusListIndex, }, } } else if (!credential.status.status_list) { credential.status.status_list = { uri: statusListId, idx: statusListIndex, } } else { credential.status.status_list = { uri: credential.status.status_list.uri || statusListId, idx: statusListIndex, } } } logger.debug('Completed SD‑JWT credential status update') }