Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cheqd/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@credo-ts/anoncreds": "workspace:*",
"@credo-ts/core": "workspace:*",
"@stablelib/ed25519": "^1.0.3",
"base64url": "^3.0.1",
"class-transformer": "^0.5.1",
"class-validator": "0.14.1",
"rxjs": "^7.8.0",
Expand Down
159 changes: 159 additions & 0 deletions packages/cheqd/src/tokenStatusList/CheqdTokenStatusListService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { AgentContext, CredoError, getKeyFromVerificationMethod, Buffer, utils } from '@credo-ts/core'

Check failure on line 1 in packages/cheqd/src/tokenStatusList/CheqdTokenStatusListService.ts

View workflow job for this annotation

GitHub Actions / Validate

There should be at least one empty line between import groups

Check failure on line 1 in packages/cheqd/src/tokenStatusList/CheqdTokenStatusListService.ts

View workflow job for this annotation

GitHub Actions / Validate

Imports "AgentContext" are only used as type
import { StatusListPayload, StatusListToken } from './types'

Check failure on line 2 in packages/cheqd/src/tokenStatusList/CheqdTokenStatusListService.ts

View workflow job for this annotation

GitHub Actions / Validate

All imports in the declaration are only used as types. Use `import type`
import { createEmptyBitmap, decodeBitmap, encodeBitmap, isBitSet, setBit } from './utils/bitmap'

Check failure on line 3 in packages/cheqd/src/tokenStatusList/CheqdTokenStatusListService.ts

View workflow job for this annotation

GitHub Actions / Validate

There should be at least one empty line between import groups
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SD-JWT VC library already has utils to create status lists.

      const statusListValues = new Array(body.size).fill(0)
      const statusList = new StatusList(statusListValues, 1)

Or updating an existing one:

import { getListFromStatusListJWT } from '@sd-jwt/jwt-status-list'

const statusList = getListFromStatusListJWT(currentStatusListJwt)
const parsedStatusListJwt = Jwt.fromSerializedJwt(currentStatusListJwt)

// Set the revoked indices to 1
for (const revokedIndex of body.revokedIndices) {
  statusList.setStatus(revokedIndex, 1)
}

import { CheqdCreateResourceOptions, CheqdDidResolver } from '../dids'

Check failure on line 4 in packages/cheqd/src/tokenStatusList/CheqdTokenStatusListService.ts

View workflow job for this annotation

GitHub Actions / Validate

`../dids` import should occur before import of `./types`

Check failure on line 4 in packages/cheqd/src/tokenStatusList/CheqdTokenStatusListService.ts

View workflow job for this annotation

GitHub Actions / Validate

There should be at least one empty line between import groups

Check failure on line 4 in packages/cheqd/src/tokenStatusList/CheqdTokenStatusListService.ts

View workflow job for this annotation

GitHub Actions / Validate

Imports "CheqdCreateResourceOptions" are only used as type
import { TokenStatusList } from './types/tokenStatusList'

Check failure on line 5 in packages/cheqd/src/tokenStatusList/CheqdTokenStatusListService.ts

View workflow job for this annotation

GitHub Actions / Validate

`./types/tokenStatusList` import should occur before import of `./utils/bitmap`

Check failure on line 5 in packages/cheqd/src/tokenStatusList/CheqdTokenStatusListService.ts

View workflow job for this annotation

GitHub Actions / Validate

There should be at least one empty line between import groups

Check failure on line 5 in packages/cheqd/src/tokenStatusList/CheqdTokenStatusListService.ts

View workflow job for this annotation

GitHub Actions / Validate

All imports in the declaration are only used as types. Use `import type`
import { CheqdApi } from '../CheqdApi'
import { parseCheqdDid } from '../anoncreds/utils/identifiers'
import base64url from 'base64url'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have base64url support in the TypedArrayEncoder in the core package


