@sphereon/ssi-sdk.vc-status-list-issuer
Version:
Sphereon SSI-SDK plugin for Status List issuance
527 lines (524 loc) • 21.4 kB
JavaScript
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