src/issuer/configuration/credentials/credential-config/credential-config.service.ts
Service for managing credential configurations.
Properties |
|
Methods |
|
constructor(credentialConfigRepository: Repository<CredentialConfig>, certService: CertService, filesService: FilesService, configImportService: ConfigImportService, configImportOrchestrator: ConfigImportOrchestratorService, presentationsService: PresentationsService, tenantActionLogService: AuditLogService)
|
||||||||||||||||||||||||||||||||
|
Constructor for CredentialConfigService.
Parameters :
|
| Async delete | ||||||||||||||||||||
delete(tenantId: string, id: string, actorToken?: TokenPayload, req?: Request)
|
||||||||||||||||||||
|
Deletes a credential configuration for a given tenant.
Parameters :
Returns :
unknown
A promise that resolves to the result of the delete operation. |
| get | ||||||||
get(tenantId: string)
|
||||||||
|
Retrieves all credential configurations for a given tenant.
Parameters :
Returns :
any
A promise that resolves to an array of CredentialConfig entities. |
| getById |
getById(tenantId: string, id: string)
|
|
Retrieves a credential configuration by its ID for a given tenant.
Returns :
any
|
| Public Async importForTenant | ||||||
importForTenant(tenantId: string)
|
||||||
|
Imports credential configs for a specific tenant.
Parameters :
Returns :
any
|
| Private Async processCredentialConfig | |||||||||
processCredentialConfig(tenantId: string, config: CredentialConfigCreate)
|
|||||||||
|
Process a credential config for import. Note: IAE action validation is skipped during import because presentation configs are imported in a later phase (REFERENCES).
Parameters :
Returns :
any
|
| Private Async replaceImageReferences | ||||||||||||
replaceImageReferences(tenantId: string, config: CredentialConfigCreate | CredentialConfigUpdate)
|
||||||||||||
|
Replaces image references (logo, background_image) with actual public URLs. This is used both during file import and API calls.
Parameters :
Returns :
Promise<void>
|
| Private Async resolveImageUrl | ||||||||||||
resolveImageUrl(tenantId: string, image: literal type | undefined, context: string)
|
||||||||||||
|
Replaces a single image reference with a public URL, or returns undefined if invalid.
Parameters :
Returns :
Promise<literal type | undefined>
|
| Private sanitizeCredentialConfigForLog | ||||||
sanitizeCredentialConfigForLog(config: CredentialConfig)
|
||||||
|
Parameters :
Returns :
Record<string, unknown>
|
| Async store | ||||||||||||||||||||||||||||||
store(tenantId: string, config: CredentialConfigCreate, skipValidation: unknown, actorToken?: TokenPayload, req?: Request)
|
||||||||||||||||||||||||||||||
|
Stores a credential configuration for a given tenant. If the configuration already exists, it will be overwritten. Automatically replaces image references with public URLs. Validates IAE action references.
Parameters :
Returns :
unknown
A promise that resolves to the stored CredentialConfig entity. |
| Async update | ||||||||||||||||||||||||
update(tenantId: string, id: string, config: CredentialConfigUpdate, actorToken?: TokenPayload, req?: Request)
|
||||||||||||||||||||||||
|
Updates a credential configuration for a given tenant. Only updates fields that are provided in the config. Set fields to null to clear them. Automatically replaces image references with public URLs. Validates IAE action references.
Parameters :
Returns :
unknown
A promise that resolves to the updated CredentialConfig entity. |
| Private Async validateAttestationKeyChain | |||||||||
validateAttestationKeyChain(tenantId: string, keyChainId?: string | null)
|
|||||||||
|
Parameters :
Returns :
Promise<void>
|
| Private Async validateIaeActions | ||||||||||||
validateIaeActions(tenantId: string, config: CredentialConfigCreate | CredentialConfigUpdate)
|
||||||||||||
|
Validates IAE actions in a credential configuration. Checks that all referenced presentation configs exist.
Parameters :
Returns :
Promise<void>
|
| Private Readonly logger |
Type : unknown
|
Default value : new Logger(CredentialConfigService.name)
|
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Request } from "express";
import { Repository } from "typeorm";
import { AuditLogService } from "../../../../audit-log/audit-log.service";
import { TokenPayload } from "../../../../auth/token.decorator";
import { CertService } from "../../../../crypto/key/cert/cert.service";
import { KeyUsageType } from "../../../../crypto/key/entities/key-chain.entity";
import {
extractRequestMeta,
getChangedFields,
resolveAuditActor,
} from "../../../../shared/utils/audit-log-context.util";
import { loadConfigDto } from "../../../../shared/utils/config-file-loader.util";
import { ConfigImportService } from "../../../../shared/utils/config-import/config-import.service";
import {
ConfigImportOrchestratorService,
ImportPhase,
} from "../../../../shared/utils/config-import/config-import-orchestrator.service";
import { FilesService } from "../../../../storage/files.service";
import { PresentationsService } from "../../../../verifier/presentations/presentations.service";
import { CredentialConfigCreate } from "../dto/credential-config-create.dto";
import { CredentialConfigUpdate } from "../dto/credential-config-update.dto";
import { CredentialConfig } from "../entities/credential.entity";
import { IaeActionType } from "../entities/iae-action.dto";
/**
* Service for managing credential configurations.
*/
@Injectable()
export class CredentialConfigService {
private readonly logger = new Logger(CredentialConfigService.name);
/**
* Constructor for CredentialConfigService.
* @param credentialConfigRepository - Repository for CredentialConfig entity.
*/
constructor(
@InjectRepository(CredentialConfig)
private readonly credentialConfigRepository: Repository<CredentialConfig>,
private readonly certService: CertService,
private readonly filesService: FilesService,
private readonly configImportService: ConfigImportService,
private readonly configImportOrchestrator: ConfigImportOrchestratorService,
private readonly presentationsService: PresentationsService,
private readonly tenantActionLogService: AuditLogService,
) {
this.configImportOrchestrator.register(
"credentials",
ImportPhase.CONFIGURATION,
(tenantId) => this.importForTenant(tenantId),
);
}
/**
* Imports credential configs for a specific tenant.
*/
public async importForTenant(tenantId: string) {
await this.configImportService.importConfigsForTenant<CredentialConfigCreate>(
tenantId,
{
subfolder: "issuance/credentials",
fileExtension: ".json",
validationClass: CredentialConfigCreate,
resourceType: "credential config",
checkExists: (tid, data) =>
this.getById(tid, data.id)
.then(() => true)
.catch(() => false),
deleteExisting: (tid, data) =>
this.credentialConfigRepository
.delete({
id: data.id,
tenantId: tid,
})
.then(() => undefined),
loadData: (filePath) =>
loadConfigDto(filePath, CredentialConfigCreate),
processItem: async (tid, config) => {
await this.processCredentialConfig(tid, config);
},
},
);
}
/**
* Process a credential config for import.
* Note: IAE action validation is skipped during import because
* presentation configs are imported in a later phase (REFERENCES).
*/
private async processCredentialConfig(
tenantId: string,
config: CredentialConfigCreate,
) {
// Replace image references with actual URLs
await this.replaceImageReferences(tenantId, config);
await this.validateAttestationKeyChain(tenantId, config.keyChainId);
// Skip IAE validation during import - presentation configs are imported later
await this.store(tenantId, config, true);
}
private async validateAttestationKeyChain(
tenantId: string,
keyChainId?: string | null,
): Promise<void> {
if (!keyChainId) {
return;
}
await this.certService.find({
tenantId,
type: KeyUsageType.Attestation,
keyId: keyChainId,
});
}
/**
* Replaces a single image reference with a public URL, or returns undefined if invalid.
*/
private async resolveImageUrl(
tenantId: string,
image: { uri?: string } | undefined,
context: string,
): Promise<{ uri: string } | undefined> {
if (!image?.uri) {
return undefined;
}
const url = await this.filesService.replaceUriWithPublicUrl(
tenantId,
image.uri,
);
if (url) {
return { uri: url };
}
this.logger.warn(
`[${tenantId}] Could not find image ${image.uri} for ${context}`,
);
return undefined;
}
/**
* Replaces image references (logo, background_image) with actual public URLs.
* This is used both during file import and API calls.
* @param tenantId - The ID of the tenant.
* @param config - The credential config to process.
*/
private async replaceImageReferences(
tenantId: string,
config: CredentialConfigCreate | CredentialConfigUpdate,
): Promise<void> {
if (!config.config?.display) {
return;
}
config.config.display = await Promise.all(
config.config.display.map(async (display) => {
display.background_image = await this.resolveImageUrl(
tenantId,
display.background_image,
"credentials config background_image",
);
display.logo = await this.resolveImageUrl(
tenantId,
display.logo,
"credentials config logo",
);
return display;
}),
);
}
/**
* Retrieves all credential configurations for a given tenant.
* @param tenantId - The ID of the tenant.
* @returns A promise that resolves to an array of CredentialConfig entities.
*/
get(tenantId: string) {
return this.credentialConfigRepository.find({
where: { tenantId },
});
}
/**
* Retrieves a credential configuration by its ID for a given tenant.
* @param tenantId
* @param id
* @returns
*/
getById(tenantId: string, id: string) {
return this.credentialConfigRepository.findOneByOrFail({
id,
tenantId,
});
}
/**
* Validates IAE actions in a credential configuration.
* Checks that all referenced presentation configs exist.
* @param tenantId - The ID of the tenant.
* @param config - The credential config to validate.
* @throws BadRequestException if a referenced presentation config doesn't exist.
*/
private async validateIaeActions(
tenantId: string,
config: CredentialConfigCreate | CredentialConfigUpdate,
): Promise<void> {
if (!config.iaeActions?.length) {
return;
}
for (const action of config.iaeActions) {
if (action.type === IaeActionType.OPENID4VP_PRESENTATION) {
const presentationConfigId = (
action as { presentationConfigId: string }
).presentationConfigId;
try {
await this.presentationsService.getPresentationConfig(
presentationConfigId,
tenantId,
);
} catch {
throw new BadRequestException(
`IAE action references presentation config '${presentationConfigId}' which does not exist`,
);
}
}
}
}
/**
* Stores a credential configuration for a given tenant.
* If the configuration already exists, it will be overwritten.
* Automatically replaces image references with public URLs.
* Validates IAE action references.
* @param tenantId - The ID of the tenant.
* @param config - The CredentialConfig entity to store.
* @param skipValidation - Skip IAE action validation (used during file imports).
* @returns A promise that resolves to the stored CredentialConfig entity.
*/
async store(
tenantId: string,
config: CredentialConfigCreate,
skipValidation = false,
actorToken?: TokenPayload,
req?: Request,
) {
await this.replaceImageReferences(tenantId, config);
await this.validateAttestationKeyChain(tenantId, config.keyChainId);
if (!skipValidation) {
await this.validateIaeActions(tenantId, config);
}
const saved = await this.credentialConfigRepository.save({
...config,
tenantId,
});
if (actorToken) {
await this.tenantActionLogService.record({
tenantId,
actionType: "credential_config_created",
actor: resolveAuditActor(actorToken),
changedFields: getChangedFields(
undefined,
this.sanitizeCredentialConfigForLog(saved),
),
after: this.sanitizeCredentialConfigForLog(saved),
requestMeta: extractRequestMeta(req),
});
}
return saved;
}
/**
* Updates a credential configuration for a given tenant.
* Only updates fields that are provided in the config.
* Set fields to null to clear them.
* Automatically replaces image references with public URLs.
* Validates IAE action references.
* @param tenantId - The ID of the tenant.
* @param id - The ID of the CredentialConfig entity to update.
* @param config - The partial CredentialConfig to update.
* @returns A promise that resolves to the updated CredentialConfig entity.
*/
async update(
tenantId: string,
id: string,
config: CredentialConfigUpdate,
actorToken?: TokenPayload,
req?: Request,
) {
await this.replaceImageReferences(tenantId, config);
await this.validateIaeActions(tenantId, config);
const existing = await this.getById(tenantId, id);
const keyChainId =
config.keyChainId !== undefined
? (config.keyChainId ?? undefined)
: existing.keyChainId;
await this.validateAttestationKeyChain(tenantId, keyChainId);
const saved = await this.credentialConfigRepository.save({
...existing,
...config,
id,
tenantId,
});
if (actorToken) {
await this.tenantActionLogService.record({
tenantId,
actionType: "credential_config_updated",
actor: resolveAuditActor(actorToken),
changedFields: getChangedFields(
this.sanitizeCredentialConfigForLog(existing),
this.sanitizeCredentialConfigForLog(saved),
),
before: this.sanitizeCredentialConfigForLog(existing),
after: this.sanitizeCredentialConfigForLog(saved),
requestMeta: extractRequestMeta(req),
});
}
return saved;
}
/**
* Deletes a credential configuration for a given tenant.
* @param tenantId - The ID of the tenant.
* @param id - The ID of the CredentialConfig entity to delete.
* @returns A promise that resolves to the result of the delete operation.
*/
async delete(
tenantId: string,
id: string,
actorToken?: TokenPayload,
req?: Request,
) {
const existing = await this.getById(tenantId, id);
const result = await this.credentialConfigRepository.delete({
id,
tenantId,
});
if (actorToken) {
await this.tenantActionLogService.record({
tenantId,
actionType: "credential_config_deleted",
actor: resolveAuditActor(actorToken),
before: this.sanitizeCredentialConfigForLog(existing),
requestMeta: extractRequestMeta(req),
});
}
return result;
}
private sanitizeCredentialConfigForLog(
config: CredentialConfig,
): Record<string, unknown> {
return {
id: config.id,
format: config.config?.format,
configVersion: config.configVersion,
config: config.config,
fields: config.fields,
vct: config.vct,
schemaMeta: config.schemaMeta,
iaeActions: config.iaeActions,
keyChainId: config.keyChainId,
};
}
}