@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
text/typescript
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)
}
})
}