UNPKG

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

Version:

Sphereon SSI-SDK plugin for Status List issuance

527 lines (524 loc) • 21.4 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/agent/StatusListPlugin.ts import { DataSources } from "@sphereon/ssi-sdk.agent-config"; import { createNewStatusList } from "@sphereon/ssi-sdk.vc-status-list"; import { getDriver as getDriver2 } from "@sphereon/ssi-sdk.vc-status-list-issuer-drivers"; import { Loggers as Loggers2 } from "@sphereon/ssi-types"; // src/functions.ts import { getDriver } from "@sphereon/ssi-sdk.vc-status-list-issuer-drivers"; import debug from "debug"; import { Loggers } from "@sphereon/ssi-types"; var logger = Loggers.DEFAULT.get("sphereon:ssi-sdk:vc-status-list-issuer"); async function getDriverAndStatusList(statusListId, opts) { const slDriver = opts?.driver ?? await getDriver({ id: statusListId, dataSource: opts?.dataSource }); const statusList = await slDriver.statusListStore.getStatusList({ id: statusListId }); return { slDriver, statusList }; } __name(getDriverAndStatusList, "getDriverAndStatusList"); function getCredentialStatusListOpts(credential, opts) { const statusListOpts = [ ...opts?.statusLists ?? [] ]; 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; } __name(getCredentialStatusListOpts, "getCredentialStatusListOpts"); function getSdJwtStatusListOpts(credential, opts) { const 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; } __name(getSdJwtStatusListOpts, "getSdJwtStatusListOpts"); async function processStatusListEntry(params) { let existingEntry = void 0; 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 = { statusListId: params.statusListId, statusListIndex, correlationId: params.statusEntryCorrelationId, value: params.opts?.value ?? "0", ...params.statusList.type === "BitstringStatusList" && { bitsPerStatus: params.statusList.bitstringStatusList?.bitsPerStatus ?? 1 } }; if (params.credentialId) { updateArgs.credentialId = params.credentialId; } const updateResult = await params.slDriver.updateStatusListEntry(updateArgs); return { statusListIndex, updateResult }; } __name(processStatusListEntry, "processStatusListEntry"); var createStatusList = /* @__PURE__ */ __name(async (args, dataSource, context) => { let statusList; 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; }, "createStatusList"); var handleCredentialStatus = /* @__PURE__ */ __name(async (credential, credentialStatusOpts) => { 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: /* @__PURE__ */ __name((index) => Boolean(index), "useIndexCondition"), checkCredentialIdMismatch: /* @__PURE__ */ __name((existingEntry, credentialId2, index) => { throw Error(`A credential with new id (${credentialId2}) is issued, but its id does not match a registered statusListEntry id ${existingEntry.credentialId} for index ${index}`); }, "checkCredentialIdMismatch") }); if (!credential.credentialStatus || Array.isArray(credential.credentialStatus)) { credential.credentialStatus = { id: `${statusListId}`, statusListCredential: statusListId, ...updateResult.credentialStatus }; } } logger.debug(`Completed status updates for credential ${credentialId ?? "without ID"}`); }, "handleCredentialStatus"); var handleSdJwtCredentialStatus = /* @__PURE__ */ __name(async (credential, credentialStatusOpts) => { logger.debug("Starting status update for SD\u2011JWT 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: /* @__PURE__ */ __name((index) => index > 0, "useIndexCondition") }); 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\u2011JWT credential status update"); }, "handleSdJwtCredentialStatus"); // src/agent/StatusListPlugin.ts var logger2 = Loggers2.DEFAULT.get("sphereon:ssi-sdk:vc-status-list"); var StatusListPlugin = class { static { __name(this, "StatusListPlugin"); } instances = new Array(); defaultStatusListId; allDataSources; initialized = false; methods = { 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) { this.instances = opts.instances ?? []; const instanceId = opts.defaultStatusListId ?? opts.instances?.[0]?.id; if (!instanceId) { logger2.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(); } async init() { if (!this.initialized) { for (const dbName of this.allDataSources.getDbNames()) { const driver = await getDriver2({ 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; } } async slImportStatusLists(imports, context) { await this.init(); for (const instanceImport of imports) { const dataSource = this.allDataSources.getDbConnection(instanceImport.dbName ?? "default"); await createStatusList(instanceImport, dataSource, context); } return true; } createStatusListInstance(statusList, dataSource, driver) { return { id: statusList.id, correlationId: statusList.correlationId, issuer: typeof statusList.issuer === "string" ? statusList.issuer : statusList.issuer.id, dataSource, driverOptions: driver.getOptions(), driverType: driver.getType() }; } async getDriverForStatusListOption(effectiveStatusListId, correlationId) { 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 getDriver2({ dataSource, correlationId }) : await getDriver2({ dataSource, id: effectiveStatusListId }); return driver; } async slGetStatusList(args) { 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 getDriver2({ id: args.id ?? sl?.id, correlationId: args.correlationId ?? sl?.correlationId, dataSource }); return await driver.getStatusList(); } async selectDatasource(args) { 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); } } async slCreateStatusList(args, context) { await this.init(); const sl = await createNewStatusList(args, context); const dataSource = await this.selectDatasource({ dbName: args.dbName, dataSource: args.dataSource }); const driver = await getDriver2({ id: sl.id, correlationId: sl.correlationId, dataSource }); let statusListDetails = void 0; try { statusListDetails = await this.slGetStatusList(args); } catch (e) { } 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({ statusListType: sl.type, statusListCredential: sl.statusListCredential, correlationId: sl.correlationId, bitsPerStatus: this.selectBitsPerStatus(sl) }); this.instances.push(this.createStatusListInstance(statusListDetails, dataSource, driver)); } return statusListDetails; } /** * Selects bitsPerStatus from the used status list type when applicable * @param sl * @private */ selectBitsPerStatus(sl) { if (sl.bitstringStatusList) { return sl.bitstringStatusList.bitsPerStatus; } if (sl.oauthStatusList) { return sl.oauthStatusList.bitsPerStatus; } return 1; } /** * 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 */ async slAddStatusToCredential(args, context) { const { credential, ...rest } = args; logger2.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; if (Array.isArray(credentialStatus)) { 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) { logger2.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 credentialId2 = credential.id ?? rest.credentialId; const dataSource = instance.dataSource ? await instance.dataSource : await this.allDataSources.getDbConnection(instance.driverOptions.dbName); const driver = await getDriver2({ dataSource, id: existingStatusId }); await handleCredentialStatus(credential, { ...rest, credentialId: credentialId2, statusLists: [ { statusListId: existingStatusId } ], driver }); return credential; } } const statusListOpts = rest.statusLists?.length ? rest.statusLists : []; logger2.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 }); } logger2.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 */ async slAddStatusToSdJwtCredential(args, context) { const { credential, ...rest } = args; logger2.debug(`Adding status to SD-JWT credential`); await this.init(); const credentialStatus = credential.status; if (credentialStatus && (!rest.statusLists || rest.statusLists.length == 0)) { let existingStatusUri; if (credentialStatus.status_list && credentialStatus.status_list.uri && credentialStatus.status_list.uri.trim() !== "") { existingStatusUri = credentialStatus.status_list.uri; } if (existingStatusUri) { logger2.debug(`Using existing status URI ${existingStatusUri} for SD-JWT credential`); const driver2 = await this.getDriverForStatusListOption(existingStatusUri); await handleSdJwtCredentialStatus(credential, { ...rest, statusLists: [ { ...rest.statusLists, statusListId: existingStatusUri } ], driver: driver2 }); return credential; } } const statusListOpts = rest.statusLists?.length ? rest.statusLists : []; logger2.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 }); logger2.debug(`Successfully added status information to SD-JWT credential`); return credential; } }; export { StatusListPlugin }; //# sourceMappingURL=index.js.map