w3pk 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/inspect/node.ts"],"sourcesContent":["/**\n * Node.js Inspection Module\n *\n * Provides filesystem-based code inspection capabilities for analyzing web3 applications\n * from the server-side or during development. Scans application source code and generates\n * security reports via the Rukh API.\n *\n * @module inspect/node\n */\n\nimport { promises as fs } from 'fs';\nimport path from 'path';\n\n/**\n * Configuration options for Node.js-based application inspection\n */\nexport interface InspectOptions {\n /**\n * The root directory of the application to inspect\n * @default process.cwd()\n */\n appPath?: string;\n\n /**\n * File patterns to include (glob patterns)\n * @default ['**\\/*.ts', '**\\/*.tsx', '**\\/*.js', '**\\/*.jsx', '**\\/*.json']\n */\n includePatterns?: string[];\n\n /**\n * Directories to exclude from inspection\n * @default ['node_modules', 'dist', '.next', '.git', 'build', 'coverage']\n */\n excludeDirs?: string[];\n\n /**\n * Maximum file size in KB to include\n * @default 500\n */\n maxFileSizeKB?: number;\n\n /**\n * Focus mode: filters files to only include those relevant to specific features\n * - 'transactions': Only files with signing, transactions, or blockchain operations\n * - 'all': Include all files (default)\n * @default 'all'\n */\n focusMode?: 'transactions' | 'all';\n}\n\n/**\n * Result of the code gathering operation\n */\nexport interface InspectResult {\n /**\n * The generated markdown content with all collected code\n */\n markdown: string;\n\n /**\n * List of files that were included\n */\n includedFiles: string[];\n\n /**\n * Total size in KB of all included files\n */\n totalSizeKB: number;\n}\n\n/**\n * Checks if file content is relevant to transactions/signing operations\n *\n * Scans file content and filename for keywords related to blockchain transactions,\n * message signing, smart contracts, and web3 operations.\n *\n * @param content - The file content to analyze\n * @param filename - The name of the file being checked\n * @returns True if the file contains transaction-related code\n * @internal\n */\nfunction isTransactionRelevant(content: string, filename: string): boolean {\n const transactionKeywords = [\n // Signing operations\n 'signMessage',\n 'signTypedData',\n 'sign(',\n 'signature',\n 'personalSign',\n 'eth_sign',\n 'sendTransaction',\n\n // Transaction operations\n 'transaction',\n 'sendTx',\n 'executeTx',\n 'Contract.connect',\n 'signer.send',\n 'wallet.send',\n 'provider.send',\n 'eth_sendTransaction',\n 'eth_sendRawTransaction',\n\n // Blockchain interaction\n 'Contract(',\n 'ethers.Contract',\n 'new Contract',\n 'ContractFactory',\n 'deployContract',\n\n // W3PK specific\n 'w3pk.sign',\n 'w3pk.send',\n 'Web3Passkey',\n 'useW3PK',\n\n // Smart contract calls\n 'call(',\n 'estimateGas',\n 'gasLimit',\n 'gasPrice',\n 'maxFeePerGas',\n 'maxPriorityFeePerGas',\n\n // Web3 providers\n 'JsonRpcProvider',\n 'Web3Provider',\n 'BrowserProvider',\n\n // EIP-7702\n 'EIP7702',\n 'authorization',\n 'delegation',\n ];\n\n const lowerContent = content.toLowerCase();\n const lowerFilename = filename.toLowerCase();\n\n // Check if filename suggests transaction relevance\n if (\n lowerFilename.includes('transaction') ||\n lowerFilename.includes('sign') ||\n lowerFilename.includes('wallet') ||\n lowerFilename.includes('contract') ||\n lowerFilename.includes('blockchain') ||\n lowerFilename.includes('web3') ||\n lowerFilename.includes('ethers')\n ) {\n return true;\n }\n\n // Check content for transaction-related keywords\n return transactionKeywords.some(keyword =>\n lowerContent.includes(keyword.toLowerCase())\n );\n}\n\n/**\n * Recursively walks a directory tree and collects matching files\n *\n * Traverses the directory structure, applying filters based on file patterns,\n * size limits, and focus mode settings.\n *\n * @param dir - The directory path to walk\n * @param options - Inspection options with filters and limits\n * @param rootDir - The root directory for calculating relative paths\n * @returns Array of file objects with path, content, and relative path\n * @internal\n */\nasync function walkDirectory(\n dir: string,\n options: Required<InspectOptions>,\n rootDir: string\n): Promise<{ path: string; content: string; relativePath: string }[]> {\n const files: { path: string; content: string; relativePath: string }[] = [];\n\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n const relativePath = path.relative(rootDir, fullPath);\n\n if (entry.isDirectory()) {\n // Skip excluded directories\n if (options.excludeDirs.includes(entry.name)) {\n continue;\n }\n\n // Recursively walk subdirectory\n const subFiles = await walkDirectory(fullPath, options, rootDir);\n files.push(...subFiles);\n } else if (entry.isFile()) {\n // Check if file matches include patterns\n const ext = path.extname(entry.name);\n const shouldInclude = options.includePatterns.some(pattern => {\n // Simple pattern matching for common extensions\n if (pattern.includes('*')) {\n const patternExt = pattern.split('.').pop();\n return ext === `.${patternExt}`;\n }\n return entry.name === pattern;\n });\n\n if (!shouldInclude) {\n continue;\n }\n\n // Check file size\n const stats = await fs.stat(fullPath);\n const fileSizeKB = stats.size / 1024;\n\n if (fileSizeKB > options.maxFileSizeKB) {\n console.warn(`Skipping ${relativePath} (${fileSizeKB.toFixed(2)} KB exceeds limit)`);\n continue;\n }\n\n // Read file content\n const content = await fs.readFile(fullPath, 'utf-8');\n\n // Apply focus mode filter if enabled\n if (options.focusMode === 'transactions') {\n if (!isTransactionRelevant(content, entry.name)) {\n continue;\n }\n }\n\n files.push({ path: fullPath, content, relativePath });\n }\n }\n } catch (error) {\n console.error(`Error reading directory ${dir}:`, error);\n }\n\n return files;\n}\n\n/**\n * Gathers application code and generates a markdown document\n *\n * @param options - Configuration options for the inspection\n * @returns An object containing the markdown content and metadata\n *\n * @example\n * ```typescript\n * const result = await gatherCode({\n * appPath: '../genji-passkey',\n * includePatterns: ['**\\/*.ts', '**\\/*.tsx']\n * });\n *\n * console.log(`Collected ${result.includedFiles.length} files`);\n * console.log(`Total size: ${result.totalSizeKB} KB`);\n * ```\n */\nexport async function gatherCode(options: InspectOptions = {}): Promise<InspectResult> {\n const opts: Required<InspectOptions> = {\n appPath: options.appPath || process.cwd(),\n includePatterns: options.includePatterns || [\n '**/*.ts',\n '**/*.tsx',\n '**/*.js',\n '**/*.jsx',\n '**/*.json'\n ],\n excludeDirs: options.excludeDirs || [\n 'node_modules',\n 'dist',\n '.next',\n '.git',\n 'build',\n 'coverage',\n '.cache',\n 'out',\n '.vercel',\n '.turbo'\n ],\n maxFileSizeKB: options.maxFileSizeKB || 500,\n focusMode: options.focusMode || 'all'\n };\n\n // Resolve app path\n const appPath = path.resolve(opts.appPath);\n\n // Check if app path exists\n try {\n await fs.access(appPath);\n } catch (error) {\n throw new Error(`App path does not exist: ${appPath}`);\n }\n\n // Walk directory and collect files\n const files = await walkDirectory(appPath, opts, appPath);\n\n // Generate markdown\n let markdown = `# Application Code Inspection\\n\\n`;\n markdown += `**Inspected Path:** \\`${appPath}\\`\\n\\n`;\n markdown += `**Files Collected:** ${files.length}\\n\\n`;\n markdown += `**Focus Mode:** ${opts.focusMode}\\n\\n`;\n markdown += `**Timestamp:** ${new Date().toISOString()}\\n\\n`;\n markdown += `---\\n\\n`;\n\n if (opts.focusMode === 'transactions') {\n markdown += `## Analysis Focus\\n\\n`;\n markdown += `This inspection is focused on **transaction and signing operations**. Only files containing:\\n\\n`;\n markdown += `- Message signing (signMessage, signTypedData, etc.)\\n`;\n markdown += `- Transaction sending (sendTransaction, eth_sendTransaction, etc.)\\n`;\n markdown += `- Smart contract interactions (Contract calls, deployments)\\n`;\n markdown += `- Blockchain providers and wallets\\n`;\n markdown += `- W3PK signing/transaction methods\\n`;\n markdown += `- EIP-7702 authorization/delegation\\n\\n`;\n markdown += `---\\n\\n`;\n }\n\n // Add table of contents\n markdown += `## Table of Contents\\n\\n`;\n files.forEach(file => {\n markdown += `- [\\`${file.relativePath}\\`](#${file.relativePath.replace(/[^a-z0-9]/gi, '-').toLowerCase()})\\n`;\n });\n markdown += `\\n---\\n\\n`;\n\n // Add file contents\n markdown += `## Files\\n\\n`;\n\n for (const file of files) {\n const ext = path.extname(file.relativePath).slice(1);\n const language = ext === 'tsx' || ext === 'ts' ? 'typescript' :\n ext === 'jsx' || ext === 'js' ? 'javascript' :\n ext === 'json' ? 'json' : ext;\n\n markdown += `### \\`${file.relativePath}\\`\\n\\n`;\n markdown += '```' + language + '\\n';\n markdown += file.content;\n markdown += '\\n```\\n\\n';\n markdown += `---\\n\\n`;\n }\n\n // Calculate total size\n const totalSizeKB = files.reduce((sum, file) => {\n return sum + Buffer.byteLength(file.content, 'utf-8') / 1024;\n }, 0);\n\n return {\n markdown,\n includedFiles: files.map(f => f.relativePath),\n totalSizeKB: Math.round(totalSizeKB * 100) / 100\n };\n}\n\n/**\n * Inspects an application and returns a security report via Rukh API\n *\n * @param appPath - Path to the application to inspect\n * @param rukhUrl - The Rukh API endpoint URL\n * @param context - The context name to use (should have an instruction file with report format)\n * @param model - The AI model to use ('anthropic', 'mistral', or 'openai')\n * @param focusMode - Focus on 'transactions' or include 'all' files\n * @returns A markdown-formatted security report\n *\n * @example\n * ```typescript\n * const report = await inspect(\n * '../genji-passkey',\n * 'https://rukh.w3hc.org',\n * 'w3pk',\n * 'anthropic',\n * 'transactions'\n * );\n *\n * console.log(report);\n * // Outputs:\n * // # Genji Passkey Report\n * // ## Available Methods\n * // ### Method #1: Sign Message\n * // ...\n * ```\n */\nexport async function inspect(\n appPath: string,\n rukhUrl: string = 'https://rukh.w3hc.org',\n context: string = 'w3pk',\n model: 'anthropic' | 'mistral' | 'openai' = 'anthropic',\n focusMode: 'transactions' | 'all' = 'transactions'\n): Promise<string> {\n // Gather application code with focus mode\n const result = await gatherCode({ appPath, focusMode });\n\n console.log(`Inspected ${result.includedFiles.length} files (${result.totalSizeKB} KB) [Focus: ${focusMode}]`);\n\n // Default message that triggers the structured report\n const message = 'Analyze this application and provide a security report listing all transaction and signing methods.';\n\n // Prepare form data\n const formData = new FormData();\n formData.append('message', message);\n formData.append('model', model);\n formData.append('context', context);\n\n // Create a blob from the markdown content\n const blob = new Blob([result.markdown], { type: 'text/markdown' });\n formData.append('file', blob, 'app-code.md');\n\n // Send request to Rukh API\n const response = await fetch(`${rukhUrl}/ask`, {\n method: 'POST',\n body: formData\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Rukh API error: ${response.status} - ${error}`);\n }\n\n const data = await response.json();\n return data.response || data.message || JSON.stringify(data);\n}\n"],"mappings":"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","names":["fs","path"]}
1
+ {"version":3,"sources":["../../src/inspect/node.ts"],"sourcesContent":["/**\n * Node.js Inspection Module\n *\n * Provides filesystem-based code inspection capabilities for analyzing web3 applications\n * from the server-side or during development. Scans application source code and generates\n * security reports via the Rukh API.\n *\n * @module inspect/node\n */\n\nimport { promises as fs } from 'fs';\nimport path from 'path';\n\n/**\n * Configuration options for Node.js-based application inspection\n */\nexport interface InspectOptions {\n /**\n * The root directory of the application to inspect\n * @default process.cwd()\n */\n appPath?: string;\n\n /**\n * File patterns to include (glob patterns)\n * @default ['**\\/*.ts', '**\\/*.tsx', '**\\/*.js', '**\\/*.jsx', '**\\/*.json']\n */\n includePatterns?: string[];\n\n /**\n * Directories to exclude from inspection\n * @default ['node_modules', 'dist', '.next', '.git', 'build', 'coverage']\n */\n excludeDirs?: string[];\n\n /**\n * Maximum file size in KB to include\n * @default 500\n */\n maxFileSizeKB?: number;\n\n /**\n * Maximum total size in KB for all collected code (prevents exceeding API token limits)\n * @default 100\n */\n maxTotalSizeKB?: number;\n\n /**\n * Focus mode: filters files to only include those relevant to specific features\n * - 'transactions': Only files with signing, transactions, or blockchain operations\n * - 'all': Include all files (default)\n * @default 'all'\n */\n focusMode?: 'transactions' | 'all';\n}\n\n/**\n * Result of the code gathering operation\n */\nexport interface InspectResult {\n /**\n * The generated markdown content with all collected code\n */\n markdown: string;\n\n /**\n * List of files that were included\n */\n includedFiles: string[];\n\n /**\n * Total size in KB of all included files\n */\n totalSizeKB: number;\n}\n\n/**\n * Checks if file content is relevant to transactions/signing operations\n *\n * Scans file content and filename for keywords related to blockchain transactions,\n * message signing, smart contracts, and web3 operations.\n *\n * @param content - The file content to analyze\n * @param filename - The name of the file being checked\n * @returns True if the file contains transaction-related code\n * @internal\n */\nfunction isTransactionRelevant(content: string, filename: string): boolean {\n const transactionKeywords = [\n // Signing operations\n 'signMessage',\n 'signTypedData',\n 'sign(',\n 'signature',\n 'personalSign',\n 'eth_sign',\n 'sendTransaction',\n\n // Transaction operations\n 'transaction',\n 'sendTx',\n 'executeTx',\n 'Contract.connect',\n 'signer.send',\n 'wallet.send',\n 'provider.send',\n 'eth_sendTransaction',\n 'eth_sendRawTransaction',\n\n // Blockchain interaction\n 'Contract(',\n 'ethers.Contract',\n 'new Contract',\n 'ContractFactory',\n 'deployContract',\n\n // W3PK specific\n 'w3pk.sign',\n 'w3pk.send',\n 'Web3Passkey',\n 'useW3PK',\n\n // Smart contract calls\n 'call(',\n 'estimateGas',\n 'gasLimit',\n 'gasPrice',\n 'maxFeePerGas',\n 'maxPriorityFeePerGas',\n\n // Web3 providers\n 'JsonRpcProvider',\n 'Web3Provider',\n 'BrowserProvider',\n\n // EIP-7702\n 'EIP7702',\n 'authorization',\n 'delegation',\n ];\n\n const lowerContent = content.toLowerCase();\n const lowerFilename = filename.toLowerCase();\n\n // Check if filename suggests transaction relevance\n if (\n lowerFilename.includes('transaction') ||\n lowerFilename.includes('sign') ||\n lowerFilename.includes('wallet') ||\n lowerFilename.includes('contract') ||\n lowerFilename.includes('blockchain') ||\n lowerFilename.includes('web3') ||\n lowerFilename.includes('ethers')\n ) {\n return true;\n }\n\n // Check content for transaction-related keywords\n return transactionKeywords.some(keyword =>\n lowerContent.includes(keyword.toLowerCase())\n );\n}\n\n/**\n * Recursively walks a directory tree and collects matching files\n *\n * Traverses the directory structure, applying filters based on file patterns,\n * size limits, and focus mode settings.\n *\n * @param dir - The directory path to walk\n * @param options - Inspection options with filters and limits\n * @param rootDir - The root directory for calculating relative paths\n * @param cumulativeSizeKB - Tracks total size collected so far\n * @returns Array of file objects with path, content, and relative path\n * @internal\n */\nasync function walkDirectory(\n dir: string,\n options: Required<InspectOptions>,\n rootDir: string,\n cumulativeSizeKB: { value: number } = { value: 0 }\n): Promise<{ path: string; content: string; relativePath: string }[]> {\n const files: { path: string; content: string; relativePath: string }[] = [];\n\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n // Check if we've reached the total size limit\n if (cumulativeSizeKB.value >= options.maxTotalSizeKB) {\n console.warn(`Reached total size limit of ${options.maxTotalSizeKB} KB - stopping collection`);\n break;\n }\n\n const fullPath = path.join(dir, entry.name);\n const relativePath = path.relative(rootDir, fullPath);\n\n if (entry.isDirectory()) {\n // Skip excluded directories\n if (options.excludeDirs.includes(entry.name)) {\n continue;\n }\n\n // Recursively walk subdirectory\n const subFiles = await walkDirectory(fullPath, options, rootDir, cumulativeSizeKB);\n files.push(...subFiles);\n } else if (entry.isFile()) {\n // Check if file matches include patterns\n const ext = path.extname(entry.name);\n const shouldInclude = options.includePatterns.some(pattern => {\n // Simple pattern matching for common extensions\n if (pattern.includes('*')) {\n const patternExt = pattern.split('.').pop();\n return ext === `.${patternExt}`;\n }\n return entry.name === pattern;\n });\n\n if (!shouldInclude) {\n continue;\n }\n\n // Check file size\n const stats = await fs.stat(fullPath);\n const fileSizeKB = stats.size / 1024;\n\n if (fileSizeKB > options.maxFileSizeKB) {\n console.warn(`Skipping ${relativePath} (${fileSizeKB.toFixed(2)} KB exceeds limit)`);\n continue;\n }\n\n // Read file content\n const content = await fs.readFile(fullPath, 'utf-8');\n\n // Apply focus mode filter if enabled\n if (options.focusMode === 'transactions') {\n if (!isTransactionRelevant(content, entry.name)) {\n continue;\n }\n }\n\n // Calculate content size\n const contentSizeKB = Buffer.byteLength(content, 'utf-8') / 1024;\n\n // Check if adding this file would exceed the total limit\n if (cumulativeSizeKB.value + contentSizeKB > options.maxTotalSizeKB) {\n console.warn(`Skipping ${relativePath} - would exceed total size limit (${(cumulativeSizeKB.value + contentSizeKB).toFixed(2)} KB > ${options.maxTotalSizeKB} KB)`);\n break;\n }\n\n // Add file and update cumulative size\n cumulativeSizeKB.value += contentSizeKB;\n files.push({ path: fullPath, content, relativePath });\n }\n }\n } catch (error) {\n console.error(`Error reading directory ${dir}:`, error);\n }\n\n return files;\n}\n\n/**\n * Gathers application code and generates a markdown document\n *\n * @param options - Configuration options for the inspection\n * @returns An object containing the markdown content and metadata\n *\n * @example\n * ```typescript\n * const result = await gatherCode({\n * appPath: '../genji-passkey',\n * includePatterns: ['**\\/*.ts', '**\\/*.tsx']\n * });\n *\n * console.log(`Collected ${result.includedFiles.length} files`);\n * console.log(`Total size: ${result.totalSizeKB} KB`);\n * ```\n */\nexport async function gatherCode(options: InspectOptions = {}): Promise<InspectResult> {\n const opts: Required<InspectOptions> = {\n appPath: options.appPath || process.cwd(),\n includePatterns: options.includePatterns || [\n '**/*.ts',\n '**/*.tsx',\n '**/*.js',\n '**/*.jsx',\n '**/*.json'\n ],\n excludeDirs: options.excludeDirs || [\n 'node_modules',\n 'dist',\n '.next',\n '.git',\n 'build',\n 'coverage',\n '.cache',\n 'out',\n '.vercel',\n '.turbo'\n ],\n maxFileSizeKB: options.maxFileSizeKB || 500,\n maxTotalSizeKB: options.maxTotalSizeKB || 100,\n focusMode: options.focusMode || 'all'\n };\n\n // Resolve app path\n const appPath = path.resolve(opts.appPath);\n\n // Check if app path exists\n try {\n await fs.access(appPath);\n } catch (error) {\n throw new Error(`App path does not exist: ${appPath}`);\n }\n\n // Walk directory and collect files\n const files = await walkDirectory(appPath, opts, appPath);\n\n // Generate markdown\n let markdown = `# Application Code Inspection\\n\\n`;\n markdown += `**Inspected Path:** \\`${appPath}\\`\\n\\n`;\n markdown += `**Files Collected:** ${files.length}\\n\\n`;\n markdown += `**Focus Mode:** ${opts.focusMode}\\n\\n`;\n markdown += `**Timestamp:** ${new Date().toISOString()}\\n\\n`;\n markdown += `---\\n\\n`;\n\n if (opts.focusMode === 'transactions') {\n markdown += `## Analysis Focus\\n\\n`;\n markdown += `This inspection is focused on **transaction and signing operations**. Only files containing:\\n\\n`;\n markdown += `- Message signing (signMessage, signTypedData, etc.)\\n`;\n markdown += `- Transaction sending (sendTransaction, eth_sendTransaction, etc.)\\n`;\n markdown += `- Smart contract interactions (Contract calls, deployments)\\n`;\n markdown += `- Blockchain providers and wallets\\n`;\n markdown += `- W3PK signing/transaction methods\\n`;\n markdown += `- EIP-7702 authorization/delegation\\n\\n`;\n markdown += `---\\n\\n`;\n }\n\n // Add table of contents\n markdown += `## Table of Contents\\n\\n`;\n files.forEach(file => {\n markdown += `- [\\`${file.relativePath}\\`](#${file.relativePath.replace(/[^a-z0-9]/gi, '-').toLowerCase()})\\n`;\n });\n markdown += `\\n---\\n\\n`;\n\n // Add file contents\n markdown += `## Files\\n\\n`;\n\n for (const file of files) {\n const ext = path.extname(file.relativePath).slice(1);\n const language = ext === 'tsx' || ext === 'ts' ? 'typescript' :\n ext === 'jsx' || ext === 'js' ? 'javascript' :\n ext === 'json' ? 'json' : ext;\n\n markdown += `### \\`${file.relativePath}\\`\\n\\n`;\n markdown += '```' + language + '\\n';\n markdown += file.content;\n markdown += '\\n```\\n\\n';\n markdown += `---\\n\\n`;\n }\n\n // Calculate total size\n const totalSizeKB = files.reduce((sum, file) => {\n return sum + Buffer.byteLength(file.content, 'utf-8') / 1024;\n }, 0);\n\n return {\n markdown,\n includedFiles: files.map(f => f.relativePath),\n totalSizeKB: Math.round(totalSizeKB * 100) / 100\n };\n}\n\n/**\n * Inspects an application and returns a security report via Rukh API\n *\n * @param appPath - Path to the application to inspect\n * @param rukhUrl - The Rukh API endpoint URL\n * @param context - The context name to use (should have an instruction file with report format)\n * @param model - The AI model to use ('anthropic', 'mistral', or 'openai')\n * @param focusMode - Focus on 'transactions' or include 'all' files\n * @returns A markdown-formatted security report\n *\n * @example\n * ```typescript\n * const report = await inspect(\n * '../genji-passkey',\n * 'https://rukh.w3hc.org',\n * 'w3pk',\n * 'anthropic',\n * 'transactions'\n * );\n *\n * console.log(report);\n * // Outputs:\n * // # Genji Passkey Report\n * // ## Available Methods\n * // ### Method #1: Sign Message\n * // ...\n * ```\n */\nexport async function inspect(\n appPath: string,\n rukhUrl: string = 'https://rukh.w3hc.org',\n context: string = 'w3pk',\n model: 'anthropic' | 'mistral' | 'openai' = 'anthropic',\n focusMode: 'transactions' | 'all' = 'transactions'\n): Promise<string> {\n // Gather application code with focus mode\n const result = await gatherCode({ appPath, focusMode });\n\n console.log(`Inspected ${result.includedFiles.length} files (${result.totalSizeKB} KB) [Focus: ${focusMode}]`);\n\n // Default message that triggers the structured report\n const message = 'Analyze this application and provide a security report listing all transaction and signing methods.';\n\n // Prepare form data\n const formData = new FormData();\n formData.append('message', message);\n formData.append('model', model);\n formData.append('context', context);\n\n // Create a blob from the markdown content\n const blob = new Blob([result.markdown], { type: 'text/markdown' });\n formData.append('file', blob, 'app-code.md');\n\n // Send request to Rukh API\n const response = await fetch(`${rukhUrl}/ask`, {\n method: 'POST',\n body: formData\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Rukh API error: ${response.status} - ${error}`);\n }\n\n const data = await response.json();\n return data.response || data.message || JSON.stringify(data);\n}\n"],"mappings":"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","names":["fs","path"]}
@@ -57,18 +57,22 @@ function isTransactionRelevant(content, filename) {
57
57
  (keyword) => lowerContent.includes(keyword.toLowerCase())
58
58
  );
59
59
  }
60
- async function walkDirectory(dir, options, rootDir) {
60
+ async function walkDirectory(dir, options, rootDir, cumulativeSizeKB = { value: 0 }) {
61
61
  const files = [];
62
62
  try {
63
63
  const entries = await fs.readdir(dir, { withFileTypes: true });
64
64
  for (const entry of entries) {
65
+ if (cumulativeSizeKB.value >= options.maxTotalSizeKB) {
66
+ console.warn(`Reached total size limit of ${options.maxTotalSizeKB} KB - stopping collection`);
67
+ break;
68
+ }
65
69
  const fullPath = path.join(dir, entry.name);
66
70
  const relativePath = path.relative(rootDir, fullPath);
67
71
  if (entry.isDirectory()) {
68
72
  if (options.excludeDirs.includes(entry.name)) {
69
73
  continue;
70
74
  }
71
- const subFiles = await walkDirectory(fullPath, options, rootDir);
75
+ const subFiles = await walkDirectory(fullPath, options, rootDir, cumulativeSizeKB);
72
76
  files.push(...subFiles);
73
77
  } else if (entry.isFile()) {
74
78
  const ext = path.extname(entry.name);
@@ -94,6 +98,12 @@ async function walkDirectory(dir, options, rootDir) {
94
98
  continue;
95
99
  }
96
100
  }
101
+ const contentSizeKB = Buffer.byteLength(content, "utf-8") / 1024;
102
+ if (cumulativeSizeKB.value + contentSizeKB > options.maxTotalSizeKB) {
103
+ console.warn(`Skipping ${relativePath} - would exceed total size limit (${(cumulativeSizeKB.value + contentSizeKB).toFixed(2)} KB > ${options.maxTotalSizeKB} KB)`);
104
+ break;
105
+ }
106
+ cumulativeSizeKB.value += contentSizeKB;
97
107
  files.push({ path: fullPath, content, relativePath });
98
108
  }
99
109
  }
@@ -125,6 +135,7 @@ async function gatherCode(options = {}) {
125
135
  ".turbo"
126
136
  ],
127
137
  maxFileSizeKB: options.maxFileSizeKB || 500,
138
+ maxTotalSizeKB: options.maxTotalSizeKB || 100,
128
139
  focusMode: options.focusMode || "all"
129
140
  };
130
141
  const appPath = path.resolve(opts.appPath);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/inspect/node.ts"],"sourcesContent":["/**\n * Node.js Inspection Module\n *\n * Provides filesystem-based code inspection capabilities for analyzing web3 applications\n * from the server-side or during development. Scans application source code and generates\n * security reports via the Rukh API.\n *\n * @module inspect/node\n */\n\nimport { promises as fs } from 'fs';\nimport path from 'path';\n\n/**\n * Configuration options for Node.js-based application inspection\n */\nexport interface InspectOptions {\n /**\n * The root directory of the application to inspect\n * @default process.cwd()\n */\n appPath?: string;\n\n /**\n * File patterns to include (glob patterns)\n * @default ['**\\/*.ts', '**\\/*.tsx', '**\\/*.js', '**\\/*.jsx', '**\\/*.json']\n */\n includePatterns?: string[];\n\n /**\n * Directories to exclude from inspection\n * @default ['node_modules', 'dist', '.next', '.git', 'build', 'coverage']\n */\n excludeDirs?: string[];\n\n /**\n * Maximum file size in KB to include\n * @default 500\n */\n maxFileSizeKB?: number;\n\n /**\n * Focus mode: filters files to only include those relevant to specific features\n * - 'transactions': Only files with signing, transactions, or blockchain operations\n * - 'all': Include all files (default)\n * @default 'all'\n */\n focusMode?: 'transactions' | 'all';\n}\n\n/**\n * Result of the code gathering operation\n */\nexport interface InspectResult {\n /**\n * The generated markdown content with all collected code\n */\n markdown: string;\n\n /**\n * List of files that were included\n */\n includedFiles: string[];\n\n /**\n * Total size in KB of all included files\n */\n totalSizeKB: number;\n}\n\n/**\n * Checks if file content is relevant to transactions/signing operations\n *\n * Scans file content and filename for keywords related to blockchain transactions,\n * message signing, smart contracts, and web3 operations.\n *\n * @param content - The file content to analyze\n * @param filename - The name of the file being checked\n * @returns True if the file contains transaction-related code\n * @internal\n */\nfunction isTransactionRelevant(content: string, filename: string): boolean {\n const transactionKeywords = [\n // Signing operations\n 'signMessage',\n 'signTypedData',\n 'sign(',\n 'signature',\n 'personalSign',\n 'eth_sign',\n 'sendTransaction',\n\n // Transaction operations\n 'transaction',\n 'sendTx',\n 'executeTx',\n 'Contract.connect',\n 'signer.send',\n 'wallet.send',\n 'provider.send',\n 'eth_sendTransaction',\n 'eth_sendRawTransaction',\n\n // Blockchain interaction\n 'Contract(',\n 'ethers.Contract',\n 'new Contract',\n 'ContractFactory',\n 'deployContract',\n\n // W3PK specific\n 'w3pk.sign',\n 'w3pk.send',\n 'Web3Passkey',\n 'useW3PK',\n\n // Smart contract calls\n 'call(',\n 'estimateGas',\n 'gasLimit',\n 'gasPrice',\n 'maxFeePerGas',\n 'maxPriorityFeePerGas',\n\n // Web3 providers\n 'JsonRpcProvider',\n 'Web3Provider',\n 'BrowserProvider',\n\n // EIP-7702\n 'EIP7702',\n 'authorization',\n 'delegation',\n ];\n\n const lowerContent = content.toLowerCase();\n const lowerFilename = filename.toLowerCase();\n\n // Check if filename suggests transaction relevance\n if (\n lowerFilename.includes('transaction') ||\n lowerFilename.includes('sign') ||\n lowerFilename.includes('wallet') ||\n lowerFilename.includes('contract') ||\n lowerFilename.includes('blockchain') ||\n lowerFilename.includes('web3') ||\n lowerFilename.includes('ethers')\n ) {\n return true;\n }\n\n // Check content for transaction-related keywords\n return transactionKeywords.some(keyword =>\n lowerContent.includes(keyword.toLowerCase())\n );\n}\n\n/**\n * Recursively walks a directory tree and collects matching files\n *\n * Traverses the directory structure, applying filters based on file patterns,\n * size limits, and focus mode settings.\n *\n * @param dir - The directory path to walk\n * @param options - Inspection options with filters and limits\n * @param rootDir - The root directory for calculating relative paths\n * @returns Array of file objects with path, content, and relative path\n * @internal\n */\nasync function walkDirectory(\n dir: string,\n options: Required<InspectOptions>,\n rootDir: string\n): Promise<{ path: string; content: string; relativePath: string }[]> {\n const files: { path: string; content: string; relativePath: string }[] = [];\n\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n const relativePath = path.relative(rootDir, fullPath);\n\n if (entry.isDirectory()) {\n // Skip excluded directories\n if (options.excludeDirs.includes(entry.name)) {\n continue;\n }\n\n // Recursively walk subdirectory\n const subFiles = await walkDirectory(fullPath, options, rootDir);\n files.push(...subFiles);\n } else if (entry.isFile()) {\n // Check if file matches include patterns\n const ext = path.extname(entry.name);\n const shouldInclude = options.includePatterns.some(pattern => {\n // Simple pattern matching for common extensions\n if (pattern.includes('*')) {\n const patternExt = pattern.split('.').pop();\n return ext === `.${patternExt}`;\n }\n return entry.name === pattern;\n });\n\n if (!shouldInclude) {\n continue;\n }\n\n // Check file size\n const stats = await fs.stat(fullPath);\n const fileSizeKB = stats.size / 1024;\n\n if (fileSizeKB > options.maxFileSizeKB) {\n console.warn(`Skipping ${relativePath} (${fileSizeKB.toFixed(2)} KB exceeds limit)`);\n continue;\n }\n\n // Read file content\n const content = await fs.readFile(fullPath, 'utf-8');\n\n // Apply focus mode filter if enabled\n if (options.focusMode === 'transactions') {\n if (!isTransactionRelevant(content, entry.name)) {\n continue;\n }\n }\n\n files.push({ path: fullPath, content, relativePath });\n }\n }\n } catch (error) {\n console.error(`Error reading directory ${dir}:`, error);\n }\n\n return files;\n}\n\n/**\n * Gathers application code and generates a markdown document\n *\n * @param options - Configuration options for the inspection\n * @returns An object containing the markdown content and metadata\n *\n * @example\n * ```typescript\n * const result = await gatherCode({\n * appPath: '../genji-passkey',\n * includePatterns: ['**\\/*.ts', '**\\/*.tsx']\n * });\n *\n * console.log(`Collected ${result.includedFiles.length} files`);\n * console.log(`Total size: ${result.totalSizeKB} KB`);\n * ```\n */\nexport async function gatherCode(options: InspectOptions = {}): Promise<InspectResult> {\n const opts: Required<InspectOptions> = {\n appPath: options.appPath || process.cwd(),\n includePatterns: options.includePatterns || [\n '**/*.ts',\n '**/*.tsx',\n '**/*.js',\n '**/*.jsx',\n '**/*.json'\n ],\n excludeDirs: options.excludeDirs || [\n 'node_modules',\n 'dist',\n '.next',\n '.git',\n 'build',\n 'coverage',\n '.cache',\n 'out',\n '.vercel',\n '.turbo'\n ],\n maxFileSizeKB: options.maxFileSizeKB || 500,\n focusMode: options.focusMode || 'all'\n };\n\n // Resolve app path\n const appPath = path.resolve(opts.appPath);\n\n // Check if app path exists\n try {\n await fs.access(appPath);\n } catch (error) {\n throw new Error(`App path does not exist: ${appPath}`);\n }\n\n // Walk directory and collect files\n const files = await walkDirectory(appPath, opts, appPath);\n\n // Generate markdown\n let markdown = `# Application Code Inspection\\n\\n`;\n markdown += `**Inspected Path:** \\`${appPath}\\`\\n\\n`;\n markdown += `**Files Collected:** ${files.length}\\n\\n`;\n markdown += `**Focus Mode:** ${opts.focusMode}\\n\\n`;\n markdown += `**Timestamp:** ${new Date().toISOString()}\\n\\n`;\n markdown += `---\\n\\n`;\n\n if (opts.focusMode === 'transactions') {\n markdown += `## Analysis Focus\\n\\n`;\n markdown += `This inspection is focused on **transaction and signing operations**. Only files containing:\\n\\n`;\n markdown += `- Message signing (signMessage, signTypedData, etc.)\\n`;\n markdown += `- Transaction sending (sendTransaction, eth_sendTransaction, etc.)\\n`;\n markdown += `- Smart contract interactions (Contract calls, deployments)\\n`;\n markdown += `- Blockchain providers and wallets\\n`;\n markdown += `- W3PK signing/transaction methods\\n`;\n markdown += `- EIP-7702 authorization/delegation\\n\\n`;\n markdown += `---\\n\\n`;\n }\n\n // Add table of contents\n markdown += `## Table of Contents\\n\\n`;\n files.forEach(file => {\n markdown += `- [\\`${file.relativePath}\\`](#${file.relativePath.replace(/[^a-z0-9]/gi, '-').toLowerCase()})\\n`;\n });\n markdown += `\\n---\\n\\n`;\n\n // Add file contents\n markdown += `## Files\\n\\n`;\n\n for (const file of files) {\n const ext = path.extname(file.relativePath).slice(1);\n const language = ext === 'tsx' || ext === 'ts' ? 'typescript' :\n ext === 'jsx' || ext === 'js' ? 'javascript' :\n ext === 'json' ? 'json' : ext;\n\n markdown += `### \\`${file.relativePath}\\`\\n\\n`;\n markdown += '```' + language + '\\n';\n markdown += file.content;\n markdown += '\\n```\\n\\n';\n markdown += `---\\n\\n`;\n }\n\n // Calculate total size\n const totalSizeKB = files.reduce((sum, file) => {\n return sum + Buffer.byteLength(file.content, 'utf-8') / 1024;\n }, 0);\n\n return {\n markdown,\n includedFiles: files.map(f => f.relativePath),\n totalSizeKB: Math.round(totalSizeKB * 100) / 100\n };\n}\n\n/**\n * Inspects an application and returns a security report via Rukh API\n *\n * @param appPath - Path to the application to inspect\n * @param rukhUrl - The Rukh API endpoint URL\n * @param context - The context name to use (should have an instruction file with report format)\n * @param model - The AI model to use ('anthropic', 'mistral', or 'openai')\n * @param focusMode - Focus on 'transactions' or include 'all' files\n * @returns A markdown-formatted security report\n *\n * @example\n * ```typescript\n * const report = await inspect(\n * '../genji-passkey',\n * 'https://rukh.w3hc.org',\n * 'w3pk',\n * 'anthropic',\n * 'transactions'\n * );\n *\n * console.log(report);\n * // Outputs:\n * // # Genji Passkey Report\n * // ## Available Methods\n * // ### Method #1: Sign Message\n * // ...\n * ```\n */\nexport async function inspect(\n appPath: string,\n rukhUrl: string = 'https://rukh.w3hc.org',\n context: string = 'w3pk',\n model: 'anthropic' | 'mistral' | 'openai' = 'anthropic',\n focusMode: 'transactions' | 'all' = 'transactions'\n): Promise<string> {\n // Gather application code with focus mode\n const result = await gatherCode({ appPath, focusMode });\n\n console.log(`Inspected ${result.includedFiles.length} files (${result.totalSizeKB} KB) [Focus: ${focusMode}]`);\n\n // Default message that triggers the structured report\n const message = 'Analyze this application and provide a security report listing all transaction and signing methods.';\n\n // Prepare form data\n const formData = new FormData();\n formData.append('message', message);\n formData.append('model', model);\n formData.append('context', context);\n\n // Create a blob from the markdown content\n const blob = new Blob([result.markdown], { type: 'text/markdown' });\n formData.append('file', blob, 'app-code.md');\n\n // Send request to Rukh API\n const response = await fetch(`${rukhUrl}/ask`, {\n method: 'POST',\n body: formData\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Rukh API error: ${response.status} - ${error}`);\n }\n\n const data = await response.json();\n return data.response || data.message || JSON.stringify(data);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/inspect/node.ts"],"sourcesContent":["/**\n * Node.js Inspection Module\n *\n * Provides filesystem-based code inspection capabilities for analyzing web3 applications\n * from the server-side or during development. Scans application source code and generates\n * security reports via the Rukh API.\n *\n * @module inspect/node\n */\n\nimport { promises as fs } from 'fs';\nimport path from 'path';\n\n/**\n * Configuration options for Node.js-based application inspection\n */\nexport interface InspectOptions {\n /**\n * The root directory of the application to inspect\n * @default process.cwd()\n */\n appPath?: string;\n\n /**\n * File patterns to include (glob patterns)\n * @default ['**\\/*.ts', '**\\/*.tsx', '**\\/*.js', '**\\/*.jsx', '**\\/*.json']\n */\n includePatterns?: string[];\n\n /**\n * Directories to exclude from inspection\n * @default ['node_modules', 'dist', '.next', '.git', 'build', 'coverage']\n */\n excludeDirs?: string[];\n\n /**\n * Maximum file size in KB to include\n * @default 500\n */\n maxFileSizeKB?: number;\n\n /**\n * Maximum total size in KB for all collected code (prevents exceeding API token limits)\n * @default 100\n */\n maxTotalSizeKB?: number;\n\n /**\n * Focus mode: filters files to only include those relevant to specific features\n * - 'transactions': Only files with signing, transactions, or blockchain operations\n * - 'all': Include all files (default)\n * @default 'all'\n */\n focusMode?: 'transactions' | 'all';\n}\n\n/**\n * Result of the code gathering operation\n */\nexport interface InspectResult {\n /**\n * The generated markdown content with all collected code\n */\n markdown: string;\n\n /**\n * List of files that were included\n */\n includedFiles: string[];\n\n /**\n * Total size in KB of all included files\n */\n totalSizeKB: number;\n}\n\n/**\n * Checks if file content is relevant to transactions/signing operations\n *\n * Scans file content and filename for keywords related to blockchain transactions,\n * message signing, smart contracts, and web3 operations.\n *\n * @param content - The file content to analyze\n * @param filename - The name of the file being checked\n * @returns True if the file contains transaction-related code\n * @internal\n */\nfunction isTransactionRelevant(content: string, filename: string): boolean {\n const transactionKeywords = [\n // Signing operations\n 'signMessage',\n 'signTypedData',\n 'sign(',\n 'signature',\n 'personalSign',\n 'eth_sign',\n 'sendTransaction',\n\n // Transaction operations\n 'transaction',\n 'sendTx',\n 'executeTx',\n 'Contract.connect',\n 'signer.send',\n 'wallet.send',\n 'provider.send',\n 'eth_sendTransaction',\n 'eth_sendRawTransaction',\n\n // Blockchain interaction\n 'Contract(',\n 'ethers.Contract',\n 'new Contract',\n 'ContractFactory',\n 'deployContract',\n\n // W3PK specific\n 'w3pk.sign',\n 'w3pk.send',\n 'Web3Passkey',\n 'useW3PK',\n\n // Smart contract calls\n 'call(',\n 'estimateGas',\n 'gasLimit',\n 'gasPrice',\n 'maxFeePerGas',\n 'maxPriorityFeePerGas',\n\n // Web3 providers\n 'JsonRpcProvider',\n 'Web3Provider',\n 'BrowserProvider',\n\n // EIP-7702\n 'EIP7702',\n 'authorization',\n 'delegation',\n ];\n\n const lowerContent = content.toLowerCase();\n const lowerFilename = filename.toLowerCase();\n\n // Check if filename suggests transaction relevance\n if (\n lowerFilename.includes('transaction') ||\n lowerFilename.includes('sign') ||\n lowerFilename.includes('wallet') ||\n lowerFilename.includes('contract') ||\n lowerFilename.includes('blockchain') ||\n lowerFilename.includes('web3') ||\n lowerFilename.includes('ethers')\n ) {\n return true;\n }\n\n // Check content for transaction-related keywords\n return transactionKeywords.some(keyword =>\n lowerContent.includes(keyword.toLowerCase())\n );\n}\n\n/**\n * Recursively walks a directory tree and collects matching files\n *\n * Traverses the directory structure, applying filters based on file patterns,\n * size limits, and focus mode settings.\n *\n * @param dir - The directory path to walk\n * @param options - Inspection options with filters and limits\n * @param rootDir - The root directory for calculating relative paths\n * @param cumulativeSizeKB - Tracks total size collected so far\n * @returns Array of file objects with path, content, and relative path\n * @internal\n */\nasync function walkDirectory(\n dir: string,\n options: Required<InspectOptions>,\n rootDir: string,\n cumulativeSizeKB: { value: number } = { value: 0 }\n): Promise<{ path: string; content: string; relativePath: string }[]> {\n const files: { path: string; content: string; relativePath: string }[] = [];\n\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n // Check if we've reached the total size limit\n if (cumulativeSizeKB.value >= options.maxTotalSizeKB) {\n console.warn(`Reached total size limit of ${options.maxTotalSizeKB} KB - stopping collection`);\n break;\n }\n\n const fullPath = path.join(dir, entry.name);\n const relativePath = path.relative(rootDir, fullPath);\n\n if (entry.isDirectory()) {\n // Skip excluded directories\n if (options.excludeDirs.includes(entry.name)) {\n continue;\n }\n\n // Recursively walk subdirectory\n const subFiles = await walkDirectory(fullPath, options, rootDir, cumulativeSizeKB);\n files.push(...subFiles);\n } else if (entry.isFile()) {\n // Check if file matches include patterns\n const ext = path.extname(entry.name);\n const shouldInclude = options.includePatterns.some(pattern => {\n // Simple pattern matching for common extensions\n if (pattern.includes('*')) {\n const patternExt = pattern.split('.').pop();\n return ext === `.${patternExt}`;\n }\n return entry.name === pattern;\n });\n\n if (!shouldInclude) {\n continue;\n }\n\n // Check file size\n const stats = await fs.stat(fullPath);\n const fileSizeKB = stats.size / 1024;\n\n if (fileSizeKB > options.maxFileSizeKB) {\n console.warn(`Skipping ${relativePath} (${fileSizeKB.toFixed(2)} KB exceeds limit)`);\n continue;\n }\n\n // Read file content\n const content = await fs.readFile(fullPath, 'utf-8');\n\n // Apply focus mode filter if enabled\n if (options.focusMode === 'transactions') {\n if (!isTransactionRelevant(content, entry.name)) {\n continue;\n }\n }\n\n // Calculate content size\n const contentSizeKB = Buffer.byteLength(content, 'utf-8') / 1024;\n\n // Check if adding this file would exceed the total limit\n if (cumulativeSizeKB.value + contentSizeKB > options.maxTotalSizeKB) {\n console.warn(`Skipping ${relativePath} - would exceed total size limit (${(cumulativeSizeKB.value + contentSizeKB).toFixed(2)} KB > ${options.maxTotalSizeKB} KB)`);\n break;\n }\n\n // Add file and update cumulative size\n cumulativeSizeKB.value += contentSizeKB;\n files.push({ path: fullPath, content, relativePath });\n }\n }\n } catch (error) {\n console.error(`Error reading directory ${dir}:`, error);\n }\n\n return files;\n}\n\n/**\n * Gathers application code and generates a markdown document\n *\n * @param options - Configuration options for the inspection\n * @returns An object containing the markdown content and metadata\n *\n * @example\n * ```typescript\n * const result = await gatherCode({\n * appPath: '../genji-passkey',\n * includePatterns: ['**\\/*.ts', '**\\/*.tsx']\n * });\n *\n * console.log(`Collected ${result.includedFiles.length} files`);\n * console.log(`Total size: ${result.totalSizeKB} KB`);\n * ```\n */\nexport async function gatherCode(options: InspectOptions = {}): Promise<InspectResult> {\n const opts: Required<InspectOptions> = {\n appPath: options.appPath || process.cwd(),\n includePatterns: options.includePatterns || [\n '**/*.ts',\n '**/*.tsx',\n '**/*.js',\n '**/*.jsx',\n '**/*.json'\n ],\n excludeDirs: options.excludeDirs || [\n 'node_modules',\n 'dist',\n '.next',\n '.git',\n 'build',\n 'coverage',\n '.cache',\n 'out',\n '.vercel',\n '.turbo'\n ],\n maxFileSizeKB: options.maxFileSizeKB || 500,\n maxTotalSizeKB: options.maxTotalSizeKB || 100,\n focusMode: options.focusMode || 'all'\n };\n\n // Resolve app path\n const appPath = path.resolve(opts.appPath);\n\n // Check if app path exists\n try {\n await fs.access(appPath);\n } catch (error) {\n throw new Error(`App path does not exist: ${appPath}`);\n }\n\n // Walk directory and collect files\n const files = await walkDirectory(appPath, opts, appPath);\n\n // Generate markdown\n let markdown = `# Application Code Inspection\\n\\n`;\n markdown += `**Inspected Path:** \\`${appPath}\\`\\n\\n`;\n markdown += `**Files Collected:** ${files.length}\\n\\n`;\n markdown += `**Focus Mode:** ${opts.focusMode}\\n\\n`;\n markdown += `**Timestamp:** ${new Date().toISOString()}\\n\\n`;\n markdown += `---\\n\\n`;\n\n if (opts.focusMode === 'transactions') {\n markdown += `## Analysis Focus\\n\\n`;\n markdown += `This inspection is focused on **transaction and signing operations**. Only files containing:\\n\\n`;\n markdown += `- Message signing (signMessage, signTypedData, etc.)\\n`;\n markdown += `- Transaction sending (sendTransaction, eth_sendTransaction, etc.)\\n`;\n markdown += `- Smart contract interactions (Contract calls, deployments)\\n`;\n markdown += `- Blockchain providers and wallets\\n`;\n markdown += `- W3PK signing/transaction methods\\n`;\n markdown += `- EIP-7702 authorization/delegation\\n\\n`;\n markdown += `---\\n\\n`;\n }\n\n // Add table of contents\n markdown += `## Table of Contents\\n\\n`;\n files.forEach(file => {\n markdown += `- [\\`${file.relativePath}\\`](#${file.relativePath.replace(/[^a-z0-9]/gi, '-').toLowerCase()})\\n`;\n });\n markdown += `\\n---\\n\\n`;\n\n // Add file contents\n markdown += `## Files\\n\\n`;\n\n for (const file of files) {\n const ext = path.extname(file.relativePath).slice(1);\n const language = ext === 'tsx' || ext === 'ts' ? 'typescript' :\n ext === 'jsx' || ext === 'js' ? 'javascript' :\n ext === 'json' ? 'json' : ext;\n\n markdown += `### \\`${file.relativePath}\\`\\n\\n`;\n markdown += '```' + language + '\\n';\n markdown += file.content;\n markdown += '\\n```\\n\\n';\n markdown += `---\\n\\n`;\n }\n\n // Calculate total size\n const totalSizeKB = files.reduce((sum, file) => {\n return sum + Buffer.byteLength(file.content, 'utf-8') / 1024;\n }, 0);\n\n return {\n markdown,\n includedFiles: files.map(f => f.relativePath),\n totalSizeKB: Math.round(totalSizeKB * 100) / 100\n };\n}\n\n/**\n * Inspects an application and returns a security report via Rukh API\n *\n * @param appPath - Path to the application to inspect\n * @param rukhUrl - The Rukh API endpoint URL\n * @param context - The context name to use (should have an instruction file with report format)\n * @param model - The AI model to use ('anthropic', 'mistral', or 'openai')\n * @param focusMode - Focus on 'transactions' or include 'all' files\n * @returns A markdown-formatted security report\n *\n * @example\n * ```typescript\n * const report = await inspect(\n * '../genji-passkey',\n * 'https://rukh.w3hc.org',\n * 'w3pk',\n * 'anthropic',\n * 'transactions'\n * );\n *\n * console.log(report);\n * // Outputs:\n * // # Genji Passkey Report\n * // ## Available Methods\n * // ### Method #1: Sign Message\n * // ...\n * ```\n */\nexport async function inspect(\n appPath: string,\n rukhUrl: string = 'https://rukh.w3hc.org',\n context: string = 'w3pk',\n model: 'anthropic' | 'mistral' | 'openai' = 'anthropic',\n focusMode: 'transactions' | 'all' = 'transactions'\n): Promise<string> {\n // Gather application code with focus mode\n const result = await gatherCode({ appPath, focusMode });\n\n console.log(`Inspected ${result.includedFiles.length} files (${result.totalSizeKB} KB) [Focus: ${focusMode}]`);\n\n // Default message that triggers the structured report\n const message = 'Analyze this application and provide a security report listing all transaction and signing methods.';\n\n // Prepare form data\n const formData = new FormData();\n formData.append('message', message);\n formData.append('model', model);\n formData.append('context', context);\n\n // Create a blob from the markdown content\n const blob = new Blob([result.markdown], { type: 'text/markdown' });\n formData.append('file', blob, 'app-code.md');\n\n // Send request to Rukh API\n const response = await fetch(`${rukhUrl}/ask`, {\n method: 'POST',\n body: formData\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Rukh API error: ${response.status} - ${error}`);\n }\n\n const data = await response.json();\n return data.response || data.message || JSON.stringify(data);\n}\n"],"mappings":"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","names":[]}
@@ -57,6 +57,8 @@ interface PersistentSessionConfig {
57
57
  }
58
58
  ```
59
59
 
60
+ **PRF requirement:** persistent sessions are encrypted under a key derived from the WebAuthn PRF extension, which the authenticator only releases during a user-verified assertion. On authenticators without PRF support, no persistent session is stored — the device falls back to in-memory sessions only (there is no weaker at-rest fallback). The session is re-keyed at every real (prompted) login; the configured `duration` is therefore also the renewal interval. With `requireReauth: true` the decryption key is never written to disk; with `requireReauth: false` it is stored as a non-extractable `CryptoKey` to allow silent restore. See [SECURITY.md](./SECURITY.md#at-rest-encryption-what-it-protects-and-what-it-doesnt) for the full model.
61
+
60
62
  **Example:**
61
63
 
62
64
  ```typescript
@@ -2847,13 +2849,12 @@ Enable persistent sessions to maintain user login across page refreshes.
2847
2849
  4. `requireReauth: true` prompts for biometric on refresh (more secure)
2848
2850
  5. `requireReauth: false` silently restores session (more convenient)
2849
2851
 
2850
- **Security:**
2851
- - Sessions only persist for STANDARD and YOLO modes
2852
- - STRICT mode sessions are NEVER persisted
2853
- - Encrypted at rest with WebAuthn-derived key
2854
- - Requires valid WebAuthn credential to decrypt
2855
- - Time-limited expiration
2856
- - Origin-isolated via IndexedDB
2852
+ **Security (read the trade-off):**
2853
+ - Sessions only persist for STANDARD and YOLO modes; STRICT mode is NEVER persisted
2854
+ - Time-limited expiration; origin-isolated via IndexedDB
2855
+ - ⚠️ The "WebAuthn-derived key" is derived from **public** credential metadata stored in the same browser profile, **not** from an authenticator-held secret. A persistent session is therefore **decryptable by anyone who can read this origin's storage** (malicious extension, XSS exfil, disk image), and it keeps the seed in that state for the whole duration it's enabled. This is a UX-for-security trade-off, not hardware-backed protection.
2856
+ - Enabling "Remember Me" does not add a new trust dependency (you already trust your origin's code), it **widens the window** in which a compromise of your origin can reach the seed. Keep `duration` short for higher-value wallets; don't enable on shared/untrusted devices.
2857
+ - Full threat model: **[SECURITY.md → At-Rest Encryption](./SECURITY.md#at-rest-encryption-what-it-protects-and-what-it-doesnt)**.
2857
2858
 
2858
2859
  **Example:**
2859
2860
 
@@ -3075,7 +3076,7 @@ Compute IPFS CIDv1 hash for the currently installed w3pk version from unpkg CDN.
3075
3076
  ```typescript
3076
3077
  const hash = await getCurrentBuildHash()
3077
3078
  console.log('Build hash:', hash)
3078
- // => bafybeiafdhdxz3c3nhxtrhe7zpxfco5dlywpvzzscl277hojn7zosmrob4
3079
+ // => bafybeicce26ztznqcdn6n6j6hhnoye37nb7nt7gtubelwk2ku4txbp7acy
3079
3080
  ```
3080
3081
 
3081
3082
  ---
@@ -3210,89 +3211,7 @@ console.log(encoded) // => SGVsbG8gV29ybGQ=
3210
3211
  ### Cryptographic Utilities
3211
3212
 
3212
3213
  ```typescript
3213
- import {
3214
- extractRS,
3215
- deriveEncryptionKeyFromWebAuthn,
3216
- generateSalt
3217
- } from 'w3pk'
3218
- ```
3219
-
3220
- #### `deriveEncryptionKeyFromWebAuthn(prfOutput: ArrayBuffer, salt: Uint8Array): Promise<CryptoKey>`
3221
-
3222
- **🔐 SECURE - WebAuthn PRF-based encryption**
3223
-
3224
- Derives an AES-256-GCM encryption key from WebAuthn PRF (Pseudo-Random Function) extension output.
3225
-
3226
- **Security:**
3227
- - Uses authenticator-held secrets via PRF extension (never exposed)
3228
- - Implements random unique salts (no precomputation attacks)
3229
- - PBKDF2 with 210,000 iterations (OWASP 2023 recommendation)
3230
- - Prevents offline decryption attacks
3231
-
3232
- **Parameters:**
3233
- - `prfOutput: ArrayBuffer` - 32-byte secret from WebAuthn PRF extension
3234
- - `salt: Uint8Array` - 32-byte random salt (generate with `generateSalt()`)
3235
-
3236
- **Returns:** `Promise<CryptoKey>` - AES-256-GCM key for encryption/decryption
3237
-
3238
- **Example:**
3239
- ```typescript
3240
- // 1. Enable PRF during WebAuthn registration
3241
- const credential = await navigator.credentials.create({
3242
- publicKey: {
3243
- challenge: new Uint8Array(32),
3244
- rp: { name: "Example" },
3245
- user: { id: userId, name: "user", displayName: "User" },
3246
- pubKeyCredParams: [{ alg: -7, type: "public-key" }],
3247
- extensions: {
3248
- prf: {} // Enable PRF extension
3249
- }
3250
- }
3251
- });
3252
-
3253
- // 2. Get PRF output during authentication
3254
- const assertion = await navigator.credentials.get({
3255
- publicKey: {
3256
- challenge: new Uint8Array(32),
3257
- extensions: {
3258
- prf: {
3259
- eval: {
3260
- first: new Uint8Array(32) // Salt for PRF
3261
- }
3262
- }
3263
- }
3264
- }
3265
- });
3266
-
3267
- const prfOutput = assertion.getClientExtensionResults().prf.results.first;
3268
-
3269
- // 3. Generate and store salt
3270
- const salt = generateSalt(); // Store this with your ciphertext
3271
-
3272
- // 4. Derive encryption key
3273
- const encryptionKey = await deriveEncryptionKeyFromWebAuthn(
3274
- prfOutput,
3275
- salt
3276
- );
3277
-
3278
- // 5. Encrypt sensitive data
3279
- const encrypted = await crypto.subtle.encrypt(
3280
- { name: "AES-GCM", iv: crypto.getRandomValues(new Uint8Array(12)) },
3281
- encryptionKey,
3282
- new TextEncoder().encode("secret data")
3283
- );
3284
- ```
3285
-
3286
- #### `generateSalt(): Uint8Array`
3287
-
3288
- Generates a cryptographically secure random 32-byte salt for encryption.
3289
-
3290
- **Returns:** `Uint8Array` - 32 random bytes
3291
-
3292
- **Example:**
3293
- ```typescript
3294
- const salt = generateSalt();
3295
- // Store salt alongside ciphertext for later decryption
3214
+ import { extractRS } from 'w3pk'
3296
3215
  ```
3297
3216
 
3298
3217
  #### `extractRS(derSignature: Uint8Array): { r: string; s: string }`
@@ -148,42 +148,33 @@ m/44'/60'/0'/0/2 ← Third address
148
148
 
149
149
  **Purpose**: Link WebAuthn authentication to wallet encryption
150
150
 
151
- **Implementation**: [src/wallet/crypto.ts](src/wallet/crypto.ts)
152
-
153
- ✅ **SECURITY**: Uses PRF-based encryption with authenticator-held secrets and random salts
151
+ **Implementation**: [src/wallet/crypto.ts:21-66](src/wallet/crypto.ts#L21-L66)
154
152
 
155
- **SECURE Implementation (PRF-based)**:
153
+ **Process**:
156
154
  ```typescript
157
155
  function deriveEncryptionKeyFromWebAuthn(
158
- prfOutput: ArrayBuffer, // From WebAuthn PRF extension
159
- salt: Uint8Array // Random 32-byte salt
156
+ credentialId: string,
157
+ publicKey: string
160
158
  ): CryptoKey {
161
- // Step 1: Validate inputs
162
- assert(prfOutput.byteLength === 32);
163
- assert(salt.byteLength === 32);
159
+ // Step 1: Combine inputs
160
+ const keyMaterial = `w3pk-v4:${credentialId}:${publicKey}`;
164
161
 
165
- // Step 2: Import PRF output (authenticator-held secret)
166
- const keyMaterial = importKey(prfOutput);
162
+ // Step 2: Hash with SHA-256
163
+ const hash = SHA256(keyMaterial);
167
164
 
168
- // Step 3: PBKDF2 key derivation with random salt
169
- const encryptionKey = PBKDF2(keyMaterial, salt, 210000);
165
+ // Step 3: PBKDF2 key derivation (210,000 iterations)
166
+ const encryptionKey = PBKDF2(hash, salt, 210000);
170
167
 
171
168
  // Result: AES-256-GCM key (NEVER STORED)
172
169
  return encryptionKey;
173
170
  }
174
171
  ```
175
172
 
176
- **Security Properties**:
177
- - **Authenticator Secret**: Uses PRF output from hardware (never exposed to application)
178
- - **Random Salts**: Each encryption uses unique 32-byte salt (prevents precomputation)
179
- - **PBKDF2**: 210,000 iterations (OWASP 2023 recommendation)
180
- - **AES-256-GCM**: Strong authenticated encryption
181
- - **Authentication-gated**: Requires biometric/PIN to trigger PRF
182
-
183
- **Implementation Note**:
184
- The SDK uses `deriveEncryptionKeyAuto()` as a helper that:
185
- 1. Uses PRF-based encryption when available (secure)
186
- 2. Falls back to v2 implementation for existing wallets without PRF support
173
+ **Properties**:
174
+ - **Deterministic**: Same inputs Same output
175
+ - **Secure**: 210,000 PBKDF2 iterations (OWASP 2023)
176
+ - **Stateless**: No key storage required
177
+ - **Authentication-gated**: Requires biometric/PIN to access inputs
187
178
 
188
179
  ---
189
180
 
@@ -226,12 +217,11 @@ STEP 2: Create WebAuthn Credential (INDEPENDENT)
226
217
 
227
218
  STEP 3: Derive Encryption Key
228
219
  ┌──────────────────────────────────────┐
229
- deriveEncryptionKeyFromWebAuthn()
220
+ deriveEncryptionKeyFromWebAuthn()
230
221
  │ │
231
- Input: PRF output (32 bytes secret)
232
- │ + random salt (32 bytes) │
222
+ Input: credentialId + publicKey
233
223
  │ ↓ │
234
- Import PRF as key material
224
+ SHA-256 hash
235
225
  │ ↓ │
236
226
  │ PBKDF2 (210,000 iterations) │
237
227
  │ ↓ │
@@ -496,29 +486,43 @@ const signedTx = await wallet.signTransaction(tx);
496
486
  **Implementation**: [src/wallet/crypto.ts:21-66](src/wallet/crypto.ts#L21-L66)
497
487
 
498
488
  ```javascript
499
- // SECURE: PRF-based key derivation
500
489
  async function deriveEncryptionKeyFromWebAuthn(
501
- prfOutput: ArrayBuffer, // From WebAuthn PRF extension
502
- salt: Uint8Array // Random 32-byte salt
490
+ credentialId: string,
491
+ publicKey: string
503
492
  ): Promise<CryptoKey> {
504
- // 1. Import PRF output as key material (authenticator secret)
505
- const keyMaterial = await crypto.subtle.importKey(
493
+ // 1. Combine inputs into key material
494
+ const keyMaterial = `w3pk-v4:${credentialId}:${publicKey}`;
495
+
496
+ // 2. SHA-256 hash
497
+ const keyMaterialHash = await crypto.subtle.digest(
498
+ "SHA-256",
499
+ new TextEncoder().encode(keyMaterial)
500
+ );
501
+
502
+ // 3. Import as PBKDF2 base key
503
+ const importedKey = await crypto.subtle.importKey(
506
504
  "raw",
507
- prfOutput, // SECRET from authenticator
505
+ keyMaterialHash,
508
506
  { name: "PBKDF2" },
509
507
  false,
510
508
  ["deriveKey"]
511
509
  );
512
510
 
513
- // 2. Derive AES-256-GCM key with PBKDF2
511
+ // 4. Generate deterministic salt
512
+ const salt = await crypto.subtle.digest(
513
+ "SHA-256",
514
+ new TextEncoder().encode("w3pk-salt-v4")
515
+ );
516
+
517
+ // 5. Derive AES-256-GCM key
514
518
  return crypto.subtle.deriveKey(
515
519
  {
516
520
  name: "PBKDF2",
517
- salt: salt, // Random 32-byte salt (stored with ciphertext)
521
+ salt: new Uint8Array(salt),
518
522
  iterations: 210000, // OWASP 2023 recommendation
519
523
  hash: "SHA-256",
520
524
  },
521
- keyMaterial, // PRF output from authenticator
525
+ importedKey,
522
526
  { name: "AES-GCM", length: 256 },
523
527
  false,
524
528
  ["encrypt", "decrypt"]
@@ -526,12 +530,11 @@ async function deriveEncryptionKeyFromWebAuthn(
526
530
  }
527
531
  ```
528
532
 
529
- **Security Properties**:
530
- - **Authenticator-Bound**: PRF output is hardware-held secret (never exposed)
531
- - **Unique Salts**: Each encryption uses random salt (no precomputation)
532
- - **Strong KDF**: 210,000 PBKDF2 iterations make brute-force impractical (OWASP 2023)
533
+ **Properties**:
534
+ - **Deterministic**: Same inputs always produce same output
535
+ - **Secure**: 210,000 iterations make brute-force impractical
533
536
  - **Stateless**: No need to store the encryption key
534
- - **Fast**: ~100-200ms on modern devices
537
+ - **Fast enough**: ~100-200ms on modern devices
535
538
 
536
539
  ---
537
540
 
@@ -72,7 +72,7 @@ import { getCurrentBuildHash } from 'w3pk';
72
72
 
73
73
  const hash = await getCurrentBuildHash();
74
74
  console.log('Build hash:', hash);
75
- // => bafybeiafdhdxz3c3nhxtrhe7zpxfco5dlywpvzzscl277hojn7zosmrob4
75
+ // => bafybeicce26ztznqcdn6n6j6hhnoye37nb7nt7gtubelwk2ku4txbp7acy
76
76
  ```
77
77
 
78
78
  ### `getW3pkBuildHash(distUrl)`
@@ -108,7 +108,7 @@ Verifies if the current build matches an expected hash.
108
108
  ```typescript
109
109
  import { verifyBuildHash } from 'w3pk';
110
110
 
111
- const trustedHash = 'bafybeiafdhdxz3c3nhxtrhe7zpxfco5dlywpvzzscl277hojn7zosmrob4';
111
+ const trustedHash = 'bafybeicce26ztznqcdn6n6j6hhnoye37nb7nt7gtubelwk2ku4txbp7acy';
112
112
  const isValid = await verifyBuildHash(trustedHash);
113
113
 
114
114
  if (isValid) {
@@ -152,7 +152,7 @@ Output:
152
152
  📊 Total size: 273992 bytes
153
153
 
154
154
  🔐 IPFS Build Hash (CIDv1):
155
- bafybeiafdhdxz3c3nhxtrhe7zpxfco5dlywpvzzscl277hojn7zosmrob4
155
+ bafybeicce26ztznqcdn6n6j6hhnoye37nb7nt7gtubelwk2ku4txbp7acy
156
156
  bafyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
157
157
 
158
158
  📌 Version: x.x.x
@@ -1412,13 +1412,10 @@ const strictWallet = await w3pk.deriveWallet('STRICT')
1412
1412
  ```
1413
1413
 
1414
1414
  **Persistent Session Security:**
1415
- - STANDARD mode: Persistent sessions allowed
1416
- - YOLO mode: Persistent sessions ✅ allowed
1417
- - STRICT mode: Persistent sessions NEVER allowed
1418
- - Sessions encrypted with WebAuthn-derived keys
1419
- - Requires valid credential to decrypt
1420
- - Time-limited expiration
1421
- - Origin-isolated via IndexedDB
1415
+ - STANDARD / YOLO: persistent sessions allowed. STRICT: ❌ NEVER allowed.
1416
+ - Time-limited expiration; origin-isolated via IndexedDB.
1417
+ - ⚠️ The at-rest key is derived from **public** credential metadata in the same browser profile, not an authenticator secret — a persistent session is **decryptable by anyone who can read this origin's storage**, for the whole duration it's enabled. "Remember Me" is a UX-for-security trade-off that widens the exposure window; it does not add a new trust dependency beyond the origin-code integrity you already rely on. Keep the duration short for higher-value wallets and don't enable on shared devices.
1418
+ - Decide this deliberately against your threat model: **[SECURITY.md → At-Rest Encryption](./SECURITY.md#at-rest-encryption-what-it-protects-and-what-it-doesnt)**.
1422
1419
 
1423
1420
  ### Build Verification
1424
1421
 
@@ -1426,7 +1423,7 @@ const strictWallet = await w3pk.deriveWallet('STRICT')
1426
1423
  // ✅ Verify package integrity on app initialization
1427
1424
  import { verifyBuildHash } from 'w3pk'
1428
1425
 
1429
- const TRUSTED_HASH = 'bafybeiafdhdxz3c3nhxtrhe7zpxfco5dlywpvzzscl277hojn7zosmrob4'
1426
+ const TRUSTED_HASH = 'bafybeicce26ztznqcdn6n6j6hhnoye37nb7nt7gtubelwk2ku4txbp7acy'
1430
1427
 
1431
1428
  async function initializeApp() {
1432
1429
  const isValid = await verifyBuildHash(TRUSTED_HASH)
@@ -1486,7 +1483,7 @@ class WalletManager {
1486
1483
 
1487
1484
  async initialize() {
1488
1485
  // Verify package integrity
1489
- const TRUSTED_HASH = 'bafybeiafdhdxz3c3nhxtrhe7zpxfco5dlywpvzzscl277hojn7zosmrob4'
1486
+ const TRUSTED_HASH = 'bafybeicce26ztznqcdn6n6j6hhnoye37nb7nt7gtubelwk2ku4txbp7acy'
1490
1487
  const isValid = await verifyBuildHash(TRUSTED_HASH)
1491
1488
  if (!isValid) throw new Error('Package integrity check failed')
1492
1489
  }