File

src/issuer/status-list/status-list.service.ts

Index

Properties
Methods

Constructor

constructor(configService: ConfigService, cryptoService: CryptoService, statusMappingRepository: Repository<StatusMapping>)
Parameters :
Name Type Optional
configService ConfigService No
cryptoService CryptoService No
statusMappingRepository Repository<StatusMapping> No

Methods

Async createEntry
createEntry(session: Session, credentialConfigurationId: string)

Get the next free entry in the status list

Parameters :
Name Type Optional
session Session No
credentialConfigurationId string No
Returns : Promise<JWTwithStatusListPayload>
Async createList
createList(tenantId: string)

Create a new status list and stored it in the file

Parameters :
Name Type Optional
tenantId string No
Returns : any
Private getConfig
getConfig(tenantId: string)
Parameters :
Name Type Optional
tenantId string No
Returns : StatusListFile
getList
getList(tenantId: string)
Parameters :
Name Type Optional
tenantId string No
Returns : string
Private Async init
init(tenantId: string)

Initialize the status list service by checking if the status list file exists. If it does not exist, create a new status list with 10,000 entries and a stack of 10,000 indexes. The stack is shuffled to ensure randomness in the order of entries. The status list is stored in the file system as a JSON file.

Parameters :
Name Type Optional
tenantId string No
Returns : any
onModuleInit
onModuleInit()
Returns : void
onTenantInit
onTenantInit(tenantId: string)
Decorators :
@OnEvent(TENANT_EVENTS.TENANT_KEYS, {async: true})
Parameters :
Name Type Optional
tenantId string No
Returns : any
Private setEntry
setEntry(id: number, value: number, tenantId: string)

Update the value of an entry in the status list

Parameters :
Name Type Optional
id number No
value number No
tenantId string No
Returns : any
Private storeConfig
storeConfig(content: StatusListFile, tenantId: string)
Parameters :
Name Type Optional
content StatusListFile No
tenantId string No
Returns : void
Async updateStatus
updateStatus(value: StatusUpdateDto, tenantId: string)

Update the status of a session and its credential configuration

Parameters :
Name Type Optional
value StatusUpdateDto No
tenantId string No
Returns : any

Properties

Private fileName
Type : string
Default value : 'status-list.json'
import { ConflictException, Injectable, OnModuleInit } from '@nestjs/common';
import { join } from 'path';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import {
    BitsPerStatus,
    createHeaderAndPayload,
    JWTwithStatusListPayload,
    StatusList,
    StatusListJWTHeaderParameters,
} from '@sd-jwt/jwt-status-list';
import { JwtPayload } from '@sd-jwt/types';
import { ConfigService } from '@nestjs/config';
import { CryptoService } from '../../crypto/crypto.service';
import { InjectRepository } from '@nestjs/typeorm';
import { StatusMapping } from './entities/status-mapping.entity';
import { Repository } from 'typeorm';
import { StatusUpdateDto } from './dto/status-update.dto';
import { OnEvent } from '@nestjs/event-emitter';
import { Session } from '../../session/entities/session.entity';
import { TENANT_EVENTS } from '../../auth/tenant-events';

interface StatusListFile {
    elements: number[];
    stack: number[];
    bits: BitsPerStatus;
    jwt?: string;
}

@Injectable()
export class StatusListService implements OnModuleInit {
    private fileName: string = 'status-list.json';

    constructor(
        private configService: ConfigService,
        private cryptoService: CryptoService,
        @InjectRepository(StatusMapping)
        private statusMappingRepository: Repository<StatusMapping>,
    ) {}
    onModuleInit() {}

    @OnEvent(TENANT_EVENTS.TENANT_KEYS, { async: true })
    onTenantInit(tenantId: string) {
        return this.init(tenantId);
    }

