{"version":3,"sources":["../src/utils.ts","../src/message.ts","../src/session-storage.ts","../src/verify.ts"],"names":["generateNonce","length","charset","array","secureCrypto","result","i","nowISO","addSecondsISO","seconds","parseChainId","chainId","colonIndex","isValidNonce","nonce","isValidDomain","domain","DEFAULT_NONCE_LENGTH","SIWX_VERSION","CAIP2_PATTERN","RFC3339_PATTERN","REQUEST_ID_PATTERN","FIELD_PREFIXES","isValidRfc3339","value","hasControlCharacter","char","code","hasInvalidStatementCharacter","isValidUri","validateParams","params"],"mappings":"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","file":"/home/runner/work/connect-lib/connect-lib/packages/siwx/dist/index.cjs","sourcesContent":["/**\n * SIWx Utility Functions\n */\n\n/**\n * Generate a cryptographically-random nonce.\n * Uses crypto.getRandomValues for browser/Node compatibility.\n * Produces an alphanumeric string of the specified length (default 16).\n */\nexport function generateNonce(length: number = 16): string {\n  const charset =\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\";\n  const array = new Uint8Array(length);\n  const secureCrypto = globalThis.crypto;\n  if (!secureCrypto?.getRandomValues) {\n    throw new Error(\n      \"A cryptographically secure random source is required to generate a SIWx nonce.\",\n    );\n  }\n  secureCrypto.getRandomValues(array);\n  let result = \"\";\n  for (let i = 0; i < length; i++) {\n    result += charset[array[i] % charset.length];\n  }\n  return result;\n}\n\n/**\n * Format the current time as an ISO 8601 string (UTC).\n */\nexport function nowISO(): string {\n  return new Date().toISOString();\n}\n\n/**\n * Add seconds to the current time and return an ISO 8601 string.\n */\nexport function addSecondsISO(seconds: number): string {\n  return new Date(Date.now() + seconds * 1000).toISOString();\n}\n\n/**\n * Parse a CAIP-2 chain ID into namespace and reference.\n * Example: \"eip155:1\" -> { namespace: \"eip155\", reference: \"1\" }\n */\nexport function parseChainId(chainId: string): {\n  namespace: string;\n  reference: string;\n} {\n  if (!/^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/.test(chainId)) {\n    throw new Error(\n      `Invalid CAIP-2 chain ID: \"${chainId}\". Expected format: \"namespace:reference\"`,\n    );\n  }\n  const colonIndex = chainId.indexOf(\":\");\n  return {\n    namespace: chainId.slice(0, colonIndex),\n    reference: chainId.slice(colonIndex + 1),\n  };\n}\n\n/**\n * Validate the nonce grammar required by EIP-4361/CAIP-122 text messages:\n * at least eight ASCII alphanumeric characters.\n */\nexport function isValidNonce(nonce: string): boolean {\n  return /^[A-Za-z0-9]{8,}$/.test(nonce);\n}\n\n/**\n * Validate that a domain is a valid RFC 4501 URI host.\n * Accepts hostnames (e.g. \"example.com\", \"localhost\") and optional port.\n */\nexport function isValidDomain(domain: string): boolean {\n  // Allow localhost, hostnames, IPs with optional port\n  return /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)*[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(:[0-9]+)?$/.test(\n    domain,\n  );\n}\n","/**\n * CAIP-122 Message Format — Sign-In With X\n *\n * Creates and parses SIWx messages according to CAIP-122.\n * Supports all chain namespaces (EVM, Solana, XRPL).\n *\n * References:\n *   - CAIP-122: https://standards.chainagnostic.org/CAIPs/caip-122\n *   - EIP-4361: https://eips.ethereum.org/EIPS/eip-4361\n */\n\nimport type { SiwxMessage, SiwxParams } from \"./types\";\nimport { isValidDomain, nowISO } from \"./utils\";\n\n/**\n * Default nonce length for SIWx messages.\n */\nexport const DEFAULT_NONCE_LENGTH = 16;\n\n/**\n * Current version of the SIWx message format.\n */\nexport const SIWX_VERSION = 1;\n\nconst CAIP2_PATTERN = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/;\nconst RFC3339_PATTERN =\n  /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$/;\nconst REQUEST_ID_PATTERN =\n  /^(?:[A-Za-z0-9._~-]|%[0-9A-Fa-f]{2}|[!$&'()*+,;=:@])*$/;\nconst FIELD_PREFIXES = [\n  \"URI: \",\n  \"Version: \",\n  \"Chain ID: \",\n  \"Nonce: \",\n  \"Issued At: \",\n  \"Expiration Time: \",\n  \"Not Before: \",\n  \"Request ID: \",\n  \"Resources:\",\n] as const;\n\nfunction isValidRfc3339(value: string): boolean {\n  return (\n    RFC3339_PATTERN.test(value) && Number.isFinite(new Date(value).getTime())\n  );\n}\n\nfunction hasControlCharacter(value: string): boolean {\n  return [...value].some((char) => {\n    const code = char.charCodeAt(0);\n    return code <= 0x20 || code === 0x7f;\n  });\n}\n\nfunction hasInvalidStatementCharacter(value: string): boolean {\n  return [...value].some((char) => {\n    const code = char.charCodeAt(0);\n    return code < 0x20 || code === 0x7f || code > 0x7f;\n  });\n}\n\nfunction isValidUri(value: string): boolean {\n  if (!value || hasControlCharacter(value)) return false;\n  try {\n    // URL accepts the URI schemes used by CAIP-122 resources (https, ipfs,\n    // urn, did, etc.) and rejects malformed authorities and control bytes.\n    new URL(value);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nfunction validateParams(params: SiwxParams): void {\n  if (!isValidDomain(params.domain)) {\n    throw new Error(`Invalid SIWx domain: \"${params.domain}\"`);\n  }\n  if (!params.address || /[\\r\\n]/.test(params.address)) {\n    throw new Error(\"Invalid SIWx account address\");\n  }\n  if (!isValidUri(params.uri)) {\n    throw new Error(`Invalid SIWx URI: \"${params.uri}\"`);\n  }\n  if (params.version !== undefined && params.version !== SIWX_VERSION) {\n    throw new Error(`Unsupported SIWx version: ${params.version}`);\n  }\n  if (!CAIP2_PATTERN.test(params.chainId)) {\n    throw new Error(`Invalid CAIP-2 chain ID: \"${params.chainId}\"`);\n  }\n  if (\n    params.chainId.startsWith(\"eip155:\") &&\n    !/^eip155:[1-9][0-9]*$/.test(params.chainId)\n  ) {\n    throw new Error(`Invalid EIP-155 chain ID: \"${params.chainId}\"`);\n  }\n  if (\n    params.chainId.startsWith(\"eip155:\") &&\n    !/^0x[0-9a-fA-F]{40}$/.test(params.address)\n  ) {\n    throw new Error(\"Invalid EIP-155 account address\");\n  }\n  if (!/^[A-Za-z0-9]{8,}$/.test(params.nonce)) {\n    throw new Error(\n      \"Invalid SIWx nonce: expected at least 8 alphanumeric characters\",\n    );\n  }\n  const issuedAt = params.issuedAt ?? nowISO();\n  if (!isValidRfc3339(issuedAt)) {\n    throw new Error(`Invalid SIWx issuedAt: \"${issuedAt}\"`);\n  }\n  for (const [label, value] of [\n    [\"expirationTime\", params.expirationTime],\n    [\"notBefore\", params.notBefore],\n  ] as const) {\n    if (value !== undefined && !isValidRfc3339(value)) {\n      throw new Error(`Invalid SIWx ${label}: \"${value}\"`);\n    }\n  }\n  if (\n    params.statement !== undefined &&\n    hasInvalidStatementCharacter(params.statement)\n  ) {\n    throw new Error(\"Invalid SIWx statement: must be single-line ASCII\");\n  }\n  if (\n    params.requestId !== undefined &&\n    !REQUEST_ID_PATTERN.test(params.requestId)\n  ) {\n    throw new Error(`Invalid SIWx requestId: \"${params.requestId}\"`);\n  }\n  for (const resource of params.resources ?? []) {\n    if (!isValidUri(resource)) {\n      throw new Error(`Invalid SIWx resource URI: \"${resource}\"`);\n    }\n  }\n}\n\n/**\n * Derive a human-readable blockchain name from a CAIP-2 chain ID.\n *\n * Reference: Reown AppKit's `getNetworkNameByCaipNetworkId`\n * maps CAIP-2 namespace → display name.\n */\nexport function getBlockchainName(chainId: string): string {\n  if (chainId.startsWith(\"eip155:\")) return \"Ethereum\";\n  if (chainId.startsWith(\"solana:\")) return \"Solana\";\n  if (chainId.startsWith(\"xrpl:\")) return \"XRP Ledger\";\n  return \"blockchain\";\n}\n\n/**\n * Create a CAIP-122 formatted SIWx message string.\n * The returned string is what the user signs.\n */\nexport function createSiwxMessage(params: SiwxParams): string {\n  validateParams(params);\n  const domain = params.domain;\n  const address = params.address;\n  const statement = params.statement;\n  const uri = params.uri;\n  const version = params.version ?? SIWX_VERSION;\n  const chainId = params.chainId;\n  const nonce = params.nonce;\n  const issuedAt = params.issuedAt ?? nowISO();\n  const expirationTime = params.expirationTime;\n  const notBefore = params.notBefore;\n  const resources = params.resources ?? [];\n  const requestId = params.requestId;\n\n  const lines: string[] = [];\n\n  // Line 1: Domain wants you to sign in with your {blockchain} account:\n  const blockchainName =\n    params.blockchain ?? getBlockchainName(chainId) ?? \"blockchain\";\n  lines.push(\n    `${domain} wants you to sign in with your ${blockchainName} account:`,\n  );\n  lines.push(`${address}`);\n\n  // Optional statement\n  if (statement) {\n    lines.push(\"\");\n    lines.push(statement);\n  }\n\n  // URI\n  lines.push(\"\");\n  lines.push(`URI: ${uri}`);\n\n  // Version\n  lines.push(`Version: ${version}`);\n\n  // Chain ID (CAIP-2)\n  lines.push(`Chain ID: ${chainId}`);\n\n  // Nonce\n  lines.push(`Nonce: ${nonce}`);\n\n  // Issued At\n  lines.push(`Issued At: ${issuedAt}`);\n\n  // Expiration Time (optional)\n  if (expirationTime) {\n    lines.push(`Expiration Time: ${expirationTime}`);\n  }\n\n  // Not Before (optional)\n  if (notBefore) {\n    lines.push(`Not Before: ${notBefore}`);\n  }\n\n  // Request ID (optional)\n  if (requestId) {\n    lines.push(`Request ID: ${requestId}`);\n  }\n\n  // Resources (optional)\n  if (resources.length > 0) {\n    lines.push(`Resources:`);\n    for (const resource of resources) {\n      lines.push(`- ${resource}`);\n    }\n  }\n\n  return lines.join(\"\\n\");\n}\n\n/**\n * Parse a CAIP-122 SIWx message string into its structured components.\n * Returns null if parsing fails.\n */\nexport function parseSiwxMessage(raw: string): SiwxMessage | null {\n  try {\n    // The canonical serialization uses LF; accepting CRLF would create a\n    // different signed byte sequence across implementations.\n    if (!raw || raw.includes(\"\\r\")) return null;\n    const lines = raw.split(\"\\n\");\n    if (lines.length < 4) return null;\n\n    // Line 1: \"{domain} wants you to sign in with your {blockchain} account:\"\n    // OR legacy: \"{domain} wants you to sign in with your account:\"\n    // Split on literal delimiter to avoid ReDoS from nested greedy regex quantifiers\n    const firstLine = lines[0];\n    const PREFIX = \" wants you to sign in with your\";\n    const prefixIdx = firstLine.indexOf(PREFIX);\n    if (prefixIdx === -1) return null;\n    const domain = firstLine.slice(0, prefixIdx).trim();\n    const rest = firstLine.slice(prefixIdx + PREFIX.length);\n    const blockMatch = rest.match(/^ (.+) account:$/);\n    const blockchain = blockMatch?.[1]?.trim() || \"blockchain\";\n\n    // Line 2: blockchain address\n    const address = lines[1].trim();\n\n    // Find field lines by scanning for known prefixes\n    let statement: string | null = null;\n    let uri = \"\";\n    let version = 0;\n    let chainId = \"\";\n    let nonce = \"\";\n    let issuedAt: string | null = null;\n    let expirationTime: string | null = null;\n    let notBefore: string | null = null;\n    let requestId: string | null = null;\n    const resources: string[] = [];\n    let inResources = false;\n    const statementLines: string[] = [];\n\n    // Statement is everything between address (line 1) and the first field\n    // Fields are: URI:, Version:, Chain ID:, Nonce:, Issued At:, etc.\n    let fieldStart = -1;\n    for (let i = 2; i < lines.length; i++) {\n      if (FIELD_PREFIXES.some((prefix) => lines[i].startsWith(prefix))) {\n        fieldStart = i;\n        break;\n      }\n      statementLines.push(lines[i]);\n    }\n\n    if (statementLines.length > 0) {\n      statement = statementLines\n        .map((l) => l.trim())\n        .filter((l) => l.length > 0)\n        .join(\"\\n\");\n      if (statement.length === 0) statement = null;\n    }\n\n    if (fieldStart === -1) return null;\n\n    // Parse fields. Each field appears at most once and follows the order in\n    // the EIP-4361/CAIP-122 text representation.\n    const fieldOrder = [\n      \"URI: \",\n      \"Version: \",\n      \"Chain ID: \",\n      \"Nonce: \",\n      \"Issued At: \",\n      \"Expiration Time: \",\n      \"Not Before: \",\n      \"Request ID: \",\n      \"Resources:\",\n    ];\n    const seen = new Set<string>();\n    let previousOrder = -1;\n    for (let i = fieldStart; i < lines.length; i++) {\n      const line = lines[i];\n\n      const field = fieldOrder.find((prefix) => line.startsWith(prefix));\n      if (field) {\n        const order = fieldOrder.indexOf(field);\n        if (seen.has(field) || order < previousOrder) return null;\n        seen.add(field);\n        previousOrder = order;\n      }\n\n      if (line.startsWith(\"URI: \")) {\n        uri = line.slice(\"URI: \".length);\n      } else if (line.startsWith(\"Version: \")) {\n        const value = line.slice(\"Version: \".length);\n        if (!/^1$/.test(value)) return null;\n        version = SIWX_VERSION;\n      } else if (line.startsWith(\"Chain ID: \")) {\n        chainId = line.slice(\"Chain ID: \".length);\n      } else if (line.startsWith(\"Nonce: \")) {\n        nonce = line.slice(\"Nonce: \".length);\n      } else if (line.startsWith(\"Issued At: \")) {\n        issuedAt = line.slice(\"Issued At: \".length);\n      } else if (line.startsWith(\"Expiration Time: \")) {\n        expirationTime = line.slice(\"Expiration Time: \".length);\n      } else if (line.startsWith(\"Not Before: \")) {\n        notBefore = line.slice(\"Not Before: \".length);\n      } else if (line.startsWith(\"Request ID: \")) {\n        requestId = line.slice(\"Request ID: \".length);\n      } else if (line === \"Resources:\") {\n        inResources = true;\n      } else if (inResources && line.startsWith(\"- \")) {\n        resources.push(line.slice(2));\n      } else {\n        // Unknown field-like lines would otherwise be omitted from the\n        // signed data model while still being shown to a user.\n        return null;\n      }\n    }\n\n    // Validate required fields\n    if (\n      !isValidDomain(domain) ||\n      !address ||\n      !uri ||\n      !isValidUri(uri) ||\n      version !== SIWX_VERSION ||\n      !CAIP2_PATTERN.test(chainId) ||\n      (chainId.startsWith(\"eip155:\") &&\n        !/^eip155:[1-9][0-9]*$/.test(chainId)) ||\n      (chainId.startsWith(\"eip155:\") && !/^0x[0-9a-fA-F]{40}$/.test(address)) ||\n      !/^[A-Za-z0-9]{8,}$/.test(nonce) ||\n      !issuedAt ||\n      !isValidRfc3339(issuedAt) ||\n      (statement !== null && hasInvalidStatementCharacter(statement)) ||\n      (expirationTime !== null && !isValidRfc3339(expirationTime)) ||\n      (notBefore !== null && !isValidRfc3339(notBefore)) ||\n      (requestId !== null && !REQUEST_ID_PATTERN.test(requestId)) ||\n      resources.some((resource) => !isValidUri(resource))\n    ) {\n      return null;\n    }\n\n    return {\n      raw,\n      domain,\n      address,\n      statement: statement ?? null,\n      uri,\n      version,\n      chainId,\n      nonce,\n      issuedAt: issuedAt ?? null,\n      expirationTime: expirationTime ?? null,\n      notBefore: notBefore ?? null,\n      resources,\n      requestId: requestId ?? null,\n      blockchain,\n    };\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Type guard to check if an object is a valid SiwxMessage.\n */\nexport function isSiwxMessage(obj: unknown): obj is SiwxMessage {\n  if (!obj || typeof obj !== \"object\") return false;\n  const m = obj as Record<string, unknown>;\n  return (\n    typeof m.raw === \"string\" &&\n    typeof m.domain === \"string\" &&\n    typeof m.address === \"string\" &&\n    typeof m.uri === \"string\" &&\n    typeof m.version === \"number\" &&\n    typeof m.chainId === \"string\" &&\n    typeof m.nonce === \"string\" &&\n    typeof m.blockchain === \"string\"\n  );\n}\n","/**\n * SIWx Session Storage\n *\n * Provides storage adapters for SiwxSession persistence.\n * Supports localStorage and in-memory storage with auto-cleanup of expired sessions.\n */\n\nimport type { SiwxSession } from \"./session\";\n\n// ---------------------------------------------------------------------------\n// Interface\n// ---------------------------------------------------------------------------\n\n/**\n * Storage interface for SiwxSession persistence.\n * All operations are async for compatibility with diverse storage backends.\n */\nexport interface SiwxSessionStorage {\n  /** Retrieve a stored session, or null if none exists / expired */\n  get(): Promise<SiwxSession | null>;\n  /** Persist a session */\n  set(session: SiwxSession): Promise<void>;\n  /** Remove the stored session */\n  remove(): Promise<void>;\n  /** Check whether a session exists in storage */\n  has(): Promise<boolean>;\n  /** Clear all sessions managed by this adapter */\n  clear(): Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n// Auto-cleanup helper\n// ---------------------------------------------------------------------------\n\nfunction isExpired(session: SiwxSession): boolean {\n  if (!session.expiresAt) return false;\n  return new Date(session.expiresAt).getTime() <= Date.now();\n}\n\n// ---------------------------------------------------------------------------\n// localStorage-backed storage\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a SiwxSessionStorage backed by window.localStorage.\n * On `get()`, if the stored session is expired it will be automatically removed.\n */\nexport function createLocalStorageSiwxSessionStorage(\n  key: string,\n): SiwxSessionStorage {\n  return {\n    async get(): Promise<SiwxSession | null> {\n      try {\n        const raw = localStorage.getItem(key);\n        if (!raw) return null;\n        const session = JSON.parse(raw) as SiwxSession;\n        if (isExpired(session)) {\n          localStorage.removeItem(key);\n          return null;\n        }\n        return session;\n      } catch {\n        // Corrupt data — clean up\n        localStorage.removeItem(key);\n        return null;\n      }\n    },\n\n    async set(session: SiwxSession): Promise<void> {\n      localStorage.setItem(key, JSON.stringify(session));\n    },\n\n    async remove(): Promise<void> {\n      localStorage.removeItem(key);\n    },\n\n    async has(): Promise<boolean> {\n      const raw = localStorage.getItem(key);\n      if (!raw) return false;\n      try {\n        const session = JSON.parse(raw) as SiwxSession;\n        if (isExpired(session)) {\n          localStorage.removeItem(key);\n          return false;\n        }\n        return true;\n      } catch {\n        localStorage.removeItem(key);\n        return false;\n      }\n    },\n\n    async clear(): Promise<void> {\n      localStorage.removeItem(key);\n    },\n  };\n}\n\n// ---------------------------------------------------------------------------\n// In-memory storage\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a SiwxSessionStorage backed by an in-memory Map.\n * Used primarily for testing and server-side environments.\n * On `get()`, expired sessions are automatically evicted.\n */\nexport function createMemorySiwxSessionStorage(\n  key: string,\n): SiwxSessionStorage {\n  const store = new Map<string, string>();\n\n  return {\n    async get(): Promise<SiwxSession | null> {\n      const raw = store.get(key);\n      if (!raw) return null;\n      try {\n        const session = JSON.parse(raw) as SiwxSession;\n        if (isExpired(session)) {\n          store.delete(key);\n          return null;\n        }\n        return session;\n      } catch {\n        store.delete(key);\n        return null;\n      }\n    },\n\n    async set(session: SiwxSession): Promise<void> {\n      store.set(key, JSON.stringify(session));\n    },\n\n    async remove(): Promise<void> {\n      store.delete(key);\n    },\n\n    async has(): Promise<boolean> {\n      const raw = store.get(key);\n      if (!raw) return false;\n      try {\n        const session = JSON.parse(raw) as SiwxSession;\n        if (isExpired(session)) {\n          store.delete(key);\n          return false;\n        }\n        return true;\n      } catch {\n        store.delete(key);\n        return false;\n      }\n    },\n\n    async clear(): Promise<void> {\n      store.clear();\n    },\n  };\n}\n\n/**\n * Auto-cleanup scoped to a specific session — checks the session's expiry\n * and returns null if expired. Used by consumers that hold a session reference\n * and want a lightweight check without re-reading storage.\n */\nexport function checkSessionExpired(session: SiwxSession | null): boolean {\n  if (!session) return true;\n  return isExpired(session);\n}\n","/**\n * SIWx Verification — verify SiwxMessage signatures\n *\n * This module provides chain-agnostic SIWx message verification.\n * It validates message structure, time constraints, and delegates\n * cryptographic signature recovery to a user-provided callback.\n *\n * Chain-specific verifier factories are provided for convenience:\n *   - createEVMVerifier()       — uses viem recoverMessageAddress\n *   - createSolanaVerifier()    — uses tweetnacl + bs58\n *   - createXRPLVerifier()      — uses ripple-keypairs\n *\n * Usage:\n * ```ts\n * import { verifySiwxMessage, createEVMVerifier } from \"@naculus/siwx\";\n *\n * const result = await verifySiwxMessage({\n *   raw: rawMessage,\n *   signature: \"0x...\",\n *   recoverAddress: createEVMVerifier(),\n *   expectedAddress: \"0x...\",\n *   domain: \"example.com\",\n *   nonce: \"abc123\",\n * });\n * ```\n */\n\nimport { parseSiwxMessage } from \"./message\";\nimport { consumeNonceIfValid, isNonceIssued } from \"./nonce-consumption\";\nimport type { SiwxMessage, SiwxVerificationResult } from \"./types\";\nimport {\n  type EthCall,\n  decodeErc6492Signature,\n  hashPersonalMessage,\n  verifyErc1271,\n} from \"./contract-signature\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Parameters for verifySiwxMessage.\n */\nexport interface VerifySiwxMessageParams {\n  /** The raw SIWx message string that was signed */\n  raw: string;\n  /** The cryptographic signature (hex-encoded for EVM, base58 for Solana) */\n  signature: string;\n  /**\n   * Callback that recovers the signer address from a message + signature.\n   * The `message` passed here is the raw message string (not the Ethereum-prefixed one;\n   * the verifier is responsible for applying any required prefix/hash).\n   */\n  recoverAddress: (params: {\n    message: string;\n    signature: string;\n    /** Public key/address needed by non-recoverable signature schemes. */\n    publicKey?: string;\n  }) => string | boolean | Promise<string | boolean>;\n  /** Expected signer address. If provided, verification checks address match. */\n  expectedAddress?: string;\n  /** Public key for schemes such as Solana and XRPL that cannot recover it. */\n  publicKey?: string;\n  /** Expected domain. If provided, verification checks domain match. */\n  domain?: string;\n  /** Expected nonce. If provided, verification checks nonce match. */\n  nonce?: string;\n  /**\n   * Reference timestamp (ISO 8601) for checking expirationTime/notBefore.\n   * Defaults to current time.\n   */\n  timestamp?: string;\n}\n\n/**\n * Additional options controlling validation strictness.\n */\nexport interface VerifyOptions {\n  /**\n   * Verify a message without binding it to a domain.\n   *\n   * Domain binding is what stops a signature harvested on a phishing site\n   * from being replayed against the real one: the signature is genuine and\n   * the nonce is genuine, and the domain line is the only field that says who\n   * the user meant to sign in to. Verification therefore requires\n   * `params.domain` by default, and opting out has to be spelled out.\n   *\n   * Legitimate uses are narrow — inspecting a message whose origin is already\n   * established by other means, or tests. Never set it on a login path.\n   */\n  allowUnboundDomain?: boolean;\n  /** When true, messages without expirationTime are rejected (default: false) */\n  requireExpirationTime?: boolean;\n  /** When true, expirationTime check is skipped (default: false) */\n  skipExpirationCheck?: boolean;\n  /** When true, notBefore check is skipped (default: false) */\n  skipNotBeforeCheck?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Verification\n// ---------------------------------------------------------------------------\n\n/**\n * Verify a SIWx message and its cryptographic signature.\n *\n * Steps:\n * 1. Parse the raw message\n * 2. Validate structural integrity\n * 3. Check constraints (domain is required, then nonce, expiry, notBefore)\n * 4. Recover the signer address from the signature\n * 5. Compare recovered address with expected address (if provided)\n *\n * Returns a SiwxVerificationResult with the recovered address and validity flag.\n */\nexport async function verifySiwxMessage(\n  params: VerifySiwxMessageParams,\n  options?: VerifyOptions,\n): Promise<SiwxVerificationResult> {\n  // 1. Parse message\n  const parsed = parseSiwxMessage(params.raw);\n  if (!parsed) {\n    return {\n      address: params.expectedAddress ?? \"\",\n      isValid: false,\n      error: \"Failed to parse SIWx message: invalid format\",\n    };\n  }\n\n  // 2. Validate constraints\n  const constraintError = validateConstraints(parsed, params, options);\n  if (constraintError) {\n    return {\n      address: parsed.address,\n      isValid: false,\n      error: constraintError,\n    };\n  }\n\n  // 3. Recover address from signature\n  let recoveredAddress: string;\n  try {\n    const verificationKey =\n      params.publicKey ?? params.expectedAddress ?? parsed.address;\n    const recovered = await params.recoverAddress({\n      message: params.raw,\n      signature: params.signature,\n      publicKey: verificationKey,\n    });\n    if (typeof recovered === \"boolean\") {\n      if (!recovered) {\n        return {\n          address: params.expectedAddress ?? parsed.address,\n          isValid: false,\n          error: \"Signature verification failed\",\n        };\n      }\n      // A boolean only proves that `verificationKey` signed. It does not prove\n      // that an unrelated address named by the message owns that key. This is\n      // naturally bound for Solana, where the public key is the address. Other\n      // schemes (XRPL, for example) must return the address derived from the\n      // verified public key instead of a bare true.\n      const claimedAddress = params.expectedAddress ?? parsed.address;\n      if (!compareAddresses(parsed.chainId, verificationKey, claimedAddress)) {\n        return {\n          address: claimedAddress,\n          isValid: false,\n          error:\n            \"Signature verifier confirmed a public key that is not the claimed account address\",\n        };\n      }\n      recoveredAddress = verificationKey;\n    } else {\n      recoveredAddress = recovered;\n    }\n  } catch (err) {\n    return {\n      address: parsed.address,\n      isValid: false,\n      error:\n        \"Signature recovery failed: \" +\n        (err instanceof Error ? err.message : String(err)),\n    };\n  }\n\n  // 4. Compare addresses using the namespace's canonical rules.\n  const expected = params.expectedAddress ?? parsed.address;\n  if (\n    params.expectedAddress !== undefined &&\n    !compareAddresses(parsed.chainId, parsed.address, params.expectedAddress)\n  ) {\n    return {\n      address: params.expectedAddress,\n      isValid: false,\n      error:\n        \"SIWx message address does not match expected address. Message: \" +\n        parsed.address +\n        \", Expected: \" +\n        params.expectedAddress,\n    };\n  }\n  const addressesMatch = compareAddresses(\n    parsed.chainId,\n    recoveredAddress,\n    expected,\n  );\n\n  if (!addressesMatch) {\n    return {\n      address: expected,\n      isValid: false,\n      error:\n        \"Signature does not match expected address. Recovered: \" +\n        recoveredAddress +\n        \", Expected: \" +\n        expected,\n    };\n  }\n\n  // 5. Validate and consume nonce to prevent replay attacks\n  if (parsed.nonce) {\n    const consumed = await consumeNonceIfValid(parsed.nonce);\n    if (!consumed) {\n      // The atomic operation owns the security decision. This read is only for\n      // a useful diagnostic after the operation has already failed.\n      const wasIssued = await isNonceIssued(parsed.nonce);\n      return {\n        address: recoveredAddress,\n        isValid: false,\n        error: wasIssued\n          ? `replay: nonce already consumed for nonce=\"${parsed.nonce}\"`\n          : `unissued nonce: nonce=\"${parsed.nonce}\" was not issued by this system`,\n      };\n    }\n  }\n\n  return {\n    address: recoveredAddress,\n    isValid: true,\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Constraint validation\n// ---------------------------------------------------------------------------\n\nfunction validateConstraints(\n  parsed: SiwxMessage,\n  params: VerifySiwxMessageParams,\n  options?: VerifyOptions,\n): string | null {\n  // Domain binding. Absent expected domain means the caller has no way to\n  // tell a message signed for their site from one signed for an attacker's.\n  if (params.domain === undefined && !options?.allowUnboundDomain) {\n    return (\n      \"Refusing to verify without domain binding: pass `domain` so a \" +\n      'signature obtained on another site cannot be replayed here, or set ' +\n      \"`allowUnboundDomain` if this is deliberately not a login check.\"\n    );\n  }\n\n  // Domain check\n  if (params.domain !== undefined && parsed.domain !== params.domain) {\n    return (\n      'Domain mismatch: expected \"' +\n      params.domain +\n      '\", got \"' +\n      parsed.domain +\n      '\"'\n    );\n  }\n\n  // Nonce check\n  if (params.nonce !== undefined && parsed.nonce !== params.nonce) {\n    return (\n      'Nonce mismatch: expected \"' +\n      params.nonce +\n      '\", got \"' +\n      parsed.nonce +\n      '\"'\n    );\n  }\n\n  // Require expiration time\n  if (options?.requireExpirationTime && !parsed.expirationTime) {\n    return \"Expiration time is required but not present in message\";\n  }\n\n  // Timestamp-based checks\n  const refTime = params.timestamp\n    ? new Date(params.timestamp).getTime()\n    : Date.now();\n\n  if (Number.isNaN(refTime)) {\n    return (\n      'Invalid reference timestamp: \"' + (params.timestamp ?? \"undefined\") + '\"'\n    );\n  }\n\n  // Expiration time check\n  if (parsed.expirationTime && !options?.skipExpirationCheck) {\n    const expTime = new Date(parsed.expirationTime).getTime();\n    if (Number.isNaN(expTime)) {\n      return (\n        'Invalid expiration time in message: \"' + parsed.expirationTime + '\"'\n      );\n    }\n    if (refTime > expTime) {\n      return \"SIWx message expired at \" + parsed.expirationTime;\n    }\n  }\n\n  // NotBefore check\n  if (parsed.notBefore && !options?.skipNotBeforeCheck) {\n    const nbfTime = new Date(parsed.notBefore).getTime();\n    if (Number.isNaN(nbfTime)) {\n      return 'Invalid notBefore time in message: \"' + parsed.notBefore + '\"';\n    }\n    if (refTime < nbfTime) {\n      return (\n        \"SIWx message is not yet valid (notBefore: \" + parsed.notBefore + \")\"\n      );\n    }\n  }\n\n  return null;\n}\n\n// ---------------------------------------------------------------------------\n// Address comparison\n// ---------------------------------------------------------------------------\n\n/**\n * Compare two blockchain addresses case-insensitively.\n * Handles Ethereum addresses (case-insensitive hex) and Solana base58 addresses.\n */\nfunction compareAddresses(chainId: string, a: string, b: string): boolean {\n  const namespace = chainId.split(\":\", 1)[0];\n  return namespace === \"eip155\" ? a.toLowerCase() === b.toLowerCase() : a === b;\n}\n\n// ---------------------------------------------------------------------------\n// Chain-specific verifier factories\n// ---------------------------------------------------------------------------\n\n/**\n * Create an EVM (EIP-4361 SIWE) verifier using viem's recoverMessageAddress.\n *\n * Requires `viem` to be installed.\n * Throws if viem cannot be imported.\n */\nexport interface EVMVerifierOptions {\n  /**\n   * Performs an `eth_call`. Supplying it enables contract-account\n   * verification (ERC-1271 and ERC-6492, including counterfactual accounts).\n   * Without it only externally owned accounts can be verified, because\n   * `ecrecover` has nothing to say about a contract signature.\n   */\n  call?: EthCall;\n  /** Returns the deployed bytecode at an address, or \"0x\" when there is none. */\n  getCode?: (address: string) => Promise<string>;\n}\n\nexport function createEVMVerifier(\n  options?: EVMVerifierOptions,\n): (params: {\n  message: string;\n  signature: string;\n  publicKey?: string;\n}) => Promise<string | boolean> {\n  return async ({ message, signature, publicKey }) => {\n    let recoverMessageAddress: typeof import(\"viem\").recoverMessageAddress;\n    let encodeAbiParameters: typeof import(\"viem\").encodeAbiParameters;\n    let erc6492SignatureValidatorByteCode: typeof import(\"viem\").erc6492SignatureValidatorByteCode;\n    try {\n      ({\n        recoverMessageAddress,\n        encodeAbiParameters,\n        erc6492SignatureValidatorByteCode,\n      } = await import(\"viem\"));\n    } catch (err) {\n      throw new Error(\n        \"viem is required for EVM SIWx verification. Install it via: pnpm add viem\",\n        { cause: err },\n      );\n    }\n\n    // Contract accounts sign through their own logic, so there is no address\n    // to recover; they are verified against the address that claims them.\n    const account = publicKey;\n    const wrapped = decodeErc6492Signature(signature);\n\n    if (options?.call && account) {\n      const hash = hashPersonalMessage(message);\n\n      if (wrapped) {\n        const code = options.getCode\n          ? await options.getCode(account).catch(() => \"0x\")\n          : \"0x\";\n        if (code && code !== \"0x\") {\n          // Deployed after signing: the wrapper is now redundant and the\n          // inner signature is what the account would answer for.\n          return verifyErc1271(account, hash, wrapped.signature, options.call);\n        }\n        // The ERC-6492 validator's address-free form runs its creation bytecode\n        // directly, so it needs no already-deployed validator address. The full\n        // wrapper is required: it carries the factory call that creates the\n        // counterfactual account before checking its signature.\n        try {\n          const args = encodeAbiParameters(\n            [{ type: \"address\" }, { type: \"bytes32\" }, { type: \"bytes\" }],\n            [\n              account as `0x${string}`,\n              hash as `0x${string}`,\n              signature as `0x${string}`,\n            ],\n          );\n          const result = await options.call({\n            data: `${erc6492SignatureValidatorByteCode}${args.slice(2)}`,\n          });\n          const normalized = result.toLowerCase();\n          return normalized === \"0x01\" || normalized === `0x${\"0\".repeat(63)}1`;\n        } catch {\n          return false;\n        }\n      }\n\n      const contractResult = await verifyErc1271(\n        account,\n        hash,\n        signature,\n        options.call,\n      );\n      if (contractResult) return true;\n      // Fall through: an EOA's signature is not an ERC-1271 one.\n    }\n\n    if (wrapped) {\n      // No chain access, so the wrapper cannot be resolved. Recovering from\n      // the envelope bytes would return a meaningless address.\n      return false;\n    }\n\n    return recoverMessageAddress({\n      message,\n      signature: signature as `0x${string}`,\n    });\n  };\n}\n\n/**\n * Rethrow a failed dynamic import with a message that names the real cause.\n *\n * The packages guarded this way are declared dependencies of `@naculus/siwx`\n * and are installed alongside it, so reaching this path means module\n * resolution broke — a `pnpm.overrides` entry, a broken hoist, a bundler\n * `external` rule. It does not mean the consumer forgot to install something,\n * and the message must not tell them to.\n */\nfunction moduleLoadFailed(pkg: string, err: unknown): never {\n  throw new Error(\n    `Could not load \"${pkg}\", a dependency of @naculus/siwx. It ships with ` +\n      `this package, so this is a module resolution failure, not a missing ` +\n      `install.`,\n    { cause: err },\n  );\n}\n\n/**\n * Create a Solana (SIWS) verifier using tweetnacl and bs58.\n *\n * Both ship as dependencies of this package; a consumer does not install them\n * separately.\n *\n * Only the imports are guarded. Decoding and verification failures belong to\n * the caller's input and propagate unchanged: an invalid base58 string throws\n * `Non-base58 character` from bs58, and a wrong-length signature or key throws\n * `bad signature size` / `bad public key size` from tweetnacl. Translating\n * those into \"install the package\" was wrong — it named a cause that was never\n * true and sent the reader looking in the wrong place.\n */\nexport function createSolanaVerifier(): (params: {\n  message: string;\n  signature: string;\n  publicKey?: string;\n}) => Promise<boolean> {\n  return async ({ message, signature, publicKey }) => {\n    if (!publicKey)\n      throw new Error(\"Solana publicKey is required for verification\");\n    const nacl = (\n      await import(\"tweetnacl\").catch((err) =>\n        moduleLoadFailed(\"tweetnacl\", err),\n      )\n    ).default;\n    const bs58 = (\n      await import(\"bs58\").catch((err) => moduleLoadFailed(\"bs58\", err))\n    ).default;\n\n    const messageBytes = new TextEncoder().encode(message);\n    const signatureBytes = bs58.decode(signature);\n    const publicKeyBytes = bs58.decode(publicKey);\n\n    return nacl.sign.detached.verify(\n      messageBytes,\n      signatureBytes,\n      publicKeyBytes,\n    );\n  };\n}\n\n/**\n * Create an XRPL verifier using ripple-keypairs.\n *\n * `ripple-keypairs` is a runtime dependency of this package. Import failures\n * therefore indicate a broken module-resolution/bundling setup, while invalid\n * signatures remain ordinary input failures and are not rewritten as install\n * errors.\n */\nexport function createXRPLVerifier(): (params: {\n  message: string;\n  signature: string;\n  publicKey?: string;\n}) => Promise<string | boolean> {\n  return async ({ message, signature, publicKey }) => {\n    if (!publicKey)\n      throw new Error(\"XRPL publicKey is required for verification\");\n    const keypairs = await import(\"ripple-keypairs\").catch((err) =>\n      moduleLoadFailed(\"ripple-keypairs\", err),\n    );\n    const messageHex = Array.from(new TextEncoder().encode(message))\n      .map((byte) => byte.toString(16).padStart(2, \"0\"))\n      .join(\"\");\n    if (!keypairs.verify(messageHex, signature, publicKey)) return false;\n    // Unlike Solana, an XRPL account address is not the public key itself.\n    // Return the address derived from the key that actually verified so the\n    // common verifier can compare signer identity with the SIWx claim.\n    return keypairs.deriveAddress(publicKey);\n  };\n}\n"]}