All files / lib/identity generate.ts

80.45% Statements 70/87
87.5% Branches 14/16
85.71% Functions 6/7
80.45% Lines 70/87

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 1701x                                             119x 119x     119x 119x 119x                   119x 119x   476x 476x   119x 119x           131x 131x 131x   131x 524x 524x   524x 4192x 2165x 4192x 2027x 2027x 4192x 524x   131x 131x         131x 131x 131x     131x 131x 524x 524x 524x   131x 131x         1x                                     1x   131x 131x 131x 131x             1x 119x 119x 119x   119x   119x     119x     119x   119x 119x   119x 119x           1x       16x   16x 16x 16x   16x 1x 1x     15x 15x 15x 15x 15x 15x  
/**
 * ProofID Identity Code Generator
 *
 * Generates unique, collision-safe identity codes in the format: PFID-XXXX-XXXX
 *
 * The identity code consists of:
 * - Prefix: "PFID" (ProofID identifier)
 * - Payload: 4 characters from Base58 alphabet (~11.3M combinations)
 * - Checksum: 4 characters derived from CRC16 of the payload
 */
 
import {
  BASE58_ALPHABET,
  BASE58_ALPHABET_UPPER,
  IDENTITY_PREFIX,
  IDENTITY_PAYLOAD_LENGTH,
  IDENTITY_CHECKSUM_LENGTH,
  RESERVED_IDENTITY_CODES,
} from './constants';
 
/**
 * Generate a random string from the Base58 alphabet (uppercase only for new codes)
 */
function generateBase58String(length: number): string {
  const array = new Uint8Array(length);
 
  // Use crypto.getRandomValues in browser, crypto.randomBytes in Node
  if (typeof window !== 'undefined' && window.crypto) {
    window.crypto.getRandomValues(array);
  } else {
    // Node.js environment
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const crypto = require('crypto') as typeof import('crypto');
    const randomBytes = crypto.randomBytes(length);
    for (let i = 0; i < length; i++) {
      array[i] = randomBytes[i];
    }
  }
 
  let result = '';
  for (let i = 0; i < length; i++) {
    // Map each byte to a Base58 character (uppercase only for new codes)
    result += BASE58_ALPHABET_UPPER[array[i] % BASE58_ALPHABET_UPPER.length];
  }
 
  return result;
}
 
/**
 * Calculate CRC16-CCITT checksum
 * This provides error detection for the identity code
 */
function crc16(data: string): number {
  let crc = 0xffff;
  const polynomial = 0x1021;
 
  for (let i = 0; i < data.length; i++) {
    const byte = data.charCodeAt(i);
    crc ^= byte << 8;
 
    for (let j = 0; j < 8; j++) {
      if (crc & 0x8000) {
        crc = ((crc << 1) ^ polynomial) & 0xffff;
      } else {
        crc = (crc << 1) & 0xffff;
      }
    }
  }
 
  return crc;
}
 
/**
 * Encode a number to Base58 with fixed length (uppercase only)
 */
function encodeBase58(num: number, length: number): string {
  const base = BASE58_ALPHABET_UPPER.length;
  let result = '';
 
  // Convert number to Base58 (uppercase)
  let remaining = num;
  while (result.length < length) {
    result = BASE58_ALPHABET_UPPER[remaining % base] + result;
    remaining = Math.floor(remaining / base);
  }
 
  return result.slice(0, length);
}
 
/**
 * Decode a Base58 string to number
 */
export function decodeBase58(str: string): number {
  const base = BASE58_ALPHABET.length;
  let result = 0;
 
  for (let i = 0; i < str.length; i++) {
    const index = BASE58_ALPHABET.indexOf(str[i]);
    if (index === -1) {
      throw new Error(`Invalid Base58 character: ${str[i]}`);
    }
    result = result * base + index;
  }
 
  return result;
}
 
/**
 * Generate the checksum for a payload
 * Normalizes to uppercase for consistent checksum across case variations
 */
export function generateChecksum(payload: string): string {
  // Normalize to uppercase for consistent checksum calculation
  const normalizedPayload = payload.toUpperCase();
  const checksumValue = crc16(normalizedPayload);
  return encodeBase58(checksumValue, IDENTITY_CHECKSUM_LENGTH);
}
 
/**
 * Generate a new identity code
 *
 * @returns Identity code in format PFID-XXXX-XXXX
 */
export function generateIdentityCode(): string {
  let code: string;
  let attempts = 0;
  const maxAttempts = 10;
 
  do {
    // Generate random payload
    const payload = generateBase58String(IDENTITY_PAYLOAD_LENGTH);
 
    // Calculate checksum
    const checksum = generateChecksum(payload);
 
    // Combine into full identity code
    code = `${IDENTITY_PREFIX}-${payload}-${checksum}`;
 
    attempts++;
  } while (RESERVED_IDENTITY_CODES.includes(code) && attempts < maxAttempts);
 
  return code;
}
 
/**
 * Extract parts from an identity code
 * Returns uppercase values for consistent handling
 */
export function parseIdentityCode(code: string): {
  prefix: string;
  payload: string;
  checksum: string;
} | null {
  // First try matching with the full Base58 alphabet (handles old mixed-case codes)
  const match = code.match(
    /^([A-Za-z]+)-([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{4})-([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{4})$/
  );
 
  if (!match) {
    return null;
  }
 
  // Return uppercase values for consistent handling
  return {
    prefix: match[1].toUpperCase(),
    payload: match[2].toUpperCase(),
    checksum: match[3].toUpperCase(),
  };
}