UNPKG

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

Version:

Sphereon SSI-SDK plugin for Status List management, like StatusList2021. Issuer drivers module

367 lines (332 loc) • 16.8 kB
import { checkAuth, sendErrorResponse } from '@sphereon/ssi-express-support' import { checkStatusIndexFromStatusListCredential, CreateNewStatusListFuncArgs, StatusListResult, updateStatusIndexFromStatusListCredential, } from '@sphereon/ssi-sdk.vc-status-list' import { getDriver } from '@sphereon/ssi-sdk.vc-status-list-issuer-drivers' import Debug from 'debug' import { Request, Response, Router } from 'express' import { EntryIdType, ICredentialStatusListEndpointOpts, IRequiredContext, IW3CredentialStatusEndpointOpts, StatusListIdType, UpdateIndexedCredentialStatusRequest, UpdateW3cCredentialStatusRequest, } from './types' import { StatusListCredential, StatusListType } from '@sphereon/ssi-types' import { IBitstringStatusListEntryEntity, IStatusListEntryEntity } from '@sphereon/ssi-sdk.data-store' const debug = Debug('sphereon:ssi-sdk:status-list') function sendStatuslistResponse(details: StatusListResult, statuslistPayload: StatusListCredential, response: Response) { let payload: Buffer | StatusListCredential switch (details.proofFormat) { case 'jwt': case 'cbor': payload = Buffer.from(statuslistPayload as string, 'ascii') break default: payload = statuslistPayload } return response.status(200).setHeader('Content-Type', details.statuslistContentType).send(payload) } function buildStatusListId(request: Request): string { const protocol = request.headers['x-forwarded-proto']?.toString() ?? request.protocol let host = request.headers['x-forwarded-host']?.toString() ?? request.get('host') const forwardedPort = request.headers['x-forwarded-port']?.toString() if (forwardedPort && !(protocol === 'https' && forwardedPort === '443') && !(protocol === 'http' && forwardedPort === '80')) { host += `:${forwardedPort}` } const forwardedPrefix = request.headers['x-forwarded-prefix']?.toString() ?? '' return `${protocol}://${host}${forwardedPrefix}${request.originalUrl.split('?')[0].replace(/\/status\/index\/.*/, '')}` } export function createNewStatusListEndpoint(router: Router, context: IRequiredContext, opts: ICredentialStatusListEndpointOpts) { if (opts?.enabled === false) { console.log(`Create new status list endpoint is disabled`) return } const path = opts?.path ?? '/status-lists' router.post(path, checkAuth(opts?.endpoint), async (request: Request, response: Response) => { try { const statusListArgs: CreateNewStatusListFuncArgs = request.body.statusList if (!statusListArgs) { return sendErrorResponse(response, 400, 'No statusList details supplied') } const details = await context.agent.slCreateStatusList(statusListArgs) const statuslistPayload = details.statusListCredential return sendStatuslistResponse(details, statuslistPayload, response) } catch (e) { return sendErrorResponse(response, 500, (e as Error).message, e) } }) } export function getStatusListCredentialEndpoint(router: Router, context: IRequiredContext, opts: ICredentialStatusListEndpointOpts) { if (opts?.enabled === false) { console.log(`Get statusList credential endpoint is disabled`) return } const path = opts?.path ?? '/status-lists/:index' router.get(path, checkAuth(opts?.endpoint), async (request: Request, response: Response) => { try { //todo: Check index against correlationId first. Then match originalUrl against statusList id //const correlationId = request.query.correlationId?.toString() ?? request.params.index?.toString() ?? request.originalUrl TODO I so not get these const correlationId = request.query.correlationId?.toString() const driver = await getDriver({ ...(correlationId ? { correlationId } : { id: buildStatusListId(request) }), dbName: opts.dbName, }) const details = await driver.getStatusList() const statuslistPayload = details.statusListCredential return sendStatuslistResponse(details, statuslistPayload, response) } catch (e) { return sendErrorResponse(response, 500, (e as Error).message, e) } }) } function lookupType(details: StatusListResult) { switch (details.type) { case StatusListType.StatusList2021: return 'StatusList2021Entry' case StatusListType.BitstringStatusList: return 'BitstringStatusListEntry' default: return details.type } } export function getStatusListCredentialIndexStatusEndpoint(router: Router, context: IRequiredContext, opts: ICredentialStatusListEndpointOpts) { if (opts?.enabled === false) { console.log(`Get statusList credential index status endpoint is disabled`) return } const path = opts?.path ?? '/status-lists/:statusListId/status/entry-by-id/:entryId' router.get(path, checkAuth(opts?.endpoint), async (request: Request, response: Response) => { try { const statusListIdType = (request.query.statusListIdType as StatusListIdType) ?? StatusListIdType.StatusListId const entryIdType = (request.query.entryIdType as EntryIdType) ?? EntryIdType.StatusListIndex let statusListIndex: number | undefined let entityCorrelationId: string | undefined let statusListId: string | undefined let statusListCorrelationId: string | undefined if (entryIdType === EntryIdType.StatusListIndex) { try { statusListIndex = Number.parseInt(request.params.entryId) if (!statusListIndex || statusListIndex < 0) { return sendErrorResponse(response, 400, `Please provide a proper statusListIndex`) } } catch (error) { return sendErrorResponse(response, 400, `Please provide a proper statusListIndex`) } } else { entityCorrelationId = request.params.entryId } if (statusListIdType === StatusListIdType.StatusListId) { statusListId = request.params.statusListId } else { statusListCorrelationId = request.params.statusListId } const driver = await getDriver({ ...(statusListCorrelationId ? { correlationId: statusListCorrelationId } : { id: statusListId }), dbName: opts.dbName, }) const details = await driver.getStatusList() if (statusListIndex && statusListIndex > details.length) { return sendErrorResponse(response, 400, `Please provide a proper statusListIndex`) } let entry = await driver.getStatusListEntryByIndex({ statusListId: details.id, ...(entityCorrelationId ? { correlationId: entityCorrelationId } : { statusListIndex }), errorOnNotFound: false, }) const type = lookupType(details) const resultStatusIndex = entry?.statusListIndex ?? statusListIndex ?? 0 const status = await checkStatusIndexFromStatusListCredential({ statusListCredential: details.statusListCredential, type, id: details.id, statusListIndex: resultStatusIndex, ...(details.type === StatusListType.StatusList2021 && { statusPurpose: details.statusList2021?.statusPurpose }), ...(details.type === StatusListType.BitstringStatusList && { statusPurpose: details.bitstringStatusList?.statusPurpose, bitsPerStatus: details.bitstringStatusList?.bitsPerStatus, }), }) if (!entry) { entry = { statusListId: details.id, value: '0', statusListIndex: resultStatusIndex, } } response.statusCode = 200 const result = { ...entry, status } delete result.statusList // the API caller requested the index status, not the entire status list which may be big. Filter it out return response.json(result) } catch (e) { return sendErrorResponse(response, 500, (e as Error).message, e) } }) } export function getStatusListCredentialIndexStatusEndpointLegacy(router: Router, context: IRequiredContext, opts: ICredentialStatusListEndpointOpts) { if (opts?.enabled === false) { console.log(`Get statusList credential index status endpoint is disabled`) return } const path = opts?.path ?? '/status-lists/:index/status/index/:statusListIndex' router.get(path, checkAuth(opts?.endpoint), async (request: Request, response: Response) => { try { //todo: Check index against correlationId first. Then match originalUrl against statusList id const statusListIndexStr = request.params.statusListIndex let statusListIndex: number try { statusListIndex = Number.parseInt(statusListIndexStr) } catch (error) { return sendErrorResponse(response, 400, `Please provide a proper statusListIndex`) } if (!statusListIndex || statusListIndex < 0) { return sendErrorResponse(response, 400, `Please provide a proper statusListIndex`) } //const correlationId = request.query.correlationId?.toString() ?? request.params.index?.toString() ?? request.originalUrl TODO I so not get these const statusListCorrelationId = request.query.correlationId?.toString() const driver = await getDriver({ ...(statusListCorrelationId ? { correlationId: statusListCorrelationId } : { id: buildStatusListId(request) }), dbName: opts.dbName, }) const details = await driver.getStatusList() if (statusListIndex > details.length) { return sendErrorResponse(response, 400, `Please provide a proper statusListIndex`) } const entityCorrelationId = request.query.entityCorrelationId?.toString() let entry = await driver.getStatusListEntryByIndex({ statusListId: details.id, ...(entityCorrelationId ? { correlationId: entityCorrelationId } : { statusListIndex: statusListIndex }), errorOnNotFound: false, }) const type = details.type === StatusListType.StatusList2021 ? 'StatusList2021Entry' : details.type const status = await checkStatusIndexFromStatusListCredential({ statusListCredential: details.statusListCredential, ...(details.type === StatusListType.StatusList2021 && { statusPurpose: details.statusList2021?.statusPurpose }), ...(details.type === StatusListType.BitstringStatusList && { statusPurpose: details.bitstringStatusList?.statusPurpose, bitsPerStatus: details.bitstringStatusList?.bitsPerStatus, }), type, id: details.id, statusListIndex, }) if (!entry) { // The fact we have nothing on it means the status is okay entry = { statusListId: details.id, value: '0', statusListIndex, } } response.statusCode = 200 const result = { ...entry, status } delete result.statusList // we asked for the status, not the entire list return response.json(result) } catch (e) { return sendErrorResponse(response, 500, (e as Error).message, e) } }) } export function updateStatusEndpoint(router: Router, context: IRequiredContext, opts: IW3CredentialStatusEndpointOpts) { if (opts?.enabled === false) { console.log(`Update credential status endpoint is disabled`) return } router.post(opts?.path ?? '/credentials/status', checkAuth(opts?.endpoint), async (request: Request, response: Response) => { try { debug(JSON.stringify(request.body, null, 2)) const updateRequest = request.body as UpdateW3cCredentialStatusRequest | UpdateIndexedCredentialStatusRequest const statusListId = updateRequest.statusListId ?? request.query.statusListId?.toString() ?? opts.statusListId // TODO why query params when we have a JSON body ?? const statusListCorrelationId = updateRequest.statusListCorrelationId ?? request.query.statusListrelationId?.toString() ?? opts.correlationId const entryCorrelationId = updateRequest.entryCorrelationId ?? request.query.entryCorrelationId?.toString() // TODO: Move mostly to driver if (!statusListId && !statusListCorrelationId) { return sendErrorResponse(response, 400, 'No statusList id or correlation Id provided or deduced for the API or in the request') } else if (!updateRequest.credentialStatus || updateRequest.credentialStatus.length === 0) { return sendErrorResponse(response, 400, 'No statusList updates supplied') } const driver = await getDriver({ ...(statusListCorrelationId ? { correlationId: statusListCorrelationId } : { id: buildStatusListId(request) }), dbName: opts.dbName, }) let statusListResult: StatusListResult = await driver.getStatusList() // Get status list entry based on request type let statusListEntry: IStatusListEntryEntity | IBitstringStatusListEntryEntity | undefined if ('credentialId' in updateRequest) { if (!updateRequest.credentialId) { return sendErrorResponse(response, 400, 'No credentialId supplied') } // unfortunately the W3C API works by credentialId. Which means you will have to map listIndices during issuance statusListEntry = await driver.getStatusListEntryByCredentialId({ statusListId, statusListCorrelationId, entryCorrelationId, credentialId: updateRequest.credentialId, errorOnNotFound: true, }) } else { statusListEntry = await driver.getStatusListEntryByIndex({ ...(statusListResult.id && { statusListId: statusListResult.id }), ...(statusListResult.correlationId && { statusListCorrelationId: statusListResult.correlationId }), ...(entryCorrelationId ? { entryCorrelationId } : { statusListIndex: updateRequest.statusListIndex }), errorOnNotFound: true, }) } if (!statusListEntry) { const identifier = 'credentialId' in updateRequest ? updateRequest.credentialId : `index ${updateRequest.statusListIndex}` return sendErrorResponse(response, 404, `Status list entry for ${identifier} not found for ${statusListId}`) } let statusListCredential = statusListResult.statusListCredential for (const updateItem of updateRequest.credentialStatus) { if (!updateItem.status) { return sendErrorResponse(response, 400, `Required 'status' value was missing in the credentialStatus array`) } let value: string = '1' if (updateItem.status === '0' || updateItem.status.toLowerCase() === 'false') { value = '0' } else if (updateItem.status !== '1' && updateItem.status.toLowerCase() !== 'true') { if (updateItem.type === StatusListType.StatusList2021) { // 2021 only allows 0 and 1 return sendErrorResponse(response, 400, `Invalid 'status' value in the credentialStatus array: ${updateItem.status}`) } else if (parseInt(updateItem.status) < 0 || parseInt(updateItem.status) > 255) { return sendErrorResponse(response, 400, `Invalid 'status' value in the credentialStatus array: ${updateItem.status}`) } value = `${parseInt(updateItem.status)}` } const updStatusListId = statusListId ?? ('statusList' in statusListEntry && statusListEntry.statusList?.id) ?? statusListEntry?.statusListId // When input was statusListCorrelationId the statusList id should come from statusListEntry if (!updStatusListId) { return sendErrorResponse(response, 400, 'statuslist id could not be determined') } await driver.updateStatusListEntry({ ...statusListEntry, statusListId: updStatusListId, value }) const bitsPerStatus = 'bitsPerStatus' in statusListEntry && typeof statusListEntry.bitsPerStatus === 'number' ? statusListEntry.bitsPerStatus : undefined // todo: optimize. We are now creating a new VC for every item passed in. Probably wise to look at DB as well statusListResult = await updateStatusIndexFromStatusListCredential( { statusListCredential: statusListCredential, statusListIndex: statusListEntry.statusListIndex, ...(bitsPerStatus && { bitsPerStatus }), value: parseInt(value), keyRef: opts.keyRef, }, context, ) if (!statusListResult.correlationId) { return Promise.reject(Error('Cannot update the statuslist, correlationId is missing from the status list result')) } statusListResult = await driver.updateStatusList({ correlationId: statusListResult.correlationId, statusListCredential: statusListResult.statusListCredential, }) } return sendStatuslistResponse(statusListResult, statusListResult.statusListCredential, response) } catch (e) { return sendErrorResponse(response, 500, (e as Error).message, e) } }) }