{"version":3,"file":"RevocationService.mjs","names":["RevocationService","cacheRepo: OpenBadgesRevocationCacheRepository","statusListRepo: StatusListRepository","index: number | undefined","url: string | undefined","encodedList: string | undefined"],"sources":["../../src/services/RevocationService.ts"],"sourcesContent":["import type { AgentContext } from '@credo-ts/core'\nimport { injectable, inject } from '@credo-ts/core'\nimport { OpenBadgesRevocationCacheRepository } from '../repository/OpenBadgesRevocationCacheRepository'\nimport { StatusListRepository } from '../repository/StatusListRepository'\nimport { StatusListRecord } from '../repository/StatusListRecord'\nimport { ONE_EDTECH_REVOCATION_LIST, VC_V2_CONTEXT } from '../constants'\nimport type {\n  StatusPurpose,\n  StatusListEntry,\n  CreateStatusListInput,\n  StatusListCredential,\n} from '../models/StatusListCredential'\nimport { DEFAULT_STATUS_LIST_CAPACITY, STATUS_LIST_2021_CONTEXT } from '../models/StatusListCredential'\nimport { randomUUID as uuidv4 } from 'node:crypto'\nimport * as pako from 'pako'\n\n/**\n * Convert base64url to bytes\n */\nfunction b64ToBytes(b64: string): Uint8Array {\n  const pad = b64.length % 4 === 0 ? '' : '='.repeat(4 - (b64.length % 4))\n  const base64 = (b64 + pad).replace(/-/g, '+').replace(/_/g, '/')\n  return Buffer.from(base64, 'base64')\n}\n\n/**\n * Convert bytes to base64url\n */\nfunction bytesToB64url(bytes: Uint8Array): string {\n  return Buffer.from(bytes).toString('base64url')\n}\n\n/**\n * Check if a bit is set at the given index\n */\nfunction isBitSet(bytes: Uint8Array, index: number): boolean {\n  const byteIndex = Math.floor(index / 8)\n  const bitIndex = index % 8\n  if (byteIndex >= bytes.length) return false\n  const mask = 1 << (7 - bitIndex)\n  return (bytes[byteIndex] & mask) !== 0\n}\n\n/**\n * Set or clear a bit at the given index\n */\nfunction setBit(bytes: Uint8Array, index: number, value: boolean): void {\n  const byteIndex = Math.floor(index / 8)\n  const bitIndex = index % 8\n  if (byteIndex >= bytes.length) return\n  const mask = 1 << (7 - bitIndex)\n  if (value) {\n    bytes[byteIndex] |= mask\n  } else {\n    bytes[byteIndex] &= ~mask\n  }\n}\n\n/**\n * Create an empty bitstring of the given capacity\n */\nfunction createBitstring(capacity: number): Uint8Array {\n  return new Uint8Array(Math.ceil(capacity / 8))\n}\n\n/**\n * GZIP compress and base64url encode a bitstring\n * Uses pako for cross-platform compatibility (Node.js + React Native)\n */\nfunction encodeStatusList(bitstring: Uint8Array): string {\n  const compressed = pako.gzip(bitstring)\n  return bytesToB64url(compressed)\n}\n\n/**\n * Base64url decode and GZIP decompress a status list\n * Uses pako for cross-platform compatibility (Node.js + React Native)\n */\nfunction decodeStatusList(encoded: string): Uint8Array {\n  const compressed = b64ToBytes(encoded)\n  const decompressed = pako.ungzip(compressed)\n  return new Uint8Array(decompressed)\n}\n\n@injectable()\nexport class RevocationService {\n  public constructor(\n    @inject(OpenBadgesRevocationCacheRepository) private readonly cacheRepo: OpenBadgesRevocationCacheRepository,\n    @inject(StatusListRepository) private readonly statusListRepo: StatusListRepository\n  ) {}\n\n  public async isRevoked(agentContext: AgentContext, credential: any): Promise<boolean> {\n    const cs = credential?.credentialStatus\n    const entries = Array.isArray(cs) ? cs : cs ? [cs] : []\n    for (const entry of entries) {\n      const type = entry?.type\n      if (type !== ONE_EDTECH_REVOCATION_LIST && type !== 'StatusList2021Entry') continue\n      const index: number | undefined = typeof entry?.statusListIndex === 'number' ? entry.statusListIndex : parseInt(entry?.statusListIndex ?? '', 10)\n      const url: string | undefined = entry?.statusListCredential || entry?.statusListUrl || entry?.id\n      if (!url || Number.isNaN(index)) continue\n\n      const { bytes } = await this.getStatusListBits(agentContext, url)\n      if (isBitSet(bytes, index!)) return true\n    }\n    return false\n  }\n\n  private async getStatusListBits(agentContext: AgentContext, url: string): Promise<{ bytes: Uint8Array }> {\n    try {\n      const cached = await this.cacheRepo.findByStatusListUrl(agentContext, url)\n      if (cached?.bitstringBase64) {\n        return { bytes: b64ToBytes(cached.bitstringBase64) }\n      }\n    } catch {}\n\n    const res = await fetch(url)\n    if (!res.ok) throw new Error(`Failed to fetch status list: ${res.status}`)\n    const json = (await res.json()) as {\n      encodedList?: string\n      credentialSubject?: { encodedList?: string }\n    }\n    // Try multiple shapes\n    const encodedList: string | undefined = json?.encodedList ?? json?.credentialSubject?.encodedList\n    if (!encodedList) throw new Error('encodedList not found in status list')\n\n    try {\n      const rec = await this.cacheRepo.findByStatusListUrl(agentContext, url)\n      if (rec) {\n        rec.bitstringBase64 = encodedList\n        rec.lastUpdatedAt = new Date()\n        await this.cacheRepo.update(agentContext, rec)\n      } else {\n        await this.cacheRepo.save(agentContext, {\n          statusListUrl: url,\n          bitstringBase64: encodedList,\n          lastUpdatedAt: new Date(),\n        } as any)\n      }\n    } catch {}\n\n    return { bytes: b64ToBytes(encodedList) }\n  }\n\n  // ========================================\n  // Issuer-side Status List Management\n  // ========================================\n\n  /**\n   * Create a new status list for credential revocation or suspension\n   */\n  public async createStatusList(\n    agentContext: AgentContext,\n    input: CreateStatusListInput\n  ): Promise<StatusListRecord> {\n    const capacity = input.capacity ?? DEFAULT_STATUS_LIST_CAPACITY\n    const bitstring = createBitstring(capacity)\n\n    const record = new StatusListRecord({\n      listId: input.listId,\n      issuerDid: input.issuerDid,\n      purpose: input.purpose,\n      bitstringBase64: bytesToB64url(bitstring),\n      capacity,\n      nextIndex: 0,\n      credentialIndexMap: {},\n      baseUrl: input.baseUrl,\n    })\n\n    await this.statusListRepo.save(agentContext, record)\n    agentContext.config.logger.debug('[OB][RevocationService] Created status list', { listId: input.listId, capacity })\n    return record\n  }\n\n  /**\n   * Allocate the next available index in a status list for a credential\n   * Returns the allocated index\n   */\n  public async allocateIndex(\n    agentContext: AgentContext,\n    listId: string,\n    credentialId?: string\n  ): Promise<number> {\n    const record = await this.statusListRepo.findByListId(agentContext, listId)\n    if (!record) {\n      throw new Error(`Status list not found: ${listId}`)\n    }\n\n    if (record.nextIndex >= record.capacity) {\n      throw new Error(`Status list ${listId} is full (capacity: ${record.capacity})`)\n    }\n\n    const index = record.nextIndex\n    record.nextIndex++\n\n    if (credentialId) {\n      record.credentialIndexMap[credentialId] = index\n    }\n\n    await this.statusListRepo.update(agentContext, record)\n    agentContext.config.logger.debug('[OB][RevocationService] Allocated index', { index, listId })\n    return index\n  }\n\n  /**\n   * Set the status (revoked/suspended or active) for a specific index\n   */\n  public async setStatus(\n    agentContext: AgentContext,\n    options: {\n      listId: string\n      index: number\n      status: boolean // true = revoked/suspended, false = active\n    }\n  ): Promise<void> {\n    const record = await this.statusListRepo.findByListId(agentContext, options.listId)\n    if (!record) {\n      throw new Error(`Status list not found: ${options.listId}`)\n    }\n\n    if (options.index < 0 || options.index >= record.capacity) {\n      throw new Error(`Index ${options.index} out of range (0-${record.capacity - 1})`)\n    }\n\n    // Decode the bitstring\n    const bitstring = b64ToBytes(record.bitstringBase64)\n\n    // Set the bit\n    setBit(bitstring, options.index, options.status)\n\n    // Update the record\n    record.bitstringBase64 = bytesToB64url(bitstring)\n    await this.statusListRepo.update(agentContext, record)\n\n    agentContext.config.logger.debug('[OB][RevocationService] Set status', {\n      index: options.index,\n      status: options.status ? 'revoked' : 'active',\n    })\n  }\n\n  /**\n   * Revoke a credential by its credential ID (if tracked)\n   */\n  public async revokeByCredentialId(\n    agentContext: AgentContext,\n    listId: string,\n    credentialId: string\n  ): Promise<void> {\n    const record = await this.statusListRepo.findByListId(agentContext, listId)\n    if (!record) {\n      throw new Error(`Status list not found: ${listId}`)\n    }\n\n    const index = record.credentialIndexMap[credentialId]\n    if (index === undefined) {\n      throw new Error(`Credential ${credentialId} not found in status list ${listId}`)\n    }\n\n    await this.setStatus(agentContext, { listId, index, status: true })\n  }\n\n  /**\n   * Get the status of a specific index\n   */\n  public async getStatus(\n    agentContext: AgentContext,\n    listId: string,\n    index: number\n  ): Promise<boolean> {\n    const record = await this.statusListRepo.findByListId(agentContext, listId)\n    if (!record) {\n      throw new Error(`Status list not found: ${listId}`)\n    }\n\n    const bitstring = b64ToBytes(record.bitstringBase64)\n    return isBitSet(bitstring, index)\n  }\n\n  /**\n   * Build an unsigned StatusList2021Credential for a status list\n   * This can then be signed using the ProofService\n   */\n  public buildStatusListCredential(\n    record: StatusListRecord\n  ): StatusListCredential {\n    const bitstring = b64ToBytes(record.bitstringBase64)\n    const encodedList = encodeStatusList(bitstring)\n\n    const credentialId = `${record.baseUrl}/status-list/${record.listId}`\n\n    return {\n      '@context': [VC_V2_CONTEXT, STATUS_LIST_2021_CONTEXT],\n      type: ['VerifiableCredential', 'StatusList2021Credential'],\n      id: credentialId,\n      issuer: record.issuerDid,\n      validFrom: new Date().toISOString(),\n      credentialSubject: {\n        id: `${credentialId}#list`,\n        type: 'StatusList2021',\n        statusPurpose: record.purpose,\n        encodedList,\n      },\n    }\n  }\n\n  /**\n   * Create a StatusListEntry to add to a credential\n   */\n  public createStatusEntry(\n    record: StatusListRecord,\n    index: number,\n    entryId?: string\n  ): StatusListEntry {\n    const statusListCredentialUrl = `${record.baseUrl}/status-list/${record.listId}`\n\n    return {\n      id: entryId ?? `${statusListCredentialUrl}#${index}`,\n      type: 'StatusList2021Entry',\n      statusPurpose: record.purpose,\n      statusListIndex: String(index),\n      statusListCredential: statusListCredentialUrl,\n    }\n  }\n\n  /**\n   * Add a credentialStatus field to a credential\n   * This should be called before signing the credential\n   */\n  public addCredentialStatus(\n    credential: any,\n    statusEntry: StatusListEntry\n  ): any {\n    return {\n      ...credential,\n      credentialStatus: statusEntry,\n    }\n  }\n\n  /**\n   * Get a status list record by its ID\n   */\n  public async getStatusList(\n    agentContext: AgentContext,\n    listId: string\n  ): Promise<StatusListRecord | null> {\n    return this.statusListRepo.findByListId(agentContext, listId)\n  }\n\n  /**\n   * Get all status lists for an issuer\n   */\n  public async getStatusListsByIssuer(\n    agentContext: AgentContext,\n    issuerDid: string\n  ): Promise<StatusListRecord[]> {\n    return this.statusListRepo.findByIssuer(agentContext, issuerDid)\n  }\n}\n\n"],"mappings":"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"}