{"version":3,"file":"index.cjs","sources":["../src/utils.ts","../src/pem.ts","../src/signing.ts"],"sourcesContent":["import type {Param} from './types'\n\n/**\n * Converts a base64 string to a byte array.\n *\n * @internal\n * @param base64 - The base64 string to convert.\n * @returns The converted byte array.\n */\nexport function base64ToBytes(base64: string): Uint8Array {\n  if (typeof Buffer !== 'undefined') {\n    // Node: base64 → Buffer → Uint8Array\n    return new Uint8Array(Buffer.from(base64, 'base64'))\n  }\n  // Browser: base64 → atob → Uint8Array\n  const bin = atob(base64)\n  return Uint8Array.from(bin, (c) => c.charCodeAt(0))\n}\n\n/**\n * Converts a byte array to a base64 string.\n *\n * @internal\n * @param bytes - The byte array to convert.\n * @returns The converted base64-encoded string.\n */\nexport function bytesToBase64(bytes: Uint8Array): string {\n  if (typeof Buffer !== 'undefined') {\n    // Node: Uint8Array → Buffer → base64\n    return Buffer.from(bytes).toString('base64')\n  }\n  // Browser: Uint8Array → binary string → btoa\n  const bin = String.fromCharCode(...bytes)\n  return btoa(bin)\n}\n\n/**\n * Extract the 32-byte Ed25519 private seed from a PKCS#8 DER-encoded key.\n *\n * PKCS#8 is a standard format for storing private keys. Internally it uses a\n * binary encoding called ASN.1 DER, which structures data as TLV blocks:\n *   [Tag][Length][Value]\n *\n * For Ed25519 keys, the actual private seed we need is stored as:\n *   - Tag = 0x04 (this means \"OCTET STRING\" = just raw bytes)\n *   - Length = 0x20 (32 in decimal)\n *   - Value = the 32 raw seed bytes\n *\n * Multiple OCTET STRINGs can appear in the file, so we scan **backwards**\n * from the end of the buffer to make sure we find the last 32-byte one,\n * which is the real seed.\n *\n * @internal\n * @param der - The DER-encoded PKCS#8 key as a byte array\n * @returns A Uint8Array of the 32-byte seed\n * @throws If a 32-byte OCTET STRING cannot be found\n */\nexport function ed25519SeedFromPkcs8(der: Uint8Array): Uint8Array {\n  const TAG_OCTET_STRING = 0x04 // ASN.1 tag for OCTET STRING / \"raw bytes\"\n  const SEED_LEN = 32 // Ed25519 private seeds are exactly 32 bytes\n\n  // Each block has at least 2 bytes before the actual value:\n  //   1 byte for Tag + 1 byte for Length\n  // So start scanning from the end minus (32 + 2) to ensure there's room.\n  for (let i = der.length - (SEED_LEN + 2); i >= 0; i--) {\n    // Look for a block where:\n    //   - Tag byte = OCTET STRING\n    //   - Length byte = 32\n    if (der[i] === TAG_OCTET_STRING && der[i + 1] === SEED_LEN) {\n      // Return just the 32 value bytes that follow\n      return der.subarray(i + 2, i + 2 + SEED_LEN)\n    }\n  }\n  throw new Error('Ed25519 32-byte seed not found in PKCS#8')\n}\n\n/**\n * Extracts the base64-encoded contents from a PEM-formatted key.\n *\n * The PEM format wraps base64-encoded DER data with header and footer lines.\n * This function removes those lines and any whitespace to return just the\n * base64-encoded contents.\n *\n * @internal\n * @param pem - The PEM-formatted key\n * @returns The base64-encoded contents between the header and footer\n */\nexport function extractPemContents(pem: string): string {\n  return pem\n    .replace(/^-----BEGIN [^-]+-----$/gim, '') // Remove the header\n    .replace(/^-----END [^-]+-----$/gim, '') // Remove the footer\n    .replace(/\\s+/g, '') // Remove all whitespace\n}\n\n/**\n * Lexicographically sorts two [key, value] parameter tuples.\n *\n * Sorting is first by key, then by value if keys are identical.\n *\n * @internal\n * @param a - The first parameter tuple to compare\n * @param b - The second parameter tuple to compare\n * @returns A negative number if a < b, positive if a > b, or 0 if equal\n */\nexport function lexographicSort(a: Param, b: Param): number {\n  const [keyA, valueA] = a\n  const [keyB, valueB] = b\n\n  // First compare the keys\n  if (keyA < keyB) return -1\n  if (keyA > keyB) return 1\n\n  // Keys are equal → compare the values\n  if (valueA < valueB) return -1\n  if (valueA > valueB) return 1\n\n  // Keys and values are identical\n  return 0\n}\n\n/**\n * Normalizes base64url encoding to match RFC 4648 \"URL and Filename Safe\"\n * Base64 Alphabet, but with padding. Replaces '+' with '-', and '/' with '_',\n * but maintains '=' padding.\n *\n * @internal\n * @param base64 - The base64 string to normalize\n * @returns The normalized base64url string\n */\nexport function normalizeBase64Url(base64: string): string {\n  return base64.replace(/\\+/g, '-').replace(/\\//g, '_')\n}\n\n/**\n * Normalizes expiry to ISO string format with validation.\n *\n * @internal\n * @param expiry - The expiry date to normalize\n * @returns The normalized expiry date as an ISO string\n */\nexport function normalizeExpiry(expiry: Date | number | string): string {\n  let date: Date\n  if (expiry instanceof Date) {\n    date = expiry\n  } else {\n    date = new Date(expiry)\n    if (isNaN(date.getTime())) {\n      throw new Error('Invalid expiry date format')\n    }\n  }\n\n  const now = new Date()\n  if (date.getTime() <= now.getTime()) {\n    throw new Error('Expiry date must be in the future')\n  }\n\n  // Format as 'YYYY-MM-DDTHH:mm:ssZ' (strip milliseconds)\n  return date.toISOString().replace(/\\.\\d{3}Z$/, 'Z')\n}\n\n/**\n * Encodes a string per RFC 3986 for use in URLs.\n *\n * This function uses `encodeURIComponent` and additionally encodes\n * characters that are not encoded by it: `!'()*`\n *\n * @internal\n * @param str - The string to encode\n * @returns The RFC 3986 encoded string\n */\nexport function rfc3986(str: string) {\n  return encodeURIComponent(str).replace(\n    /[!'()*]/g,\n    (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,\n  )\n}\n\n/**\n * Converts a byte array to base64url with padding.\n *\n * @internal\n * @param bytes - The byte array to convert\n * @returns The base64url-encoded string\n */\nexport function toBase64UrlWithPadding(bytes: Uint8Array): string {\n  let b64 = bytesToBase64(bytes)\n\n  // Ensure padding, although likely unnecessary as an Ed25519 signature is\n  // always 64 bytes / 88 chars when encoded in b64\n  if (b64.length % 4) b64 += '='.repeat(4 - (b64.length % 4))\n\n  return normalizeBase64Url(b64)\n}\n","import {etc} from '@noble/ed25519'\n\nimport {base64ToBytes, ed25519SeedFromPkcs8, extractPemContents} from './utils'\n\n/**\n * Converts a PEM-formatted PKCS#8 Ed25519 private key to its 32-byte seed (Uint8Array).\n * @public\n * @param pem - The PEM-formatted PKCS#8 key (e.g. \"BEGIN PRIVATE KEY\")\n * @returns The 32-byte Ed25519 seed.\n * @throws If the PEM/DER is invalid or a 32-byte seed cannot be found.\n */\nexport function pemToEd25519Bytes(pem: string): Uint8Array {\n  // Extract the DER from the PEM\n  const der = base64ToBytes(extractPemContents(pem))\n  // Return the 32-byte Ed25519 seed from the DER\n  return ed25519SeedFromPkcs8(der)\n}\n\n/**\n * Converts a PEM-formatted PKCS#8 Ed25519 private key to its 32-byte seed (hex string).\n * @public\n * @param pem - The PEM-formatted PKCS#8 key (e.g. \"BEGIN PRIVATE KEY\")\n * @returns The 32-byte Ed25519 seed as a lowercase hex string.\n * @throws If the PEM/DER is invalid or a 32-byte seed cannot be found.\n */\nexport function pemToEd25519Hex(pem: string): string {\n  return etc.bytesToHex(pemToEd25519Bytes(pem))\n}\n","import {etc, hashes, sign} from '@noble/ed25519'\nimport {sha512} from '@noble/hashes/sha2.js'\n\nimport type {Param, SigningOptions} from './types'\n\nimport {lexographicSort, normalizeExpiry, rfc3986, toBase64UrlWithPadding} from './utils'\n\nhashes.sha512 = sha512\n\n/**\n * Extracts user-defined query parameters from a URL, excluding reserved signing parameters.\n *\n * @internal\n * @param url - The URL from which to extract user parameters\n * @returns An array of [key, value] tuples for user-defined parameters\n */\nexport function extractUserParams(url: URL): Param[] {\n  const reservedKeys = ['keyid', 'expiry', 'signature']\n  return Array.from(url.searchParams).reduce<Param[]>((params, param) => {\n    const key = param[0]\n    if (!reservedKeys.includes(key)) params.push(param)\n    return params\n  }, [])\n}\n\n/**\n * Generates an Ed25519 signature for a given URL.\n *\n * @public\n * @param url - The URL to sign\n * @param privateKey - The private key to use for signing\n * @returns The base64url-encoded signature\n */\nexport function generateSignature(url: string, privateKey: string): string {\n  // Encode the URL as bytes\n  const urlBytes = new TextEncoder().encode(url)\n  // Encode the private key as bytes\n  const privateKeyBytes = etc.hexToBytes(privateKey)\n  // Get the signed URL as bytes\n  const signatureBytes = sign(urlBytes, privateKeyBytes)\n  // Convert the signature to a URL-safe base64 string\n  const urlSafeSignature = toBase64UrlWithPadding(signatureBytes)\n  // Return the URL-safe signature, encoded per RFC 3986\n  return urlSafeSignature\n}\n\n/**\n * Generates a lexicographically sorted canonical query string from parameter tuples.\n *\n * The canonical query string is formed by:\n *   - Encoding each key and value per RFC 3986\n *   - Sorting parameters lexicographically by key, then by value\n *   - Joining as key=value pairs with '&' separation\n *\n * @internal\n * @param params - An array of [key, value] parameter tuples\n * @returns The canonical query string of user parameters\n */\nexport function getCanonicalQuery(params: Param[]): string {\n  // Encode each key and value\n  const encodedParams = params.map((param) => param.map(rfc3986) as Param)\n  // Sort params lexicographically by key, then by value\n  encodedParams.sort(lexographicSort)\n  // Join as key=value pairs with '&' separation\n  return encodedParams.map((param) => param.join('=')).join('&')\n}\n\n/**\n * Signs a URL with Ed25519 signature, adding keyid, expiry, and signature parameters.\n *\n * @public\n * @param url - The URL to sign\n * @param options - The signing options to use\n * @returns The signed URL\n */\nexport function signUrl(url: string | URL, options: SigningOptions): string {\n  validateSigningOptions(options)\n\n  const urlObj = new URL(url)\n  // Extract user-defined query parameters, excluding reserved signing parameters\n  const userParams = extractUserParams(urlObj)\n  // Canonicalize the query string from the user parameters\n  const canonicalQuery = getCanonicalQuery(userParams)\n  // Get the URL with signing parameters (keyid and expiry)\n  const urlToSign = urlWithSigningParams(urlObj, canonicalQuery, options)\n  // Generate a signature from the fully canonicalized URL\n  const signature = generateSignature(urlToSign, options.privateKey)\n  // Append the RFC 3986 encoded signature\n  const urlWithSignature = `${urlToSign}&signature=${rfc3986(signature)}`\n  return urlWithSignature\n}\n\n/**\n * Returns a ready-to-sign URL object with signing parameters (keyid and expiry).\n *\n * @public\n * @param url - The base URL to which signing parameters will be added\n * @param query - The canonical query string of user parameters\n * @param options - The signing options containing keyId and expiry\n * @returns A \"signable\" URL string with keyid and expiry parameters\n */\nexport function urlWithSigningParams(\n  url: string | URL,\n  query: string,\n  signingOptions: Omit<SigningOptions, 'privateKey'>,\n): string {\n  const {origin, pathname} = new URL(url)\n  const parts = [origin, pathname, '?']\n\n  if (query.length > 0) {\n    parts.push(query)\n    parts.push('&')\n  }\n\n  parts.push(`keyid=${rfc3986(signingOptions.keyId)}`)\n  parts.push(`&expiry=${rfc3986(normalizeExpiry(signingOptions.expiry))}`)\n\n  return parts.join('')\n}\n\n/**\n * Validates signing options parameters.\n *\n * @internal\n * @param options - The signing options to validate\n * @throws When required parameters are missing or empty\n */\nfunction validateSigningOptions(options: SigningOptions): void {\n  // Validate required parameters\n  if (!options.keyId) {\n    throw new Error('Missing required parameter: keyId')\n  }\n  if (!options.privateKey) {\n    throw new Error('Missing required parameter: privateKey')\n  }\n  if (!options.expiry) {\n    throw new Error('Missing required parameter: expiry')\n  }\n\n  // Validate strings are not empty\n  if (typeof options.keyId === 'string' && options.keyId.trim() === '') {\n    throw new Error('keyId cannot be empty')\n  }\n  if (typeof options.privateKey === 'string' && options.privateKey.trim() === '') {\n    throw new Error('privateKey cannot be empty')\n  }\n}\n"],"names":["etc","hashes","sha512","sign"],"mappings":"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"}