export class CheqdTokenStatusListService implements TokenStatusList {
private async loadStatusList(
agentContext: AgentContext,
statusListId: string
): Promise<{
metadata: any
bitmap: Buffer
jwt: string
}> {
const api = agentContext.dependencyManager.resolve(CheqdApi)
const resource = await api.resolveResource(statusListId)
const jwt = resource.resource?.data.toString()
const payload = JSON.parse(Buffer.from(jwt!.split('.')[1], 'base64').toString()) as StatusListPayload
Comment on lines +21 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WE have tools like the Jwt.fromSerializedJwt to parse JWTs.

Also should we not verify it first?

return {
metadata: resource.resourceMetadata,
bitmap: decodeBitmap(payload.status_list.bits),
jwt,
}
}

async createStatusList(
agentContext: AgentContext,
did: string,
name: string,
tag: string,
size: number,
signer: any // what could be passed as the signer
): Promise<StatusListToken> {
const api = agentContext.dependencyManager.resolve(CheqdApi)
const bitmap = createEmptyBitmap(size)
const payload: StatusListPayload = {
iss: did,
iat: Math.floor(Date.now() / 1000),
status_list: {
encoding: 'bitstring',
bits: encodeBitmap(bitmap),
},
}

const jwt = await signer.signJWT(payload)

const resource = {
collectionId: did.split(':')[3],
id: utils.uuid(),
name: name,
resourceType: 'StatusList',
data: jwt,
version: tag || utils.uuid(),
} satisfies CheqdCreateResourceOptions

await api.createResource(did, resource)

return {
jwt,
metadata: {
statusListId: resource.id,
issuedAt: payload.iat,
size: size,
},
}
Comment on lines +40 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have quite some utils also to create JWTs. See the Jwt service, also we should probably make a generic status list service that creates the status list (like we have an SD-JWT VC service) which works with a signer.method (see sd-jwt-vc service) which can be a cheqd did or a x509 cert for example.

Making a cheqd focused status list service will just result in duplication. So I don't think we can merge the PR like this, and it would first need to be refactored to a generic status list service.

Then i see two options for the cheqd integration:

  • you call two methods
    • we have a agent.modules.tokenStatusList.createStatusList
    • we have a agent.modules.cheqd.uploadStatusList
  • we create one wrapper method
    • agent.modules.cheqd.createStatustList - which call the agent.modules.tokenStatusList.createStatusList and then uploads it as a cheqd resource, but still using the generic implementation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TimoGlastra Is this the right way to create JWTs?

        const jwsService = agentContext.dependencyManager.resolve(JwsService)
        const jwt = await jwsService.createJwsCompact(agentContext, {
            payload: jwtPayload,
            keyId: issuer.publicJwk.keyId,
            protectedHeaderOptions: {
                alg: issuer.alg,
                typ: "statuslist+jwt"
            }
        })

Copy link
Contributor Author

@DaevMithran DaevMithran Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TimoGlastra to publish status lists in different registries. How about following the pattern similar to registrar

agent.modules.tokenStatusList.registerRegistry([new CheqdStatusListRegistry(), new HttpStatusListRegistry]); etc.
and the functions within TokenStatusListService call registry.publish(), registry.retreive() etc

}

async revokeIndex(
agentContext: AgentContext,
statusListId: string,
index: number,
tag: string,
signer: string
): Promise<StatusListToken> {
const api = agentContext.dependencyManager.resolve(CheqdApi)
const parsedDid = parseCheqdDid(statusListId)
if (!parsedDid) throw new CredoError(`Invalid statusListId: ${statusListId}`)

const { bitmap, metadata } = await this.loadStatusList(agentContext, statusListId)
setBit(bitmap, index)

// Build payload
const payload: StatusListPayload = {
iss: parsedDid.did,
iat: Math.floor(Date.now() / 1000),
status_list: {
encoding: 'bitstring',
bits: encodeBitmap(bitmap),
},
}

const resolverService = agentContext.dependencyManager.resolve(CheqdDidResolver)
const { didDocument } = await resolverService.resolve(agentContext, parsedDid.did, parsedDid)
if (!didDocument || !didDocument.verificationMethod || !didDocument.verificationMethod.length)
throw new Error('Did is not valid')
const method = didDocument.verificationMethod[0]

// Build header
const header = {
alg: 'ES256',
kid: method.id, // e.g. 'did:cheqd:testnet:xyz123#key-1'
}

// encode
const encodedHeader = base64url.encode(JSON.stringify(header))
const encodedPayload = base64url.encode(JSON.stringify(payload))
const signingInput = `${encodedHeader}.${encodedPayload}`
// sign payload
const key = getKeyFromVerificationMethod(method)
const signature = await agentContext.wallet.sign({ data: Buffer.from(signingInput), key })

// construct jwt
const jwt = `${signingInput}.${base64url.encode(signature.toLocaleString())}`

const resource = {
collectionId: parsedDid.did,
id: utils.uuid(),
name: metadata.name,
resourceType: metadata.type,
data: jwt,
version: tag || utils.uuid(),
} satisfies CheqdCreateResourceOptions

await api.createResource(parsedDid.did, resource)

return {
jwt,
metadata: {
statusListId,
issuedAt: payload.iat,
size: bitmap.length * 8,
},
}
}

async isRevoked(agentContext: AgentContext, statusListId: string, index: number): Promise<boolean> {
const { bitmap } = await this.loadStatusList(agentContext, statusListId)
return isBitSet(bitmap, index)
}

async getStatusListToken(agentContext: AgentContext, statusListId: string): Promise<StatusListToken> {
const { jwt, metadata, bitmap } = await this.loadStatusList(agentContext, statusListId)
const parsedDid = parseCheqdDid(statusListId)
if (!parsedDid) throw new CredoError(`Invalid statusListId: ${statusListId}`)

return {
jwt,
metadata: {
statusListId,
issuedAt: metadata.createdAt,
size: bitmap.length * 8,
},
}
}
}
17 changes: 17 additions & 0 deletions packages/cheqd/src/tokenStatusList/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export type StatusListToken = {
jwt: string // Signed JWT
metadata: {
statusListId: string
issuedAt: number
size: number
}
}

