{"version":3,"file":"ksuid.mjs","names":["KSUID_BYTES","KSUID_STRING_LEN","timestamp","ksuid: Ksuid"],"sources":["../../src/ksuid/base62.ts","../../src/ksuid/ksuid.ts"],"sourcesContent":["/**\n * Note: once ES2025 is widely adopted, we can use the built-in `Uint8Array.prototype.toHex` method.\n */\n// import { toHex } from 'hextreme'\n\nimport { BufferError, ParseError } from '../errors'\n\n/**\n * Base62 encoding/decoding for KSUID.\n * Alphabet: 0-9A-Za-z (standard Base62 ordering)\n *\n * KSUID binary format: 20 bytes (160 bits)\n * - 4 bytes: timestamp (seconds since KSUID epoch)\n * - 16 bytes: payload (cryptographically random)\n *\n * String format: 27 characters of Base62\n */\n\n// Base62 alphabet: digits (0-9), uppercase (A-Z), lowercase (a-z)\nconst BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'\nconst BASE = 62n\nconst KSUID_BYTES = 20\nconst KSUID_STRING_LEN = 27\n\n// Pre-computed decode table indexed by ASCII code (0-127)\n// Uses Uint8Array for cache-efficient lookups via charCodeAt\n// Note: Base62 is case-sensitive - 'A' (value 10) and 'a' (value 36) are different\nconst DECODING = new Uint8Array(128)\nDECODING.fill(255) // 255 = invalid marker\n\nfor (let i = 0; i < BASE62_ALPHABET.length; i += 1) {\n  DECODING[BASE62_ALPHABET.charCodeAt(i)] = i\n}\n\n/**\n * Encode a 20-byte KSUID to a 27-character Base62 string.\n *\n * Algorithm: Convert the 160-bit number to base 62 using BigInt.\n * V8 has highly optimized BigInt operations for this size.\n */\nexport function encodeBase62(bytes: Uint8Array): string {\n  if (bytes.length < KSUID_BYTES) {\n    throw new BufferError(\n      'KSUID_BYTES_TOO_SHORT',\n      `KSUID bytes must be at least ${KSUID_BYTES} bytes, got ${bytes.length}`,\n    )\n  }\n\n  // Convert bytes to BigInt (big-endian)\n  let num = 0n\n  for (let i = 0; i < KSUID_BYTES; i += 1) {\n    num = (num << 8n) | BigInt(bytes[i])\n  }\n\n  // Convert to Base62 string (build from right to left)\n  // Direct string concatenation is faster than array + join in V8\n  let encoded = ''\n  while (num > 0n) {\n    const remainder = num % BASE\n    num = num / BASE\n    encoded = BASE62_ALPHABET[Number(remainder)] + encoded\n  }\n\n  // Pad the result with the zero-character ('0') to ensure a fixed length\n  return encoded.padStart(KSUID_STRING_LEN, '0')\n}\n\n/**\n * Decode a 27-character Base62 string to a 20-byte KSUID.\n *\n * Algorithm: Convert Base62 string to BigInt, then to bytes.\n * V8 has highly optimized BigInt operations.\n */\nexport function decodeBase62(str: string): Uint8Array {\n  if (str.length !== KSUID_STRING_LEN) {\n    throw new ParseError(\n      'KSUID_INVALID_LENGTH',\n      `KSUID string must be ${KSUID_STRING_LEN} characters, got ${str.length}`,\n    )\n  }\n\n  // Convert Base62 string to BigInt\n  let num = 0n\n  for (let i = 0; i < KSUID_STRING_LEN; i += 1) {\n    const code = str.charCodeAt(i)\n    if (code >= 128) {\n      throw new ParseError('KSUID_INVALID_CHAR', `Invalid KSUID character: ${str[i]}`)\n    }\n    const value = DECODING[code]\n    if (value === 255) {\n      throw new ParseError('KSUID_INVALID_CHAR', `Invalid KSUID character: ${str[i]}`)\n    }\n    num = num * BASE + BigInt(value)\n  }\n\n  // Convert BigInt to bytes (big-endian)\n  const bytes = new Uint8Array(KSUID_BYTES)\n  for (let i = KSUID_BYTES - 1; i >= 0; i -= 1) {\n    bytes[i] = Number(num & 0xffn)\n    num = num >> 8n\n  }\n\n  return bytes\n}\n","import { writeTimestamp32 } from '../common/bytes'\nimport { rng } from '../common/random'\nimport { BufferError, InvalidInputError } from '../errors'\nimport { decodeBase62, encodeBase62 } from './base62'\n\n/**\n * KSUID (K-Sortable Unique Identifier)\n *\n * A 160-bit identifier consisting of:\n * - 4 bytes: timestamp (seconds since KSUID epoch: May 13, 2014)\n * - 16 bytes: cryptographically random payload\n *\n * Encoded as a 27-character Base62 string.\n */\n\n// KSUID epoch: May 13, 2014 00:00:00 UTC (Unix timestamp in seconds)\nconst KSUID_EPOCH = 1400000000\n\nconst KSUID_BYTES = 20\nconst KSUID_STRING_LEN = 27\nconst TIMESTAMP_BYTES = 4\nconst PAYLOAD_BYTES = 16\n\n// Validation regex: 27 alphanumeric characters\n// Note: Both cases are valid Base62 characters, but they decode to different values\n// (e.g., 'A' = 10, 'a' = 36). The regex validates format, not semantic equivalence.\nconst KSUID_REGEX = /^[0-9A-Za-z]{27}$/\n\nexport type KsuidOptions = {\n  /**\n   * 16 bytes of random data to use for KSUID payload.\n   */\n  random?: Uint8Array\n  /**\n   * Timestamp in seconds since Unix epoch.\n   * Defaults to Math.floor(Date.now() / 1000).\n   * KSUID natively uses second precision.\n   */\n  secs?: number\n}\n\nexport type Ksuid = {\n  (): string\n  <TBuf extends Uint8Array = Uint8Array>(options: KsuidOptions | undefined, buf: TBuf, offset?: number): TBuf\n  (options?: KsuidOptions, buf?: undefined, offset?: number): string\n  toBytes(id: string): Uint8Array\n  fromBytes(bytes: Uint8Array): string\n  timestamp(id: string): number\n  isValid(id: unknown): id is string\n  /** The nil KSUID (all zeros) */\n  NIL: string\n  /** The max KSUID (maximum valid value) */\n  MAX: string\n}\n\n/**\n * Write KSUID bytes to a buffer.\n */\nfunction ksuidBytes(timestamp: number, payload: Uint8Array, buf?: Uint8Array, offset = 0): Uint8Array {\n  if (!buf) {\n    buf = new Uint8Array(KSUID_BYTES)\n    offset = 0\n  } else if (offset < 0 || offset + KSUID_BYTES > buf.length) {\n    throw new BufferError(\n      'KSUID_BUFFER_OUT_OF_BOUNDS',\n      `KSUID byte range ${offset}:${offset + KSUID_BYTES - 1} is out of buffer bounds`,\n    )\n  }\n\n  // Timestamp (32-bit big-endian seconds since KSUID epoch) -> bytes 0-3\n  writeTimestamp32(buf, offset, timestamp)\n\n  // Payload (128 bits) -> bytes 4-19\n  // copy from payload[TIMESTAMP_BYTES] into buf\n  buf.set(payload.subarray(TIMESTAMP_BYTES, TIMESTAMP_BYTES + PAYLOAD_BYTES), offset + TIMESTAMP_BYTES)\n  for (let i = 0; i < PAYLOAD_BYTES; i += 1) {\n    buf[offset + TIMESTAMP_BYTES + i] = payload[i]\n  }\n\n  return buf\n}\n\n/*\n * Overload: no buffer => return a KSUID string.\n */\nfunction ksuidFn(options?: KsuidOptions, buf?: undefined, offset?: number): string\n/*\n * Overload: caller provides a buffer slice to fill with KSUID bytes.\n */\nfunction ksuidFn<TBuf extends Uint8Array = Uint8Array>(\n  options: KsuidOptions | undefined,\n  buf: TBuf,\n  offset?: number,\n): TBuf\nfunction ksuidFn<TBuf extends Uint8Array = Uint8Array>(options?: KsuidOptions, buf?: TBuf, offset = 0): string | TBuf {\n  if (options) {\n    if (options.random && options.random.length < PAYLOAD_BYTES) {\n      throw new InvalidInputError('KSUID_RANDOM_BYTES_TOO_SHORT', 'Random bytes length must be >= 16 for KSUID')\n    }\n\n    if (options.secs && options.secs < KSUID_EPOCH) {\n      throw new InvalidInputError('KSUID_TIMESTAMP_TOO_LOW', 'Timestamp must be >= KSUID epoch')\n    }\n\n    if (options.secs) {\n      options.secs = options.secs - KSUID_EPOCH\n    }\n  }\n\n  /**\n   * Note: by default, Cloudflare Workers \"freezes\" time during request handling to prevent\n   * side-channel attacks. This means that Date.now() will return the same value for the entire\n   * duration of a request.\n   * Implications:\n   * - all KSUIDs generated within a single request will have the same timestamp.\n   */\n  const timestamp = options?.secs ?? Math.floor(Date.now() / 1000 - KSUID_EPOCH)\n  const payload = options?.random ?? rng()\n\n  if (buf) {\n    ksuidBytes(timestamp, payload, buf, offset)\n    return buf\n  }\n\n  const bytes = ksuidBytes(timestamp, payload)\n\n  // String mode: create bytes then encode to Base62\n  return encodeBase62(bytes)\n}\n\n/**\n * Convert a KSUID string to 20 bytes.\n *\n * Note: Base62 is case-sensitive. 'A' (value 10) and 'a' (value 36) decode\n * to different byte values. This function accepts both cases as valid input.\n */\nfunction toBytes(id: string): Uint8Array {\n  return decodeBase62(id)\n}\n\n/**\n * Convert 20 bytes to a KSUID string.\n */\nfunction fromBytes(bytes: Uint8Array): string {\n  return encodeBase62(bytes)\n}\n\n/**\n * Extract the timestamp from a KSUID string.\n * Returns Unix timestamp in milliseconds for API consistency with ulid/uuidv7.\n * Note: KSUID only has second precision, so the returned value will always end in 000.\n */\nfunction timestamp(id: string): number {\n  const bytes = decodeBase62(id)\n  // First 4 bytes are big-endian timestamp (seconds since KSUID epoch)\n  const secs = ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0\n  // Add KSUID epoch and convert to milliseconds\n  return (secs + KSUID_EPOCH) * 1000\n}\n\n/**\n * Validate a KSUID string format.\n *\n * Note: Both uppercase and lowercase letters are valid Base62 characters,\n * but they represent different values (e.g., 'A' = 10, 'a' = 36).\n */\nfunction isValid(id: unknown): id is string {\n  return typeof id === 'string' && id.length === KSUID_STRING_LEN && KSUID_REGEX.test(id)\n}\n\n/**\n * Generate a KSUID string or write the bytes into a buffer.\n * Also includes helpers to convert to and from byte arrays.\n */\nexport const ksuid: Ksuid = Object.assign(ksuidFn, {\n  toBytes,\n  fromBytes,\n  timestamp,\n  isValid,\n  NIL: '000000000000000000000000000',\n  MAX: 'aWgEPTl1tmebfsQzFP4bxwgy80V',\n})\n\nexport { BufferError, InvalidInputError, ParseError, UniqueIdError } from '../errors'\n"],"mappings":"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"}