[
  {
    "path": ".gitattributes",
    "content": "###############################################################################\n# Set default behavior to automatically normalize line endings.\n###############################################################################\n* text=auto\n\n###############################################################################\n# Set default behavior for command prompt diff.\n#\n# This is need for earlier builds of msysgit that does not have it on by\n# default for csharp files.\n# Note: This is only used by command line\n###############################################################################\n#*.cs     diff=csharp\n\n###############################################################################\n# Set the merge driver for project and solution files\n#\n# Merging from the command prompt will add diff markers to the files if there\n# are conflicts (Merging from VS is not affected by the settings below, in VS\n# the diff markers are never inserted). Diff markers may cause the following \n# file extensions to fail to load in VS. An alternative would be to treat\n# these files as binary and thus will always conflict and require user\n# intervention with every merge. To do so, just uncomment the entries below\n###############################################################################\n#*.sln       merge=binary\n#*.csproj    merge=binary\n#*.vbproj    merge=binary\n#*.vcxproj   merge=binary\n#*.vcproj    merge=binary\n#*.dbproj    merge=binary\n#*.fsproj    merge=binary\n#*.lsproj    merge=binary\n#*.wixproj   merge=binary\n#*.modelproj merge=binary\n#*.sqlproj   merge=binary\n#*.wwaproj   merge=binary\n\n###############################################################################\n# behavior for image files\n#\n# image files are treated as binary by default.\n###############################################################################\n#*.jpg   binary\n#*.png   binary\n#*.gif   binary\n\n###############################################################################\n# diff behavior for common document formats\n# \n# Convert binary document formats to text before diffing them. This feature\n# is only available from the command line. Turn it on by uncommenting the \n# entries below.\n###############################################################################\n#*.doc   diff=astextplain\n#*.DOC   diff=astextplain\n#*.docx  diff=astextplain\n#*.DOCX  diff=astextplain\n#*.dot   diff=astextplain\n#*.DOT   diff=astextplain\n#*.pdf   diff=astextplain\n#*.PDF   diff=astextplain\n#*.rtf   diff=astextplain\n#*.RTF   diff=astextplain\n"
  },
  {
    "path": ".github/workflows/claude-code-review.yml",
    "content": "name: Claude Code Review\n\non:\n  pull_request:\n    types: [opened, synchronize]\n    # Optional: Only run on specific file changes\n    # paths:\n    #   - \"src/**/*.ts\"\n    #   - \"src/**/*.tsx\"\n    #   - \"src/**/*.js\"\n    #   - \"src/**/*.jsx\"\n\njobs:\n  claude-review:\n    # Optional: Filter by PR author\n    # if: |\n    #   github.event.pull_request.user.login == 'external-contributor' ||\n    #   github.event.pull_request.user.login == 'new-developer' ||\n    #   github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'\n\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: read\n      issues: read\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: Run Claude Code Review\n        id: claude-review\n        uses: anthropics/claude-code-action@v1\n        with:\n          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n          prompt: |\n            REPO: ${{ github.repository }}\n            PR NUMBER: ${{ github.event.pull_request.number }}\n\n            Please review this pull request and provide feedback on:\n            - Code quality and best practices\n            - Potential bugs or issues\n            - Performance considerations\n            - Security concerns\n            - Test coverage\n\n            Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.\n\n            Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.\n\n          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md\n          # or https://code.claude.com/docs/en/cli-reference for available options\n          claude_args: '--allowed-tools \"Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)\"'\n\n"
  },
  {
    "path": ".github/workflows/claude.yml",
    "content": "name: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issues:\n    types: [opened, assigned]\n  pull_request_review:\n    types: [submitted]\n\njobs:\n  claude:\n    if: |\n      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||\n      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: read\n      issues: read\n      id-token: write\n      actions: read # Required for Claude to read CI results on PRs\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: Run Claude Code\n        id: claude\n        uses: anthropics/claude-code-action@v1\n        with:\n          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n\n          # This is an optional setting that allows Claude to read CI results on PRs\n          additional_permissions: |\n            actions: read\n\n          # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.\n          # prompt: 'Update the pull request description to include a summary of changes.'\n\n          # Optional: Add claude_args to customize behavior and configuration\n          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md\n          # or https://code.claude.com/docs/en/cli-reference for available options\n          # claude_args: '--allowed-tools Bash(gh pr:*)'\n\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Mono auto generated files\nmono_crash.*\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Oo]ut/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUnit\n*.VisualState.xml\nTestResult.xml\nnunit-*.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# ASP.NET Scaffolding\nScaffoldingReadMe.txt\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Coverlet is a free, cross platform Code Coverage Tool\ncoverage*.json\ncoverage*.xml\ncoverage*.info\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# NuGet Symbol Packages\n*.snupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n*.appxbundle\n*.appxupload\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- [Bb]ackup.rdl\n*- [Bb]ackup ([0-9]).rdl\n*- [Bb]ackup ([0-9][0-9]).rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\nMigrationBackup/\n\n# Ionide (cross platform F# VS Code tools) working folder\n.ionide/\n\n# Fody - auto-generated XML schema\nFodyWeavers.xsd\n\n# Claude Code\n.claude/\nCLAUDE.md\n\n# Environment files\n**/.env\n**/.env.*\n!**/.env.example\n\n# SQLite databases\n*.db\n*.sqlite\n*.sqlite3\n\n# Local config (users will copy from .example)\n**/appsettings.json\n!**/appsettings.example.json\n**/appsettings.Development.json\n**/appsettings.Production.json"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to UnsecuredAPIKeys Open Source.\n\n## [1.0.0] - 2025-12-09 - Lite Version Release\n\nThis release transforms the project from a full-featured web platform into a streamlined CLI tool for local use.\n\n### Why This Change?\n\nThe original open-source release included the full platform architecture (WebAPI, UI, PostgreSQL, 15+ providers). This created barriers for users who just wanted to:\n- Learn about API key security\n- Run simple searches locally\n- Understand how key discovery works\n\nThe lite version removes these barriers while the full platform remains available at [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com).\n\n---\n\n### Added\n\n#### New CLI Application (`UnsecuredAPIKeys.CLI/`)\n- **Menu-driven interface** using Spectre.Console for rich terminal UI\n- **ScraperService** - Searches GitHub for exposed API keys continuously\n- **VerifierService** - Maintains exactly 50 valid keys with automatic re-verification\n  - **Fallback validation**: Tries multiple providers when assigned provider fails\n  - **Auto-reclassification**: Updates key's ApiType if different provider validates it\n- **DatabaseService** - Handles SQLite initialization, statistics, and exports\n- **Constants.cs** - Centralized limits (`MAX_VALID_KEYS = 50`) and app info\n- **appsettings.example.json** - Configuration template for self-hosting\n\n#### Documentation\n- **CHANGELOG.md** - This file\n- **Badges** in README (GitHub Stars, .NET 10, License)\n- **Stars thank you** message for community support\n- **Self-hosting sections**: Database management, Search Queries, Rate Limiting, Troubleshooting\n- **Platform support** documented (Windows, macOS, Linux)\n\n#### Default Search Queries (Auto-seeded)\n- OpenAI: `sk-proj-`, `sk-or-v1-`, `OPENAI_API_KEY`, `openai.api_key`\n- Anthropic: `sk-ant-api`, `ANTHROPIC_API_KEY`, `anthropic_api_key`\n- Google: `AIzaSy`, `GOOGLE_API_KEY`, `gemini_api_key`\n\n---\n\n### Changed\n\n#### Database: PostgreSQL → SQLite\n- **Before**: Required PostgreSQL server, connection strings, migrations\n- **After**: Single `unsecuredapikeys.db` file, auto-created on first run\n- No migrations needed - uses `EnsureCreated()` for simplicity\n- Package change: `Npgsql.EntityFrameworkCore.PostgreSQL` → `Microsoft.EntityFrameworkCore.Sqlite`\n\n#### Providers: 15+ → 3\n- **Kept**: OpenAI, Anthropic, Google AI\n- **Removed**: Cohere, DeepSeek, ElevenLabs, Groq, HuggingFace, MistralAI, OpenRouter, PerplexityAI, Replicate, StabilityAI, TogetherAI\n\n#### Search Providers: 3 → 1\n- **Kept**: GitHub (via Octokit)\n- **Removed**: GitLab, SourceGraph\n\n#### Architecture: Web Platform → CLI Tool\n- **Before**: WebAPI + Next.js UI + Separate Bots + PostgreSQL\n- **After**: Single CLI application + SQLite\n\n#### Valid Key Limit\n- **Before**: Configurable/unlimited\n- **After**: Hard cap of 50 keys (enforced in `LiteLimits.MAX_VALID_KEYS`)\n\n---\n\n### Removed\n\n#### Projects Deleted\n| Project | Description |\n|---------|-------------|\n| `UnsecuredAPIKeys.WebAPI/` | REST API, SignalR hub, controllers |\n| `UnsecuredAPIKeys.UI/` | Next.js frontend, React components |\n| `UnsecuredAPIKeys.Bots.Scraper/` | Standalone scraper service |\n| `UnsecuredAPIKeys.Bots.Verifier/` | Standalone verifier service |\n| `UnsecuredAPIKeys.Common/` | Shared utilities (was empty) |\n\n#### AI Providers Removed (11)\n- CohereProvider.cs\n- DeepSeekProvider.cs\n- ElevenLabsProvider.cs\n- GroqProvider.cs\n- HuggingFaceProvider.cs\n- MistralAIProvider.cs\n- OpenRouterProvider.cs\n- PerplexityAIProvider.cs\n- ReplicateProvider.cs\n- StabilityAIProvider.cs\n- TogetherAIProvider.cs\n\n#### Search Providers Removed (2)\n- GitLabSearchProvider.cs\n- SourceGraphSearchProvider.cs\n\n#### Services Removed\n- GitHubIssueService.cs (auto issue creation)\n- SnitchLeaderboardService.cs (community leaderboard)\n- UserModerationService.cs (user bans)\n\n#### Database Models Removed (15)\n| Model | Purpose |\n|-------|---------|\n| DiscordUser | Discord OAuth integration |\n| UserSession | Session management |\n| UserBan | User moderation |\n| DonationTracking | PayPal donations |\n| DonationSupporter | Donor recognition |\n| IssueSubmissionTracking | GitHub issue automation |\n| IssueVerification | Issue verification flow |\n| SnitchLeaderboard | Community rankings |\n| VerificationBatch | Batch job tracking |\n| VerificationBatchResult | Batch results |\n| KeyInvalidation | Key lifecycle tracking |\n| KeyRotation | Key rotation events |\n| PatternEffectiveness | Search pattern analytics |\n| Proxy | Proxy rotation support |\n| RateLimitLog | API rate limit tracking |\n\n#### Database Migrations Removed (30+ files)\nAll PostgreSQL migrations deleted - SQLite uses runtime schema creation.\n\n#### Configuration Files Removed\n- `.dockerignore`\n- `.vite/` directories\n- `Deploy-VerificationBot.ps1`\n- `package.json`, `package-lock.json`\n- `analyze-unmatched-keys.ps1`\n- `check-unmatched-keys.ps1`\n- All `appsettings.json` files (kept `.example` versions)\n\n#### Features Not in Lite Version\n- Web UI dashboard\n- Real-time SignalR updates\n- Discord OAuth login\n- PayPal donation integration\n- GitHub issue auto-creation\n- Snitch leaderboard\n- User bans/moderation\n- Proxy rotation\n- Rate limit tracking tables\n- Multi-search-provider support\n- Batch verification locking\n\n---\n\n### Security\n\n- **Enforced .gitignore** rules for:\n  - `*.db`, `*.sqlite`, `*.sqlite3` (database files)\n  - `.env`, `.env.*` (environment files)\n  - `.claude/` (AI assistant files)\n  - `appsettings.json` (local config with tokens)\n- **Warning comments** added throughout code about not publishing results publicly\n- **Security Warning** section prominent in README\n\n---\n\n### Migration Guide\n\n#### For Users of the Old Version\n\nThe lite version is a **complete rewrite**. There is no migration path - it's designed for fresh local use.\n\nIf you need the original Web UI + WebAPI architecture:\n- Check the [`legacy_ui`](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/tree/legacy_ui) branch (no longer maintained)\n\nIf you need the full platform features:\n- Visit [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com)\n\n#### For Contributors\n\nOpen PRs against the old architecture are now outdated:\n- PR #5 (Docker Compose + Postgres) - No longer applicable\n- PR #8 (Next.js dependency bump) - UI removed\n- PR #9 (start.ps1) - Empty PR\n\nNew contributions should target the CLI architecture.\n\n---\n\n### Lite vs Full Comparison\n\n| Feature | Lite (This Repo) | Full Version |\n|---------|------------------|--------------|\n| Search Providers | GitHub | GitHub, GitLab, SourceGraph |\n| API Providers | 3 (OpenAI, Anthropic, Google) | 15+ |\n| Valid Key Cap | 50 | Higher limits |\n| Interface | CLI | Web UI + API |\n| Database | SQLite (local file) | PostgreSQL |\n| Real-time Updates | No | SignalR |\n| Community Features | No | Leaderboard, Discord |\n| Self-hosted | Yes | Yes (complex) |\n| Beginner Friendly | Yes | No |\n\n---\n\n### Technical Details\n\n#### Dependencies (CLI)\n```xml\n<PackageReference Include=\"Spectre.Console\" Version=\"0.49.1\" />\n<PackageReference Include=\"Microsoft.EntityFrameworkCore.Sqlite\" Version=\"9.0.0\" />\n<PackageReference Include=\"Octokit\" Version=\"13.0.1\" />\n```\n\n#### Rate Limits (Built-in)\n| Operation | Delay |\n|-----------|-------|\n| Between searches | 5,000ms |\n| Between verifications | 1,000ms |\n| Verification batch size | 10 keys |\n\n#### File Structure\n```\nUnsecuredAPIKeys-OpenSource/\n├── UnsecuredAPIKeys.CLI/\n│   ├── Program.cs              # Entry point, menu UI\n│   ├── Constants.cs            # LiteLimits, AppInfo\n│   ├── Services/\n│   │   ├── ScraperService.cs   # GitHub search\n│   │   ├── VerifierService.cs  # Key validation\n│   │   └── DatabaseService.cs  # SQLite ops\n│   └── appsettings.example.json\n├── UnsecuredAPIKeys.Data/\n│   ├── DBContext.cs            # EF Core context\n│   ├── Models/                 # 5 essential models\n│   └── Common/CommonEnums.cs   # Simplified enums\n├── UnsecuredAPIKeys.Providers/\n│   ├── AI Providers/           # 3 providers\n│   ├── Search Providers/       # GitHub only\n│   └── _Base/, _Interfaces/    # Framework\n├── CHANGELOG.md\n├── CLAUDE.md\n├── LICENSE\n└── README.md\n```\n\n---\n\n## [1.0.0] - 2024-07-21 - Initial Open Source Release\n\nInitial release of the full UnsecuredAPIKeys platform as open source.\n\n### Included\n- WebAPI with REST endpoints and SignalR\n- Next.js frontend with HeroUI components\n- PostgreSQL database with EF Core migrations\n- 15+ API key validation providers\n- 3 search providers (GitHub, GitLab, SourceGraph)\n- Discord OAuth integration\n- PayPal donation integration\n- Automated scraper and verifier bots\n- Snitch leaderboard\n- User moderation system\n\n---\n\n**Full Version**: [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com)\n"
  },
  {
    "path": "LICENSE",
    "content": "UnsecuredAPIKeys Open Source License\nBased on MIT License with Attribution Requirements\n\nCopyright (c) 2025 TSCarterJr\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\n1. The above copyright notice and this permission notice shall be included in all\n   copies or substantial portions of the Software.\n\n2. MANDATORY UI ATTRIBUTION REQUIREMENT:\n   Any software, application, or system that incorporates, uses, modifies, or \n   derives from ANY portion of this Software (including but not limited to APIs, \n   backend processes, algorithms, data structures, bots, validation logic, or any \n   other component) and provides a public-facing user interface MUST display a \n   prominent link to the original project repository: \n   https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource\n\n   This attribution must be:\n   - Clearly visible in the user interface\n   - Accessible from the main/home page or footer\n   - Contain the text \"Based on UnsecuredAPIKeys Open Source\" or similar\n   - Link directly to https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource\n   \n   This requirement applies regardless of the extent of modification, whether the\n   entire codebase is used or only small portions, and whether the derivative work\n   is commercial or non-commercial.\n\n3. SCOPE OF APPLICATION:\n   This attribution requirement applies to any use of the Software, including but\n   not limited to:\n   - Using the backend APIs or database schemas\n   - Implementing the validation algorithms or provider patterns\n   - Using the bot architecture or scraping logic  \n   - Incorporating the data models or business logic\n   - Using any code, concepts, or implementations from this project\n\n4. EDUCATIONAL PURPOSE AND USER RESPONSIBILITY:\n   This Software is provided for EDUCATIONAL and SECURITY RESEARCH purposes only.\n\n   By using this Software, you acknowledge and agree that:\n   - You are solely responsible for your use of this Software\n   - You will comply with all applicable laws and regulations\n   - You will not use this Software for unauthorized access to any systems\n   - You will not use discovered API keys for any malicious or illegal purposes\n   - You will practice responsible disclosure when discovering exposed credentials\n\n   The author(s) and copyright holder(s) expressly disclaim any responsibility for:\n   - How you choose to use this Software\n   - Any actions you take based on information obtained through this Software\n   - Any legal consequences arising from your use or misuse of this Software\n   - Any damages caused by unauthorized or unethical use of this Software\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\nUSE OF THIS SOFTWARE FOR ANY ILLEGAL, UNAUTHORIZED, OR UNETHICAL PURPOSES IS\nSTRICTLY PROHIBITED. THE USER ASSUMES ALL RISKS AND FULL RESPONSIBILITY FOR\nTHEIR ACTIONS.\n\nVIOLATION OF THE ATTRIBUTION REQUIREMENT CONSTITUTES COPYRIGHT INFRINGEMENT\nAND BREACH OF LICENSE TERMS, SUBJECT TO LEGAL ACTION AND DAMAGES.\n"
  },
  {
    "path": "README.md",
    "content": "# UnsecuredAPIKeys Lite\n\n[![GitHub Stars](https://img.shields.io/github/stars/TSCarterJr/UnsecuredAPIKeys-OpenSource?style=social)](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource)\n[![.NET 10](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/download/dotnet/10.0)\n[![License](https://img.shields.io/badge/License-Custom-blue)](LICENSE)\n![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/TSCarterJr/UnsecuredAPIKeys-OpenSource?utm_source=oss&utm_medium=github&utm_campaign=TSCarterJr%2FUnsecuredAPIKeys-OpenSource&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews)\n\n> **Thank you to everyone who has starred this project!** Your support helps raise awareness about API key security and encourages responsible disclosure practices.\n\n> **Full Version Available:** [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com)\n>\n> The full version offers: Web UI, all API providers, community features, and more.\n\nA command-line tool for discovering and validating exposed API keys on GitHub. This lite version focuses on educational and security awareness purposes.\n\n## Lite Version Limits\n\n| Feature | Lite (This Repo) | Full Version |\n|---------|------------------|--------------|\n| Search Provider | GitHub only | GitHub, GitLab, SourceGraph |\n| API Providers | OpenAI, Anthropic, Google | 15+ providers |\n| Valid Key Cap | 50 keys | Higher limits |\n| Interface | CLI | Web UI + API |\n| Database | SQLite (local) | PostgreSQL |\n\n## ⚠️ Educational Purpose Only\n\nThis tool is for **educational and security awareness purposes only**.\n\n- **Learn** how API keys get exposed in public repositories\n- **Understand** the importance of secret management\n- **Report** exposed keys responsibly to repository owners\n- **Never** use discovered keys for unauthorized access\n\n**Do NOT publish your database or results publicly.** This would expose working API keys to malicious actors.\n\n## Quick Start\n\n### 1. Download\n\nDownload the latest release for your platform from [**Releases**](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/releases):\n\n| Platform | File |\n|----------|------|\n| Windows | `unsecuredapikeys-win-x64.exe` |\n| Linux | `unsecuredapikeys-linux-x64` |\n\n**No .NET runtime required** - these are self-contained executables.\n\n### 2. Run\n\n**Windows:**\n```bash\n.\\unsecuredapikeys-win-x64.exe\n```\n\n**Linux:**\n```bash\nchmod +x unsecuredapikeys-linux-x64\n./unsecuredapikeys-linux-x64\n```\n\n### 3. Configure GitHub Token\n\nOn first run, go to **Configure Settings** > **Set GitHub Token**.\n\nCreate a token at: https://github.com/settings/tokens\nRequired scope: `public_repo`\n\n### 4. Start Searching\n\n- **Start Scraper**: Searches GitHub for exposed API keys (runs continuously)\n- **Start Verifier**: Maintains up to 50 valid keys (re-checks as needed)\n- **View Status**: Shows current statistics\n- **Export Keys**: Export to JSON or CSV\n\n### Building from Source (Optional)\n\nIf you prefer to build from source:\n\n```bash\ngit clone https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource.git\ncd UnsecuredAPIKeys-OpenSource\ndotnet build\ncd UnsecuredAPIKeys.CLI\ndotnet run\n```\n\n## How It Works\n\n### Scraper\n1. Uses your GitHub token to search for common API key patterns\n2. Extracts potential keys using regex patterns for OpenAI, Anthropic, and Google\n3. Stores discovered keys in a local SQLite database\n\n### Verifier\n1. Validates discovered keys against the actual provider APIs\n2. Maintains exactly 50 valid keys (lite limit)\n3. Re-checks existing valid keys periodically\n4. When a key becomes invalid, verifies new ones until back to 50\n\n## Project Structure\n\n```\nUnsecuredAPIKeys-OpenSource/\n├── UnsecuredAPIKeys.CLI/         # Main CLI application\n├── UnsecuredAPIKeys.Data/        # SQLite database layer\n├── UnsecuredAPIKeys.Providers/   # API validation providers\n├── unsecuredapikeys.db           # SQLite database (auto-created)\n└── README.md\n```\n\n## Prerequisites\n\n- **.NET 10 SDK** - [Download here](https://dotnet.microsoft.com/download/dotnet/10.0)\n- **GitHub Personal Access Token** - [Create here](https://github.com/settings/tokens)\n- **Platform**: Windows, macOS, or Linux\n\n## Supported Providers (Lite)\n\n| Provider | Pattern Examples |\n|----------|------------------|\n| OpenAI | `sk-proj-*`, `sk-or-v1-*` |\n| Anthropic | `sk-ant-api*` |\n| Google AI | `AIzaSy*` |\n\n## Configuration\n\nCopy `appsettings.example.json` to `appsettings.json` and configure:\n\n```json\n{\n  \"GitHub\": {\n    \"Token\": \"ghp_YOUR_TOKEN\"\n  },\n  \"Database\": {\n    \"Path\": \"unsecuredapikeys.db\"\n  }\n}\n```\n\nOr configure directly via the CLI menu.\n\n## Database\n\nThe SQLite database (`unsecuredapikeys.db`) is auto-created on first run in the working directory.\n\n| Action | How |\n|--------|-----|\n| **Location** | Same folder as the executable |\n| **Reset** | Delete `unsecuredapikeys.db` and restart |\n| **Backup** | Copy the `.db` file |\n| **View data** | Use any SQLite browser (e.g., DB Browser for SQLite) |\n\n## Search Queries\n\nOn first run, default search queries are automatically seeded:\n\n- `sk-proj-`, `sk-or-v1-`, `OPENAI_API_KEY` (OpenAI)\n- `sk-ant-api`, `ANTHROPIC_API_KEY` (Anthropic)\n- `AIzaSy`, `GOOGLE_API_KEY` (Google)\n\nThe scraper rotates through these queries automatically.\n\n## Rate Limiting\n\nBuilt-in delays prevent API abuse:\n\n| Operation | Delay |\n|-----------|-------|\n| Between searches | 5 seconds |\n| Between verifications | 1 second |\n| Batch size | 10 keys |\n\nGitHub's API allows ~30 searches/minute with authentication.\n\n## Troubleshooting\n\n| Issue | Solution |\n|-------|----------|\n| \"No GitHub token configured\" | Go to Configure Settings > Set GitHub Token |\n| \"Rate limit exceeded\" | Wait 60 seconds, or use a different token |\n| Build fails | Ensure .NET 10 SDK is installed: `dotnet --version` |\n| No keys found | Check your token has `public_repo` scope |\n| Database locked | Close other apps using the .db file |\n\n## Legal & Ethical Use\n\n- **Educational Purpose**: This tool demonstrates API security vulnerabilities\n- **Responsible Use**: Only use for legitimate security research\n- **No Abuse**: Do not use discovered keys for unauthorized access\n- **Compliance**: Follow all applicable laws and terms of service\n\n## License\n\nThis project uses a **custom attribution-required license** based on MIT.\n\n### Attribution Required\n\nAny use of this code requires visible attribution:\n- Display: \"Based on UnsecuredAPIKeys Open Source\"\n- Link to: https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource\n- Must be visible in UI/documentation\n\nSee [LICENSE](LICENSE) for full details.\n\n## Legacy UI Version\n\nLooking for the original Web UI + WebAPI architecture? Check the [`legacy_ui`](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/tree/legacy_ui) branch.\n\n> **Note**: The legacy branch is no longer actively maintained. For the full-featured web experience, use [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com).\n\n## Full Version\n\nFor higher limits, more providers, web interface, and community features:\n\n**[www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com)**\n\n---\n\n**Remember**: Use responsibly and in accordance with applicable laws.\n"
  },
  {
    "path": "UnsecuredAPIKeys-OpenSource.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.13.35931.197\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"UnsecuredAPIKeys.Data\", \"UnsecuredAPIKeys.Data\\UnsecuredAPIKeys.Data.csproj\", \"{F4BC4B91-7945-4171-BA6E-BECFEF771A21}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"UnsecuredAPIKeys.Providers\", \"UnsecuredAPIKeys.Providers\\UnsecuredAPIKeys.Providers.csproj\", \"{7389D703-A7C1-4853-A857-C1C97C1C6178}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"UnsecuredAPIKeys.CLI\", \"UnsecuredAPIKeys.CLI\\UnsecuredAPIKeys.CLI.csproj\", \"{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tDebug|x64 = Debug|x64\n\t\tDebug|x86 = Debug|x86\n\t\tRelease|Any CPU = Release|Any CPU\n\t\tRelease|x64 = Release|x64\n\t\tRelease|x86 = Release|x86\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x64.Build.0 = Release|Any CPU\n\t\t{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x86.Build.0 = Release|Any CPU\n\t\t{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x64.Build.0 = Release|Any CPU\n\t\t{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x86.Build.0 = Release|Any CPU\n\t\t{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x64.ActiveCfg = Debug|Any CPU\n\t\t{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x64.Build.0 = Debug|Any CPU\n\t\t{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x86.ActiveCfg = Debug|Any CPU\n\t\t{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x86.Build.0 = Debug|Any CPU\n\t\t{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x64.ActiveCfg = Release|Any CPU\n\t\t{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x64.Build.0 = Release|Any CPU\n\t\t{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x86.ActiveCfg = Release|Any CPU\n\t\t{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x86.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {FEDEE8B0-6BD4-48B2-BA35-31BFC542BC0D}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "UnsecuredAPIKeys.CLI/Constants.cs",
    "content": "namespace UnsecuredAPIKeys.CLI;\n\n/// <summary>\n/// Constants for the lite version of UnsecuredAPIKeys.\n/// Full version available at www.UnsecuredAPIKeys.com\n/// </summary>\npublic static class LiteLimits\n{\n    /// <summary>\n    /// Maximum valid keys for lite version.\n    ///\n    /// WARNING: If you modify this limit, do NOT publish your database\n    /// or results to a public repository. This would expose working API\n    /// keys to malicious actors who could abuse them.\n    ///\n    /// For higher limits, use www.UnsecuredAPIKeys.com\n    /// </summary>\n    public const int MAX_VALID_KEYS = 50;\n\n    /// <summary>\n    /// Delay between verification batches (milliseconds).\n    /// </summary>\n    public const int VERIFICATION_DELAY_MS = 1000;\n\n    /// <summary>\n    /// Delay between GitHub search queries (milliseconds).\n    /// </summary>\n    public const int SEARCH_DELAY_MS = 5000;\n\n    /// <summary>\n    /// Number of keys to process per verification batch.\n    /// </summary>\n    public const int VERIFICATION_BATCH_SIZE = 10;\n}\n\n/// <summary>\n/// Application-wide constants.\n/// </summary>\npublic static class AppInfo\n{\n    public const string Name = \"UnsecuredAPIKeys Lite\";\n    public const string Version = \"1.0.0\";\n    public const string FullVersionUrl = \"www.UnsecuredAPIKeys.com\";\n    public const string DatabaseName = \"unsecuredapikeys.db\";\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.CLI/Program.cs",
    "content": "using Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Spectre.Console;\nusing UnsecuredAPIKeys.CLI;\nusing UnsecuredAPIKeys.CLI.Services;\nusing UnsecuredAPIKeys.Data;\n\n// Initialize services\nvar services = new ServiceCollection();\nservices.AddLogging(builder => builder\n    .SetMinimumLevel(LogLevel.Warning)\n    .AddConsole());\nservices.AddHttpClient();\n\nawait using var serviceProvider = services.BuildServiceProvider();\nvar httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();\n\n// Initialize database\nvar dbService = new DatabaseService(AppInfo.DatabaseName);\nDBContext? dbContext = null;\n\ntry\n{\n    dbContext = await dbService.InitializeDatabaseAsync();\n}\ncatch (Exception ex)\n{\n    AnsiConsole.MarkupLine($\"[red]Failed to initialize database: {Markup.Escape(ex.Message)}[/]\");\n    return;\n}\n\n// Display banner\nDisplayBanner();\n\n// Main menu loop\nvar running = true;\nwhile (running)\n{\n    var choice = AnsiConsole.Prompt(\n        new SelectionPrompt<string>()\n            .Title(\"[yellow]What would you like to do?[/]\")\n            .PageSize(10)\n            .AddChoices(new[]\n            {\n                \"1. Start Scraper (search GitHub for keys)\",\n                \"2. Start Verifier (maintain valid keys)\",\n                \"3. View Status\",\n                \"4. Configure Settings\",\n                \"5. Export Keys\",\n                \"6. Exit\"\n            }));\n\n    AnsiConsole.WriteLine();\n\n    switch (choice[0])\n    {\n        case '1':\n            await RunScraperAsync(dbContext, httpClientFactory);\n            break;\n        case '2':\n            await RunVerifierAsync(dbContext, httpClientFactory);\n            break;\n        case '3':\n            await ShowStatusAsync(dbContext, dbService);\n            break;\n        case '4':\n            await ConfigureSettingsAsync(dbContext, dbService);\n            break;\n        case '5':\n            await ExportKeysAsync(dbContext, dbService);\n            break;\n        case '6':\n            running = false;\n            break;\n    }\n\n    if (running)\n    {\n        AnsiConsole.WriteLine();\n        AnsiConsole.MarkupLine(\"[dim]Press any key to continue...[/]\");\n        Console.ReadKey(true);\n        AnsiConsole.Clear();\n        DisplayBanner();\n    }\n}\n\nAnsiConsole.MarkupLine(\"[green]Goodbye![/]\");\ndbContext?.Dispose();\n\n// === Helper Methods ===\n\nvoid DisplayBanner()\n{\n    AnsiConsole.Write(\n        new FigletText(AppInfo.Name)\n            .LeftJustified()\n            .Color(Color.Cyan1));\n\n    AnsiConsole.Write(new Rule(\"[dim]Lite Version[/]\").RuleStyle(\"grey\").LeftJustified());\n    AnsiConsole.MarkupLine($\"[dim]Full version: [link]{Markup.Escape(AppInfo.FullVersionUrl)}[/][/]\");\n    AnsiConsole.MarkupLine($\"[dim]Valid key limit: [yellow]{LiteLimits.MAX_VALID_KEYS}[/][/]\");\n    AnsiConsole.WriteLine();\n\n    // Educational purpose notice\n    var warningPanel = new Panel(\n        \"[yellow]This tool is for EDUCATIONAL PURPOSES ONLY.[/]\\n\\n\" +\n        \"If you discover exposed API keys, please help secure them:\\n\" +\n        \"  [green]1.[/] Open an issue on the repository to notify the owner\\n\" +\n        \"  [green]2.[/] Never use keys for unauthorized access\\n\" +\n        \"  [green]3.[/] Do NOT publish your results publicly\\n\\n\" +\n        \"[dim]Help make the internet more secure by reporting, not exploiting.[/]\")\n        .Header(\"[yellow]Educational Use Only[/]\")\n        .Border(BoxBorder.Rounded)\n        .BorderColor(Color.Yellow);\n\n    AnsiConsole.Write(warningPanel);\n    AnsiConsole.WriteLine();\n}\n\nasync Task RunScraperAsync(DBContext db, IHttpClientFactory factory)\n{\n    AnsiConsole.Write(new Rule(\"[cyan]GitHub Scraper[/]\").RuleStyle(\"cyan\"));\n    AnsiConsole.MarkupLine(\"[dim]Searches GitHub for exposed API keys. Runs continuously.[/]\");\n    AnsiConsole.MarkupLine(\"[dim]Press [yellow]Ctrl+C[/] to stop.[/]\");\n    AnsiConsole.WriteLine();\n\n    using var cts = new CancellationTokenSource();\n    Console.CancelKeyPress += (s, e) =>\n    {\n        e.Cancel = true;\n        cts.Cancel();\n        AnsiConsole.MarkupLine(\"\\n[yellow]Stopping scraper...[/]\");\n    };\n\n    var scraper = new ScraperService(db, factory);\n    await scraper.RunAsync(cts.Token);\n}\n\nasync Task RunVerifierAsync(DBContext db, IHttpClientFactory factory)\n{\n    AnsiConsole.Write(new Rule(\"[green]Key Verifier[/]\").RuleStyle(\"green\"));\n    AnsiConsole.MarkupLine($\"[dim]Maintains up to [yellow]{LiteLimits.MAX_VALID_KEYS}[/] valid keys.[/]\");\n    AnsiConsole.MarkupLine(\"[dim]Re-checks valid keys and verifies new ones as needed.[/]\");\n    AnsiConsole.MarkupLine(\"[dim]Press [yellow]Ctrl+C[/] to stop.[/]\");\n    AnsiConsole.WriteLine();\n\n    using var cts = new CancellationTokenSource();\n    Console.CancelKeyPress += (s, e) =>\n    {\n        e.Cancel = true;\n        cts.Cancel();\n        AnsiConsole.MarkupLine(\"\\n[yellow]Stopping verifier...[/]\");\n    };\n\n    var verifier = new VerifierService(db, factory);\n    await verifier.RunAsync(cts.Token);\n}\n\nasync Task ShowStatusAsync(DBContext db, DatabaseService dbService)\n{\n    AnsiConsole.Write(new Rule(\"[blue]Current Status[/]\").RuleStyle(\"blue\"));\n\n    var stats = await AnsiConsole.Status()\n        .Spinner(Spinner.Known.Dots)\n        .SpinnerStyle(Style.Parse(\"blue\"))\n        .StartAsync(\"Loading statistics...\", async ctx =>\n        {\n            return await dbService.GetStatisticsAsync(db);\n        });\n\n    // Create status table\n    var table = new Table()\n        .Border(TableBorder.Rounded)\n        .BorderColor(Color.Grey)\n        .AddColumn(new TableColumn(\"[bold]Metric[/]\").LeftAligned())\n        .AddColumn(new TableColumn(\"[bold]Value[/]\").RightAligned());\n\n    table.AddRow(\"Total Keys Found\", stats.TotalKeys.ToString());\n    table.AddRow(\"Valid Keys\", $\"[green]{stats.ValidKeys}[/] / [yellow]{LiteLimits.MAX_VALID_KEYS}[/]\");\n    table.AddRow(\"Valid (No Credits)\", $\"[yellow]{stats.ValidNoCreditsKeys}[/]\");\n    table.AddRow(\"Invalid Keys\", $\"[red]{stats.InvalidKeys}[/]\");\n    table.AddRow(\"Pending Verification\", $\"[blue]{stats.UnverifiedKeys}[/]\");\n    table.AddRow(new Rule().RuleStyle(\"dim\"));\n    table.AddRow(\"OpenAI Keys\", stats.OpenAIKeys.ToString());\n    table.AddRow(\"Anthropic Keys\", stats.AnthropicKeys.ToString());\n    table.AddRow(\"Google Keys\", stats.GoogleKeys.ToString());\n    table.AddRow(new Rule().RuleStyle(\"dim\"));\n    table.AddRow(\"Database\", $\"[dim]{Markup.Escape(AppInfo.DatabaseName)}[/]\");\n    table.AddRow(\"GitHub Token\", stats.HasGitHubToken ? \"[green]Configured[/]\" : \"[red]Not configured[/]\");\n\n    AnsiConsole.Write(table);\n}\n\nasync Task ConfigureSettingsAsync(DBContext db, DatabaseService dbService)\n{\n    AnsiConsole.Write(new Rule(\"[magenta]Configuration[/]\").RuleStyle(\"magenta\"));\n\n    var configChoice = AnsiConsole.Prompt(\n        new SelectionPrompt<string>()\n            .Title(\"[yellow]What would you like to configure?[/]\")\n            .AddChoices(new[]\n            {\n                \"1. Set GitHub Token\",\n                \"2. View Current Settings\",\n                \"3. Reset Database\",\n                \"4. Back to Main Menu\"\n            }));\n\n    switch (configChoice[0])\n    {\n        case '1':\n            await SetGitHubTokenAsync(db, dbService);\n            break;\n        case '2':\n            await ShowCurrentSettingsAsync(db, dbService);\n            break;\n        case '3':\n            await ResetDatabaseAsync(dbService);\n            break;\n    }\n}\n\nasync Task SetGitHubTokenAsync(DBContext db, DatabaseService dbService)\n{\n    AnsiConsole.MarkupLine(\"[dim]Enter your GitHub Personal Access Token.[/]\");\n    AnsiConsole.MarkupLine(\"[dim]Create one at: https:[[//]]github.com[[/]]settings[[/]]tokens[/]\");\n    AnsiConsole.MarkupLine(\"[dim]Required scopes: [yellow]public_repo[/] (for searching public repos)[/]\");\n    AnsiConsole.WriteLine();\n\n    var token = AnsiConsole.Prompt(\n        new TextPrompt<string>(\"[green]GitHub Token:[/]\")\n            .Secret());\n\n    if (string.IsNullOrWhiteSpace(token))\n    {\n        AnsiConsole.MarkupLine(\"[red]Token cannot be empty.[/]\");\n        return;\n    }\n\n    // Validate token format\n    if (!token.StartsWith(\"ghp_\") && !token.StartsWith(\"github_pat_\"))\n    {\n        var proceed = AnsiConsole.Confirm(\n            \"[yellow]Token doesn't match expected GitHub token format. Save anyway?[/]\",\n            false);\n\n        if (!proceed) return;\n    }\n\n    await dbService.SaveGitHubTokenAsync(db, token);\n    AnsiConsole.MarkupLine(\"[green]GitHub token saved successfully![/]\");\n}\n\nasync Task ShowCurrentSettingsAsync(DBContext db, DatabaseService dbService)\n{\n    var stats = await dbService.GetStatisticsAsync(db);\n\n    var table = new Table()\n        .Border(TableBorder.Rounded)\n        .BorderColor(Color.Grey)\n        .AddColumn(\"[bold]Setting[/]\")\n        .AddColumn(\"[bold]Value[/]\");\n\n    var dbPath = Path.Combine(Environment.CurrentDirectory, AppInfo.DatabaseName);\n    table.AddRow(\"Database Path\", Markup.Escape(dbPath));\n    table.AddRow(\"GitHub Token\", stats.HasGitHubToken ? \"[green]Configured[/]\" : \"[red]Not configured[/]\");\n    table.AddRow(\"Max Valid Keys\", LiteLimits.MAX_VALID_KEYS.ToString());\n    table.AddRow(\"Supported Providers\", \"OpenAI, Anthropic, Google\");\n\n    AnsiConsole.Write(table);\n}\n\nasync Task ResetDatabaseAsync(DatabaseService dbService)\n{\n    var confirm = AnsiConsole.Confirm(\n        \"[red]Are you sure you want to reset the database? All data will be lost![/]\",\n        false);\n\n    if (!confirm)\n    {\n        AnsiConsole.MarkupLine(\"[dim]Database reset cancelled.[/]\");\n        return;\n    }\n\n    var doubleConfirm = AnsiConsole.Confirm(\n        \"[red]This action is irreversible. Are you absolutely sure?[/]\",\n        false);\n\n    if (!doubleConfirm)\n    {\n        AnsiConsole.MarkupLine(\"[dim]Database reset cancelled.[/]\");\n        return;\n    }\n\n    await AnsiConsole.Status()\n        .Spinner(Spinner.Known.Dots)\n        .SpinnerStyle(Style.Parse(\"red\"))\n        .StartAsync(\"Resetting database...\", async ctx =>\n        {\n            await dbService.ResetDatabaseAsync();\n        });\n\n    AnsiConsole.MarkupLine(\"[green]Database reset complete.[/]\");\n}\n\nasync Task ExportKeysAsync(DBContext db, DatabaseService dbService)\n{\n    AnsiConsole.Write(new Rule(\"[yellow]Export Keys[/]\").RuleStyle(\"yellow\"));\n\n    var exportChoice = AnsiConsole.Prompt(\n        new SelectionPrompt<string>()\n            .Title(\"[yellow]Export format:[/]\")\n            .AddChoices(new[]\n            {\n                \"1. JSON\",\n                \"2. CSV\",\n                \"3. Back to Main Menu\"\n            }));\n\n    if (exportChoice[0] == '3') return;\n\n    var validOnly = AnsiConsole.Confirm(\"Export only valid keys?\", true);\n\n    var format = exportChoice[0] == '1' ? \"json\" : \"csv\";\n    var defaultFileName = exportChoice[0] == '1' ? \"keys.json\" : \"keys.csv\";\n    var fileName = AnsiConsole.Prompt(\n        new TextPrompt<string>(\"[green]Output file name:[/]\")\n            .DefaultValue(defaultFileName));\n\n    await AnsiConsole.Status()\n        .Spinner(Spinner.Known.Dots)\n        .SpinnerStyle(Style.Parse(\"yellow\"))\n        .StartAsync($\"Exporting to {Markup.Escape(fileName)}...\", async ctx =>\n        {\n            await dbService.ExportKeysAsync(db, fileName, validOnly, format);\n        });\n\n    AnsiConsole.MarkupLine($\"[green]Exported to [bold]{Markup.Escape(fileName)}[/][/]\");\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.CLI/Services/DatabaseService.cs",
    "content": "using Microsoft.EntityFrameworkCore;\nusing Spectre.Console;\nusing UnsecuredAPIKeys.Data;\nusing UnsecuredAPIKeys.Data.Common;\nusing UnsecuredAPIKeys.Data.Models;\n\nnamespace UnsecuredAPIKeys.CLI.Services;\n\n/// <summary>\n/// Service for database initialization and common operations.\n/// </summary>\npublic class DatabaseService(string dbPath = \"unsecuredapikeys.db\")\n{\n    public async Task<DBContext> InitializeDatabaseAsync()\n    {\n        var dbContext = new DBContext(dbPath);\n\n        // Ensure database is created\n        await dbContext.Database.EnsureCreatedAsync();\n\n        // Seed default data if needed\n        await SeedDefaultDataAsync(dbContext);\n\n        return dbContext;\n    }\n\n    private async Task SeedDefaultDataAsync(DBContext dbContext)\n    {\n        // Add default search queries if none exist\n        if (!await dbContext.SearchQueries.AnyAsync())\n        {\n            var defaultQueries = new[]\n            {\n                // OpenAI patterns\n                \"sk-proj-\",\n                \"sk-or-v1-\",\n                \"sk-\",\n                \"OPENAI_API_KEY\",\n                \"openai.api_key\",\n                \"chatgpt api key\",\n                \"gpt-4 api key\",\n\n                // Anthropic patterns\n                \"sk-ant-api\",\n                \"ANTHROPIC_API_KEY\",\n                \"anthropic_api_key\",\n                \"claude api key\",\n\n                // Google AI patterns\n                \"AIzaSy\",\n                \"GOOGLE_API_KEY\",\n                \"gemini_api_key\",\n\n                // Other AI providers (patterns only, validation limited to lite providers)\n                \"r8_\",           // Replicate\n                \"fw_\",           // Fireworks\n                \"hf_\",           // HuggingFace\n                \"AI_API_KEY\"     // Generic\n            };\n\n            foreach (var query in defaultQueries)\n            {\n                dbContext.SearchQueries.Add(new SearchQuery\n                {\n                    Query = query,\n                    IsEnabled = true,\n                    LastSearchUTC = DateTime.UtcNow.AddDays(-1)\n                });\n            }\n\n            await dbContext.SaveChangesAsync();\n            AnsiConsole.MarkupLine($\"[dim]Added {defaultQueries.Length} default search queries.[/]\");\n        }\n    }\n\n    public async Task<Statistics> GetStatisticsAsync(DBContext dbContext)\n    {\n        var stats = new Statistics\n        {\n            TotalKeys = await dbContext.APIKeys.CountAsync(),\n            ValidKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.Valid),\n            InvalidKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.Invalid),\n            UnverifiedKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.Unverified),\n            ValidNoCreditsKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.ValidNoCredits),\n            OpenAIKeys = await dbContext.APIKeys.CountAsync(k => k.ApiType == ApiTypeEnum.OpenAI),\n            AnthropicKeys = await dbContext.APIKeys.CountAsync(k => k.ApiType == ApiTypeEnum.AnthropicClaude),\n            GoogleKeys = await dbContext.APIKeys.CountAsync(k => k.ApiType == ApiTypeEnum.GoogleAI),\n            HasGitHubToken = await dbContext.SearchProviderTokens\n                .AnyAsync(t => t.IsEnabled && t.SearchProvider == SearchProviderEnum.GitHub)\n        };\n\n        return stats;\n    }\n\n    public async Task SaveGitHubTokenAsync(DBContext dbContext, string token)\n    {\n        var existing = await dbContext.SearchProviderTokens\n            .FirstOrDefaultAsync(t => t.SearchProvider == SearchProviderEnum.GitHub);\n\n        if (existing != null)\n        {\n            existing.Token = token;\n            existing.IsEnabled = true;\n        }\n        else\n        {\n            dbContext.SearchProviderTokens.Add(new SearchProviderToken\n            {\n                Token = token,\n                SearchProvider = SearchProviderEnum.GitHub,\n                IsEnabled = true\n            });\n        }\n\n        await dbContext.SaveChangesAsync();\n    }\n\n    public async Task ResetDatabaseAsync()\n    {\n        if (File.Exists(dbPath))\n        {\n            File.Delete(dbPath);\n        }\n\n        // Reinitialize\n        await InitializeDatabaseAsync();\n    }\n\n    public async Task ExportKeysAsync(DBContext dbContext, string filePath, bool validOnly, string format)\n    {\n        var query = dbContext.APIKeys.AsQueryable();\n\n        if (validOnly)\n        {\n            query = query.Where(k => k.Status == ApiStatusEnum.Valid || k.Status == ApiStatusEnum.ValidNoCredits);\n        }\n\n        var keys = await query\n            .Include(k => k.References)\n            .ToListAsync();\n\n        if (format.ToLower() == \"json\")\n        {\n            await ExportAsJsonAsync(keys, filePath);\n        }\n        else\n        {\n            await ExportAsCsvAsync(keys, filePath);\n        }\n    }\n\n    private async Task ExportAsJsonAsync(List<APIKey> keys, string filePath)\n    {\n        var exportData = keys.Select(k => new\n        {\n            k.Id,\n            k.ApiKey,\n            Type = k.ApiType.ToString(),\n            Status = k.Status.ToString(),\n            k.FirstFoundUTC,\n            k.LastCheckedUTC,\n            Sources = k.References.Select(r => new\n            {\n                r.RepoURL,\n                r.RepoOwner,\n                r.RepoName,\n                r.FilePath,\n                r.FoundUTC\n            })\n        });\n\n        var json = System.Text.Json.JsonSerializer.Serialize(exportData, new System.Text.Json.JsonSerializerOptions\n        {\n            WriteIndented = true\n        });\n\n        await File.WriteAllTextAsync(filePath, json);\n    }\n\n    private async Task ExportAsCsvAsync(List<APIKey> keys, string filePath)\n    {\n        var lines = new List<string>\n        {\n            \"Id,ApiKey,Type,Status,FirstFoundUTC,LastCheckedUTC,RepoURL\"\n        };\n\n        foreach (var key in keys)\n        {\n            var repoUrl = key.References.FirstOrDefault()?.RepoURL ?? \"\";\n            lines.Add($\"{key.Id},\\\"{key.ApiKey}\\\",{key.ApiType},{key.Status},{key.FirstFoundUTC:O},{key.LastCheckedUTC:O},\\\"{repoUrl}\\\"\");\n        }\n\n        await File.WriteAllLinesAsync(filePath, lines);\n    }\n}\n\npublic class Statistics\n{\n    public int TotalKeys { get; set; }\n    public int ValidKeys { get; set; }\n    public int InvalidKeys { get; set; }\n    public int UnverifiedKeys { get; set; }\n    public int ValidNoCreditsKeys { get; set; }\n    public int OpenAIKeys { get; set; }\n    public int AnthropicKeys { get; set; }\n    public int GoogleKeys { get; set; }\n    public bool HasGitHubToken { get; set; }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.CLI/Services/ScraperService.cs",
    "content": "using Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.Logging;\nusing Spectre.Console;\nusing UnsecuredAPIKeys.Data;\nusing UnsecuredAPIKeys.Data.Common;\nusing UnsecuredAPIKeys.Data.Models;\nusing UnsecuredAPIKeys.Providers;\nusing UnsecuredAPIKeys.Providers._Interfaces;\nusing UnsecuredAPIKeys.Providers.Search_Providers;\n\nnamespace UnsecuredAPIKeys.CLI.Services;\n\n/// <summary>\n/// Scraper service for finding API keys on GitHub.\n/// Lite version: GitHub only, 3 AI providers.\n/// Full version: www.UnsecuredAPIKeys.com\n/// </summary>\npublic class ScraperService\n{\n    private readonly DBContext _dbContext;\n    private readonly IHttpClientFactory _httpClientFactory;\n    private readonly ILogger<ScraperService>? _logger;\n    private readonly IReadOnlyList<IApiKeyProvider> _providers;\n    private CancellationTokenSource? _cancellationTokenSource;\n\n    private int _newKeysFound;\n    private int _duplicateKeysFound;\n\n    public ScraperService(DBContext dbContext, IHttpClientFactory httpClientFactory, ILogger<ScraperService>? logger = null)\n    {\n        _dbContext = dbContext;\n        _httpClientFactory = httpClientFactory;\n        _logger = logger;\n        _providers = ApiProviderRegistry.ScraperProviders;\n    }\n\n    public async Task RunAsync(CancellationToken cancellationToken)\n    {\n        _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n\n        AnsiConsole.MarkupLine(\"[cyan]Starting GitHub scraper...[/]\");\n        AnsiConsole.MarkupLine($\"[dim]Loaded {_providers.Count} API key providers[/]\");\n\n        foreach (var provider in _providers)\n        {\n            AnsiConsole.MarkupLine($\"  [dim]- {Markup.Escape(provider.ProviderName)}[/]\");\n        }\n\n        // Get GitHub token\n        var token = await _dbContext.SearchProviderTokens\n            .Where(t => t.IsEnabled && t.SearchProvider == SearchProviderEnum.GitHub)\n            .FirstOrDefaultAsync(cancellationToken);\n\n        if (token == null)\n        {\n            AnsiConsole.MarkupLine(\"[red]No GitHub token configured. Use 'Configure Settings' to add one.[/]\");\n            return;\n        }\n\n        // Run continuously\n        while (!_cancellationTokenSource.Token.IsCancellationRequested)\n        {\n            try\n            {\n                await RunScrapingCycleAsync(token);\n\n                if (_cancellationTokenSource.Token.IsCancellationRequested)\n                    break;\n\n                // Wait before next cycle\n                AnsiConsole.MarkupLine($\"[dim]Waiting {LiteLimits.SEARCH_DELAY_MS / 1000}s before next search...[/]\");\n                await Task.Delay(LiteLimits.SEARCH_DELAY_MS, _cancellationTokenSource.Token);\n\n                // Reset counters\n                _newKeysFound = 0;\n                _duplicateKeysFound = 0;\n            }\n            catch (OperationCanceledException)\n            {\n                break;\n            }\n            catch (Exception ex)\n            {\n                AnsiConsole.MarkupLine($\"[red]Error during scraping: {Markup.Escape(ex.Message)}[/]\");\n                _logger?.LogError(ex, \"Scraping cycle error\");\n                await Task.Delay(5000, _cancellationTokenSource.Token);\n            }\n        }\n\n        AnsiConsole.MarkupLine(\"[green]Scraper stopped.[/]\");\n    }\n\n    private async Task RunScrapingCycleAsync(SearchProviderToken token)\n    {\n        // Get next query to process\n        var query = await _dbContext.SearchQueries\n            .Where(x => x.IsEnabled && x.LastSearchUTC < DateTime.UtcNow.AddHours(-1))\n            .OrderBy(x => x.LastSearchUTC)\n            .FirstOrDefaultAsync(_cancellationTokenSource!.Token);\n\n        if (query == null)\n        {\n            AnsiConsole.MarkupLine(\"[dim]No queries due for search. Waiting...[/]\");\n            return;\n        }\n\n        AnsiConsole.MarkupLine($\"[cyan]Searching: {Markup.Escape(query.Query)}[/]\");\n\n        // Update last search time\n        query.LastSearchUTC = DateTime.UtcNow;\n        await _dbContext.SaveChangesAsync(_cancellationTokenSource.Token);\n\n        // Search GitHub\n        var searchProvider = new GitHubSearchProvider(_dbContext);\n        IEnumerable<RepoReference>? results;\n\n        try\n        {\n            results = await searchProvider.SearchAsync(query, token);\n        }\n        catch (Exception ex)\n        {\n            AnsiConsole.MarkupLine($\"[red]Search error: {Markup.Escape(ex.Message)}[/]\");\n            return;\n        }\n\n        if (results == null)\n        {\n            AnsiConsole.MarkupLine(\"[yellow]No results from search.[/]\");\n            return;\n        }\n\n        var resultsList = results.ToList();\n        AnsiConsole.MarkupLine($\"[dim]Found {resultsList.Count} potential matches[/]\");\n\n        // Process each result\n        await AnsiConsole.Progress()\n            .StartAsync(async ctx =>\n            {\n                var task = ctx.AddTask($\"[cyan]Processing results[/]\", maxValue: resultsList.Count);\n\n                foreach (var repoRef in resultsList)\n                {\n                    if (_cancellationTokenSource!.Token.IsCancellationRequested)\n                        break;\n\n                    await ProcessResultAsync(repoRef, token, query);\n                    task.Increment(1);\n                }\n            });\n\n        // Summary\n        var table = new Table()\n            .Border(TableBorder.Rounded)\n            .AddColumn(\"[bold]Metric[/]\")\n            .AddColumn(\"[bold]Value[/]\");\n\n        table.AddRow(\"Query\", Markup.Escape(query.Query));\n        table.AddRow(\"Results Processed\", resultsList.Count.ToString());\n        table.AddRow(\"New Keys\", $\"[green]{_newKeysFound}[/]\");\n        table.AddRow(\"Duplicates\", $\"[dim]{_duplicateKeysFound}[/]\");\n\n        AnsiConsole.Write(table);\n    }\n\n    private async Task ProcessResultAsync(RepoReference repoRef, SearchProviderToken token, SearchQuery query)\n    {\n        try\n        {\n            // Get file content\n            var content = await FetchFileContentAsync(repoRef, token);\n            if (string.IsNullOrEmpty(content))\n                return;\n\n            // Search for API keys using all provider patterns\n            foreach (var provider in _providers)\n            {\n                foreach (var pattern in provider.RegexPatterns)\n                {\n                    var regex = new System.Text.RegularExpressions.Regex(pattern);\n                    var matches = regex.Matches(content);\n\n                    foreach (System.Text.RegularExpressions.Match match in matches)\n                    {\n                        var apiKey = match.Value;\n\n                        // Check if already exists\n                        var exists = await _dbContext.APIKeys\n                            .AnyAsync(k => k.ApiKey == apiKey, _cancellationTokenSource!.Token);\n\n                        if (exists)\n                        {\n                            Interlocked.Increment(ref _duplicateKeysFound);\n                            continue;\n                        }\n\n                        // Add new key\n                        var newKey = new APIKey\n                        {\n                            ApiKey = apiKey,\n                            ApiType = provider.ApiType,\n                            Status = ApiStatusEnum.Unverified,\n                            SearchProvider = SearchProviderEnum.GitHub,\n                            FirstFoundUTC = DateTime.UtcNow,\n                            LastFoundUTC = DateTime.UtcNow\n                        };\n\n                        // Add repo reference\n                        repoRef.SearchQueryId = query.Id;\n                        repoRef.FoundUTC = DateTime.UtcNow;\n                        repoRef.Provider = \"GitHub\";\n                        newKey.References.Add(repoRef);\n\n                        _dbContext.APIKeys.Add(newKey);\n                        await _dbContext.SaveChangesAsync(_cancellationTokenSource!.Token);\n\n                        Interlocked.Increment(ref _newKeysFound);\n                        AnsiConsole.MarkupLine($\"[green]+ New {Markup.Escape(provider.ProviderName)} key found![/]\");\n                    }\n                }\n            }\n        }\n        catch (Exception ex)\n        {\n            _logger?.LogWarning(ex, \"Error processing result: {Url}\", repoRef.FileURL);\n        }\n    }\n\n    private async Task<string?> FetchFileContentAsync(RepoReference repoRef, SearchProviderToken token)\n    {\n        try\n        {\n            using var client = _httpClientFactory.CreateClient();\n            client.DefaultRequestHeaders.UserAgent.ParseAdd(\"UnsecuredAPIKeys-Lite/1.0\");\n            client.DefaultRequestHeaders.Authorization =\n                new System.Net.Http.Headers.AuthenticationHeaderValue(\"Bearer\", token.Token);\n\n            // Build raw content URL from repo info\n            // Format: https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}\n            string? url = null;\n\n            if (!string.IsNullOrEmpty(repoRef.RepoOwner) &&\n                !string.IsNullOrEmpty(repoRef.RepoName) &&\n                !string.IsNullOrEmpty(repoRef.FilePath))\n            {\n                var branch = repoRef.Branch ?? \"main\";\n                url = $\"https://raw.githubusercontent.com/{repoRef.RepoOwner}/{repoRef.RepoName}/{branch}/{repoRef.FilePath}\";\n            }\n\n            if (string.IsNullOrEmpty(url))\n                return null;\n\n            var response = await client.GetAsync(url, _cancellationTokenSource!.Token);\n\n            // Try 'master' if 'main' fails\n            if (!response.IsSuccessStatusCode && repoRef.Branch == null)\n            {\n                url = $\"https://raw.githubusercontent.com/{repoRef.RepoOwner}/{repoRef.RepoName}/master/{repoRef.FilePath}\";\n                response = await client.GetAsync(url, _cancellationTokenSource!.Token);\n            }\n\n            if (!response.IsSuccessStatusCode)\n                return null;\n\n            return await response.Content.ReadAsStringAsync(_cancellationTokenSource.Token);\n        }\n        catch\n        {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.CLI/Services/VerifierService.cs",
    "content": "using Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.Logging;\nusing Spectre.Console;\nusing UnsecuredAPIKeys.Data;\nusing UnsecuredAPIKeys.Data.Common;\nusing UnsecuredAPIKeys.Data.Models;\nusing UnsecuredAPIKeys.Providers;\nusing UnsecuredAPIKeys.Providers._Interfaces;\n\nnamespace UnsecuredAPIKeys.CLI.Services;\n\n/// <summary>\n/// Verifier service that maintains up to 50 valid API keys.\n/// When a key becomes invalid, verifies new keys to maintain the limit.\n/// Lite version: 50 key cap.\n/// Full version: www.UnsecuredAPIKeys.com\n/// </summary>\npublic class VerifierService(\n    DBContext dbContext,\n    IHttpClientFactory httpClientFactory,\n    ILogger<VerifierService>? logger = null)\n{\n    private readonly IReadOnlyList<IApiKeyProvider> _providers = ApiProviderRegistry.VerifierProviders;\n    private CancellationTokenSource? _cancellationTokenSource;\n\n    private int _validCount;\n    private int _invalidCount;\n    private int _verifiedCount;\n\n    public async Task RunAsync(CancellationToken cancellationToken)\n    {\n        _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n\n        AnsiConsole.MarkupLine(\"[green]Starting verifier service...[/]\");\n        AnsiConsole.MarkupLine($\"[dim]Target valid keys: [yellow]{LiteLimits.MAX_VALID_KEYS}[/][/]\");\n        AnsiConsole.MarkupLine($\"[dim]Loaded {_providers.Count} verification providers[/]\");\n\n        foreach (var provider in _providers)\n        {\n            AnsiConsole.MarkupLine($\"  [dim]- {Markup.Escape(provider.ProviderName)}[/]\");\n        }\n\n        // Run continuously\n        while (!_cancellationTokenSource.Token.IsCancellationRequested)\n        {\n            try\n            {\n                await RunVerificationCycleAsync();\n\n                if (_cancellationTokenSource.Token.IsCancellationRequested)\n                    break;\n\n                // Wait before next cycle\n                AnsiConsole.MarkupLine($\"[dim]Waiting {LiteLimits.VERIFICATION_DELAY_MS / 1000}s before next verification cycle...[/]\");\n                await Task.Delay(LiteLimits.VERIFICATION_DELAY_MS, _cancellationTokenSource.Token);\n\n                // Reset counters\n                _validCount = 0;\n                _invalidCount = 0;\n                _verifiedCount = 0;\n            }\n            catch (OperationCanceledException)\n            {\n                break;\n            }\n            catch (Exception ex)\n            {\n                AnsiConsole.MarkupLine($\"[red]Error during verification: {Markup.Escape(ex.Message)}[/]\");\n                logger?.LogError(ex, \"Verification cycle error\");\n                await Task.Delay(5000, _cancellationTokenSource.Token);\n            }\n        }\n\n        AnsiConsole.MarkupLine(\"[green]Verifier stopped.[/]\");\n    }\n\n    private async Task RunVerificationCycleAsync()\n    {\n        // Count current valid keys\n        var currentValidCount = await dbContext.APIKeys\n            .CountAsync(k => k.Status == ApiStatusEnum.Valid, _cancellationTokenSource!.Token);\n\n        AnsiConsole.MarkupLine($\"[dim]Current valid keys: [yellow]{currentValidCount}[/] / [yellow]{LiteLimits.MAX_VALID_KEYS}[/][/]\");\n\n        if (currentValidCount >= LiteLimits.MAX_VALID_KEYS)\n        {\n            // Re-verify existing valid keys to ensure they're still valid\n            await ReVerifyExistingKeysAsync();\n        }\n        else\n        {\n            // Verify unverified keys until we reach the limit\n            await VerifyNewKeysAsync(LiteLimits.MAX_VALID_KEYS - currentValidCount);\n        }\n\n        // Display summary\n        var table = new Table()\n            .Border(TableBorder.Rounded)\n            .AddColumn(\"[bold]Metric[/]\")\n            .AddColumn(\"[bold]Value[/]\");\n\n        table.AddRow(\"Keys Verified\", _verifiedCount.ToString());\n        table.AddRow(\"Now Valid\", $\"[green]{_validCount}[/]\");\n        table.AddRow(\"Now Invalid\", $\"[red]{_invalidCount}[/]\");\n\n        var newValidCount = await dbContext.APIKeys\n            .CountAsync(k => k.Status == ApiStatusEnum.Valid, _cancellationTokenSource!.Token);\n        table.AddRow(\"Total Valid\", $\"[yellow]{newValidCount}[/] / [yellow]{LiteLimits.MAX_VALID_KEYS}[/]\");\n\n        AnsiConsole.Write(table);\n    }\n\n    private async Task ReVerifyExistingKeysAsync()\n    {\n        AnsiConsole.MarkupLine(\"[dim]Re-verifying existing valid keys...[/]\");\n\n        // Get oldest verified keys first\n        var keysToReVerify = await dbContext.APIKeys\n            .Where(k => k.Status == ApiStatusEnum.Valid)\n            .OrderBy(k => k.LastCheckedUTC)\n            .Take(LiteLimits.VERIFICATION_BATCH_SIZE)\n            .ToListAsync(_cancellationTokenSource!.Token);\n\n        await AnsiConsole.Progress()\n            .StartAsync(async ctx =>\n            {\n                var task = ctx.AddTask(\"[green]Re-verifying keys[/]\", maxValue: keysToReVerify.Count);\n\n                foreach (var key in keysToReVerify)\n                {\n                    if (_cancellationTokenSource.Token.IsCancellationRequested)\n                        break;\n\n                    await VerifyKeyAsync(key);\n                    task.Increment(1);\n                }\n            });\n\n        await dbContext.SaveChangesAsync(_cancellationTokenSource.Token);\n    }\n\n    private async Task VerifyNewKeysAsync(int neededCount)\n    {\n        AnsiConsole.MarkupLine($\"[dim]Verifying unverified keys (need {neededCount} more valid)...[/]\");\n\n        // Get unverified keys\n        var keysToVerify = await dbContext.APIKeys\n            .Where(k => k.Status == ApiStatusEnum.Unverified)\n            .OrderBy(k => k.FirstFoundUTC)\n            .Take(Math.Max(neededCount * 2, LiteLimits.VERIFICATION_BATCH_SIZE))\n            .ToListAsync(_cancellationTokenSource!.Token);\n\n        if (keysToVerify.Count == 0)\n        {\n            AnsiConsole.MarkupLine(\"[yellow]No unverified keys available.[/]\");\n            return;\n        }\n\n        await AnsiConsole.Progress()\n            .StartAsync(async ctx =>\n            {\n                var task = ctx.AddTask(\"[green]Verifying new keys[/]\", maxValue: keysToVerify.Count);\n                var validFound = 0;\n\n                foreach (var key in keysToVerify)\n                {\n                    if (_cancellationTokenSource.Token.IsCancellationRequested)\n                        break;\n\n                    // Stop if we've reached our target\n                    if (validFound >= neededCount)\n                    {\n                        task.Value = keysToVerify.Count;\n                        break;\n                    }\n\n                    var wasValid = await VerifyKeyAsync(key);\n                    if (wasValid)\n                        validFound++;\n\n                    task.Increment(1);\n                }\n            });\n\n        await dbContext.SaveChangesAsync(_cancellationTokenSource.Token);\n    }\n\n    private async Task<bool> VerifyKeyAsync(APIKey key)\n    {\n        Interlocked.Increment(ref _verifiedCount);\n\n        // Build list of providers to try, starting with the assigned one\n        var providersToTry = GetProvidersToTry(key);\n\n        if (providersToTry.Count == 0)\n        {\n            key.Status = ApiStatusEnum.Error;\n            key.LastCheckedUTC = DateTime.UtcNow;\n            AnsiConsole.MarkupLine($\"[yellow]No matching providers for key[/]\");\n            return false;\n        }\n\n        // Try each matching provider until one succeeds\n        foreach (var provider in providersToTry)\n        {\n            try\n            {\n                var result = await provider.ValidateKeyAsync(key.ApiKey, httpClientFactory);\n                key.LastCheckedUTC = DateTime.UtcNow;\n\n                switch (result.Status)\n                {\n                    case Providers.Common.ValidationAttemptStatus.Valid:\n                        // Update the key's API type if a different provider validated it\n                        if (key.ApiType != provider.ApiType)\n                        {\n                            AnsiConsole.MarkupLine($\"[dim]Reclassified from {key.ApiType} to {provider.ApiType}[/]\");\n                            key.ApiType = provider.ApiType;\n                        }\n                        key.Status = ApiStatusEnum.Valid;\n                        key.ErrorCount = 0;\n                        Interlocked.Increment(ref _validCount);\n                        AnsiConsole.MarkupLine($\"[green]Valid: {Markup.Escape(provider.ProviderName)} key[/]\");\n                        return true;\n\n                    case Providers.Common.ValidationAttemptStatus.HttpError:\n                        // Check if it's a quota/credits issue based on detail\n                        if (result.Detail?.Contains(\"quota\", StringComparison.OrdinalIgnoreCase) == true ||\n                            result.Detail?.Contains(\"credit\", StringComparison.OrdinalIgnoreCase) == true ||\n                            result.Detail?.Contains(\"billing\", StringComparison.OrdinalIgnoreCase) == true)\n                        {\n                            // Update the key's API type if a different provider validated it\n                            if (key.ApiType != provider.ApiType)\n                            {\n                                AnsiConsole.MarkupLine($\"[dim]Reclassified from {key.ApiType} to {provider.ApiType}[/]\");\n                                key.ApiType = provider.ApiType;\n                            }\n                            key.Status = ApiStatusEnum.ValidNoCredits;\n                            key.ErrorCount = 0;\n                            Interlocked.Increment(ref _validCount);\n                            AnsiConsole.MarkupLine($\"[yellow]Valid [[no credits]]: {Markup.Escape(provider.ProviderName)} key[/]\");\n                            return true;\n                        }\n                        // HTTP error but not quota - try next provider\n                        continue;\n\n                    case Providers.Common.ValidationAttemptStatus.Unauthorized:\n                        // This provider explicitly rejected it - try next provider\n                        continue;\n\n                    case Providers.Common.ValidationAttemptStatus.NetworkError:\n                        // Network error - don't try other providers, just increment error count\n                        key.ErrorCount++;\n                        if (key.ErrorCount >= 3)\n                        {\n                            key.Status = ApiStatusEnum.Error;\n                        }\n                        return false;\n\n                    default:\n                        // Provider-specific error - try next provider\n                        continue;\n                }\n            }\n            catch (Exception ex)\n            {\n                logger?.LogWarning(ex, \"Error verifying key {KeyId} with provider {Provider}\", key.Id, provider.ProviderName);\n                // Continue to next provider on exception\n                continue;\n            }\n        }\n\n        // All providers failed - mark as invalid\n        key.Status = ApiStatusEnum.Invalid;\n        Interlocked.Increment(ref _invalidCount);\n        return false;\n    }\n\n    /// <summary>\n    /// Gets providers to try for a key, ordered by: assigned provider first, then other matching providers.\n    /// </summary>\n    private List<IApiKeyProvider> GetProvidersToTry(APIKey key)\n    {\n        var result = new List<IApiKeyProvider>();\n\n        // First, add the assigned provider (if it exists)\n        var assignedProvider = _providers.FirstOrDefault(p => p.ApiType == key.ApiType);\n        if (assignedProvider != null)\n        {\n            result.Add(assignedProvider);\n        }\n\n        // Then add other providers whose patterns match this key\n        foreach (var provider in _providers)\n        {\n            // Skip the already-added assigned provider\n            if (provider.ApiType == key.ApiType)\n                continue;\n\n            // Check if any of this provider's patterns match the key\n            foreach (var pattern in provider.RegexPatterns)\n            {\n                try\n                {\n                    var regex = new System.Text.RegularExpressions.Regex(pattern);\n                    if (regex.IsMatch(key.ApiKey))\n                    {\n                        result.Add(provider);\n                        break; // One match is enough for this provider\n                    }\n                }\n                catch\n                {\n                    // Invalid regex pattern - skip it\n                }\n            }\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.CLI/UnsecuredAPIKeys.CLI.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>net10.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <AssemblyName>unsecuredapikeys</AssemblyName>\n    <RootNamespace>UnsecuredAPIKeys.CLI</RootNamespace>\n    <Product>UnsecuredAPIKeys - Open Source</Product>\n    <Authors>https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/</Authors>\n    <Company>https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/</Company>\n    <PackageProjectUrl>https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/</RepositoryUrl>\n    <RepositoryType>git</RepositoryType>\n    <EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>\n    <AnalysisLevel>latest-recommended</AnalysisLevel>\n    <Version>1.0.0</Version>\n\n    <!-- Single-file publishing settings -->\n    <PublishSingleFile>true</PublishSingleFile>\n    <SelfContained>true</SelfContained>\n    <EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>\n    <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Spectre.Console\" Version=\"0.54.0\" />\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore.Sqlite\" Version=\"10.0.1\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.Json\" Version=\"10.0.1\" />\n    <PackageReference Include=\"Microsoft.Extensions.DependencyInjection\" Version=\"10.0.1\" />\n    <PackageReference Include=\"Microsoft.Extensions.Http\" Version=\"10.0.1\" />\n    <PackageReference Include=\"Microsoft.Extensions.Logging.Console\" Version=\"10.0.1\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\UnsecuredAPIKeys.Data\\UnsecuredAPIKeys.Data.csproj\" />\n    <ProjectReference Include=\"..\\UnsecuredAPIKeys.Providers\\UnsecuredAPIKeys.Providers.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"appsettings.example.json\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "UnsecuredAPIKeys.CLI/appsettings.example.json",
    "content": "{\n  \"// README\": \"Copy this file to appsettings.json and configure your GitHub token\",\n  \"// IMPORTANT\": \"Do NOT commit appsettings.json to version control\",\n\n  \"GitHub\": {\n    \"Token\": \"ghp_YOUR_GITHUB_TOKEN_HERE\",\n    \"// TokenInfo\": \"Create at https://github.com/settings/tokens - requires 'public_repo' scope\"\n  },\n\n  \"Database\": {\n    \"Path\": \"unsecuredapikeys.db\",\n    \"// PathInfo\": \"SQLite database file path. Can be absolute or relative.\"\n  },\n\n  \"Limits\": {\n    \"MaxValidKeys\": 50,\n    \"// MaxValidKeysInfo\": \"Lite version limit. For higher limits, use www.UnsecuredAPIKeys.com\"\n  }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Data/Common/ApiProviderAttribute.cs",
    "content": "﻿namespace UnsecuredAPIKeys.Data.Common;\n\n[AttributeUsage(AttributeTargets.Class)]\npublic class ApiProviderAttribute : Attribute\n{\n    /// <summary>\n    /// Whether this provider should be used by the Scraper bot\n    /// </summary>\n    public bool ScraperUse { get; set; } = true;\n\n    /// <summary>\n    /// Whether this provider should be used by the Verifier bot\n    /// </summary>\n    public bool VerificationUse { get; set; } = true;\n\n    /// <summary>\n    /// Creates an ApiProvider attribute with default usage (enabled for both scraper and verifier)\n    /// </summary>\n    public ApiProviderAttribute()\n    {\n    }\n\n    /// <summary>\n    /// Creates an ApiProvider attribute with specific usage flags\n    /// </summary>\n    /// <param name=\"scraperUse\">Enable for scraper bot</param>\n    /// <param name=\"verificationUse\">Enable for verifier bot</param>\n    public ApiProviderAttribute(bool scraperUse, bool verificationUse)\n    {\n        ScraperUse = scraperUse;\n        VerificationUse = verificationUse;\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Data/Common/CommonEnums.cs",
    "content": "namespace UnsecuredAPIKeys.Data.Common\n{\n    /// <summary>\n    /// Search provider for finding API keys.\n    /// Lite version: GitHub only.\n    /// Full version: www.UnsecuredAPIKeys.com\n    /// </summary>\n    public enum SearchProviderEnum\n    {\n        Unknown = -99,\n        GitHub = 1\n    }\n\n    /// <summary>\n    /// Status of an API key in the system.\n    /// </summary>\n    public enum ApiStatusEnum\n    {\n        /// <summary>The key was found but not yet checked for validity.</summary>\n        Unverified = -99,\n\n        /// <summary>The key was checked and is valid/working.</summary>\n        Valid = 1,\n\n        /// <summary>The key was checked and is not working (invalid, expired, revoked, etc.).</summary>\n        Invalid = 0,\n\n        /// <summary>The key is valid but has no credits/quota.</summary>\n        ValidNoCredits = 7,\n\n        /// <summary>The key was checked and is erroring out for some reason.</summary>\n        Error = 6\n    }\n\n    /// <summary>\n    /// Type of API provider.\n    /// Lite version: OpenAI, Anthropic, Google only.\n    /// Full version with all providers: www.UnsecuredAPIKeys.com\n    /// </summary>\n    public enum ApiTypeEnum\n    {\n        Unknown = -99,\n\n        // AI Services - Lite version only supports these 3\n        OpenAI = 100,\n        AnthropicClaude = 120,\n        GoogleAI = 130\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Data/DBContext.cs",
    "content": "using Microsoft.EntityFrameworkCore;\nusing UnsecuredAPIKeys.Data.Models;\n\nnamespace UnsecuredAPIKeys.Data\n{\n    /// <summary>\n    /// SQLite database context for UnsecuredAPIKeys Lite.\n    /// Full version with PostgreSQL: www.UnsecuredAPIKeys.com\n    /// </summary>\n    public class DBContext : DbContext\n    {\n        private readonly string _dbPath;\n\n        public DBContext(DbContextOptions<DBContext> options) : base(options)\n        {\n            _dbPath = \"unsecuredapikeys.db\";\n        }\n\n        public DBContext(string dbPath = \"unsecuredapikeys.db\")\n        {\n            _dbPath = dbPath;\n        }\n\n        // Core entities\n        public DbSet<APIKey> APIKeys { get; set; } = null!;\n        public DbSet<RepoReference> RepoReferences { get; set; } = null!;\n        public DbSet<SearchQuery> SearchQueries { get; set; } = null!;\n        public DbSet<SearchProviderToken> SearchProviderTokens { get; set; } = null!;\n        public DbSet<ApplicationSetting> ApplicationSettings { get; set; } = null!;\n\n        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)\n        {\n            if (!optionsBuilder.IsConfigured)\n            {\n                optionsBuilder.UseSqlite($\"Data Source={_dbPath}\");\n            }\n        }\n\n        protected override void OnModelCreating(ModelBuilder modelBuilder)\n        {\n            base.OnModelCreating(modelBuilder);\n\n            // APIKey indexes for performance\n            modelBuilder.Entity<APIKey>()\n                .HasIndex(k => k.ApiKey)\n                .IsUnique()\n                .HasDatabaseName(\"IX_APIKeys_ApiKey\");\n\n            modelBuilder.Entity<APIKey>()\n                .HasIndex(k => new { k.Status, k.ApiType })\n                .HasDatabaseName(\"IX_APIKeys_Status_ApiType\");\n\n            modelBuilder.Entity<APIKey>()\n                .HasIndex(k => k.LastCheckedUTC)\n                .HasDatabaseName(\"IX_APIKeys_LastCheckedUTC\");\n\n            modelBuilder.Entity<APIKey>()\n                .HasIndex(k => k.Status)\n                .HasDatabaseName(\"IX_APIKeys_Status\");\n\n            // RepoReference indexes\n            modelBuilder.Entity<RepoReference>()\n                .HasIndex(r => r.APIKeyId)\n                .HasDatabaseName(\"IX_RepoReferences_ApiKeyId\");\n\n            // SearchQuery indexes\n            modelBuilder.Entity<SearchQuery>()\n                .HasIndex(q => new { q.IsEnabled, q.LastSearchUTC })\n                .HasDatabaseName(\"IX_SearchQueries_IsEnabled_LastSearchUTC\");\n\n            // SearchProviderToken indexes\n            modelBuilder.Entity<SearchProviderToken>()\n                .HasIndex(t => t.SearchProvider)\n                .HasDatabaseName(\"IX_SearchProviderTokens_SearchProvider\");\n\n            // Relationships\n            modelBuilder.Entity<RepoReference>()\n                .HasOne(r => r.APIKey)\n                .WithMany(k => k.References)\n                .HasForeignKey(r => r.APIKeyId)\n                .OnDelete(DeleteBehavior.Cascade);\n        }\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Data/DesignTimeDbContextFactory.cs",
    "content": "using Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Design;\n\nnamespace UnsecuredAPIKeys.Data\n{\n    /// <summary>\n    /// Factory for creating DBContext during EF Core design-time operations (migrations).\n    /// Uses SQLite for the lite version.\n    /// </summary>\n    public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<DBContext>\n    {\n        public DBContext CreateDbContext(string[] args)\n        {\n            var optionsBuilder = new DbContextOptionsBuilder<DBContext>();\n\n            // Use SQLite for the lite version\n            optionsBuilder.UseSqlite(\"Data Source=unsecuredapikeys.db\");\n\n            return new DBContext(optionsBuilder.Options);\n        }\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Data/Models/APIKey.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.ComponentModel.DataAnnotations.Schema;\nusing UnsecuredAPIKeys.Data.Common;\nusing System.Text.Json.Serialization; // <-- Add this using directive\n\nnamespace UnsecuredAPIKeys.Data.Models\n{\n    public class APIKey\n    {\n        [Key]\n        public long Id { get; set; }\n\n        [Required]\n        public required string ApiKey { get; set; }\n\n        [JsonConverter(typeof(JsonStringEnumConverter))]\n        public ApiStatusEnum Status { get; set; }\n\n        [JsonConverter(typeof(JsonStringEnumConverter))]\n        public ApiTypeEnum ApiType { get; set; } = ApiTypeEnum.Unknown;\n\n        public SearchProviderEnum SearchProvider { get; set; }\n\n        public DateTime? LastCheckedUTC { get; set; }\n        public DateTime FirstFoundUTC { get; set; }\n        public DateTime LastFoundUTC { get; set; }\n\n        public int TimesDisplayed { get; set; }\n        \n        // Error tracking for verification failures\n        public int ErrorCount { get; set; } = 0;\n\n        // Navigation property to where this key was found\n        public virtual ICollection<RepoReference> References { get; set; } = [];\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Data/Models/ApplicationSetting.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace UnsecuredAPIKeys.Data.Models\n{\n    public class ApplicationSetting\n    {\n        [Key] public required string Key { get; init; }\n        public required string Value { get; init; }\n        public string? Description { get; init; }\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Data/Models/RepoReference.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace UnsecuredAPIKeys.Data.Models\n{\n    public class RepoReference\n    {\n        [Key]\n        public long Id { get; set; }\n        public long APIKeyId { get; set; }  // Foreign key to APIKey\n        public virtual APIKey? APIKey { get; set; }  // Navigation property\n\n        // Repository information\n        [Required]\n        public string? RepoURL { get; set; }  // Just base repo URL\n        public string? RepoOwner { get; set; }  // Owner username/organization\n        public string? RepoName { get; set; }  // Repository name\n        public string? RepoDescription { get; set; }\n        public long RepoId { get; set; }  // GitHub's repo ID\n\n        // File information\n        [Required]\n        public string? FileURL { get; set; }  // Full path with commit hash\n        public string? FileName { get; set; }\n        public string? FilePath { get; set; }  // Path within repo\n        public string? FileSHA { get; set; }\n        public string? ApiContentUrl { get; set; } // URL to fetch raw content via API\n\n        // Context information\n        public string? CodeContext { get; set; }  // Surrounding code\n        public int LineNumber { get; set; }\n\n        // Discovery metadata\n        public long SearchQueryId { get; set; }  // Which query found this\n        public DateTime FoundUTC { get; set; }\n        public string? Provider { get; set; } // e.g., GitHub, GitLab\n        public string? Branch { get; set; } // Branch where the file was found\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Data/Models/SearchProviderToken.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing UnsecuredAPIKeys.Data.Common;\n\nnamespace UnsecuredAPIKeys.Data.Models\n{\n    public class SearchProviderToken\n    {\n        [Key] public int Id { get; set; }\n        public string Token { get; set; } = string.Empty;\n        public SearchProviderEnum SearchProvider { get; set; } = SearchProviderEnum.Unknown;\n        public bool IsEnabled { get; set; }\n\n        // Make it nullable so we can identify never-used tokens  \n        public DateTime? LastUsedUTC { get; set; }\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Data/Models/SearchQuery.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace UnsecuredAPIKeys.Data.Models\n{\n    public class SearchQuery\n    {\n        [Key] public long Id { get; set; }\n        public string Query { get; set; } = string.Empty;\n        public bool IsEnabled { get; set; }\n        public int SearchResultsCount { get; set; }\n\n        public DateTime LastSearchUTC { get; set; }\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Data/UnsecuredAPIKeys.Data.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore\" Version=\"10.0.1\" />\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore.Sqlite\" Version=\"10.0.1\" />\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore.Design\" Version=\"10.0.1\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/AI Providers/AnthropicProvider.cs",
    "content": "using System.Net;\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\n\nusing Microsoft.Extensions.Logging;\n\nusing UnsecuredAPIKeys.Data.Common;\nusing UnsecuredAPIKeys.Providers._Base;\nusing UnsecuredAPIKeys.Providers.Common;\n\nnamespace UnsecuredAPIKeys.Providers.AI_Providers\n{\n    /// <summary>\n    /// Provider implementation for handling Anthropic (Claude) API keys with enhanced validation.\n    /// </summary>\n    [ApiProvider]\n    public class AnthropicProvider : BaseApiKeyProvider\n    {\n        private const string API_ENDPOINT = \"https://api.anthropic.com/v1/messages\";\n        private const string ANTHROPIC_VERSION = \"2023-06-01\";\n        private const string DEFAULT_MODEL = \"claude-sonnet-4-20250514\";\n        private const int MAX_RETRIES = 3;\n        private const int TIMEOUT_SECONDS = 30;\n\n        // Anthropic-specific response keywords (additional to base class)\n        private static readonly HashSet<string> InvalidKeyIndicators = new(StringComparer.OrdinalIgnoreCase)\n        {\n            \"invalid_api_key\",\n            \"authentication_error\",\n            \"invalid x-api-key\",\n            \"unauthorized\"\n        };\n\n        public override string ProviderName => \"Anthropic\";\n\n        public override ApiTypeEnum ApiType => ApiTypeEnum.AnthropicClaude;\n\n        // Enhanced regex patterns with compiled regex for better performance\n        public override IEnumerable<string> RegexPatterns =>\n        [\n            @\"sk-ant-api\\d{0,2}-[a-zA-Z0-9\\-_]{40,120}\",\n            @\"sk-ant-[a-zA-Z0-9\\-_]{40,95}\",\n            @\"sk-ant-v\\d+-[a-zA-Z0-9\\-_]{40,95}\",\n            @\"sk-ant-[a-zA-Z0-9]+-[a-zA-Z0-9\\-_]{20,120}\",\n            @\"sk-ant-[a-zA-Z0-9]{40,64}\",\n            @\"\\bsk-ant-[a-zA-Z0-9\\-_]{20,120}\\b\"\n        ];\n\n        public AnthropicProvider() : base()\n        {\n        }\n\n        public AnthropicProvider(ILogger<AnthropicProvider>? logger) : base(logger)\n        {\n        }\n\n        protected override async Task<ValidationResult> ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient)\n        {\n            using var request = CreateValidationRequest(apiKey);\n\n            var response = await httpClient.SendAsync(request);\n            var responseBody = await response.Content.ReadAsStringAsync();\n\n            _logger?.LogDebug(\"Anthropic API response: Status={StatusCode}, Body={Body}\",\n                response.StatusCode, responseBody.Length > 200 ? responseBody.Substring(0, 200) + \"...\" : responseBody);\n\n            return InterpretResponse(response.StatusCode, responseBody);\n        }\n\n        private HttpRequestMessage CreateValidationRequest(string apiKey)\n        {\n            var request = new HttpRequestMessage(HttpMethod.Post, API_ENDPOINT);\n\n            // Set headers\n            request.Headers.Add(\"x-api-key\", apiKey);\n            request.Headers.Add(\"anthropic-version\", ANTHROPIC_VERSION);\n            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(\"application/json\"));\n\n            // Ultra-minimal payload for lowest cost\n            var payload = new\n            {\n                model = DEFAULT_MODEL,\n                max_tokens = 1,\n                messages = new[]\n                {\n                    new { role = \"user\", content = \"1\" }\n                },\n                temperature = 0,\n                stop_sequences = new[] { \"1\", \"2\", \"3\", \"4\", \"5\" }\n            };\n\n            var jsonContent = JsonSerializer.Serialize(payload);\n            request.Content = new StringContent(jsonContent, Encoding.UTF8, \"application/json\");\n\n            return request;\n        }\n\n        private ValidationResult InterpretResponse(HttpStatusCode statusCode, string responseBody)\n        {\n            // Success cases\n            if (IsSuccessStatusCode(statusCode))\n            {\n                return ValidationResult.Success(statusCode);\n            }\n\n            var bodyLower = responseBody.ToLowerInvariant();\n\n            switch (statusCode)\n            {\n                case HttpStatusCode.Unauthorized: // 401\n                    if (ContainsAny(bodyLower, InvalidKeyIndicators))\n                    {\n                        return ValidationResult.IsUnauthorized(statusCode);\n                    }\n                    return ValidationResult.IsUnauthorized(statusCode);\n\n                case HttpStatusCode.Forbidden: // 403\n                    if (ContainsAny(bodyLower, PermissionIndicators))\n                    {\n                        _logger?.LogInformation(\"API key has permission restrictions but is valid\");\n                        return ValidationResult.Success(statusCode);\n                    }\n                    return ValidationResult.HasHttpError(statusCode, $\"Forbidden: {TruncateResponse(responseBody)}\");\n\n                case HttpStatusCode.BadRequest: // 400\n                    if (ContainsAny(bodyLower, QuotaIndicators))\n                    {\n                        _logger?.LogInformation(\"API key is valid but has quota/billing issues\");\n                        return ValidationResult.Success(statusCode);\n                    }\n                    return ValidationResult.HasHttpError(statusCode, $\"Bad request: {TruncateResponse(responseBody)}\");\n\n                case HttpStatusCode.PaymentRequired: // 402\n                case HttpStatusCode.TooManyRequests: // 429\n                    return ValidationResult.Success(statusCode);\n\n                case HttpStatusCode.ServiceUnavailable: // 503\n                case HttpStatusCode.GatewayTimeout: // 504\n                    return ValidationResult.HasNetworkError($\"Service unavailable: {statusCode}\");\n\n                default:\n                    if (ContainsAny(bodyLower, QuotaIndicators))\n                    {\n                        return ValidationResult.Success(statusCode);\n                    }\n\n                    return ValidationResult.HasHttpError(statusCode,\n                        $\"API request failed with status {statusCode}. Response: {TruncateResponse(responseBody)}\");\n            }\n        }\n\n        protected override bool IsValidKeyFormat(string apiKey)\n        {\n            if (string.IsNullOrWhiteSpace(apiKey) || apiKey.Length < 20)\n                return false;\n\n            if (!apiKey.StartsWith(\"sk-ant-\", StringComparison.Ordinal))\n                return false;\n\n            return apiKey.All(c => char.IsLetterOrDigit(c) || c == '-' || c == '_');\n        }\n    }\n}"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/AI Providers/GoogleProvider.cs",
    "content": "using System.Net;\nusing System.Net.Http.Headers;\nusing System.Text.Json;\nusing Microsoft.Extensions.Logging;\nusing UnsecuredAPIKeys.Data.Common;\nusing UnsecuredAPIKeys.Providers._Base;\nusing UnsecuredAPIKeys.Providers.Common;\n\nnamespace UnsecuredAPIKeys.Providers.AI_Providers\n{\n    /// <summary>\n    /// Provider implementation for handling Google AI API keys.\n    /// </summary>\n    [ApiProvider]\n    public class GoogleProvider : BaseApiKeyProvider\n    {\n        public override string ProviderName => \"Google\";\n        public override ApiTypeEnum ApiType => ApiTypeEnum.GoogleAI;\n\n        // Regex patterns specific to Google AI keys (from Scraper_Program.cs)\n        public override IEnumerable<string> RegexPatterns =>\n        [\n            @\"AIza[0-9A-Za-z\\-_]{35}\",  // Standard length is exactly 39 characters total\n            @\"AIza[0-9A-Za-z\\-_]{35,40}\" // Allow for some variation in newer keys\n        ];\n\n        public GoogleProvider() : base()\n        {\n        }\n\n        public GoogleProvider(ILogger<GoogleProvider>? logger) : base(logger)\n        {\n        }\n\n        protected override async Task<ValidationResult> ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient)\n        {\n            // Use Google's models endpoint for lightweight validation\n            using var modelRequest = new HttpRequestMessage(HttpMethod.Get, \"https://generativelanguage.googleapis.com/v1beta/models\");\n            // Google uses x-goog-api-key header\n            modelRequest.Headers.Add(\"x-goog-api-key\", apiKey);\n            \n            var modelResponse = await httpClient.SendAsync(modelRequest);\n            string responseBody = await modelResponse.Content.ReadAsStringAsync();\n\n            _logger?.LogDebug(\"Google AI models API response: Status={StatusCode}, Body={Body}\",\n                modelResponse.StatusCode, TruncateResponse(responseBody));\n\n            if (IsSuccessStatusCode(modelResponse.StatusCode))\n            {\n                // Parse the models from the response\n                var models = ParseGoogleModels(responseBody);\n                return ValidationResult.Success(modelResponse.StatusCode, models);\n            }\n            else if (modelResponse.StatusCode == HttpStatusCode.Unauthorized || \n                     modelResponse.StatusCode == HttpStatusCode.Forbidden)\n            {\n                // Google often uses 403 for invalid keys\n                if (ContainsAny(responseBody, UnauthorizedIndicators))\n                {\n                    return ValidationResult.IsUnauthorized(modelResponse.StatusCode);\n                }\n                return ValidationResult.IsUnauthorized(modelResponse.StatusCode);\n            }\n            else if (modelResponse.StatusCode == HttpStatusCode.BadRequest)\n            {\n                // Check specific error types\n                if (ContainsAny(responseBody, UnauthorizedIndicators))\n                {\n                    return ValidationResult.IsUnauthorized(modelResponse.StatusCode);\n                }\n                return ValidationResult.HasHttpError(modelResponse.StatusCode, $\"Bad request. Response: {TruncateResponse(responseBody)}\");\n            }\n            else if ((int)modelResponse.StatusCode == 429)\n            {\n                // Rate limited means the key is valid\n                return ValidationResult.Success(modelResponse.StatusCode);\n            }\n            else\n            {\n                // Check for quota/billing issues\n                if (ContainsAny(responseBody, QuotaIndicators))\n                {\n                    return ValidationResult.Success(modelResponse.StatusCode);\n                }\n                \n                return ValidationResult.HasHttpError(modelResponse.StatusCode, \n                    $\"API request failed with status {modelResponse.StatusCode}. Response: {TruncateResponse(responseBody)}\");\n            }\n        }\n\n        protected override bool IsValidKeyFormat(string apiKey)\n        {\n            return !string.IsNullOrWhiteSpace(apiKey) && \n                   apiKey.StartsWith(\"AIza\") && \n                   apiKey.Length >= 39; // AIza + 35 chars\n        }\n\n        private List<ModelInfo>? ParseGoogleModels(string jsonResponse)\n        {\n            try\n            {\n                using var doc = JsonDocument.Parse(jsonResponse);\n                if (!doc.RootElement.TryGetProperty(\"models\", out var modelsArray))\n                {\n                    return null;\n                }\n\n                var models = new List<ModelInfo>();\n                foreach (var modelElement in modelsArray.EnumerateArray())\n                {\n                    var model = new ModelInfo\n                    {\n                        ModelId = modelElement.GetProperty(\"name\").GetString() ?? \"\",\n                        DisplayName = modelElement.TryGetProperty(\"displayName\", out var displayName) ? displayName.GetString() : null,\n                        Description = modelElement.TryGetProperty(\"description\", out var description) ? description.GetString() : null,\n                        Version = modelElement.TryGetProperty(\"version\", out var version) ? version.GetString() : null,\n                        InputTokenLimit = modelElement.TryGetProperty(\"inputTokenLimit\", out var inputLimit) ? inputLimit.GetInt64() : null,\n                        OutputTokenLimit = modelElement.TryGetProperty(\"outputTokenLimit\", out var outputLimit) ? outputLimit.GetInt64() : null,\n                        Temperature = modelElement.TryGetProperty(\"temperature\", out var temp) ? (float?)temp.GetDouble() : null,\n                        TopP = modelElement.TryGetProperty(\"topP\", out var topP) ? (float?)topP.GetDouble() : null,\n                        TopK = modelElement.TryGetProperty(\"topK\", out var topK) ? topK.GetInt32() : null,\n                        MaxTemperature = modelElement.TryGetProperty(\"maxTemperature\", out var maxTemp) ? (float?)maxTemp.GetDouble() : null\n                    };\n\n                    // Parse supported methods\n                    if (modelElement.TryGetProperty(\"supportedGenerationMethods\", out var methods))\n                    {\n                        model.SupportedMethods = new List<string>();\n                        foreach (var method in methods.EnumerateArray())\n                        {\n                            if (method.GetString() is string methodStr)\n                            {\n                                model.SupportedMethods.Add(methodStr);\n                            }\n                        }\n                    }\n\n                    // Extract model group from the display name\n                    if (model.DisplayName != null)\n                    {\n                        // Extract model family (e.g., \"Gemini 1.5\", \"Gemini 2.0\", etc.)\n                        if (model.DisplayName.Contains(\"Gemini\"))\n                        {\n                            var parts = model.DisplayName.Split(' ');\n                            if (parts.Length >= 2)\n                            {\n                                model.ModelGroup = $\"{parts[0]} {parts[1]}\";\n                            }\n                        }\n                    }\n\n                    models.Add(model);\n                }\n\n                return models;\n            }\n            catch (Exception ex)\n            {\n                _logger?.LogError(ex, \"Error parsing Google models response\");\n                return null;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/AI Providers/OpenAIProvider.cs",
    "content": "using System.Net;\nusing System.Net.Http.Headers;\nusing System.Text.Json;\nusing Microsoft.Extensions.Logging;\nusing UnsecuredAPIKeys.Data.Common;\nusing UnsecuredAPIKeys.Providers._Base;\nusing UnsecuredAPIKeys.Providers.Common;\n\nnamespace UnsecuredAPIKeys.Providers.AI_Providers\n{\n    /// <summary>\n    /// Provider implementation for handling OpenAI API keys.\n    /// </summary>\n    [ApiProvider]\n    public class OpenAIProvider : BaseApiKeyProvider\n    {\n        public override string ProviderName => \"OpenAI\";\n        public override ApiTypeEnum ApiType => ApiTypeEnum.OpenAI;\n\n        // Enhanced regex patterns for OpenAI keys\n        public override IEnumerable<string> RegexPatterns =>\n        [\n            @\"sk-[A-Za-z0-9\\-]{20,}\",\n            @\"sk-proj-[A-Za-z0-9\\-]{20,}\",\n            @\"sk-svcacct-[A-Za-z0-9\\-]{20,}\",\n            @\"sk-[A-Za-z0-9]{48}\",  // Standard format\n            @\"Bearer sk-[A-Za-z0-9\\-]{20,}\"  // Keys in auth headers\n        ];\n\n        public OpenAIProvider() : base()\n        {\n        }\n\n        public OpenAIProvider(ILogger<OpenAIProvider>? logger) : base(logger)\n        {\n        }\n\n        protected override async Task<ValidationResult> ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient)\n        {\n            // First, try a lightweight model listing endpoint\n            using var modelRequest = new HttpRequestMessage(HttpMethod.Get, \"https://api.openai.com/v1/models\");\n            modelRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n            \n            var modelResponse = await httpClient.SendAsync(modelRequest);\n            string responseBody = await modelResponse.Content.ReadAsStringAsync();\n\n            _logger?.LogDebug(\"OpenAI models API response: Status={StatusCode}, Body={Body}\",\n                modelResponse.StatusCode, TruncateResponse(responseBody));\n\n            if (IsSuccessStatusCode(modelResponse.StatusCode))\n            {\n                // Parse the models from the response\n                var models = ParseOpenAIModels(responseBody);\n                return ValidationResult.Success(modelResponse.StatusCode, models);\n            }\n            else if (modelResponse.StatusCode == HttpStatusCode.Unauthorized)\n            {\n                return ValidationResult.IsUnauthorized(modelResponse.StatusCode);\n            }\n            else if ((int)modelResponse.StatusCode == 429)\n            {\n                // Rate limited means the key is valid\n                return ValidationResult.Success(modelResponse.StatusCode);\n            }\n            else if (modelResponse.StatusCode == HttpStatusCode.PaymentRequired)\n            {\n                // Payment required means valid key but no credits\n                return ValidationResult.Success(modelResponse.StatusCode);\n            }\n            else\n            {\n                // Check response body for quota/billing issues\n                if (ContainsAny(responseBody, QuotaIndicators))\n                {\n                    return ValidationResult.Success(modelResponse.StatusCode);\n                }\n                \n                return ValidationResult.HasHttpError(modelResponse.StatusCode, \n                    $\"API request failed with status {modelResponse.StatusCode}. Response: {TruncateResponse(responseBody)}\");\n            }\n        }\n\n        protected override bool IsValidKeyFormat(string apiKey)\n        {\n            return !string.IsNullOrWhiteSpace(apiKey) && \n                   apiKey.StartsWith(\"sk-\") && \n                   apiKey.Length >= 23; // sk- + at least 20 chars\n        }\n\n        private List<ModelInfo>? ParseOpenAIModels(string jsonResponse)\n        {\n            try\n            {\n                using var doc = JsonDocument.Parse(jsonResponse);\n                if (!doc.RootElement.TryGetProperty(\"data\", out var dataArray))\n                {\n                    return null;\n                }\n\n                var models = new List<ModelInfo>();\n                foreach (var modelElement in dataArray.EnumerateArray())\n                {\n                    var model = new ModelInfo\n                    {\n                        ModelId = modelElement.GetProperty(\"id\").GetString() ?? \"\",\n                        DisplayName = modelElement.GetProperty(\"id\").GetString() ?? \"\", // OpenAI uses id as display name\n                        Description = modelElement.TryGetProperty(\"description\", out var desc) ? desc.GetString() : null\n                    };\n\n                    // Extract model group from the ID\n                    if (!string.IsNullOrEmpty(model.ModelId))\n                    {\n                        // Group models by family (e.g., \"gpt-4\", \"gpt-3.5\", \"text-embedding\")\n                        if (model.ModelId.StartsWith(\"gpt-4\"))\n                        {\n                            model.ModelGroup = \"GPT-4\";\n                            \n                            // Check for specific capabilities\n                            if (model.ModelId.Contains(\"turbo\"))\n                            {\n                                model.Description = \"GPT-4 Turbo model with enhanced capabilities\";\n                            }\n                            else if (model.ModelId.Contains(\"vision\"))\n                            {\n                                model.Description = \"GPT-4 model with vision capabilities\";\n                            }\n                        }\n                        else if (model.ModelId.StartsWith(\"gpt-3.5\"))\n                        {\n                            model.ModelGroup = \"GPT-3.5\";\n                        }\n                        else if (model.ModelId.StartsWith(\"o1\"))\n                        {\n                            model.ModelGroup = \"O1\";\n                            model.Description = \"OpenAI's reasoning model\";\n                        }\n                        else if (model.ModelId.StartsWith(\"text-embedding\"))\n                        {\n                            model.ModelGroup = \"Embeddings\";\n                            model.Description = \"Text embedding model\";\n                        }\n                        else if (model.ModelId.StartsWith(\"dall-e\"))\n                        {\n                            model.ModelGroup = \"DALL-E\";\n                            model.Description = \"Image generation model\";\n                        }\n                        else if (model.ModelId.StartsWith(\"whisper\"))\n                        {\n                            model.ModelGroup = \"Whisper\";\n                            model.Description = \"Speech recognition model\";\n                        }\n                        else if (model.ModelId.StartsWith(\"tts\"))\n                        {\n                            model.ModelGroup = \"TTS\";\n                            model.Description = \"Text-to-speech model\";\n                        }\n                        else\n                        {\n                            model.ModelGroup = \"Other\";\n                        }\n                    }\n\n                    models.Add(model);\n                }\n\n                return models;\n            }\n            catch (Exception ex)\n            {\n                _logger?.LogError(ex, \"Error parsing OpenAI models response\");\n                return null;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/ApiProviderRegistry.cs",
    "content": "﻿using System.Reflection;\nusing UnsecuredAPIKeys.Data.Common;\nusing UnsecuredAPIKeys.Providers._Interfaces;\n\nnamespace UnsecuredAPIKeys.Providers\n{\n    public static class ApiProviderRegistry\n    {\n        private static readonly Lazy<List<IApiKeyProvider>> _allProviders = new(() =>\n        {\n            return [.. Assembly.GetExecutingAssembly()\n                .GetTypes()\n                .Where(type => type.GetCustomAttribute<ApiProviderAttribute>() != null\n                               && typeof(IApiKeyProvider).IsAssignableFrom(type)\n                               && !type.IsInterface\n                               && !type.IsAbstract)\n                .Select(type => (IApiKeyProvider)Activator.CreateInstance(type)!)];\n        });\n\n        private static readonly Lazy<List<IApiKeyProvider>> _scraperProviders = new(() =>\n        {\n            return [.. Assembly.GetExecutingAssembly()\n                .GetTypes()\n                .Where(type => {\n                    var attr = type.GetCustomAttribute<ApiProviderAttribute>();\n                    return attr != null\n                           && attr.ScraperUse\n                           && typeof(IApiKeyProvider).IsAssignableFrom(type)\n                           && !type.IsInterface\n                           && !type.IsAbstract;\n                })\n                .Select(type => (IApiKeyProvider)Activator.CreateInstance(type)!)];\n        });\n\n        private static readonly Lazy<List<IApiKeyProvider>> _verifierProviders = new(() =>\n        {\n            return [.. Assembly.GetExecutingAssembly()\n                .GetTypes()\n                .Where(type => {\n                    var attr = type.GetCustomAttribute<ApiProviderAttribute>();\n                    return attr != null\n                           && attr.VerificationUse\n                           && typeof(IApiKeyProvider).IsAssignableFrom(type)\n                           && !type.IsInterface\n                           && !type.IsAbstract;\n                })\n                .Select(type => (IApiKeyProvider)Activator.CreateInstance(type)!)];\n        });\n\n        /// <summary>\n        /// Gets all providers with ApiProvider attribute (backward compatibility)\n        /// </summary>\n        public static IReadOnlyList<IApiKeyProvider> Providers => _allProviders.Value;\n\n        /// <summary>\n        /// Gets providers that are enabled for scraper use\n        /// </summary>\n        public static IReadOnlyList<IApiKeyProvider> ScraperProviders => _scraperProviders.Value;\n\n        /// <summary>\n        /// Gets providers that are enabled for verifier use\n        /// </summary>\n        public static IReadOnlyList<IApiKeyProvider> VerifierProviders => _verifierProviders.Value;\n\n        /// <summary>\n        /// Gets providers for a specific bot type\n        /// </summary>\n        /// <param name=\"botType\">The type of bot (Scraper or Verifier)</param>\n        /// <returns>List of providers enabled for the specified bot type</returns>\n        public static IReadOnlyList<IApiKeyProvider> GetProvidersForBot(BotType botType)\n        {\n            return botType switch\n            {\n                BotType.Scraper => ScraperProviders,\n                BotType.Verifier => VerifierProviders,\n                _ => Providers\n            };\n        }\n    }\n\n    /// <summary>\n    /// Enumeration of bot types for provider filtering\n    /// </summary>\n    public enum BotType\n    {\n        Scraper,\n        Verifier\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/Common/ValidationResult.cs",
    "content": "using System.Net;\n\nnamespace UnsecuredAPIKeys.Providers.Common\n{\n    public enum ValidationAttemptStatus\n    {\n        Valid,                 // Key is valid and working.\n        Unauthorized,          // Key is explicitly unauthorized (e.g., HTTP 401).\n        HttpError,             // An HTTP error occurred (e.g., 403, 404, 429, 5xx).\n        NetworkError,          // A network-level error occurred (e.g., DNS, timeout, connection refused).\n        ProviderSpecificError  // An unexpected error within the provider's logic.\n    }\n\n    public class ModelInfo\n    {\n        public string ModelId { get; set; } = string.Empty;\n        public string? DisplayName { get; set; }\n        public string? Description { get; set; }\n        public string? Version { get; set; }\n        public long? InputTokenLimit { get; set; }\n        public long? OutputTokenLimit { get; set; }\n        public List<string>? SupportedMethods { get; set; }\n        public float? Temperature { get; set; }\n        public float? TopP { get; set; }\n        public int? TopK { get; set; }\n        public float? MaxTemperature { get; set; }\n        public string? ModelGroup { get; set; } // For grouping similar models\n    }\n\n    public class ValidationResult\n    {\n        public ValidationAttemptStatus Status { get; set; }\n        public HttpStatusCode? HttpStatusCode { get; set; } // Null if not an HTTP-related error.\n        public string? Detail { get; set; } = string.Empty; // Optional error message or detail.\n        \n        // Model information discovered during validation\n        public List<ModelInfo>? AvailableModels { get; set; }\n\n        // Helper factory methods for convenience\n        public static ValidationResult Success(HttpStatusCode statusCode, List<ModelInfo>? models = null) =>\n            new() { Status = ValidationAttemptStatus.Valid, HttpStatusCode = statusCode, AvailableModels = models };\n\n        public static ValidationResult IsUnauthorized(HttpStatusCode statusCode, string? detail = null) =>\n            new() { Status = ValidationAttemptStatus.Unauthorized, HttpStatusCode = statusCode, Detail = detail };\n\n        public static ValidationResult HasHttpError(HttpStatusCode statusCode, string? detail = null) =>\n            new() { Status = ValidationAttemptStatus.HttpError, HttpStatusCode = statusCode, Detail = detail };\n\n        public static ValidationResult HasNetworkError(string detail) =>\n            new() { Status = ValidationAttemptStatus.NetworkError, Detail = detail };\n        \n        public static ValidationResult HasProviderSpecificError(string detail) =>\n            new() { Status = ValidationAttemptStatus.ProviderSpecificError, Detail = detail };\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/Search Providers/GitHubSearchProvider.cs",
    "content": "using Microsoft.Extensions.Logging;\nusing Octokit;\nusing UnsecuredAPIKeys.Data;\nusing UnsecuredAPIKeys.Data.Models;\nusing UnsecuredAPIKeys.Providers._Interfaces;\n// Assuming logging might be needed later\n\nnamespace UnsecuredAPIKeys.Providers.Search_Providers\n{\n    /// <summary>\n    /// Implements the ISearchProvider interface for searching code on GitHub.\n    /// </summary>\n    public class GitHubSearchProvider(DBContext dbContext, ILogger<GitHubSearchProvider>? logger = null) : ISearchProvider\n    {\n        /// <inheritdoc />\n        public string ProviderName => \"GitHub\";\n\n        /// <inheritdoc />\n        public async Task<IEnumerable<RepoReference>> SearchAsync(SearchQuery query, SearchProviderToken? token)\n        {\n            if (token == null || string.IsNullOrWhiteSpace(token.Token))\n            {\n                logger?.LogError(\"GitHub token is missing or invalid.\"); // Use _logger field\n                throw new ArgumentNullException(nameof(token), \"A valid GitHub token is required.\");\n            }\n\n            if (query == null || string.IsNullOrWhiteSpace(query.Query))\n            {\n                logger?.LogError(\"Search query is missing or invalid.\"); // Use _logger field\n                throw new ArgumentNullException(nameof(query), \"A valid search query is required.\");\n            }\n\n            var client = new GitHubClient(new ProductHeaderValue(\"UnsecuredAPIKeys-Scraper\"))\n            {\n                Credentials = new Credentials(token.Token)\n            };\n\n            var results = new List<RepoReference>();\n            int page = 1;\n            const int perPage = 100; // Max allowed by GitHub API\n\n            try\n            {\n                logger?.LogInformation(\"Starting GitHub search for query: {Query}\", query.Query); // Use _logger field\n\n                while (true) // Loop to handle pagination\n                {\n                    var request = new SearchCodeRequest(query.Query)\n                    {\n                        // Consider adding filters like language, user, repo if needed\n                        Page = page,\n                        PerPage = perPage\n                        // Order = SortDirection.Descending\n                    };\n\n                    SearchCodeResult searchResult;\n                    try\n                    {\n                        searchResult = await client.Search.SearchCode(request);\n\n                        if (page == 1)\n                        {\n                            query.SearchResultsCount = searchResult.TotalCount;\n                            dbContext.SearchQueries.Update(query);\n                            await dbContext.SaveChangesAsync();\n                        }\n                    }\n                    catch (RateLimitExceededException ex)\n                    {\n                        logger?.LogWarning(\"GitHub API rate limit exceeded. Waiting until {ResetTime}.\", ex.Reset.ToString(\"o\")); // Use _logger field\n\n                        // Wait until the rate limit resets\n                        var delay = ex.Reset - DateTimeOffset.UtcNow;\n                        if (delay > TimeSpan.Zero)\n                        {\n                            if (delay.TotalMinutes > 1)\n                            {\n                                Environment.Exit(200);\n                            }\n\n                            await Task.Delay(delay);\n                        }\n                        continue; // Retry the same page\n                    }\n                    catch (ApiException apiEx)\n                    {\n                        logger?.LogError(apiEx, \"GitHub API error during search on page {Page}. Status: {StatusCode}\", page, apiEx.StatusCode); // Use _logger field\n                        // Decide how to handle API errors (e.g., stop, retry after delay)\n                        break; // Stop searching on API error for now\n                    }\n\n                    if (searchResult?.Items == null || !searchResult.Items.Any())\n                    {\n                        logger?.LogInformation(\"No more results found for query '{Query}' on page {Page}.\", query.Query, page); // Use _logger field\n                        break; // No more results\n                    }\n\n                    logger?.LogDebug(\"Found {Count} results on page {Page} for query '{Query}'.\", searchResult.Items.Count, page, query.Query); // Use _logger field\n\n                    foreach (var item in searchResult.Items)\n                    {\n                        results.Add(new RepoReference\n                        {\n                            SearchQueryId = query.Id,\n                            Provider = ProviderName,\n                            RepoOwner = item.Repository?.Owner?.Login, // Corrected field name\n                            RepoName = item.Repository?.Name, // Corrected field name\n                            FilePath = item.Path,\n                            FileURL = item.HtmlUrl, // HTML URL for viewing in browser\n                            ApiContentUrl = item.Url, // API URL for fetching content\n                            Branch = item.Repository?.DefaultBranch, // Assuming default branch, might need refinement\n                            FileSHA = item.Sha, // Corrected field name (SHA of the file blob)\n                            FoundUTC = DateTime.UtcNow, // Corrected field name (Record when this specific reference was found)\n                            RepoURL = item.Repository?.HtmlUrl,\n                            RepoDescription = item.Repository?.Description,\n                            FileName = item.Name\n                        });\n                    }\n\n                    // Basic check to prevent infinite loops if API behaves unexpectedly\n                    if (searchResult.Items.Count < perPage || results.Count >= 1000) // GitHub limits code search results to 1000\n                    {\n                        logger?.LogInformation(\"Finished GitHub search for query '{Query}'. Total results processed: {TotalCount}. Reached page limit or result cap.\", query.Query, results.Count); // Use _logger field\n                        break;\n                    }\n\n                    page++; // Move to the next page\n                    await Task.Delay(TimeSpan.FromSeconds(2)); // Add a small delay to be polite to the API\n                }\n            }\n            catch (Exception ex)\n            {\n                logger?.LogError(ex, \"An unexpected error occurred during GitHub search for query: {Query}\", query.Query); // Use _logger field\n            }\n\n            logger?.LogInformation(\"Completed GitHub search for query '{Query}'. Found {Count} potential references.\", query.Query, results.Count); // Use _logger field\n            return results;\n        }\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/UnsecuredAPIKeys.Providers.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\UnsecuredAPIKeys.Data\\UnsecuredAPIKeys.Data.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Extensions.Http\" Version=\"10.0.1\" />\n    <PackageReference Include=\"Octokit\" Version=\"14.0.0\" />\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore.Relational\" Version=\"10.0.1\" />\n  </ItemGroup>\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n\n</Project>\n"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/_Base/BaseApiKeyProvider.cs",
    "content": "using System.Net;\nusing Microsoft.Extensions.Logging;\nusing UnsecuredAPIKeys.Data.Common;\nusing UnsecuredAPIKeys.Providers._Interfaces;\nusing UnsecuredAPIKeys.Providers.Common;\n\nnamespace UnsecuredAPIKeys.Providers._Base\n{\n    /// <summary>\n    /// Base class for API key providers with common functionality and retry logic.\n    /// Lite version: OpenAI, Anthropic, Google only.\n    /// Full version with all providers: www.UnsecuredAPIKeys.com\n    /// </summary>\n    public abstract class BaseApiKeyProvider(ILogger? logger = null) : IApiKeyProvider\n    {\n        protected const int DEFAULT_MAX_RETRIES = 3;\n        protected const int DEFAULT_TIMEOUT_SECONDS = 30;\n\n        protected readonly ILogger? _logger = logger;\n\n        public abstract string ProviderName { get; }\n        public abstract ApiTypeEnum ApiType { get; }\n        public abstract IEnumerable<string> RegexPatterns { get; }\n\n        /// <summary>\n        /// Validates an API key with retry logic and proper resource management.\n        /// </summary>\n        public async Task<ValidationResult> ValidateKeyAsync(string apiKey, IHttpClientFactory httpClientFactory)\n        {\n            if (string.IsNullOrWhiteSpace(apiKey))\n            {\n                return ValidationResult.HasProviderSpecificError(\"API key is null or whitespace.\");\n            }\n\n            // Clean the API key\n            apiKey = CleanApiKey(apiKey);\n\n            // Validate format if implemented\n            if (!IsValidKeyFormat(apiKey))\n            {\n                return ValidationResult.HasProviderSpecificError(\"API key format is invalid.\");\n            }\n\n            Exception? lastException = null;\n\n            for (int retry = 0; retry < GetMaxRetries(); retry++)\n            {\n                if (retry > 0)\n                {\n                    var delay = TimeSpan.FromSeconds(Math.Pow(2, retry - 1));\n                    _logger?.LogDebug(\"Retrying {Provider} validation after {Delay}ms (attempt {Retry}/{MaxRetries})\",\n                        ProviderName, delay.TotalMilliseconds, retry + 1, GetMaxRetries());\n                    await Task.Delay(delay);\n                }\n\n                try\n                {\n                    using var httpClient = CreateHttpClient(httpClientFactory);\n                    var result = await ValidateKeyWithHttpClientAsync(apiKey, httpClient);\n\n                    if (result.Status != ValidationAttemptStatus.NetworkError)\n                    {\n                        return result;\n                    }\n\n                    // Continue retrying on network errors\n                    lastException = new Exception(result.Detail);\n                }\n                catch (HttpRequestException ex)\n                {\n                    lastException = ex;\n                    _logger?.LogWarning(ex, \"HTTP request failed on attempt {Retry}/{MaxRetries} for {Provider}\",\n                        retry + 1, GetMaxRetries(), ProviderName);\n\n                    if (retry == GetMaxRetries() - 1)\n                    {\n                        return ValidationResult.HasNetworkError($\"HTTP request failed after {GetMaxRetries()} retries: {ex.Message}\");\n                    }\n                }\n                catch (TaskCanceledException ex)\n                {\n                    lastException = ex;\n                    _logger?.LogWarning(ex, \"Request timeout on attempt {Retry}/{MaxRetries} for {Provider}\",\n                        retry + 1, GetMaxRetries(), ProviderName);\n\n                    if (retry == GetMaxRetries() - 1)\n                    {\n                        return ValidationResult.HasNetworkError($\"Request timeout after {GetMaxRetries()} retries: {ex.Message}\");\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _logger?.LogError(ex, \"Unexpected error during {Provider} key validation\", ProviderName);\n                    return ValidationResult.HasProviderSpecificError($\"Unexpected error: {ex.Message}\");\n                }\n            }\n\n            return ValidationResult.HasNetworkError($\"Failed after {GetMaxRetries()} retries. Last error: {lastException?.Message ?? \"Unknown error\"}\");\n        }\n\n        /// <summary>\n        /// Abstract method for provider-specific validation logic.\n        /// </summary>\n        protected abstract Task<ValidationResult> ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient);\n\n        /// <summary>\n        /// Creates an HttpClient with proper configuration.\n        /// </summary>\n        protected virtual HttpClient CreateHttpClient(IHttpClientFactory httpClientFactory)\n        {\n            try\n            {\n                var client = httpClientFactory.CreateClient(ProviderName.ToLowerInvariant().Replace(\" \", \"\"));\n                client.Timeout = TimeSpan.FromSeconds(GetTimeoutSeconds());\n                return client;\n            }\n            catch\n            {\n                // Fall back to manual creation if factory fails\n                return new HttpClient\n                {\n                    Timeout = TimeSpan.FromSeconds(GetTimeoutSeconds())\n                };\n            }\n        }\n\n        /// <summary>\n        /// Cleans the API key by removing common prefixes and whitespace.\n        /// </summary>\n        protected virtual string CleanApiKey(string apiKey)\n        {\n            apiKey = apiKey.Trim();\n\n            // Remove common prefixes\n            if (apiKey.StartsWith(\"Bearer \", StringComparison.OrdinalIgnoreCase))\n            {\n                apiKey = apiKey.Substring(7).Trim();\n            }\n            else if (apiKey.StartsWith(\"x-api-key:\", StringComparison.OrdinalIgnoreCase))\n            {\n                apiKey = apiKey.Substring(10).Trim();\n            }\n\n            return apiKey;\n        }\n\n        /// <summary>\n        /// Validates the API key format. Override in derived classes for specific validation.\n        /// </summary>\n        protected virtual bool IsValidKeyFormat(string apiKey)\n        {\n            return !string.IsNullOrWhiteSpace(apiKey) && apiKey.Length >= 10;\n        }\n\n        /// <summary>\n        /// Gets the maximum number of retries. Override in derived classes if needed.\n        /// </summary>\n        protected virtual int GetMaxRetries() => DEFAULT_MAX_RETRIES;\n\n        /// <summary>\n        /// Gets the timeout in seconds. Override in derived classes if needed.\n        /// </summary>\n        protected virtual int GetTimeoutSeconds() => DEFAULT_TIMEOUT_SECONDS;\n\n        /// <summary>\n        /// Common method to check if response body contains any of the specified indicators.\n        /// </summary>\n        protected static bool ContainsAny(string text, HashSet<string> indicators)\n        {\n            return indicators.Any(indicator => text.Contains(indicator, StringComparison.OrdinalIgnoreCase));\n        }\n\n        /// <summary>\n        /// Truncates response text for logging purposes.\n        /// </summary>\n        protected static string TruncateResponse(string response, int maxLength = 200)\n        {\n            if (string.IsNullOrEmpty(response))\n                return string.Empty;\n\n            return response.Length > maxLength\n                ? response.Substring(0, maxLength) + \"...\"\n                : response;\n        }\n\n        /// <summary>\n        /// Checks if the status code indicates success.\n        /// </summary>\n        protected static bool IsSuccessStatusCode(HttpStatusCode statusCode)\n        {\n            return (int)statusCode >= 200 && (int)statusCode < 300;\n        }\n\n        /// <summary>\n        /// Common quota/billing indicators across providers.\n        /// </summary>\n        protected static readonly HashSet<string> QuotaIndicators = new(StringComparer.OrdinalIgnoreCase)\n        {\n            \"credit\", \"quota\", \"billing\", \"insufficient_funds\", \"payment\", \"exceeded\", \"balance\", \"limit\",\n            \"insufficient_quota\", \"exceeded_quota\", \"rate_limit\", \"rate_limit_exceeded\", \"RESOURCE_EXHAUSTED\"\n        };\n\n        /// <summary>\n        /// Common unauthorized indicators across providers.\n        /// </summary>\n        protected static readonly HashSet<string> UnauthorizedIndicators = new(StringComparer.OrdinalIgnoreCase)\n        {\n            \"invalid_api_key\", \"authentication_error\", \"unauthorized\", \"invalid x-api-key\", \"API_KEY_INVALID\",\n            \"API key not valid\", \"API key expired\", \"invalid token\", \"authentication failed\"\n        };\n\n        /// <summary>\n        /// Common permission indicators across providers.\n        /// </summary>\n        protected static readonly HashSet<string> PermissionIndicators = new(StringComparer.OrdinalIgnoreCase)\n        {\n            \"permission\", \"access\", \"not_authorized_for_model\", \"forbidden\", \"read-only\", \"Pro service\"\n        };\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/_Interfaces/IApiKeyProvider.cs",
    "content": "using UnsecuredAPIKeys.Data.Common;\nusing UnsecuredAPIKeys.Providers.Common;\n\nnamespace UnsecuredAPIKeys.Providers._Interfaces\n{\n    /// <summary>\n    /// Defines the contract for an API key provider, responsible for\n    /// identifying and validating keys for a specific service.\n    /// Lite version: OpenAI, Anthropic, Google only.\n    /// Full version: www.UnsecuredAPIKeys.com\n    /// </summary>\n    public interface IApiKeyProvider\n    {\n        /// <summary>\n        /// Gets the unique name of the provider (e.g., \"OpenAI\", \"Anthropic\").\n        /// </summary>\n        string ProviderName { get; }\n\n        /// <summary>\n        /// Gets the corresponding ApiTypeEnum value for this provider.\n        /// </summary>\n        ApiTypeEnum ApiType { get; }\n\n        /// <summary>\n        /// Gets the list of regex patterns used to identify potential keys for this provider.\n        /// </summary>\n        IEnumerable<string> RegexPatterns { get; }\n\n        /// <summary>\n        /// Asynchronously validates the given API key against the provider's service.\n        /// </summary>\n        /// <param name=\"apiKey\">The API key string to validate.</param>\n        /// <param name=\"httpClientFactory\">The IHttpClientFactory for creating HttpClient instances.</param>\n        /// <returns>A ValidationResult indicating the outcome of the validation attempt.</returns>\n        Task<ValidationResult> ValidateKeyAsync(string apiKey, IHttpClientFactory httpClientFactory);\n    }\n}\n"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/_Interfaces/ISearchProvider.cs",
    "content": "using UnsecuredAPIKeys.Data.Models;\n\nnamespace UnsecuredAPIKeys.Providers._Interfaces\n{\n    /// <summary>\n    /// Defines the contract for a search provider used to find potential API keys.\n    /// </summary>\n    public interface ISearchProvider\n    {\n        /// <summary>\n        /// Gets the name of the search provider (e.g., \"GitHub\", \"GitLab\").\n        /// </summary>\n        string ProviderName { get; }\n\n        /// <summary>\n        /// Executes a search based on the provided query.\n        /// </summary>\n        /// <param name=\"query\">The search query details.</param>\n        /// <param name=\"token\">The API token to use for the search.</param>\n        /// <returns>A collection of RepoReference objects representing potential findings.</returns>\n        Task<IEnumerable<RepoReference>> SearchAsync(SearchQuery query, SearchProviderToken? token);\n    }\n}\n"
  }
]