export type StatusListPayload = {
iss: string
iat: number
status_list: {
encoding: 'bitstring'
bits: string // base64url-encoded compressed bitmap
}
}
22 changes: 22 additions & 0 deletions packages/cheqd/src/tokenStatusList/types/tokenStatusList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { AgentContext } from '@credo-ts/core'
import { StatusListToken } from './index'

export interface TokenStatusList {
createStatusList(
agentContext: AgentContext,
did: string,
name: string,
tag: string,
size: number,
signer: any
): Promise<StatusListToken>
revokeIndex(
agentContext: AgentContext,
statusListId: string,
index: number,
tag: string,
signer: any
): Promise<StatusListToken>
isRevoked(agentContext: AgentContext, statusListId: string, index: number): Promise<boolean>
getStatusListToken(agentContext: AgentContext, statusListId: string): Promise<StatusListToken>
}
31 changes: 31 additions & 0 deletions packages/cheqd/src/tokenStatusList/utils/bitmap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import zlib from 'zlib'
import base64url from 'base64url'
import { Buffer } from '@credo-ts/core'

export function createEmptyBitmap(size: number): Buffer {
const byteLength = Math.ceil(size / 8)
return Buffer.alloc(byteLength) // All bits = 0
}

export function setBit(bitmap: Buffer, index: number): Buffer {
const byteIndex = Math.floor(index / 8)
const bitIndex = index % 8
bitmap[byteIndex] |= 1 << (7 - bitIndex)
return bitmap
}

export function isBitSet(bitmap: Buffer, index: number): boolean {
const byteIndex = Math.floor(index / 8)
const bitIndex = index % 8
return (bitmap[byteIndex] & (1 << (7 - bitIndex))) !== 0
}

export function encodeBitmap(bitmap: Buffer): string {
const compressed = zlib.deflateSync(bitmap)
return base64url.encode(compressed)
}

export function decodeBitmap(encoded: string): Buffer {
const compressed = base64url.toBuffer(encoded)
return Buffer.from(zlib.inflateSync(compressed))
}
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading