@sphereon/ssi-sdk.data-store
Version:
208 lines (181 loc) • 7.8 kB
text/typescript
import { defaultHasher } from '@sphereon/ssi-sdk.core'
import type {
AddCredentialArgs,
DigitalCredential,
NonPersistedDigitalCredential,
UpdateCredentialStateArgs,
} from '@sphereon/ssi-sdk.data-store-types'
import { CredentialDocumentFormat, CredentialStateType, DocumentType, RegulationType } from '@sphereon/ssi-sdk.data-store-types'
import {
CredentialMapper,
DocumentFormat,
type IVerifiableCredential,
type IVerifiablePresentation,
ObjectUtils,
type OriginalVerifiableCredential,
type OriginalVerifiablePresentation,
type SdJwtDecodedVerifiableCredentialPayload,
} from '@sphereon/ssi-types'
import { computeEntryHash } from '@veramo/utils'
import { DigitalCredentialEntity } from '../../entities/digitalCredential/DigitalCredentialEntity'
import { replaceNullWithUndefined } from '../FormattingUtils'
function determineDocumentType(raw: string): DocumentType {
const rawDocument = parseRawDocument(raw)
if (!rawDocument) {
throw new Error(`Couldn't parse the credential: ${raw}`)
}
const hasProof = CredentialMapper.hasProof(rawDocument)
const isCredential = isHex(raw) || ObjectUtils.isBase64(raw) || CredentialMapper.isCredential(rawDocument)
const isPresentation = CredentialMapper.isPresentation(rawDocument)
if (isCredential) {
return hasProof || isHex(raw) || ObjectUtils.isBase64(raw) ? DocumentType.VC : DocumentType.C
} else if (isPresentation) {
return hasProof ? DocumentType.VP : DocumentType.P
}
throw new Error(`Couldn't determine the type of the credential: ${raw}`)
}
export function isHex(input: string) {
return input.match(/^([0-9A-Fa-f])+$/g) !== null
}
export function parseRawDocument(raw: string): OriginalVerifiableCredential | OriginalVerifiablePresentation {
if (isHex(raw) || ObjectUtils.isBase64(raw)) {
// mso_mdoc
return raw
} else if (CredentialMapper.isJwtEncoded(raw) || CredentialMapper.isSdJwtEncoded(raw)) {
return raw
}
try {
return JSON.parse(raw)
} catch (e) {
throw new Error(`Can't parse the raw credential: ${raw}`)
}
}
function determineCredentialDocumentFormat(documentFormat: DocumentFormat): CredentialDocumentFormat {
switch (documentFormat) {
case DocumentFormat.JSONLD:
return CredentialDocumentFormat.JSON_LD
case DocumentFormat.JWT:
return CredentialDocumentFormat.JWT
case DocumentFormat.SD_JWT_VC:
return CredentialDocumentFormat.SD_JWT
case DocumentFormat.MSO_MDOC:
return CredentialDocumentFormat.MSO_MDOC
default:
throw new Error(`Not supported document format: ${documentFormat}`)
}
}
/**
* Normalizes nullable fields by converting undefined to null.
* This ensures TypeORM actually clears the database fields instead of ignoring them.
*/
export function normalizeNullableFields<T extends Record<string, any>>(obj: T, nullableKeys: Array<keyof T>): T {
const normalized = { ...obj }
for (const key of nullableKeys) {
if (normalized[key] === undefined) {
normalized[key] = null as any
}
}
return normalized
}
function getValidUntil(uniformDocument: IVerifiableCredential | IVerifiablePresentation | SdJwtDecodedVerifiableCredentialPayload): Date | undefined {
if ('expirationDate' in uniformDocument && uniformDocument.expirationDate) {
return new Date(uniformDocument.expirationDate)
} else if ('validUntil' in uniformDocument && uniformDocument.validUntil) {
return new Date(uniformDocument.validUntil)
} else if ('exp' in uniformDocument && uniformDocument.exp) {
return new Date(uniformDocument.exp * 1000)
}
return undefined
}
function getValidFrom(uniformDocument: IVerifiableCredential | IVerifiablePresentation | SdJwtDecodedVerifiableCredentialPayload): Date | undefined {
if ('issuanceDate' in uniformDocument && uniformDocument.issuanceDate) {
return new Date(uniformDocument.issuanceDate)
} else if ('validFrom' in uniformDocument && uniformDocument.validFrom) {
return new Date(uniformDocument['validFrom'])
} else if ('nbf' in uniformDocument && uniformDocument.nbf) {
return new Date(uniformDocument['nbf'] * 1000)
} else if ('iat' in uniformDocument && uniformDocument.iat) {
return new Date(uniformDocument['iat'] * 1000)
}
return undefined
}
const safeStringify = (object: any): string => {
if (typeof object === 'string') {
return object
}
return JSON.stringify(object)
}
export const nonPersistedDigitalCredentialEntityFromAddArgs = (addCredentialArgs: AddCredentialArgs): NonPersistedDigitalCredential => {
const documentType: DocumentType = determineDocumentType(addCredentialArgs.rawDocument)
const documentFormat: DocumentFormat = CredentialMapper.detectDocumentType(addCredentialArgs.rawDocument)
const hasher = addCredentialArgs?.opts?.hasher ?? defaultHasher
if (documentFormat === DocumentFormat.SD_JWT_VC && !addCredentialArgs.opts?.hasher) {
throw new Error('No hasher function is provided for SD_JWT credential.')
}
const uniformDocument =
documentType === DocumentType.VC || documentType === DocumentType.C
? CredentialMapper.toUniformCredential(addCredentialArgs.rawDocument, { hasher })
: CredentialMapper.toUniformPresentation(addCredentialArgs.rawDocument, { hasher })
const validFrom: Date | undefined = getValidFrom(uniformDocument)
const validUntil: Date | undefined = getValidUntil(uniformDocument)
const hash = computeEntryHash(addCredentialArgs.rawDocument)
const regulationType = addCredentialArgs.regulationType ?? RegulationType.NON_REGULATED
return {
...addCredentialArgs,
regulationType,
documentType,
documentFormat: determineCredentialDocumentFormat(documentFormat),
createdAt: new Date(),
credentialId: uniformDocument.id ?? hash,
hash,
uniformDocument: safeStringify(uniformDocument),
validFrom,
...(validUntil && { validUntil }),
lastUpdatedAt: new Date(),
}
}
export const persistedDigitalCredentialEntityFromUpdateArgs = (
existingCredential: DigitalCredentialEntity,
updates: Partial<DigitalCredential>,
): DigitalCredentialEntity => {
const entity = new DigitalCredentialEntity()
// Copy all fields from existing credential
Object.assign(entity, existingCredential)
// Normalize nullable fields before applying updates
const normalizedUpdates = normalizeNullableFields(updates, ['linkedVpId', 'linkedVpFrom', 'linkedVpUntil'])
// Apply updates
Object.assign(entity, normalizedUpdates)
// Ensure these fields are never overwritten
entity.id = existingCredential.id
entity.hash = existingCredential.hash
entity.createdAt = existingCredential.createdAt
entity.lastUpdatedAt = new Date()
return entity
}
export const persistedDigitalCredentialEntityFromStateArgs = (
existingCredential: DigitalCredentialEntity,
args: UpdateCredentialStateArgs,
): DigitalCredentialEntity => {
const entity = new DigitalCredentialEntity()
// Copy all fields from existing credential
Object.assign(entity, existingCredential)
// Apply state updates
entity.verifiedState = args.verifiedState
entity.lastUpdatedAt = new Date()
if (args.verifiedState === CredentialStateType.REVOKED && args.revokedAt) {
entity.revokedAt = args.revokedAt
}
if (args.verifiedState !== CredentialStateType.REVOKED && args.verifiedAt) {
entity.verifiedAt = args.verifiedAt
}
return entity
}
export const digitalCredentialFrom = (credentialEntity: DigitalCredentialEntity): DigitalCredential => {
const result: DigitalCredential = {
...credentialEntity,
}
return replaceNullWithUndefined(result)
}
export const digitalCredentialsFrom = (credentialEntities: Array<DigitalCredentialEntity>): DigitalCredential[] => {
return credentialEntities.map((credentialEntity) => digitalCredentialFrom(credentialEntity))
}