    /**
     * Initialize the status list service by checking if the status list file exists.
     * If it does not exist, create a new status list with 10,000 entries and a stack
     * of 10,000 indexes. The stack is shuffled to ensure randomness in the order of
     * entries. The status list is stored in the file system as a JSON file.
     */
    private async init(tenantId: string) {
        const file = join(
            this.configService.getOrThrow<string>('FOLDER'),
            tenantId,
            this.fileName,
        );
        if (!existsSync(file)) {
            const size = 10000;
            // create an empty array with the size of 1000
            const elements = new Array(size).fill(0).map(() => 0);
            // create a list of 1000 indexes and shuffel them
            const stack = new Array(size)
                .fill(0)
                .map((_, i) => i)
                .sort(() => 0.5 - Math.random());

            writeFileSync(
                file,
                JSON.stringify({ elements, stack, bits: 1 } as StatusListFile),
            );
            await this.createList(tenantId);
        }
    }

    /**
     * Create a new status list and stored it in the file
     */
    async createList(tenantId: string) {
        const file = this.getConfig(tenantId);
        const list = new StatusList(file.elements, file.bits);
        const iss = `${this.configService.getOrThrow<string>('PUBLIC_URL')}`;

        const sub = join(
            this.configService.getOrThrow<string>('PUBLIC_URL'),
            tenantId,
            'status-management',
            'status-list',
        );

        const prePayload: JwtPayload = {
            iss,
            sub,
            iat: Math.floor(Date.now() / 1000),
        };
        const preHeader: StatusListJWTHeaderParameters = {
            alg: 'ES256',
            typ: 'statuslist+jwt',
            x5c: this.cryptoService.getCertChain('signing', tenantId),
        };
        const { header, payload } = createHeaderAndPayload(
            list,
            prePayload,
            preHeader,
        );

        const jwt = await this.cryptoService.signJwt(header, payload, tenantId);
        file.jwt = jwt;
        this.storeConfig(file, tenantId);
    }

    getList(tenantId: string) {
        return this.getConfig(tenantId).jwt;
    }

    private getConfig(tenantId: string) {
        const file = join(
            this.configService.getOrThrow<string>('FOLDER'),
            tenantId,
            this.fileName,
        );
        return JSON.parse(readFileSync(file, 'utf-8')) as StatusListFile;
    }

    private storeConfig(content: StatusListFile, tenantId: string) {
        const file = join(
            this.configService.getOrThrow<string>('FOLDER'),
            tenantId,
            this.fileName,
        );
        writeFileSync(file, JSON.stringify(content));
    }

    /**
     * Get the next free entry in the status list
     * @returns
     */
    async createEntry(
        session: Session,
        credentialConfigurationId: string,
    ): Promise<JWTwithStatusListPayload> {
        const file = this.getConfig(session.tenantId);
        // get the last element from the stack
        const idx = file.stack.pop();
        //TODO: what to do if the stack is empty
        if (idx === undefined) {
            throw new Error('Stack for status list is empty!!!');
        }
        const sub = join(
            this.configService.getOrThrow<string>('PUBLIC_URL'),
            session.tenantId,
            'status-management',
            'status-list',
        );
        // store the index in the status mapping
        await this.statusMappingRepository.save({
            sessionId: session.id,
            index: idx,
            list: sub,
            credentialConfigurationId,
        });
        this.storeConfig(file, session.tenantId);
        return {
            status: {
                status_list: {
                    idx: idx,
                    uri: sub,
                },
            },
        };
    }

    /**
     * Update the value of an entry in the status list
     * @param id
     * @param value
     */
    private setEntry(id: number, value: number, tenantId: string) {
        const file = this.getConfig(tenantId);
        file.elements[id] = value;
        this.storeConfig(file, tenantId);
        return this.createList(tenantId);
    }

    /**
     * Update the status of a session and its credential configuration
     * @param value
     */
    async updateStatus(value: StatusUpdateDto, tenantId: string) {
        const entries = await this.statusMappingRepository.findBy({
            sessionId: value.sessionId,
            credentialConfigurationId: value.credentialConfigurationId,
        });
        if (entries.length === 0) {
            throw new ConflictException(
                `No status mapping found for session ${value.sessionId} and credential configuration ${value.credentialConfigurationId}`,
            );
        }
        for (const entry of entries) {
            await this.setEntry(entry.index, value.status, tenantId);
        }
    }
}

results matching ""

    No results matching ""