UNPKG

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

Version:

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

1 lines • 129 kB
{"version":3,"sources":["../src/index.ts","../src/types/index.ts","../src/functions.ts","../src/utils.ts","../src/impl/StatusList2021.ts","../src/impl/OAuthStatusList.ts","../src/impl/encoding/jwt.ts","../src/impl/encoding/common.ts","../src/impl/encoding/cbor.ts","../src/impl/StatusListFactory.ts","../src/impl/BitstringStatusListImplementation.ts"],"sourcesContent":["/**\n */\n\nexport * from './types'\nexport * from './functions'\nexport { determineStatusListType } from './utils'\n","import type { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution'\nimport {\n type CredentialProofFormat,\n type ICredential,\n type ICredentialStatus,\n type IIssuer,\n type IVerifiableCredential,\n type OrPromise,\n type StatusListCredential,\n StatusListCredentialIdMode,\n StatusListDriverType,\n type StatusListIndexingDirection,\n StatusListType,\n type StatusPurpose2021,\n} from '@sphereon/ssi-types'\nimport type { CredentialPayload, IAgentContext, ICredentialIssuer, ICredentialVerifier, IKeyManager, IPluginMethodMap } from '@veramo/core'\nimport { DataSource } from 'typeorm'\nimport type { BitsPerStatus } from '@sd-jwt/jwt-status-list'\nimport type { SdJwtVcPayload } from '@sd-jwt/sd-jwt-vc'\nimport type { StatusListOpts } from '@sphereon/oid4vci-common'\nimport { BitstringStatusPurpose } from '@4sure-tech/vc-bitstring-status-lists'\nimport { IVcdmCredentialPlugin } from '@sphereon/ssi-sdk.credential-vcdm'\nimport { IExtractedCredentialDetails } from '../impl/IStatusList'\nimport { BitstringStatusListArgs, IStatusListEntity } from '@sphereon/ssi-sdk.data-store'\n\nexport enum StatusOAuth {\n Valid = 0,\n Invalid = 1,\n Suspended = 2,\n}\n\nexport enum Status2021 {\n Valid = 0,\n Invalid = 1,\n}\n\nexport type StatusList2021Args = {\n indexingDirection: StatusListIndexingDirection\n statusPurpose?: StatusPurpose2021\n // todo: validFrom and validUntil\n}\n\nexport type OAuthStatusListArgs = {\n bitsPerStatus: BitsPerStatus\n expiresAt?: Date\n}\n\nexport type BaseCreateNewStatusListArgs = {\n type: StatusListType\n id: string\n issuer: string | IIssuer\n correlationId?: string\n length?: number\n proofFormat?: CredentialProofFormat\n keyRef?: string\n statusList2021?: StatusList2021Args\n oauthStatusList?: OAuthStatusListArgs\n bitstringStatusList?: BitstringStatusListArgs\n driverType?: StatusListDriverType\n}\n\nexport type UpdateStatusList2021Args = {\n statusPurpose: StatusPurpose2021\n}\n\nexport type UpdateOAuthStatusListArgs = {\n bitsPerStatus: BitsPerStatus\n expiresAt?: Date\n}\n\nexport type UpdateBitstringStatusListArgs = {\n statusPurpose: BitstringStatusPurpose\n bitsPerStatus: number\n validFrom?: Date\n validUntil?: Date\n ttl?: number\n}\n\nexport interface UpdateStatusListFromEncodedListArgs {\n type?: StatusListType\n statusListIndex: number | string\n value: number\n proofFormat?: CredentialProofFormat\n keyRef?: string\n correlationId?: string\n encodedList: string\n issuer: string | IIssuer\n id: string\n statusList2021?: UpdateStatusList2021Args\n oauthStatusList?: UpdateOAuthStatusListArgs\n bitstringStatusList?: UpdateBitstringStatusListArgs\n}\n\nexport interface UpdateStatusListFromStatusListCredentialArgs {\n statusListCredential: StatusListCredential // | CompactJWT\n keyRef?: string\n statusListIndex: number | string\n value: number | Status2021 | StatusOAuth\n}\n\nexport interface StatusListResult {\n id: string\n encodedList: string\n issuer: string | IIssuer\n type: StatusListType\n proofFormat: CredentialProofFormat\n length: number\n statusListCredential: StatusListCredential\n statuslistContentType: string\n correlationId?: string\n driverType?: StatusListDriverType\n\n statusList2021?: {\n indexingDirection: StatusListIndexingDirection\n statusPurpose: StatusPurpose2021\n credentialIdMode: StatusListCredentialIdMode\n }\n oauthStatusList?: {\n bitsPerStatus: number\n expiresAt?: Date\n }\n bitstringStatusList?: {\n statusPurpose: BitstringStatusPurpose | BitstringStatusPurpose[]\n bitsPerStatus?: number\n validFrom?: Date\n validUntil?: Date\n ttl?: number\n }\n}\n\nexport interface StatusList2021EntryCredentialStatus extends ICredentialStatus {\n type: 'StatusList2021Entry'\n statusPurpose: StatusPurpose2021\n statusListIndex: string\n statusListCredential: string\n}\n\nexport interface StatusListOAuthEntryCredentialStatus extends ICredentialStatus {\n type: 'OAuthStatusListEntry'\n bitsPerStatus: number\n statusListIndex: string\n statusListCredential: string\n expiresAt?: Date\n}\n\nexport interface StatusList2021ToVerifiableCredentialArgs {\n issuer: string | IIssuer\n id: string\n type?: StatusListType\n proofFormat?: CredentialProofFormat\n keyRef?: string\n encodedList: string\n statusPurpose: StatusPurpose2021\n}\n\nexport interface CreateStatusListArgs {\n issuer: string | IIssuer\n id: string\n proofFormat?: CredentialProofFormat\n keyRef?: string\n correlationId?: string\n length?: number\n statusList2021?: StatusList2021Args\n oauthStatusList?: OAuthStatusListArgs\n bitstringStatusList?: BitstringStatusListArgs\n}\n\nexport interface UpdateStatusListIndexArgs {\n statusListCredential: StatusListCredential // | CompactJWT\n statusListIndex: number | string\n value: number | Status2021 | StatusOAuth\n bitsPerStatus?: number\n keyRef?: string\n expiresAt?: Date\n}\n\nexport interface CheckStatusIndexArgs {\n statusListCredential: StatusListCredential // | CompactJWT\n statusListIndex: string | number\n bitsPerStatus?: number\n}\n\n// For the CREATE and READ contexts\nexport interface IToDetailsFromCredentialArgs {\n // The source credential we are converting\n statusListCredential: StatusListCredential\n\n // The required metadata that is NOT in the credential itself\n statusListType: StatusListType\n bitsPerStatus?: number\n correlationId?: string\n driverType?: StatusListDriverType\n}\n\n// For the UPDATE context\nexport interface IMergeDetailsWithEntityArgs {\n extractedDetails: IExtractedCredentialDetails\n statusListEntity: IStatusListEntity\n}\n\n/**\n * The interface definition for a plugin that can add statuslist info to a credential\n *\n * @remarks Please see {@link https://www.w3.org/TR/vc-data-model | W3C Verifiable Credentials data model}\n *\n * @beta This API is likely to change without a BREAKING CHANGE notice\n */\nexport interface IStatusListPlugin extends IPluginMethodMap {\n /**\n * Create a new status list\n *\n * @param args Status list information like type and size\n * @param context - This reserved param is automatically added and handled by the framework, *do not override*\n *\n * @returns - The details of the newly created status list\n */\n slCreateStatusList(args: CreateNewStatusListArgs, context: IRequiredContext): Promise<StatusListResult>\n\n /**\n * Ensures status list info like index and list id is added to a credential\n *\n * @param args - Arguments necessary to add the statuslist info.\n * @param context - This reserved param is automatically added and handled by the framework, *do not override*\n *\n * @returns - a promise that resolves to the credential now with status support\n *\n * @beta This API is likely to change without a BREAKING CHANGE notice\n */\n slAddStatusToCredential(args: IAddStatusToCredentialArgs, context: IRequiredContext): Promise<CredentialWithStatusSupport>\n\n slAddStatusToSdJwtCredential(args: IAddStatusToSdJwtCredentialArgs, context: IRequiredContext): Promise<SdJwtVcPayload>\n\n /**\n * Get the status list using the configured driver for the SL. Normally a correlationId or id should suffice. Optionally accepts a dbName/datasource\n * @param args\n * @param context\n */\n slGetStatusList(args: GetStatusListArgs, context: IRequiredContext): Promise<StatusListResult>\n\n /**\n * Import status lists when noy yet present\n *\n * @param imports Array of status list information like type and size\n * @param context - This reserved param is automatically added and handled by the framework, *do not override*\n */\n slImportStatusLists(imports: Array<CreateNewStatusListArgs>, context: IRequiredContext): Promise<boolean>\n}\n\nexport type CreateNewStatusListFuncArgs = BaseCreateNewStatusListArgs\n\nexport type CreateNewStatusListArgs = BaseCreateNewStatusListArgs & {\n dbName?: string\n dataSource?: OrPromise<DataSource>\n isDefault?: boolean\n}\n\nexport type IAddStatusToCredentialArgs = Omit<IIssueCredentialStatusOpts, 'dataSource'> & {\n credential: CredentialWithStatusSupport\n}\n\nexport type IAddStatusToSdJwtCredentialArgs = Omit<IIssueCredentialStatusOpts, 'dataSource'> & {\n credential: SdJwtVcPayload\n}\n\nexport interface IIssueCredentialStatusOpts {\n dataSource?: DataSource\n statusLists?: Array<StatusListOpts>\n credentialId?: string // An id to use for the credential. Normally should be set as the crdential.id value\n value?: string\n}\n\nexport type GetStatusListArgs = {\n id?: string\n correlationId?: string\n dataSource?: OrPromise<DataSource>\n dbName?: string\n}\n\nexport type CredentialWithStatusSupport = ICredential | CredentialPayload | IVerifiableCredential\n\nexport type SignedStatusListData = {\n statusListCredential: StatusListCredential\n encodedList: string\n}\n\nexport type IRequiredPlugins = IVcdmCredentialPlugin & IIdentifierResolution\nexport type IRequiredContext = IAgentContext<ICredentialIssuer & ICredentialVerifier & IIdentifierResolution & IKeyManager & IVcdmCredentialPlugin>\n","import type { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution'\nimport { CredentialMapper, type CredentialProofFormat, type StatusListCredential, StatusListType, type StatusPurpose2021 } from '@sphereon/ssi-types'\nimport type { CredentialStatus, DIDDocument, IAgentContext, ProofFormat as VeramoProofFormat } from '@veramo/core'\n\nimport {\n BitstringStatusListEntryCredentialStatus,\n IBitstringStatusListEntryEntity,\n IStatusListEntryEntity,\n StatusListEntity,\n} from '@sphereon/ssi-sdk.data-store'\n\nimport { checkStatus } from '@sphereon/vc-status-list'\n\n// @ts-ignore\nimport { CredentialJwtOrJSON, StatusMethod } from 'credential-status'\nimport {\n CreateNewStatusListFuncArgs,\n IMergeDetailsWithEntityArgs,\n IToDetailsFromCredentialArgs,\n Status2021,\n StatusList2021EntryCredentialStatus,\n StatusList2021ToVerifiableCredentialArgs,\n StatusListOAuthEntryCredentialStatus,\n StatusListResult,\n StatusOAuth,\n UpdateStatusListFromEncodedListArgs,\n UpdateStatusListIndexArgs,\n} from './types'\nimport { assertValidProofType, determineStatusListType, getAssertedValue, getAssertedValues } from './utils'\nimport { getStatusListImplementation } from './impl/StatusListFactory'\nimport { IVcdmCredentialPlugin } from '@sphereon/ssi-sdk.credential-vcdm'\nimport {\n IBitstringStatusListImplementationResult,\n IExtractedCredentialDetails,\n IOAuthStatusListImplementationResult,\n IStatusList2021ImplementationResult,\n} from './impl/IStatusList'\n\n/**\n * Fetches a status list credential from a URL\n * @param args - Object containing the status list credential URL\n * @returns Promise resolving to the fetched StatusListCredential\n */\nexport async function fetchStatusListCredential(args: { statusListCredential: string }): Promise<StatusListCredential> {\n const url = getAssertedValue('statusListCredential', args.statusListCredential)\n try {\n const response = await fetch(url)\n if (!response.ok) {\n throw Error(`Fetching status list ${url} resulted in an error: ${response.status} : ${response.statusText}`)\n }\n const responseAsText = await response.text()\n if (responseAsText.trim().startsWith('{')) {\n return JSON.parse(responseAsText) as StatusListCredential\n }\n return responseAsText as StatusListCredential\n } catch (error) {\n console.error(`Fetching status list ${url} resulted in an unexpected error: ${error instanceof Error ? error.message : JSON.stringify(error)}`)\n throw error\n }\n}\n\n/**\n * Creates a status checking function for credential-status plugin\n * @param args - Configuration options for status verification\n * @returns StatusMethod function for checking credential status\n */\nexport function statusPluginStatusFunction(args: {\n documentLoader: any\n suite: any\n mandatoryCredentialStatus?: boolean\n verifyStatusListCredential?: boolean\n verifyMatchingIssuers?: boolean\n errorUnknownListType?: boolean\n}): StatusMethod {\n return async (credential: CredentialJwtOrJSON, didDoc: DIDDocument): Promise<CredentialStatus> => {\n const result = await checkStatusForCredential({\n ...args,\n documentLoader: args.documentLoader,\n credential: credential as StatusListCredential,\n errorUnknownListType: args.errorUnknownListType,\n })\n\n return {\n revoked: !result.verified || result.error,\n ...(result.error && { error: result.error }),\n }\n }\n}\n\n/**\n * Function that can be used together with @digitalbazar/vc and @digitialcredentials/vc\n * @param args - Configuration options for status verification\n * @returns Function for checking credential status\n */\nexport function vcLibCheckStatusFunction(args: {\n mandatoryCredentialStatus?: boolean\n verifyStatusListCredential?: boolean\n verifyMatchingIssuers?: boolean\n errorUnknownListType?: boolean\n}) {\n const { mandatoryCredentialStatus, verifyStatusListCredential, verifyMatchingIssuers, errorUnknownListType } = args\n return (args: {\n credential: StatusListCredential\n documentLoader: any\n suite: any\n }): Promise<{\n verified: boolean\n error?: any\n }> => {\n return checkStatusForCredential({\n ...args,\n mandatoryCredentialStatus,\n verifyStatusListCredential,\n verifyMatchingIssuers,\n errorUnknownListType,\n })\n }\n}\n\n/**\n * Checks the status of a credential using its credential status information\n * @param args - Parameters for credential status verification\n * @returns Promise resolving to verification result with error details if any\n */\nexport async function checkStatusForCredential(args: {\n credential: StatusListCredential\n documentLoader: any\n suite: any\n mandatoryCredentialStatus?: boolean\n verifyStatusListCredential?: boolean\n verifyMatchingIssuers?: boolean\n errorUnknownListType?: boolean\n}): Promise<{ verified: boolean; error?: any }> {\n const verifyStatusListCredential = args.verifyStatusListCredential ?? true\n const verifyMatchingIssuers = args.verifyMatchingIssuers ?? true\n const uniform = CredentialMapper.toUniformCredential(args.credential)\n if (!('credentialStatus' in uniform) || !uniform.credentialStatus) {\n if (args.mandatoryCredentialStatus) {\n const error = 'No credential status object found in the Verifiable Credential and it is mandatory'\n console.log(error)\n return { verified: false, error }\n }\n return { verified: true }\n }\n if ('credentialStatus' in uniform && uniform.credentialStatus) {\n if (uniform.credentialStatus.type === 'StatusList2021Entry' || uniform.credentialStatus.type === 'BitstringStatusListEntry') {\n return checkStatus({ ...args, verifyStatusListCredential, verifyMatchingIssuers })\n } else if (args?.errorUnknownListType) {\n const error = `Credential status type ${uniform.credentialStatus.type} is not supported, and check status has been configured to not allow for that`\n console.log(error)\n return { verified: false, error }\n } else {\n console.log(`Skipped verification of status type ${uniform.credentialStatus.type} as we do not support it (yet)`)\n }\n }\n return { verified: true }\n}\n\nexport async function simpleCheckStatusFromStatusListUrl(args: {\n statusListCredential: string\n statusPurpose?: StatusPurpose2021\n type?: StatusListType | 'StatusList2021Entry'\n id?: string\n statusListIndex: string\n}): Promise<number | Status2021 | StatusOAuth> {\n return checkStatusIndexFromStatusListCredential({\n ...args,\n statusListCredential: await fetchStatusListCredential(args),\n })\n}\n\n/**\n * Checks the status at a specific index in a status list credential\n * @param args - Parameters including credential and index to check\n * @returns Promise resolving to status value at the specified index\n */\nexport async function checkStatusIndexFromStatusListCredential(args: {\n statusListCredential: StatusListCredential\n statusPurpose?: StatusPurpose2021 | string | string[]\n type?: StatusListType | 'StatusList2021Entry' | 'BitstringStatusListEntry'\n id?: string\n statusListIndex: string | number\n bitsPerStatus?: number\n}): Promise<number | Status2021 | StatusOAuth> {\n const statusListType: StatusListType = determineStatusListType(args.statusListCredential)\n const implementation = getStatusListImplementation(statusListType)\n return implementation.checkStatusIndex(args)\n}\n\nexport async function createNewStatusList(\n args: CreateNewStatusListFuncArgs,\n context: IAgentContext<(IVcdmCredentialPlugin | any) /*IvcdMCredentialPlugin is not available*/ & IIdentifierResolution>,\n): Promise<StatusListResult> {\n const { type } = getAssertedValues(args)\n const implementation = getStatusListImplementation(type)\n return implementation.createNewStatusList(args, context)\n}\n\n/**\n * Updates a status index in a status list credential\n * @param args - Parameters for status update including credential and new value\n * @param context - Agent context with required plugins\n * @returns Promise resolving to updated status list details\n */\nexport async function updateStatusIndexFromStatusListCredential(\n args: UpdateStatusListIndexArgs,\n context: IAgentContext<IVcdmCredentialPlugin & IIdentifierResolution>,\n): Promise<StatusListResult> {\n const credential = getAssertedValue('statusListCredential', args.statusListCredential)\n const statusListType: StatusListType = determineStatusListType(credential)\n const implementation = getStatusListImplementation(statusListType)\n return implementation.updateStatusListIndex(args, context)\n}\n\n/**\n * Extracts credential details from a status list credential\n * @param statusListCredential - The status list credential to extract from\n * @returns Promise resolving to extracted credential details\n */\nexport async function extractCredentialDetails(statusListCredential: StatusListCredential): Promise<IExtractedCredentialDetails> {\n const statusListType = determineStatusListType(statusListCredential)\n const implementation = getStatusListImplementation(statusListType)\n return implementation.extractCredentialDetails(statusListCredential)\n}\n\nexport async function toStatusListDetails(\n args: IToDetailsFromCredentialArgs,\n): Promise<StatusListResult & (IStatusList2021ImplementationResult | IOAuthStatusListImplementationResult | IBitstringStatusListImplementationResult)>\n\nexport async function toStatusListDetails(\n args: IMergeDetailsWithEntityArgs,\n): Promise<StatusListResult & (IStatusList2021ImplementationResult | IOAuthStatusListImplementationResult | IBitstringStatusListImplementationResult)>\n\n/**\n * Converts credential and metadata into detailed status list information\n * Handles both CREATE/READ and UPDATE contexts based on input arguments\n * @param args - Either credential-based args or entity-based args for merging\n * @returns Promise resolving to complete status list details\n */\nexport async function toStatusListDetails(\n args: IToDetailsFromCredentialArgs | IMergeDetailsWithEntityArgs,\n): Promise<\n StatusListResult & (IStatusList2021ImplementationResult | IOAuthStatusListImplementationResult | IBitstringStatusListImplementationResult)\n> {\n if ('statusListCredential' in args) {\n // CREATE/READ context\n const statusListType = args.statusListType\n const implementation = getStatusListImplementation(statusListType)\n return implementation.toStatusListDetails(args)\n } else {\n // UPDATE context\n const statusListType = args.statusListEntity.type\n const implementation = getStatusListImplementation(statusListType)\n return implementation.toStatusListDetails(args)\n }\n}\n\n/**\n * Creates a credential status object from status list and entry information\n * @param args - Parameters including status list, entry, and index\n * @returns Promise resolving to appropriate credential status type\n */\nexport async function createCredentialStatusFromStatusList(args: {\n statusList: StatusListEntity\n statusListEntry: IStatusListEntryEntity | IBitstringStatusListEntryEntity\n statusListIndex: number\n}): Promise<StatusList2021EntryCredentialStatus | StatusListOAuthEntryCredentialStatus | BitstringStatusListEntryCredentialStatus> {\n const { statusList, statusListEntry, statusListIndex } = args\n\n // Determine the status list type and delegate to appropriate implementation\n const statusListType = determineStatusListType(statusList.statusListCredential!)\n const implementation = getStatusListImplementation(statusListType)\n\n // Each implementation should have a method to create credential status\n return implementation.createCredentialStatus({\n statusList,\n statusListEntry,\n statusListIndex,\n })\n}\n\n/**\n * Updates a status list using a base64 encoded list of statuses\n * @param args - Parameters including encoded list and update details\n * @param context - Agent context with required plugins\n * @returns Promise resolving to updated status list details\n */\nexport async function updateStatusListIndexFromEncodedList(\n args: UpdateStatusListFromEncodedListArgs,\n context: IAgentContext<IVcdmCredentialPlugin & IIdentifierResolution>,\n): Promise<StatusListResult> {\n const { type } = getAssertedValue('type', args)\n const implementation = getStatusListImplementation(type!)\n return implementation.updateStatusListFromEncodedList(args, context)\n}\n\n/**\n * Converts a StatusList2021 to a verifiable credential\n * @param args - Parameters for credential creation including issuer and encoded list\n * @param context - Agent context with required plugins\n * @returns Promise resolving to signed status list credential\n */\nexport async function statusList2021ToVerifiableCredential(\n args: StatusList2021ToVerifiableCredentialArgs,\n context: IAgentContext<IVcdmCredentialPlugin & IIdentifierResolution>,\n): Promise<StatusListCredential> {\n const { issuer, id, type } = getAssertedValues(args)\n const identifier = await context.agent.identifierManagedGet({\n identifier: typeof issuer === 'string' ? issuer : issuer.id,\n vmRelationship: 'assertionMethod',\n offlineWhenNoDIDRegistered: true, // FIXME Fix identifier resolution for EBSI\n })\n const proofFormat: CredentialProofFormat = args?.proofFormat ?? 'lds'\n assertValidProofType(StatusListType.StatusList2021, proofFormat)\n const veramoProofFormat: VeramoProofFormat = proofFormat as VeramoProofFormat\n\n const encodedList = getAssertedValue('encodedList', args.encodedList)\n const statusPurpose = getAssertedValue('statusPurpose', args.statusPurpose)\n const credential = {\n '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/vc/status-list/2021/v1'],\n id,\n issuer,\n // issuanceDate: \"2021-03-10T04:24:12.164Z\",\n type: ['VerifiableCredential', `${type}Credential`],\n credentialSubject: {\n id,\n type,\n statusPurpose,\n encodedList,\n },\n }\n // TODO copy statuslist schema to local and disable fetching remote contexts\n const verifiableCredential = await context.agent.createVerifiableCredential({\n credential,\n keyRef: identifier.kmsKeyRef,\n proofFormat: veramoProofFormat,\n fetchRemoteContexts: true,\n })\n\n return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as StatusListCredential).original as StatusListCredential\n}\n","import {\n CredentialMapper,\n type IIssuer,\n type CredentialProofFormat,\n StatusListType,\n StatusListType as StatusListTypeW3C,\n type StatusListCredential,\n DocumentFormat,\n} from '@sphereon/ssi-types'\nimport { jwtDecode } from 'jwt-decode'\n\nexport function getAssertedStatusListType(type?: StatusListType) {\n const assertedType = type ?? StatusListType.StatusList2021\n if (![StatusListType.StatusList2021, StatusListType.OAuthStatusList, StatusListType.BitstringStatusList].includes(assertedType)) {\n throw Error(`StatusList type ${assertedType} is not supported (yet)`)\n }\n return assertedType\n}\n\nexport function getAssertedValue<T>(name: string, value: T): NonNullable<T> {\n if (value === undefined || value === null) {\n throw Error(`Missing required ${name} value`)\n }\n return value\n}\n\nexport function getAssertedValues(args: { issuer: string | IIssuer; id: string; type?: StatusListTypeW3C | StatusListType }) {\n const type = getAssertedStatusListType(args?.type)\n const id = getAssertedValue('id', args.id)\n const issuer = getAssertedValue('issuer', args.issuer)\n return { id, issuer, type }\n}\n\nexport function getAssertedProperty<T extends object>(propertyName: string, obj: T): NonNullable<any> {\n if (!(propertyName in obj)) {\n throw Error(`The input object does not contain required property: ${propertyName}`)\n }\n return getAssertedValue(propertyName, (obj as any)[propertyName])\n}\n\nconst ValidProofTypeMap = new Map<StatusListType, CredentialProofFormat[]>([\n [StatusListType.StatusList2021, ['jwt', 'lds']],\n [StatusListType.OAuthStatusList, ['jwt', 'cbor']],\n [StatusListType.BitstringStatusList, ['lds', 'vc+jwt']],\n])\n\nexport function assertValidProofType(type: StatusListType, proofFormat: CredentialProofFormat) {\n const validProofTypes = ValidProofTypeMap.get(type)\n if (!validProofTypes?.includes(proofFormat)) {\n throw Error(`Invalid proof format '${proofFormat}' for status list type ${type}`)\n }\n}\n\nexport function determineStatusListType(credential: StatusListCredential): StatusListType {\n const proofFormat = determineProofFormat(credential)\n\n switch (proofFormat) {\n case 'jwt':\n return determineJwtStatusListType(credential as string)\n case 'lds':\n return determineLdsStatusListType(credential)\n case 'cbor':\n return StatusListType.OAuthStatusList\n default:\n throw new Error('Cannot determine status list type from credential payload')\n }\n}\n\nfunction determineJwtStatusListType(credential: string): StatusListType {\n const payload: any = jwtDecode(credential)\n\n // OAuth status list format\n if ('status_list' in payload) {\n return StatusListType.OAuthStatusList\n }\n\n // Direct credential subject\n if ('credentialSubject' in payload) {\n return getStatusListTypeFromSubject(payload.credentialSubject)\n }\n\n // Wrapped VC format\n if ('vc' in payload && 'credentialSubject' in payload.vc) {\n return getStatusListTypeFromSubject(payload.vc.credentialSubject)\n }\n\n throw new Error('Invalid status list credential: credentialSubject not found')\n}\n\nfunction determineLdsStatusListType(credential: StatusListCredential): StatusListType {\n const uniform = CredentialMapper.toUniformCredential(credential)\n const statusListType = uniform.type.find((type) => Object.values(StatusListType).some((statusType) => type.includes(statusType)))\n\n if (!statusListType) {\n throw new Error('Invalid status list credential type')\n }\n\n return statusListType.replace('Credential', '') as StatusListType\n}\n\nfunction getStatusListTypeFromSubject(credentialSubject: any): StatusListType {\n switch (credentialSubject.type) {\n case 'StatusList2021':\n return StatusListType.StatusList2021\n case 'BitstringStatusList':\n return StatusListType.BitstringStatusList\n default:\n throw new Error(`Unknown credential subject type: ${credentialSubject.type}`)\n }\n}\n\nexport function determineProofFormat(credential: StatusListCredential): CredentialProofFormat {\n const type: DocumentFormat = CredentialMapper.detectDocumentType(credential)\n switch (type) {\n case DocumentFormat.JWT:\n return 'jwt'\n case DocumentFormat.MSO_MDOC:\n // Not really mdoc, just assume Cbor for now, I'd need to decode at least the header to what type of Cbor we have\n return 'cbor'\n case DocumentFormat.JSONLD:\n return 'lds'\n default:\n throw Error('Cannot determine credential payload type')\n }\n}\n\n/**\n * Ensures a value is converted to a Date object if it's a valid date string,\n * otherwise returns the original value or undefined\n *\n * @param value - The value to convert to Date (can be Date, string, or undefined)\n * @returns Date object, undefined, or the original value if conversion fails\n */\nexport function ensureDate(value: Date | string | undefined): Date | undefined {\n if (value === undefined || value === null) {\n return undefined\n }\n\n if (value instanceof Date) {\n return value\n }\n\n if (typeof value === 'string') {\n if (value.trim() === '') {\n return undefined\n }\n\n const date = new Date(value)\n // Check if the date is valid\n if (isNaN(date.getTime())) {\n return undefined\n }\n return date\n }\n\n return undefined\n}\n","import type { IAgentContext, ProofFormat as VeramoProofFormat } from '@veramo/core'\nimport type { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution'\nimport {\n CredentialMapper,\n type CredentialProofFormat,\n DocumentFormat,\n type IIssuer,\n type StatusListCredential,\n StatusListCredentialIdMode,\n StatusListType,\n} from '@sphereon/ssi-types'\n\nimport { StatusList } from '@sphereon/vc-status-list'\nimport type { IExtractedCredentialDetails, IStatusList, IStatusList2021ImplementationResult } from './IStatusList'\nimport type {\n CheckStatusIndexArgs,\n CreateStatusListArgs,\n IMergeDetailsWithEntityArgs,\n IToDetailsFromCredentialArgs,\n StatusListResult,\n UpdateStatusListFromEncodedListArgs,\n UpdateStatusListIndexArgs,\n} from '../types'\nimport { Status2021, StatusList2021EntryCredentialStatus } from '../types'\nimport { assertValidProofType, getAssertedProperty, getAssertedValue, getAssertedValues } from '../utils'\nimport { IBitstringStatusListEntryEntity, IStatusListEntryEntity, StatusList2021Entity, StatusListEntity } from '@sphereon/ssi-sdk.data-store'\nimport { IVcdmCredentialPlugin } from '@sphereon/ssi-sdk.credential-vcdm'\n\nexport const DEFAULT_LIST_LENGTH = 250000\nexport const DEFAULT_PROOF_FORMAT = 'lds' as CredentialProofFormat\n\nexport class StatusList2021Implementation implements IStatusList {\n async createNewStatusList(\n args: CreateStatusListArgs,\n context: IAgentContext<IVcdmCredentialPlugin & IIdentifierResolution>,\n ): Promise<StatusListResult> {\n const length = args?.length ?? DEFAULT_LIST_LENGTH\n const proofFormat: CredentialProofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT\n assertValidProofType(StatusListType.StatusList2021, proofFormat)\n const veramoProofFormat: VeramoProofFormat = proofFormat as VeramoProofFormat\n\n const { issuer, id } = args\n const correlationId = getAssertedValue('correlationId', args.correlationId)\n\n const list = new StatusList({ length })\n const encodedList = await list.encode()\n const statusPurpose = 'revocation'\n\n const statusListCredential = await this.createVerifiableCredential(\n {\n ...args,\n encodedList,\n proofFormat: veramoProofFormat,\n },\n context,\n )\n\n return {\n encodedList,\n statusListCredential: statusListCredential,\n statusList2021: {\n statusPurpose,\n indexingDirection: 'rightToLeft',\n credentialIdMode: StatusListCredentialIdMode.ISSUANCE,\n },\n length,\n type: StatusListType.StatusList2021,\n proofFormat,\n id,\n correlationId,\n issuer,\n statuslistContentType: this.buildContentType(proofFormat),\n }\n }\n\n async updateStatusListIndex(\n args: UpdateStatusListIndexArgs,\n context: IAgentContext<IVcdmCredentialPlugin & IIdentifierResolution>,\n ): Promise<StatusListResult> {\n const credential = args.statusListCredential\n const uniform = CredentialMapper.toUniformCredential(credential)\n const { issuer, credentialSubject } = uniform\n const id = getAssertedValue('id', uniform.id)\n const origEncodedList = getAssertedProperty('encodedList', credentialSubject)\n\n const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex)\n const statusList = await StatusList.decode({ encodedList: origEncodedList })\n statusList.setStatus(index, args.value != 0)\n const encodedList = await statusList.encode()\n\n const proofFormat = CredentialMapper.detectDocumentType(credential) === DocumentFormat.JWT ? 'jwt' : 'lds'\n const updatedCredential = await this.createVerifiableCredential(\n {\n ...args,\n id,\n issuer,\n encodedList,\n proofFormat: proofFormat,\n },\n context,\n )\n\n if (!('statusPurpose' in credentialSubject)) {\n return Promise.reject(Error('statusPurpose is required in credentialSubject for StatusList2021'))\n }\n\n return {\n statusListCredential: updatedCredential,\n encodedList,\n statusList2021: {\n statusPurpose: credentialSubject.statusPurpose,\n indexingDirection: 'rightToLeft',\n credentialIdMode: StatusListCredentialIdMode.ISSUANCE,\n },\n length: statusList.length - 1,\n type: StatusListType.StatusList2021,\n proofFormat: proofFormat,\n id,\n issuer,\n statuslistContentType: this.buildContentType(proofFormat),\n }\n }\n\n async updateStatusListFromEncodedList(\n args: UpdateStatusListFromEncodedListArgs,\n context: IAgentContext<IVcdmCredentialPlugin & IIdentifierResolution>,\n ): Promise<StatusListResult> {\n if (!args.statusList2021) {\n throw new Error('statusList2021 options required for type StatusList2021')\n }\n const proofFormat: CredentialProofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT\n assertValidProofType(StatusListType.StatusList2021, proofFormat)\n const veramoProofFormat: VeramoProofFormat = proofFormat as VeramoProofFormat\n\n const { issuer, id } = getAssertedValues(args)\n const statusList = await StatusList.decode({ encodedList: args.encodedList })\n const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex)\n statusList.setStatus(index, args.value !== 0)\n\n const newEncodedList = await statusList.encode()\n const credential = await this.createVerifiableCredential(\n {\n id,\n issuer,\n encodedList: newEncodedList,\n proofFormat: veramoProofFormat,\n keyRef: args.keyRef,\n },\n context,\n )\n\n return {\n type: StatusListType.StatusList2021,\n statusListCredential: credential,\n encodedList: newEncodedList,\n statusList2021: {\n statusPurpose: args.statusList2021.statusPurpose,\n indexingDirection: 'rightToLeft',\n credentialIdMode: StatusListCredentialIdMode.ISSUANCE,\n },\n length: statusList.length,\n proofFormat: args.proofFormat ?? 'lds',\n id: id,\n issuer: issuer,\n statuslistContentType: this.buildContentType(proofFormat),\n }\n }\n\n async checkStatusIndex(args: CheckStatusIndexArgs): Promise<number | Status2021> {\n const uniform = CredentialMapper.toUniformCredential(args.statusListCredential)\n const { credentialSubject } = uniform\n const encodedList = getAssertedProperty('encodedList', credentialSubject)\n\n const statusList = await StatusList.decode({ encodedList })\n const status = statusList.getStatus(typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex))\n return status ? Status2021.Invalid : Status2021.Valid\n }\n\n /**\n * Performs the initial parsing of a StatusListCredential.\n * This method handles expensive operations like JWT/CWT decoding once.\n * It extracts all details available from the credential payload itself.\n */\n async extractCredentialDetails(credential: StatusListCredential): Promise<IExtractedCredentialDetails> {\n const uniform = CredentialMapper.toUniformCredential(credential)\n const { issuer, credentialSubject } = uniform\n const subject = Array.isArray(credentialSubject) ? credentialSubject[0] : credentialSubject\n\n return {\n id: getAssertedValue('id', uniform.id),\n issuer,\n encodedList: getAssertedProperty('encodedList', subject),\n }\n }\n\n async toStatusListDetails(args: IToDetailsFromCredentialArgs): Promise<StatusListResult & IStatusList2021ImplementationResult>\n // For UPDATE contexts\n async toStatusListDetails(args: IMergeDetailsWithEntityArgs): Promise<StatusListResult & IStatusList2021ImplementationResult>\n async toStatusListDetails(\n args: IToDetailsFromCredentialArgs | IMergeDetailsWithEntityArgs,\n ): Promise<StatusListResult & IStatusList2021ImplementationResult> {\n if ('statusListCredential' in args) {\n // CREATE/READ context\n const { statusListCredential, correlationId, driverType } = args\n const uniform = CredentialMapper.toUniformCredential(statusListCredential)\n const { issuer, credentialSubject } = uniform\n const subject = Array.isArray(credentialSubject) ? credentialSubject[0] : credentialSubject\n\n const id = getAssertedValue('id', uniform.id)\n const encodedList = getAssertedProperty('encodedList', subject)\n const statusPurpose = getAssertedProperty('statusPurpose', subject)\n const proofFormat: CredentialProofFormat = CredentialMapper.detectDocumentType(statusListCredential) === DocumentFormat.JWT ? 'jwt' : 'lds'\n const list = await StatusList.decode({ encodedList })\n\n return {\n id,\n encodedList,\n issuer,\n type: StatusListType.StatusList2021,\n proofFormat,\n length: list.length,\n statusListCredential,\n statuslistContentType: this.buildContentType(proofFormat),\n correlationId,\n driverType,\n indexingDirection: 'rightToLeft',\n statusPurpose,\n statusList2021: {\n indexingDirection: 'rightToLeft',\n statusPurpose,\n credentialIdMode: StatusListCredentialIdMode.ISSUANCE,\n },\n }\n } else {\n // UPDATE context\n const { extractedDetails, statusListEntity } = args\n const statusList2021Entity = statusListEntity as StatusList2021Entity\n\n const proofFormat: CredentialProofFormat =\n CredentialMapper.detectDocumentType(statusListEntity.statusListCredential!) === DocumentFormat.JWT ? 'jwt' : 'lds'\n const list = await StatusList.decode({ encodedList: extractedDetails.encodedList })\n\n return {\n id: extractedDetails.id,\n encodedList: extractedDetails.encodedList,\n issuer: extractedDetails.issuer,\n type: StatusListType.StatusList2021,\n proofFormat,\n length: list.length,\n statusListCredential: statusListEntity.statusListCredential!,\n statuslistContentType: this.buildContentType(proofFormat),\n correlationId: statusListEntity.correlationId,\n driverType: statusListEntity.driverType,\n indexingDirection: statusList2021Entity.indexingDirection,\n statusPurpose: statusList2021Entity.statusPurpose,\n statusList2021: {\n indexingDirection: statusList2021Entity.indexingDirection,\n statusPurpose: statusList2021Entity.statusPurpose,\n credentialIdMode: StatusListCredentialIdMode.ISSUANCE,\n },\n }\n }\n }\n\n async createCredentialStatus(args: {\n statusList: StatusListEntity\n statusListEntry: IStatusListEntryEntity | IBitstringStatusListEntryEntity\n statusListIndex: number\n }): Promise<StatusList2021EntryCredentialStatus> {\n const { statusList, statusListIndex } = args\n\n // Cast to StatusList2021Entity to access specific properties\n const statusList2021 = statusList as StatusList2021Entity\n\n return {\n id: `${statusList.id}#${statusListIndex}`,\n type: 'StatusList2021Entry',\n statusPurpose: statusList2021.statusPurpose ?? 'revocation',\n statusListIndex: '' + statusListIndex,\n statusListCredential: statusList.id,\n }\n }\n\n private async createVerifiableCredential(\n args: {\n id: string\n issuer: string | IIssuer\n encodedList: string\n proofFormat: VeramoProofFormat\n keyRef?: string\n },\n context: IAgentContext<IVcdmCredentialPlugin & IIdentifierResolution>,\n ): Promise<StatusListCredential> {\n const identifier = await context.agent.identifierManagedGet({\n identifier: typeof args.issuer === 'string' ? args.issuer : args.issuer.id,\n vmRelationship: 'assertionMethod',\n offlineWhenNoDIDRegistered: true,\n })\n\n const credential = {\n '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/vc/status-list/2021/v1'],\n id: args.id,\n issuer: args.issuer,\n type: ['VerifiableCredential', 'StatusList2021Credential'],\n credentialSubject: {\n id: args.id,\n type: 'StatusList2021',\n statusPurpose: 'revocation',\n encodedList: args.encodedList,\n },\n }\n\n const verifiableCredential = await context.agent.createVerifiableCredential({\n credential,\n keyRef: args.keyRef ?? identifier.kmsKeyRef,\n proofFormat: args.proofFormat,\n fetchRemoteContexts: true,\n })\n\n return CredentialMapper.toWrappedVerifiableCredential(verifiableCredential as StatusListCredential).original as StatusListCredential\n }\n\n private buildContentType(proofFormat: CredentialProofFormat | undefined) {\n switch (proofFormat) {\n case 'jwt':\n return `application/statuslist+jwt`\n case 'cbor':\n return `application/statuslist+cwt`\n case 'lds':\n return 'application/statuslist+ld+json'\n default:\n throw Error(`Unsupported content type '${proofFormat}' for status lists`)\n }\n }\n}\n","import type { IAgentContext, IKeyManager } from '@veramo/core'\nimport { type CompactJWT, type CredentialProofFormat, type CWT, StatusListType } from '@sphereon/ssi-types'\nimport type {\n CheckStatusIndexArgs,\n CreateStatusListArgs,\n IMergeDetailsWithEntityArgs,\n IToDetailsFromCredentialArgs,\n SignedStatusListData,\n StatusListOAuthEntryCredentialStatus,\n StatusListResult,\n StatusOAuth,\n UpdateStatusListFromEncodedListArgs,\n UpdateStatusListIndexArgs,\n} from '../types'\nimport { determineProofFormat, ensureDate, getAssertedValue, getAssertedValues } from '../utils'\nimport type { IExtractedCredentialDetails, IOAuthStatusListImplementationResult, IStatusList } from './IStatusList'\nimport { StatusList } from '@sd-jwt/jwt-status-list'\nimport type { IJwtService } from '@sphereon/ssi-sdk-ext.jwt-service'\nimport type { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution'\nimport { createSignedJwt, decodeStatusListJWT } from './encoding/jwt'\nimport { createSignedCbor, decodeStatusListCWT } from './encoding/cbor'\nimport { IBitstringStatusListEntryEntity, IStatusListEntryEntity, OAuthStatusListEntity, StatusListEntity } from '@sphereon/ssi-sdk.data-store'\nimport { IVcdmCredentialPlugin } from '@sphereon/ssi-sdk.credential-vcdm'\n\ntype IRequiredContext = IAgentContext<IVcdmCredentialPlugin & IJwtService & IIdentifierResolution & IKeyManager>\n\nexport const DEFAULT_BITS_PER_STATUS = 1 // 1 bit is sufficient for 0x00 - \"VALID\" 0x01 - \"INVALID\" saving space in the process\nexport const DEFAULT_LIST_LENGTH = 250000\nexport const DEFAULT_PROOF_FORMAT = 'jwt' as CredentialProofFormat\n\nexport class OAuthStatusListImplementation implements IStatusList {\n async createNewStatusList(args: CreateStatusListArgs, context: IRequiredContext): Promise<StatusListResult> {\n if (!args.oauthStatusList) {\n throw new Error('OAuthStatusList options are required for type OAuthStatusList')\n }\n\n const proofFormat = args?.proofFormat ?? DEFAULT_PROOF_FORMAT\n const { issuer, id, oauthStatusList, keyRef } = args\n const { bitsPerStatus } = oauthStatusList\n const expiresAt = ensureDate(oauthStatusList.expiresAt)\n const length = args.length ?? DEFAULT_LIST_LENGTH\n const issuerString = typeof issuer === 'string' ? issuer : issuer.id\n const correlationId = getAssertedValue('correlationId', args.correlationId)\n\n const statusList = new StatusList(new Array(length).fill(0), bitsPerStatus ?? DEFAULT_BITS_PER_STATUS)\n const encodedList = statusList.compressStatusList()\n const { statusListCredential } = await this.createSignedStatusList(proofFormat, context, statusList, issuerString, id, expiresAt, keyRef)\n\n return {\n encodedList,\n statusListCredential,\n oauthStatusList: { bitsPerStatus },\n length,\n type: StatusListType.OAuthStatusList,\n proofFormat,\n id,\n correlationId,\n issuer,\n statuslistContentType: this.buildContentType(proofFormat),\n }\n }\n\n async updateStatusListIndex(args: UpdateStatusListIndexArgs, context: IRequiredContext): Promise<StatusListResult> {\n const { statusListCredential, value, keyRef } = args\n const expiresAt = ensureDate(args.expiresAt)\n if (typeof statusListCredential !== 'string') {\n return Promise.reject('statusListCredential in neither JWT nor CWT')\n }\n\n const proofFormat = determineProofFormat(statusListCredential)\n const decoded = proofFormat === 'jwt' ? decodeStatusListJWT(statusListCredential) : decodeStatusListCWT(statusListCredential)\n const { statusList, issuer, id } = decoded\n\n const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex)\n if (index < 0 || index >= statusList.statusList.length) {\n throw new Error('Status list index out of bounds')\n }\n\n if (typeof value !== 'number') {\n throw new Error('Status list values should be of type number')\n }\n\n statusList.setStatus(index, value)\n const { statusListCredential: signedCredential, encodedList } = await this.createSignedStatusList(\n proofFormat,\n context,\n statusList,\n issuer,\n id,\n expiresAt,\n keyRef,\n )\n\n return {\n statusListCredential: signedCredential,\n encodedList,\n oauthStatusList: {\n bitsPerStatus: statusList.getBitsPerStatus(),\n },\n length: statusList.statusList.length,\n type: StatusListType.OAuthStatusList,\n proofFormat,\n id,\n issuer,\n statuslistContentType: this.buildContentType(proofFormat),\n }\n }\n\n // FIXME: This still assumes only two values (boolean), whilst this list supports 8 bits max\n async updateStatusListFromEncodedList(args: UpdateStatusListFromEncodedListArgs, context: IRequiredContext): Promise<StatusListResult> {\n if (!args.oauthStatusList) {\n throw new Error('OAuthStatusList options are required for type OAuthStatusList')\n }\n const { proofFormat, oauthStatusList, keyRef } = args\n const { bitsPerStatus } = oauthStatusList\n const expiresAt = ensureDate(oauthStatusList.expiresAt)\n\n const { issuer, id } = getAssertedValues(args)\n const issuerString = typeof issuer === 'string' ? issuer : issuer.id\n\n const listToUpdate = StatusList.decompressStatusList(args.encodedList, bitsPerStatus ?? DEFAULT_BITS_PER_STATUS)\n const index = typeof args.statusListIndex === 'number' ? args.statusListIndex : parseInt(args.statusListIndex)\n listToUpdate.setStatus(index, args.value)\n\n const { statusListCredential, encodedList } = await this.createSignedStatusList(\n proofFormat ?? DEFAULT_PROOF_FORMAT,\n context,\n listToUpdate,\n issuerString,\n id,\n expiresAt,\n keyRef,\n )\n\n return {\n encodedList,\n statusListCredential,\n oauthStatusList: {\n bitsPerStatus,\n expiresAt,\n },\n length: listToUpdate.statusList.length,\n type: StatusListType.OAuthStatusList,\n proofFormat: proofFormat ?? DEFAULT_PROOF_FORMAT,\n id,\n issuer,\n statuslistContentType: this.buildContentType(proofFormat),\n }\n }\n\n async checkStatusIndex(args: CheckStatusIndexArgs): Promise<number | StatusOAuth> {\n const { statusListCredential, statusListIndex } = args\n if (typeof statusListCredential !== 'string') {\n return Promise.reject('statusListCredential in neither JWT nor CWT')\n }\n\n const proofFormat = determineProofFormat(statusListCredential)\n const { statusList } = proofFormat === 'jwt' ? decodeStatusListJWT(statusListCredential) : decodeStatusListCWT(statusListCredential)\n\n const index = typeof statusListIndex === 'number' ? statusListIndex : parseInt(statusListIndex)\n if (index < 0 || index >= statusList.statusList.length) {\n throw new Error(`Status list index out of bounds, has ${statusList.statusList.length} items, requested ${index}`)\n }\n\n return statusList.getStatus(index)\n }\n\n /**\n * Performs the initial parsing of a StatusListCredential.\n * This method handles expensive operations like JWT/CWT decoding once.\n * It extracts all details available from the credential payload itself.\n */\n async extractCredentialDetails(credential: CompactJWT | CWT): Promise<IExtractedCredentialDetails> {\n if (typeof credential