main 4e15a236ed18 cached
35 files
134.7 KB
31.7k tokens
90 symbols
1 requests
Download .txt
Repository: TSCarterJr/UnsecuredAPIKeys-OpenSource
Branch: main
Commit: 4e15a236ed18
Files: 35
Total size: 134.7 KB

Directory structure:
gitextract_966uldie/

├── .gitattributes
├── .github/
│   └── workflows/
│       ├── claude-code-review.yml
│       └── claude.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── UnsecuredAPIKeys-OpenSource.sln
├── UnsecuredAPIKeys.CLI/
│   ├── Constants.cs
│   ├── Program.cs
│   ├── Services/
│   │   ├── DatabaseService.cs
│   │   ├── ScraperService.cs
│   │   └── VerifierService.cs
│   ├── UnsecuredAPIKeys.CLI.csproj
│   └── appsettings.example.json
├── UnsecuredAPIKeys.Data/
│   ├── Common/
│   │   ├── ApiProviderAttribute.cs
│   │   └── CommonEnums.cs
│   ├── DBContext.cs
│   ├── DesignTimeDbContextFactory.cs
│   ├── Models/
│   │   ├── APIKey.cs
│   │   ├── ApplicationSetting.cs
│   │   ├── RepoReference.cs
│   │   ├── SearchProviderToken.cs
│   │   └── SearchQuery.cs
│   └── UnsecuredAPIKeys.Data.csproj
└── UnsecuredAPIKeys.Providers/
    ├── AI Providers/
    │   ├── AnthropicProvider.cs
    │   ├── GoogleProvider.cs
    │   └── OpenAIProvider.cs
    ├── ApiProviderRegistry.cs
    ├── Common/
    │   └── ValidationResult.cs
    ├── Search Providers/
    │   └── GitHubSearchProvider.cs
    ├── UnsecuredAPIKeys.Providers.csproj
    ├── _Base/
    │   └── BaseApiKeyProvider.cs
    └── _Interfaces/
        ├── IApiKeyProvider.cs
        └── ISearchProvider.cs

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto

###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs     diff=csharp

###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following 
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln       merge=binary
#*.csproj    merge=binary
#*.vbproj    merge=binary
#*.vcxproj   merge=binary
#*.vcproj    merge=binary
#*.dbproj    merge=binary
#*.fsproj    merge=binary
#*.lsproj    merge=binary
#*.wixproj   merge=binary
#*.modelproj merge=binary
#*.sqlproj   merge=binary
#*.wwaproj   merge=binary

###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg   binary
#*.png   binary
#*.gif   binary

###############################################################################
# diff behavior for common document formats
# 
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the 
# entries below.
###############################################################################
#*.doc   diff=astextplain
#*.DOC   diff=astextplain
#*.docx  diff=astextplain
#*.DOCX  diff=astextplain
#*.dot   diff=astextplain
#*.DOT   diff=astextplain
#*.pdf   diff=astextplain
#*.PDF   diff=astextplain
#*.rtf   diff=astextplain
#*.RTF   diff=astextplain


================================================
FILE: .github/workflows/claude-code-review.yml
================================================
name: Claude Code Review

on:
  pull_request:
    types: [opened, synchronize]
    # Optional: Only run on specific file changes
    # paths:
    #   - "src/**/*.ts"
    #   - "src/**/*.tsx"
    #   - "src/**/*.js"
    #   - "src/**/*.jsx"

jobs:
  claude-review:
    # Optional: Filter by PR author
    # if: |
    #   github.event.pull_request.user.login == 'external-contributor' ||
    #   github.event.pull_request.user.login == 'new-developer' ||
    #   github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'

    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
      issues: read
      id-token: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - name: Run Claude Code Review
        id: claude-review
        uses: anthropics/claude-code-action@v1
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          prompt: |
            REPO: ${{ github.repository }}
            PR NUMBER: ${{ github.event.pull_request.number }}

            Please review this pull request and provide feedback on:
            - Code quality and best practices
            - Potential bugs or issues
            - Performance considerations
            - Security concerns
            - Test coverage

            Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.

            Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.

          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
          # or https://code.claude.com/docs/en/cli-reference for available options
          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:*)"'



================================================
FILE: .github/workflows/claude.yml
================================================
name: Claude Code

on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
  issues:
    types: [opened, assigned]
  pull_request_review:
    types: [submitted]

jobs:
  claude:
    if: |
      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
      issues: read
      id-token: write
      actions: read # Required for Claude to read CI results on PRs
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - name: Run Claude Code
        id: claude
        uses: anthropics/claude-code-action@v1
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

          # This is an optional setting that allows Claude to read CI results on PRs
          additional_permissions: |
            actions: read

          # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
          # prompt: 'Update the pull request description to include a summary of changes.'

          # Optional: Add claude_args to customize behavior and configuration
          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
          # or https://code.claude.com/docs/en/cli-reference for available options
          # claude_args: '--allowed-tools Bash(gh pr:*)'



================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

# Mono auto generated files
mono_crash.*

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/

# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

# Visual Studio 2017 auto generated files
Generated\ Files/

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml

# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c

# Benchmark Results
BenchmarkDotNet.Artifacts/

# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/

# ASP.NET Scaffolding
ScaffoldingReadMe.txt

# StyleCop
StyleCopReport.xml

# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc

# Chutzpah Test files
_Chutzpah*

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb

# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap

# Visual Studio Trace Files
*.e2e

# TFS 2012 Local Workspace
$tf/

# Guidance Automation Toolkit
*.gpState

# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user

# TeamCity is a build add-in
_TeamCity*

# DotCover is a Code Coverage Tool
*.dotCover

# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json

# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info

# Visual Studio code coverage results
*.coverage
*.coveragexml

# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*

# MightyMoose
*.mm.*
AutoTest.Net/

# Web workbench (sass)
.sass-cache/

# Installshield output folder
[Ee]xpress/

# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html

# Click-Once directory
publish/

# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj

# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/

# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets

# Microsoft Azure Build Output
csx/
*.build.csdef

# Microsoft Azure Emulator
ecf/
rcf/

# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload

# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/

# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs

# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk

# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/

# RIA/Silverlight projects
Generated_Code/

# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak

# SQL Server files
*.mdf
*.ldf
*.ndf

# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl

# Microsoft Fakes
FakesAssemblies/

# GhostDoc plugin setting file
*.GhostDoc.xml

# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/

# Visual Studio 6 build log
*.plg

# Visual Studio 6 workspace options file
*.opt

# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw

# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions

# Paket dependency manager
.paket/paket.exe
paket-files/

# FAKE - F# Make
.fake/

# CodeRush personal settings
.cr/personal

# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc

# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config

# Tabs Studio
*.tss

# Telerik's JustMock configuration file
*.jmconfig

# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs

# OpenCover UI analysis results
OpenCover/

# Azure Stream Analytics local run output
ASALocalRun/

# MSBuild Binary and Structured Log
*.binlog

# NVidia Nsight GPU debugger configuration file
*.nvuser

# MFractors (Xamarin productivity tool) working folder
.mfractor/

# Local History for Visual Studio
.localhistory/

# BeatPulse healthcheck temp database
healthchecksdb

# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/

# Ionide (cross platform F# VS Code tools) working folder
.ionide/

# Fody - auto-generated XML schema
FodyWeavers.xsd

# Claude Code
.claude/
CLAUDE.md

# Environment files
**/.env
**/.env.*
!**/.env.example

# SQLite databases
*.db
*.sqlite
*.sqlite3

# Local config (users will copy from .example)
**/appsettings.json
!**/appsettings.example.json
**/appsettings.Development.json
**/appsettings.Production.json

================================================
FILE: CHANGELOG.md
================================================
# Changelog

All notable changes to UnsecuredAPIKeys Open Source.

## [1.0.0] - 2025-12-09 - Lite Version Release

This release transforms the project from a full-featured web platform into a streamlined CLI tool for local use.

### Why This Change?

The original open-source release included the full platform architecture (WebAPI, UI, PostgreSQL, 15+ providers). This created barriers for users who just wanted to:
- Learn about API key security
- Run simple searches locally
- Understand how key discovery works

The lite version removes these barriers while the full platform remains available at [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com).

---

### Added

#### New CLI Application (`UnsecuredAPIKeys.CLI/`)
- **Menu-driven interface** using Spectre.Console for rich terminal UI
- **ScraperService** - Searches GitHub for exposed API keys continuously
- **VerifierService** - Maintains exactly 50 valid keys with automatic re-verification
  - **Fallback validation**: Tries multiple providers when assigned provider fails
  - **Auto-reclassification**: Updates key's ApiType if different provider validates it
- **DatabaseService** - Handles SQLite initialization, statistics, and exports
- **Constants.cs** - Centralized limits (`MAX_VALID_KEYS = 50`) and app info
- **appsettings.example.json** - Configuration template for self-hosting

#### Documentation
- **CHANGELOG.md** - This file
- **Badges** in README (GitHub Stars, .NET 10, License)
- **Stars thank you** message for community support
- **Self-hosting sections**: Database management, Search Queries, Rate Limiting, Troubleshooting
- **Platform support** documented (Windows, macOS, Linux)

#### Default Search Queries (Auto-seeded)
- OpenAI: `sk-proj-`, `sk-or-v1-`, `OPENAI_API_KEY`, `openai.api_key`
- Anthropic: `sk-ant-api`, `ANTHROPIC_API_KEY`, `anthropic_api_key`
- Google: `AIzaSy`, `GOOGLE_API_KEY`, `gemini_api_key`

---

### Changed

#### Database: PostgreSQL → SQLite
- **Before**: Required PostgreSQL server, connection strings, migrations
- **After**: Single `unsecuredapikeys.db` file, auto-created on first run
- No migrations needed - uses `EnsureCreated()` for simplicity
- Package change: `Npgsql.EntityFrameworkCore.PostgreSQL` → `Microsoft.EntityFrameworkCore.Sqlite`

#### Providers: 15+ → 3
- **Kept**: OpenAI, Anthropic, Google AI
- **Removed**: Cohere, DeepSeek, ElevenLabs, Groq, HuggingFace, MistralAI, OpenRouter, PerplexityAI, Replicate, StabilityAI, TogetherAI

#### Search Providers: 3 → 1
- **Kept**: GitHub (via Octokit)
- **Removed**: GitLab, SourceGraph

#### Architecture: Web Platform → CLI Tool
- **Before**: WebAPI + Next.js UI + Separate Bots + PostgreSQL
- **After**: Single CLI application + SQLite

#### Valid Key Limit
- **Before**: Configurable/unlimited
- **After**: Hard cap of 50 keys (enforced in `LiteLimits.MAX_VALID_KEYS`)

---

### Removed

#### Projects Deleted
| Project | Description |
|---------|-------------|
| `UnsecuredAPIKeys.WebAPI/` | REST API, SignalR hub, controllers |
| `UnsecuredAPIKeys.UI/` | Next.js frontend, React components |
| `UnsecuredAPIKeys.Bots.Scraper/` | Standalone scraper service |
| `UnsecuredAPIKeys.Bots.Verifier/` | Standalone verifier service |
| `UnsecuredAPIKeys.Common/` | Shared utilities (was empty) |

#### AI Providers Removed (11)
- CohereProvider.cs
- DeepSeekProvider.cs
- ElevenLabsProvider.cs
- GroqProvider.cs
- HuggingFaceProvider.cs
- MistralAIProvider.cs
- OpenRouterProvider.cs
- PerplexityAIProvider.cs
- ReplicateProvider.cs
- StabilityAIProvider.cs
- TogetherAIProvider.cs

#### Search Providers Removed (2)
- GitLabSearchProvider.cs
- SourceGraphSearchProvider.cs

#### Services Removed
- GitHubIssueService.cs (auto issue creation)
- SnitchLeaderboardService.cs (community leaderboard)
- UserModerationService.cs (user bans)

#### Database Models Removed (15)
| Model | Purpose |
|-------|---------|
| DiscordUser | Discord OAuth integration |
| UserSession | Session management |
| UserBan | User moderation |
| DonationTracking | PayPal donations |
| DonationSupporter | Donor recognition |
| IssueSubmissionTracking | GitHub issue automation |
| IssueVerification | Issue verification flow |
| SnitchLeaderboard | Community rankings |
| VerificationBatch | Batch job tracking |
| VerificationBatchResult | Batch results |
| KeyInvalidation | Key lifecycle tracking |
| KeyRotation | Key rotation events |
| PatternEffectiveness | Search pattern analytics |
| Proxy | Proxy rotation support |
| RateLimitLog | API rate limit tracking |

#### Database Migrations Removed (30+ files)
All PostgreSQL migrations deleted - SQLite uses runtime schema creation.

#### Configuration Files Removed
- `.dockerignore`
- `.vite/` directories
- `Deploy-VerificationBot.ps1`
- `package.json`, `package-lock.json`
- `analyze-unmatched-keys.ps1`
- `check-unmatched-keys.ps1`
- All `appsettings.json` files (kept `.example` versions)

#### Features Not in Lite Version
- Web UI dashboard
- Real-time SignalR updates
- Discord OAuth login
- PayPal donation integration
- GitHub issue auto-creation
- Snitch leaderboard
- User bans/moderation
- Proxy rotation
- Rate limit tracking tables
- Multi-search-provider support
- Batch verification locking

---

### Security

- **Enforced .gitignore** rules for:
  - `*.db`, `*.sqlite`, `*.sqlite3` (database files)
  - `.env`, `.env.*` (environment files)
  - `.claude/` (AI assistant files)
  - `appsettings.json` (local config with tokens)
- **Warning comments** added throughout code about not publishing results publicly
- **Security Warning** section prominent in README

---

### Migration Guide

#### For Users of the Old Version

The lite version is a **complete rewrite**. There is no migration path - it's designed for fresh local use.

If you need the original Web UI + WebAPI architecture:
- Check the [`legacy_ui`](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/tree/legacy_ui) branch (no longer maintained)

If you need the full platform features:
- Visit [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com)

#### For Contributors

Open PRs against the old architecture are now outdated:
- PR #5 (Docker Compose + Postgres) - No longer applicable
- PR #8 (Next.js dependency bump) - UI removed
- PR #9 (start.ps1) - Empty PR

New contributions should target the CLI architecture.

---

### Lite vs Full Comparison

| Feature | Lite (This Repo) | Full Version |
|---------|------------------|--------------|
| Search Providers | GitHub | GitHub, GitLab, SourceGraph |
| API Providers | 3 (OpenAI, Anthropic, Google) | 15+ |
| Valid Key Cap | 50 | Higher limits |
| Interface | CLI | Web UI + API |
| Database | SQLite (local file) | PostgreSQL |
| Real-time Updates | No | SignalR |
| Community Features | No | Leaderboard, Discord |
| Self-hosted | Yes | Yes (complex) |
| Beginner Friendly | Yes | No |

---

### Technical Details

#### Dependencies (CLI)
```xml
<PackageReference Include="Spectre.Console" Version="0.49.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
<PackageReference Include="Octokit" Version="13.0.1" />
```

#### Rate Limits (Built-in)
| Operation | Delay |
|-----------|-------|
| Between searches | 5,000ms |
| Between verifications | 1,000ms |
| Verification batch size | 10 keys |

#### File Structure
```
UnsecuredAPIKeys-OpenSource/
├── UnsecuredAPIKeys.CLI/
│   ├── Program.cs              # Entry point, menu UI
│   ├── Constants.cs            # LiteLimits, AppInfo
│   ├── Services/
│   │   ├── ScraperService.cs   # GitHub search
│   │   ├── VerifierService.cs  # Key validation
│   │   └── DatabaseService.cs  # SQLite ops
│   └── appsettings.example.json
├── UnsecuredAPIKeys.Data/
│   ├── DBContext.cs            # EF Core context
│   ├── Models/                 # 5 essential models
│   └── Common/CommonEnums.cs   # Simplified enums
├── UnsecuredAPIKeys.Providers/
│   ├── AI Providers/           # 3 providers
│   ├── Search Providers/       # GitHub only
│   └── _Base/, _Interfaces/    # Framework
├── CHANGELOG.md
├── CLAUDE.md
├── LICENSE
└── README.md
```

---

## [1.0.0] - 2024-07-21 - Initial Open Source Release

Initial release of the full UnsecuredAPIKeys platform as open source.

### Included
- WebAPI with REST endpoints and SignalR
- Next.js frontend with HeroUI components
- PostgreSQL database with EF Core migrations
- 15+ API key validation providers
- 3 search providers (GitHub, GitLab, SourceGraph)
- Discord OAuth integration
- PayPal donation integration
- Automated scraper and verifier bots
- Snitch leaderboard
- User moderation system

---

**Full Version**: [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com)


================================================
FILE: LICENSE
================================================
UnsecuredAPIKeys Open Source License
Based on MIT License with Attribution Requirements

Copyright (c) 2025 TSCarterJr

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

1. The above copyright notice and this permission notice shall be included in all
   copies or substantial portions of the Software.

2. MANDATORY UI ATTRIBUTION REQUIREMENT:
   Any software, application, or system that incorporates, uses, modifies, or 
   derives from ANY portion of this Software (including but not limited to APIs, 
   backend processes, algorithms, data structures, bots, validation logic, or any 
   other component) and provides a public-facing user interface MUST display a 
   prominent link to the original project repository: 
   https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource

   This attribution must be:
   - Clearly visible in the user interface
   - Accessible from the main/home page or footer
   - Contain the text "Based on UnsecuredAPIKeys Open Source" or similar
   - Link directly to https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource
   
   This requirement applies regardless of the extent of modification, whether the
   entire codebase is used or only small portions, and whether the derivative work
   is commercial or non-commercial.

3. SCOPE OF APPLICATION:
   This attribution requirement applies to any use of the Software, including but
   not limited to:
   - Using the backend APIs or database schemas
   - Implementing the validation algorithms or provider patterns
   - Using the bot architecture or scraping logic  
   - Incorporating the data models or business logic
   - Using any code, concepts, or implementations from this project

4. EDUCATIONAL PURPOSE AND USER RESPONSIBILITY:
   This Software is provided for EDUCATIONAL and SECURITY RESEARCH purposes only.

   By using this Software, you acknowledge and agree that:
   - You are solely responsible for your use of this Software
   - You will comply with all applicable laws and regulations
   - You will not use this Software for unauthorized access to any systems
   - You will not use discovered API keys for any malicious or illegal purposes
   - You will practice responsible disclosure when discovering exposed credentials

   The author(s) and copyright holder(s) expressly disclaim any responsibility for:
   - How you choose to use this Software
   - Any actions you take based on information obtained through this Software
   - Any legal consequences arising from your use or misuse of this Software
   - Any damages caused by unauthorized or unethical use of this Software

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

USE OF THIS SOFTWARE FOR ANY ILLEGAL, UNAUTHORIZED, OR UNETHICAL PURPOSES IS
STRICTLY PROHIBITED. THE USER ASSUMES ALL RISKS AND FULL RESPONSIBILITY FOR
THEIR ACTIONS.

VIOLATION OF THE ATTRIBUTION REQUIREMENT CONSTITUTES COPYRIGHT INFRINGEMENT
AND BREACH OF LICENSE TERMS, SUBJECT TO LEGAL ACTION AND DAMAGES.


================================================
FILE: README.md
================================================
# UnsecuredAPIKeys Lite

[![GitHub Stars](https://img.shields.io/github/stars/TSCarterJr/UnsecuredAPIKeys-OpenSource?style=social)](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource)
[![.NET 10](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/download/dotnet/10.0)
[![License](https://img.shields.io/badge/License-Custom-blue)](LICENSE)
![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)

> **Thank you to everyone who has starred this project!** Your support helps raise awareness about API key security and encourages responsible disclosure practices.

> **Full Version Available:** [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com)
>
> The full version offers: Web UI, all API providers, community features, and more.

A command-line tool for discovering and validating exposed API keys on GitHub. This lite version focuses on educational and security awareness purposes.

## Lite Version Limits

| Feature | Lite (This Repo) | Full Version |
|---------|------------------|--------------|
| Search Provider | GitHub only | GitHub, GitLab, SourceGraph |
| API Providers | OpenAI, Anthropic, Google | 15+ providers |
| Valid Key Cap | 50 keys | Higher limits |
| Interface | CLI | Web UI + API |
| Database | SQLite (local) | PostgreSQL |

## ⚠️ Educational Purpose Only

This tool is for **educational and security awareness purposes only**.

- **Learn** how API keys get exposed in public repositories
- **Understand** the importance of secret management
- **Report** exposed keys responsibly to repository owners
- **Never** use discovered keys for unauthorized access

**Do NOT publish your database or results publicly.** This would expose working API keys to malicious actors.

## Quick Start

### 1. Download

Download the latest release for your platform from [**Releases**](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/releases):

| Platform | File |
|----------|------|
| Windows | `unsecuredapikeys-win-x64.exe` |
| Linux | `unsecuredapikeys-linux-x64` |

**No .NET runtime required** - these are self-contained executables.

### 2. Run

**Windows:**
```bash
.\unsecuredapikeys-win-x64.exe
```

**Linux:**
```bash
chmod +x unsecuredapikeys-linux-x64
./unsecuredapikeys-linux-x64
```

### 3. Configure GitHub Token

On first run, go to **Configure Settings** > **Set GitHub Token**.

Create a token at: https://github.com/settings/tokens
Required scope: `public_repo`

### 4. Start Searching

- **Start Scraper**: Searches GitHub for exposed API keys (runs continuously)
- **Start Verifier**: Maintains up to 50 valid keys (re-checks as needed)
- **View Status**: Shows current statistics
- **Export Keys**: Export to JSON or CSV

### Building from Source (Optional)

If you prefer to build from source:

```bash
git clone https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource.git
cd UnsecuredAPIKeys-OpenSource
dotnet build
cd UnsecuredAPIKeys.CLI
dotnet run
```

## How It Works

### Scraper
1. Uses your GitHub token to search for common API key patterns
2. Extracts potential keys using regex patterns for OpenAI, Anthropic, and Google
3. Stores discovered keys in a local SQLite database

### Verifier
1. Validates discovered keys against the actual provider APIs
2. Maintains exactly 50 valid keys (lite limit)
3. Re-checks existing valid keys periodically
4. When a key becomes invalid, verifies new ones until back to 50

## Project Structure

```
UnsecuredAPIKeys-OpenSource/
├── UnsecuredAPIKeys.CLI/         # Main CLI application
├── UnsecuredAPIKeys.Data/        # SQLite database layer
├── UnsecuredAPIKeys.Providers/   # API validation providers
├── unsecuredapikeys.db           # SQLite database (auto-created)
└── README.md
```

## Prerequisites

- **.NET 10 SDK** - [Download here](https://dotnet.microsoft.com/download/dotnet/10.0)
- **GitHub Personal Access Token** - [Create here](https://github.com/settings/tokens)
- **Platform**: Windows, macOS, or Linux

## Supported Providers (Lite)

| Provider | Pattern Examples |
|----------|------------------|
| OpenAI | `sk-proj-*`, `sk-or-v1-*` |
| Anthropic | `sk-ant-api*` |
| Google AI | `AIzaSy*` |

## Configuration

Copy `appsettings.example.json` to `appsettings.json` and configure:

```json
{
  "GitHub": {
    "Token": "ghp_YOUR_TOKEN"
  },
  "Database": {
    "Path": "unsecuredapikeys.db"
  }
}
```

Or configure directly via the CLI menu.

## Database

The SQLite database (`unsecuredapikeys.db`) is auto-created on first run in the working directory.

| Action | How |
|--------|-----|
| **Location** | Same folder as the executable |
| **Reset** | Delete `unsecuredapikeys.db` and restart |
| **Backup** | Copy the `.db` file |
| **View data** | Use any SQLite browser (e.g., DB Browser for SQLite) |

## Search Queries

On first run, default search queries are automatically seeded:

- `sk-proj-`, `sk-or-v1-`, `OPENAI_API_KEY` (OpenAI)
- `sk-ant-api`, `ANTHROPIC_API_KEY` (Anthropic)
- `AIzaSy`, `GOOGLE_API_KEY` (Google)

The scraper rotates through these queries automatically.

## Rate Limiting

Built-in delays prevent API abuse:

| Operation | Delay |
|-----------|-------|
| Between searches | 5 seconds |
| Between verifications | 1 second |
| Batch size | 10 keys |

GitHub's API allows ~30 searches/minute with authentication.

## Troubleshooting

| Issue | Solution |
|-------|----------|
| "No GitHub token configured" | Go to Configure Settings > Set GitHub Token |
| "Rate limit exceeded" | Wait 60 seconds, or use a different token |
| Build fails | Ensure .NET 10 SDK is installed: `dotnet --version` |
| No keys found | Check your token has `public_repo` scope |
| Database locked | Close other apps using the .db file |

## Legal & Ethical Use

- **Educational Purpose**: This tool demonstrates API security vulnerabilities
- **Responsible Use**: Only use for legitimate security research
- **No Abuse**: Do not use discovered keys for unauthorized access
- **Compliance**: Follow all applicable laws and terms of service

## License

This project uses a **custom attribution-required license** based on MIT.

### Attribution Required

Any use of this code requires visible attribution:
- Display: "Based on UnsecuredAPIKeys Open Source"
- Link to: https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource
- Must be visible in UI/documentation

See [LICENSE](LICENSE) for full details.

## Legacy UI Version

Looking for the original Web UI + WebAPI architecture? Check the [`legacy_ui`](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/tree/legacy_ui) branch.

> **Note**: The legacy branch is no longer actively maintained. For the full-featured web experience, use [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com).

## Full Version

For higher limits, more providers, web interface, and community features:

**[www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com)**

---

**Remember**: Use responsibly and in accordance with applicable laws.


================================================
FILE: UnsecuredAPIKeys-OpenSource.sln
================================================

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35931.197
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnsecuredAPIKeys.Data", "UnsecuredAPIKeys.Data\UnsecuredAPIKeys.Data.csproj", "{F4BC4B91-7945-4171-BA6E-BECFEF771A21}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnsecuredAPIKeys.Providers", "UnsecuredAPIKeys.Providers\UnsecuredAPIKeys.Providers.csproj", "{7389D703-A7C1-4853-A857-C1C97C1C6178}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnsecuredAPIKeys.CLI", "UnsecuredAPIKeys.CLI\UnsecuredAPIKeys.CLI.csproj", "{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU
		Debug|x64 = Debug|x64
		Debug|x86 = Debug|x86
		Release|Any CPU = Release|Any CPU
		Release|x64 = Release|x64
		Release|x86 = Release|x86
	EndGlobalSection
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
		{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x64.ActiveCfg = Debug|Any CPU
		{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x64.Build.0 = Debug|Any CPU
		{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x86.ActiveCfg = Debug|Any CPU
		{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x86.Build.0 = Debug|Any CPU
		{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|Any CPU.Build.0 = Release|Any CPU
		{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x64.ActiveCfg = Release|Any CPU
		{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x64.Build.0 = Release|Any CPU
		{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x86.ActiveCfg = Release|Any CPU
		{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x86.Build.0 = Release|Any CPU
		{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x64.ActiveCfg = Debug|Any CPU
		{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x64.Build.0 = Debug|Any CPU
		{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x86.ActiveCfg = Debug|Any CPU
		{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x86.Build.0 = Debug|Any CPU
		{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|Any CPU.Build.0 = Release|Any CPU
		{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x64.ActiveCfg = Release|Any CPU
		{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x64.Build.0 = Release|Any CPU
		{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x86.ActiveCfg = Release|Any CPU
		{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x86.Build.0 = Release|Any CPU
		{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x64.ActiveCfg = Debug|Any CPU
		{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x64.Build.0 = Debug|Any CPU
		{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x86.ActiveCfg = Debug|Any CPU
		{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x86.Build.0 = Debug|Any CPU
		{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|Any CPU.Build.0 = Release|Any CPU
		{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x64.ActiveCfg = Release|Any CPU
		{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x64.Build.0 = Release|Any CPU
		{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x86.ActiveCfg = Release|Any CPU
		{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x86.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE
	EndGlobalSection
	GlobalSection(ExtensibilityGlobals) = postSolution
		SolutionGuid = {FEDEE8B0-6BD4-48B2-BA35-31BFC542BC0D}
	EndGlobalSection
EndGlobal


================================================
FILE: UnsecuredAPIKeys.CLI/Constants.cs
================================================
namespace UnsecuredAPIKeys.CLI;

/// <summary>
/// Constants for the lite version of UnsecuredAPIKeys.
/// Full version available at www.UnsecuredAPIKeys.com
/// </summary>
public static class LiteLimits
{
    /// <summary>
    /// Maximum valid keys for lite version.
    ///
    /// WARNING: If you modify this limit, do NOT publish your database
    /// or results to a public repository. This would expose working API
    /// keys to malicious actors who could abuse them.
    ///
    /// For higher limits, use www.UnsecuredAPIKeys.com
    /// </summary>
    public const int MAX_VALID_KEYS = 50;

    /// <summary>
    /// Delay between verification batches (milliseconds).
    /// </summary>
    public const int VERIFICATION_DELAY_MS = 1000;

    /// <summary>
    /// Delay between GitHub search queries (milliseconds).
    /// </summary>
    public const int SEARCH_DELAY_MS = 5000;

    /// <summary>
    /// Number of keys to process per verification batch.
    /// </summary>
    public const int VERIFICATION_BATCH_SIZE = 10;
}

/// <summary>
/// Application-wide constants.
/// </summary>
public static class AppInfo
{
    public const string Name = "UnsecuredAPIKeys Lite";
    public const string Version = "1.0.0";
    public const string FullVersionUrl = "www.UnsecuredAPIKeys.com";
    public const string DatabaseName = "unsecuredapikeys.db";
}


================================================
FILE: UnsecuredAPIKeys.CLI/Program.cs
================================================
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Spectre.Console;
using UnsecuredAPIKeys.CLI;
using UnsecuredAPIKeys.CLI.Services;
using UnsecuredAPIKeys.Data;

// Initialize services
var services = new ServiceCollection();
services.AddLogging(builder => builder
    .SetMinimumLevel(LogLevel.Warning)
    .AddConsole());
services.AddHttpClient();

await using var serviceProvider = services.BuildServiceProvider();
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();

// Initialize database
var dbService = new DatabaseService(AppInfo.DatabaseName);
DBContext? dbContext = null;

try
{
    dbContext = await dbService.InitializeDatabaseAsync();
}
catch (Exception ex)
{
    AnsiConsole.MarkupLine($"[red]Failed to initialize database: {Markup.Escape(ex.Message)}[/]");
    return;
}

// Display banner
DisplayBanner();

// Main menu loop
var running = true;
while (running)
{
    var choice = AnsiConsole.Prompt(
        new SelectionPrompt<string>()
            .Title("[yellow]What would you like to do?[/]")
            .PageSize(10)
            .AddChoices(new[]
            {
                "1. Start Scraper (search GitHub for keys)",
                "2. Start Verifier (maintain valid keys)",
                "3. View Status",
                "4. Configure Settings",
                "5. Export Keys",
                "6. Exit"
            }));

    AnsiConsole.WriteLine();

    switch (choice[0])
    {
        case '1':
            await RunScraperAsync(dbContext, httpClientFactory);
            break;
        case '2':
            await RunVerifierAsync(dbContext, httpClientFactory);
            break;
        case '3':
            await ShowStatusAsync(dbContext, dbService);
            break;
        case '4':
            await ConfigureSettingsAsync(dbContext, dbService);
            break;
        case '5':
            await ExportKeysAsync(dbContext, dbService);
            break;
        case '6':
            running = false;
            break;
    }

    if (running)
    {
        AnsiConsole.WriteLine();
        AnsiConsole.MarkupLine("[dim]Press any key to continue...[/]");
        Console.ReadKey(true);
        AnsiConsole.Clear();
        DisplayBanner();
    }
}

AnsiConsole.MarkupLine("[green]Goodbye![/]");
dbContext?.Dispose();

// === Helper Methods ===

void DisplayBanner()
{
    AnsiConsole.Write(
        new FigletText(AppInfo.Name)
            .LeftJustified()
            .Color(Color.Cyan1));

    AnsiConsole.Write(new Rule("[dim]Lite Version[/]").RuleStyle("grey").LeftJustified());
    AnsiConsole.MarkupLine($"[dim]Full version: [link]{Markup.Escape(AppInfo.FullVersionUrl)}[/][/]");
    AnsiConsole.MarkupLine($"[dim]Valid key limit: [yellow]{LiteLimits.MAX_VALID_KEYS}[/][/]");
    AnsiConsole.WriteLine();

    // Educational purpose notice
    var warningPanel = new Panel(
        "[yellow]This tool is for EDUCATIONAL PURPOSES ONLY.[/]\n\n" +
        "If you discover exposed API keys, please help secure them:\n" +
        "  [green]1.[/] Open an issue on the repository to notify the owner\n" +
        "  [green]2.[/] Never use keys for unauthorized access\n" +
        "  [green]3.[/] Do NOT publish your results publicly\n\n" +
        "[dim]Help make the internet more secure by reporting, not exploiting.[/]")
        .Header("[yellow]Educational Use Only[/]")
        .Border(BoxBorder.Rounded)
        .BorderColor(Color.Yellow);

    AnsiConsole.Write(warningPanel);
    AnsiConsole.WriteLine();
}

async Task RunScraperAsync(DBContext db, IHttpClientFactory factory)
{
    AnsiConsole.Write(new Rule("[cyan]GitHub Scraper[/]").RuleStyle("cyan"));
    AnsiConsole.MarkupLine("[dim]Searches GitHub for exposed API keys. Runs continuously.[/]");
    AnsiConsole.MarkupLine("[dim]Press [yellow]Ctrl+C[/] to stop.[/]");
    AnsiConsole.WriteLine();

    using var cts = new CancellationTokenSource();
    Console.CancelKeyPress += (s, e) =>
    {
        e.Cancel = true;
        cts.Cancel();
        AnsiConsole.MarkupLine("\n[yellow]Stopping scraper...[/]");
    };

    var scraper = new ScraperService(db, factory);
    await scraper.RunAsync(cts.Token);
}

async Task RunVerifierAsync(DBContext db, IHttpClientFactory factory)
{
    AnsiConsole.Write(new Rule("[green]Key Verifier[/]").RuleStyle("green"));
    AnsiConsole.MarkupLine($"[dim]Maintains up to [yellow]{LiteLimits.MAX_VALID_KEYS}[/] valid keys.[/]");
    AnsiConsole.MarkupLine("[dim]Re-checks valid keys and verifies new ones as needed.[/]");
    AnsiConsole.MarkupLine("[dim]Press [yellow]Ctrl+C[/] to stop.[/]");
    AnsiConsole.WriteLine();

    using var cts = new CancellationTokenSource();
    Console.CancelKeyPress += (s, e) =>
    {
        e.Cancel = true;
        cts.Cancel();
        AnsiConsole.MarkupLine("\n[yellow]Stopping verifier...[/]");
    };

    var verifier = new VerifierService(db, factory);
    await verifier.RunAsync(cts.Token);
}

async Task ShowStatusAsync(DBContext db, DatabaseService dbService)
{
    AnsiConsole.Write(new Rule("[blue]Current Status[/]").RuleStyle("blue"));

    var stats = await AnsiConsole.Status()
        .Spinner(Spinner.Known.Dots)
        .SpinnerStyle(Style.Parse("blue"))
        .StartAsync("Loading statistics...", async ctx =>
        {
            return await dbService.GetStatisticsAsync(db);
        });

    // Create status table
    var table = new Table()
        .Border(TableBorder.Rounded)
        .BorderColor(Color.Grey)
        .AddColumn(new TableColumn("[bold]Metric[/]").LeftAligned())
        .AddColumn(new TableColumn("[bold]Value[/]").RightAligned());

    table.AddRow("Total Keys Found", stats.TotalKeys.ToString());
    table.AddRow("Valid Keys", $"[green]{stats.ValidKeys}[/] / [yellow]{LiteLimits.MAX_VALID_KEYS}[/]");
    table.AddRow("Valid (No Credits)", $"[yellow]{stats.ValidNoCreditsKeys}[/]");
    table.AddRow("Invalid Keys", $"[red]{stats.InvalidKeys}[/]");
    table.AddRow("Pending Verification", $"[blue]{stats.UnverifiedKeys}[/]");
    table.AddRow(new Rule().RuleStyle("dim"));
    table.AddRow("OpenAI Keys", stats.OpenAIKeys.ToString());
    table.AddRow("Anthropic Keys", stats.AnthropicKeys.ToString());
    table.AddRow("Google Keys", stats.GoogleKeys.ToString());
    table.AddRow(new Rule().RuleStyle("dim"));
    table.AddRow("Database", $"[dim]{Markup.Escape(AppInfo.DatabaseName)}[/]");
    table.AddRow("GitHub Token", stats.HasGitHubToken ? "[green]Configured[/]" : "[red]Not configured[/]");

    AnsiConsole.Write(table);
}

async Task ConfigureSettingsAsync(DBContext db, DatabaseService dbService)
{
    AnsiConsole.Write(new Rule("[magenta]Configuration[/]").RuleStyle("magenta"));

    var configChoice = AnsiConsole.Prompt(
        new SelectionPrompt<string>()
            .Title("[yellow]What would you like to configure?[/]")
            .AddChoices(new[]
            {
                "1. Set GitHub Token",
                "2. View Current Settings",
                "3. Reset Database",
                "4. Back to Main Menu"
            }));

    switch (configChoice[0])
    {
        case '1':
            await SetGitHubTokenAsync(db, dbService);
            break;
        case '2':
            await ShowCurrentSettingsAsync(db, dbService);
            break;
        case '3':
            await ResetDatabaseAsync(dbService);
            break;
    }
}

async Task SetGitHubTokenAsync(DBContext db, DatabaseService dbService)
{
    AnsiConsole.MarkupLine("[dim]Enter your GitHub Personal Access Token.[/]");
    AnsiConsole.MarkupLine("[dim]Create one at: https:[[//]]github.com[[/]]settings[[/]]tokens[/]");
    AnsiConsole.MarkupLine("[dim]Required scopes: [yellow]public_repo[/] (for searching public repos)[/]");
    AnsiConsole.WriteLine();

    var token = AnsiConsole.Prompt(
        new TextPrompt<string>("[green]GitHub Token:[/]")
            .Secret());

    if (string.IsNullOrWhiteSpace(token))
    {
        AnsiConsole.MarkupLine("[red]Token cannot be empty.[/]");
        return;
    }

    // Validate token format
    if (!token.StartsWith("ghp_") && !token.StartsWith("github_pat_"))
    {
        var proceed = AnsiConsole.Confirm(
            "[yellow]Token doesn't match expected GitHub token format. Save anyway?[/]",
            false);

        if (!proceed) return;
    }

    await dbService.SaveGitHubTokenAsync(db, token);
    AnsiConsole.MarkupLine("[green]GitHub token saved successfully![/]");
}

async Task ShowCurrentSettingsAsync(DBContext db, DatabaseService dbService)
{
    var stats = await dbService.GetStatisticsAsync(db);

    var table = new Table()
        .Border(TableBorder.Rounded)
        .BorderColor(Color.Grey)
        .AddColumn("[bold]Setting[/]")
        .AddColumn("[bold]Value[/]");

    var dbPath = Path.Combine(Environment.CurrentDirectory, AppInfo.DatabaseName);
    table.AddRow("Database Path", Markup.Escape(dbPath));
    table.AddRow("GitHub Token", stats.HasGitHubToken ? "[green]Configured[/]" : "[red]Not configured[/]");
    table.AddRow("Max Valid Keys", LiteLimits.MAX_VALID_KEYS.ToString());
    table.AddRow("Supported Providers", "OpenAI, Anthropic, Google");

    AnsiConsole.Write(table);
}

async Task ResetDatabaseAsync(DatabaseService dbService)
{
    var confirm = AnsiConsole.Confirm(
        "[red]Are you sure you want to reset the database? All data will be lost![/]",
        false);

    if (!confirm)
    {
        AnsiConsole.MarkupLine("[dim]Database reset cancelled.[/]");
        return;
    }

    var doubleConfirm = AnsiConsole.Confirm(
        "[red]This action is irreversible. Are you absolutely sure?[/]",
        false);

    if (!doubleConfirm)
    {
        AnsiConsole.MarkupLine("[dim]Database reset cancelled.[/]");
        return;
    }

    await AnsiConsole.Status()
        .Spinner(Spinner.Known.Dots)
        .SpinnerStyle(Style.Parse("red"))
        .StartAsync("Resetting database...", async ctx =>
        {
            await dbService.ResetDatabaseAsync();
        });

    AnsiConsole.MarkupLine("[green]Database reset complete.[/]");
}

async Task ExportKeysAsync(DBContext db, DatabaseService dbService)
{
    AnsiConsole.Write(new Rule("[yellow]Export Keys[/]").RuleStyle("yellow"));

    var exportChoice = AnsiConsole.Prompt(
        new SelectionPrompt<string>()
            .Title("[yellow]Export format:[/]")
            .AddChoices(new[]
            {
                "1. JSON",
                "2. CSV",
                "3. Back to Main Menu"
            }));

    if (exportChoice[0] == '3') return;

    var validOnly = AnsiConsole.Confirm("Export only valid keys?", true);

    var format = exportChoice[0] == '1' ? "json" : "csv";
    var defaultFileName = exportChoice[0] == '1' ? "keys.json" : "keys.csv";
    var fileName = AnsiConsole.Prompt(
        new TextPrompt<string>("[green]Output file name:[/]")
            .DefaultValue(defaultFileName));

    await AnsiConsole.Status()
        .Spinner(Spinner.Known.Dots)
        .SpinnerStyle(Style.Parse("yellow"))
        .StartAsync($"Exporting to {Markup.Escape(fileName)}...", async ctx =>
        {
            await dbService.ExportKeysAsync(db, fileName, validOnly, format);
        });

    AnsiConsole.MarkupLine($"[green]Exported to [bold]{Markup.Escape(fileName)}[/][/]");
}


================================================
FILE: UnsecuredAPIKeys.CLI/Services/DatabaseService.cs
================================================
using Microsoft.EntityFrameworkCore;
using Spectre.Console;
using UnsecuredAPIKeys.Data;
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Data.Models;

namespace UnsecuredAPIKeys.CLI.Services;

/// <summary>
/// Service for database initialization and common operations.
/// </summary>
public class DatabaseService(string dbPath = "unsecuredapikeys.db")
{
    public async Task<DBContext> InitializeDatabaseAsync()
    {
        var dbContext = new DBContext(dbPath);

        // Ensure database is created
        await dbContext.Database.EnsureCreatedAsync();

        // Seed default data if needed
        await SeedDefaultDataAsync(dbContext);

        return dbContext;
    }

    private async Task SeedDefaultDataAsync(DBContext dbContext)
    {
        // Add default search queries if none exist
        if (!await dbContext.SearchQueries.AnyAsync())
        {
            var defaultQueries = new[]
            {
                // OpenAI patterns
                "sk-proj-",
                "sk-or-v1-",
                "sk-",
                "OPENAI_API_KEY",
                "openai.api_key",
                "chatgpt api key",
                "gpt-4 api key",

                // Anthropic patterns
                "sk-ant-api",
                "ANTHROPIC_API_KEY",
                "anthropic_api_key",
                "claude api key",

                // Google AI patterns
                "AIzaSy",
                "GOOGLE_API_KEY",
                "gemini_api_key",

                // Other AI providers (patterns only, validation limited to lite providers)
                "r8_",           // Replicate
                "fw_",           // Fireworks
                "hf_",           // HuggingFace
                "AI_API_KEY"     // Generic
            };

            foreach (var query in defaultQueries)
            {
                dbContext.SearchQueries.Add(new SearchQuery
                {
                    Query = query,
                    IsEnabled = true,
                    LastSearchUTC = DateTime.UtcNow.AddDays(-1)
                });
            }

            await dbContext.SaveChangesAsync();
            AnsiConsole.MarkupLine($"[dim]Added {defaultQueries.Length} default search queries.[/]");
        }
    }

    public async Task<Statistics> GetStatisticsAsync(DBContext dbContext)
    {
        var stats = new Statistics
        {
            TotalKeys = await dbContext.APIKeys.CountAsync(),
            ValidKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.Valid),
            InvalidKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.Invalid),
            UnverifiedKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.Unverified),
            ValidNoCreditsKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.ValidNoCredits),
            OpenAIKeys = await dbContext.APIKeys.CountAsync(k => k.ApiType == ApiTypeEnum.OpenAI),
            AnthropicKeys = await dbContext.APIKeys.CountAsync(k => k.ApiType == ApiTypeEnum.AnthropicClaude),
            GoogleKeys = await dbContext.APIKeys.CountAsync(k => k.ApiType == ApiTypeEnum.GoogleAI),
            HasGitHubToken = await dbContext.SearchProviderTokens
                .AnyAsync(t => t.IsEnabled && t.SearchProvider == SearchProviderEnum.GitHub)
        };

        return stats;
    }

    public async Task SaveGitHubTokenAsync(DBContext dbContext, string token)
    {
        var existing = await dbContext.SearchProviderTokens
            .FirstOrDefaultAsync(t => t.SearchProvider == SearchProviderEnum.GitHub);

        if (existing != null)
        {
            existing.Token = token;
            existing.IsEnabled = true;
        }
        else
        {
            dbContext.SearchProviderTokens.Add(new SearchProviderToken
            {
                Token = token,
                SearchProvider = SearchProviderEnum.GitHub,
                IsEnabled = true
            });
        }

        await dbContext.SaveChangesAsync();
    }

    public async Task ResetDatabaseAsync()
    {
        if (File.Exists(dbPath))
        {
            File.Delete(dbPath);
        }

        // Reinitialize
        await InitializeDatabaseAsync();
    }

    public async Task ExportKeysAsync(DBContext dbContext, string filePath, bool validOnly, string format)
    {
        var query = dbContext.APIKeys.AsQueryable();

        if (validOnly)
        {
            query = query.Where(k => k.Status == ApiStatusEnum.Valid || k.Status == ApiStatusEnum.ValidNoCredits);
        }

        var keys = await query
            .Include(k => k.References)
            .ToListAsync();

        if (format.ToLower() == "json")
        {
            await ExportAsJsonAsync(keys, filePath);
        }
        else
        {
            await ExportAsCsvAsync(keys, filePath);
        }
    }

    private async Task ExportAsJsonAsync(List<APIKey> keys, string filePath)
    {
        var exportData = keys.Select(k => new
        {
            k.Id,
            k.ApiKey,
            Type = k.ApiType.ToString(),
            Status = k.Status.ToString(),
            k.FirstFoundUTC,
            k.LastCheckedUTC,
            Sources = k.References.Select(r => new
            {
                r.RepoURL,
                r.RepoOwner,
                r.RepoName,
                r.FilePath,
                r.FoundUTC
            })
        });

        var json = System.Text.Json.JsonSerializer.Serialize(exportData, new System.Text.Json.JsonSerializerOptions
        {
            WriteIndented = true
        });

        await File.WriteAllTextAsync(filePath, json);
    }

    private async Task ExportAsCsvAsync(List<APIKey> keys, string filePath)
    {
        var lines = new List<string>
        {
            "Id,ApiKey,Type,Status,FirstFoundUTC,LastCheckedUTC,RepoURL"
        };

        foreach (var key in keys)
        {
            var repoUrl = key.References.FirstOrDefault()?.RepoURL ?? "";
            lines.Add($"{key.Id},\"{key.ApiKey}\",{key.ApiType},{key.Status},{key.FirstFoundUTC:O},{key.LastCheckedUTC:O},\"{repoUrl}\"");
        }

        await File.WriteAllLinesAsync(filePath, lines);
    }
}

public class Statistics
{
    public int TotalKeys { get; set; }
    public int ValidKeys { get; set; }
    public int InvalidKeys { get; set; }
    public int UnverifiedKeys { get; set; }
    public int ValidNoCreditsKeys { get; set; }
    public int OpenAIKeys { get; set; }
    public int AnthropicKeys { get; set; }
    public int GoogleKeys { get; set; }
    public bool HasGitHubToken { get; set; }
}


================================================
FILE: UnsecuredAPIKeys.CLI/Services/ScraperService.cs
================================================
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Spectre.Console;
using UnsecuredAPIKeys.Data;
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Data.Models;
using UnsecuredAPIKeys.Providers;
using UnsecuredAPIKeys.Providers._Interfaces;
using UnsecuredAPIKeys.Providers.Search_Providers;

namespace UnsecuredAPIKeys.CLI.Services;

/// <summary>
/// Scraper service for finding API keys on GitHub.
/// Lite version: GitHub only, 3 AI providers.
/// Full version: www.UnsecuredAPIKeys.com
/// </summary>
public class ScraperService
{
    private readonly DBContext _dbContext;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger<ScraperService>? _logger;
    private readonly IReadOnlyList<IApiKeyProvider> _providers;
    private CancellationTokenSource? _cancellationTokenSource;

    private int _newKeysFound;
    private int _duplicateKeysFound;

    public ScraperService(DBContext dbContext, IHttpClientFactory httpClientFactory, ILogger<ScraperService>? logger = null)
    {
        _dbContext = dbContext;
        _httpClientFactory = httpClientFactory;
        _logger = logger;
        _providers = ApiProviderRegistry.ScraperProviders;
    }

    public async Task RunAsync(CancellationToken cancellationToken)
    {
        _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        AnsiConsole.MarkupLine("[cyan]Starting GitHub scraper...[/]");
        AnsiConsole.MarkupLine($"[dim]Loaded {_providers.Count} API key providers[/]");

        foreach (var provider in _providers)
        {
            AnsiConsole.MarkupLine($"  [dim]- {Markup.Escape(provider.ProviderName)}[/]");
        }

        // Get GitHub token
        var token = await _dbContext.SearchProviderTokens
            .Where(t => t.IsEnabled && t.SearchProvider == SearchProviderEnum.GitHub)
            .FirstOrDefaultAsync(cancellationToken);

        if (token == null)
        {
            AnsiConsole.MarkupLine("[red]No GitHub token configured. Use 'Configure Settings' to add one.[/]");
            return;
        }

        // Run continuously
        while (!_cancellationTokenSource.Token.IsCancellationRequested)
        {
            try
            {
                await RunScrapingCycleAsync(token);

                if (_cancellationTokenSource.Token.IsCancellationRequested)
                    break;

                // Wait before next cycle
                AnsiConsole.MarkupLine($"[dim]Waiting {LiteLimits.SEARCH_DELAY_MS / 1000}s before next search...[/]");
                await Task.Delay(LiteLimits.SEARCH_DELAY_MS, _cancellationTokenSource.Token);

                // Reset counters
                _newKeysFound = 0;
                _duplicateKeysFound = 0;
            }
            catch (OperationCanceledException)
            {
                break;
            }
            catch (Exception ex)
            {
                AnsiConsole.MarkupLine($"[red]Error during scraping: {Markup.Escape(ex.Message)}[/]");
                _logger?.LogError(ex, "Scraping cycle error");
                await Task.Delay(5000, _cancellationTokenSource.Token);
            }
        }

        AnsiConsole.MarkupLine("[green]Scraper stopped.[/]");
    }

    private async Task RunScrapingCycleAsync(SearchProviderToken token)
    {
        // Get next query to process
        var query = await _dbContext.SearchQueries
            .Where(x => x.IsEnabled && x.LastSearchUTC < DateTime.UtcNow.AddHours(-1))
            .OrderBy(x => x.LastSearchUTC)
            .FirstOrDefaultAsync(_cancellationTokenSource!.Token);

        if (query == null)
        {
            AnsiConsole.MarkupLine("[dim]No queries due for search. Waiting...[/]");
            return;
        }

        AnsiConsole.MarkupLine($"[cyan]Searching: {Markup.Escape(query.Query)}[/]");

        // Update last search time
        query.LastSearchUTC = DateTime.UtcNow;
        await _dbContext.SaveChangesAsync(_cancellationTokenSource.Token);

        // Search GitHub
        var searchProvider = new GitHubSearchProvider(_dbContext);
        IEnumerable<RepoReference>? results;

        try
        {
            results = await searchProvider.SearchAsync(query, token);
        }
        catch (Exception ex)
        {
            AnsiConsole.MarkupLine($"[red]Search error: {Markup.Escape(ex.Message)}[/]");
            return;
        }

        if (results == null)
        {
            AnsiConsole.MarkupLine("[yellow]No results from search.[/]");
            return;
        }

        var resultsList = results.ToList();
        AnsiConsole.MarkupLine($"[dim]Found {resultsList.Count} potential matches[/]");

        // Process each result
        await AnsiConsole.Progress()
            .StartAsync(async ctx =>
            {
                var task = ctx.AddTask($"[cyan]Processing results[/]", maxValue: resultsList.Count);

                foreach (var repoRef in resultsList)
                {
                    if (_cancellationTokenSource!.Token.IsCancellationRequested)
                        break;

                    await ProcessResultAsync(repoRef, token, query);
                    task.Increment(1);
                }
            });

        // Summary
        var table = new Table()
            .Border(TableBorder.Rounded)
            .AddColumn("[bold]Metric[/]")
            .AddColumn("[bold]Value[/]");

        table.AddRow("Query", Markup.Escape(query.Query));
        table.AddRow("Results Processed", resultsList.Count.ToString());
        table.AddRow("New Keys", $"[green]{_newKeysFound}[/]");
        table.AddRow("Duplicates", $"[dim]{_duplicateKeysFound}[/]");

        AnsiConsole.Write(table);
    }

    private async Task ProcessResultAsync(RepoReference repoRef, SearchProviderToken token, SearchQuery query)
    {
        try
        {
            // Get file content
            var content = await FetchFileContentAsync(repoRef, token);
            if (string.IsNullOrEmpty(content))
                return;

            // Search for API keys using all provider patterns
            foreach (var provider in _providers)
            {
                foreach (var pattern in provider.RegexPatterns)
                {
                    var regex = new System.Text.RegularExpressions.Regex(pattern);
                    var matches = regex.Matches(content);

                    foreach (System.Text.RegularExpressions.Match match in matches)
                    {
                        var apiKey = match.Value;

                        // Check if already exists
                        var exists = await _dbContext.APIKeys
                            .AnyAsync(k => k.ApiKey == apiKey, _cancellationTokenSource!.Token);

                        if (exists)
                        {
                            Interlocked.Increment(ref _duplicateKeysFound);
                            continue;
                        }

                        // Add new key
                        var newKey = new APIKey
                        {
                            ApiKey = apiKey,
                            ApiType = provider.ApiType,
                            Status = ApiStatusEnum.Unverified,
                            SearchProvider = SearchProviderEnum.GitHub,
                            FirstFoundUTC = DateTime.UtcNow,
                            LastFoundUTC = DateTime.UtcNow
                        };

                        // Add repo reference
                        repoRef.SearchQueryId = query.Id;
                        repoRef.FoundUTC = DateTime.UtcNow;
                        repoRef.Provider = "GitHub";
                        newKey.References.Add(repoRef);

                        _dbContext.APIKeys.Add(newKey);
                        await _dbContext.SaveChangesAsync(_cancellationTokenSource!.Token);

                        Interlocked.Increment(ref _newKeysFound);
                        AnsiConsole.MarkupLine($"[green]+ New {Markup.Escape(provider.ProviderName)} key found![/]");
                    }
                }
            }
        }
        catch (Exception ex)
        {
            _logger?.LogWarning(ex, "Error processing result: {Url}", repoRef.FileURL);
        }
    }

    private async Task<string?> FetchFileContentAsync(RepoReference repoRef, SearchProviderToken token)
    {
        try
        {
            using var client = _httpClientFactory.CreateClient();
            client.DefaultRequestHeaders.UserAgent.ParseAdd("UnsecuredAPIKeys-Lite/1.0");
            client.DefaultRequestHeaders.Authorization =
                new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token);

            // Build raw content URL from repo info
            // Format: https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}
            string? url = null;

            if (!string.IsNullOrEmpty(repoRef.RepoOwner) &&
                !string.IsNullOrEmpty(repoRef.RepoName) &&
                !string.IsNullOrEmpty(repoRef.FilePath))
            {
                var branch = repoRef.Branch ?? "main";
                url = $"https://raw.githubusercontent.com/{repoRef.RepoOwner}/{repoRef.RepoName}/{branch}/{repoRef.FilePath}";
            }

            if (string.IsNullOrEmpty(url))
                return null;

            var response = await client.GetAsync(url, _cancellationTokenSource!.Token);

            // Try 'master' if 'main' fails
            if (!response.IsSuccessStatusCode && repoRef.Branch == null)
            {
                url = $"https://raw.githubusercontent.com/{repoRef.RepoOwner}/{repoRef.RepoName}/master/{repoRef.FilePath}";
                response = await client.GetAsync(url, _cancellationTokenSource!.Token);
            }

            if (!response.IsSuccessStatusCode)
                return null;

            return await response.Content.ReadAsStringAsync(_cancellationTokenSource.Token);
        }
        catch
        {
            return null;
        }
    }
}


================================================
FILE: UnsecuredAPIKeys.CLI/Services/VerifierService.cs
================================================
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Spectre.Console;
using UnsecuredAPIKeys.Data;
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Data.Models;
using UnsecuredAPIKeys.Providers;
using UnsecuredAPIKeys.Providers._Interfaces;

namespace UnsecuredAPIKeys.CLI.Services;

/// <summary>
/// Verifier service that maintains up to 50 valid API keys.
/// When a key becomes invalid, verifies new keys to maintain the limit.
/// Lite version: 50 key cap.
/// Full version: www.UnsecuredAPIKeys.com
/// </summary>
public class VerifierService(
    DBContext dbContext,
    IHttpClientFactory httpClientFactory,
    ILogger<VerifierService>? logger = null)
{
    private readonly IReadOnlyList<IApiKeyProvider> _providers = ApiProviderRegistry.VerifierProviders;
    private CancellationTokenSource? _cancellationTokenSource;

    private int _validCount;
    private int _invalidCount;
    private int _verifiedCount;

    public async Task RunAsync(CancellationToken cancellationToken)
    {
        _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

        AnsiConsole.MarkupLine("[green]Starting verifier service...[/]");
        AnsiConsole.MarkupLine($"[dim]Target valid keys: [yellow]{LiteLimits.MAX_VALID_KEYS}[/][/]");
        AnsiConsole.MarkupLine($"[dim]Loaded {_providers.Count} verification providers[/]");

        foreach (var provider in _providers)
        {
            AnsiConsole.MarkupLine($"  [dim]- {Markup.Escape(provider.ProviderName)}[/]");
        }

        // Run continuously
        while (!_cancellationTokenSource.Token.IsCancellationRequested)
        {
            try
            {
                await RunVerificationCycleAsync();

                if (_cancellationTokenSource.Token.IsCancellationRequested)
                    break;

                // Wait before next cycle
                AnsiConsole.MarkupLine($"[dim]Waiting {LiteLimits.VERIFICATION_DELAY_MS / 1000}s before next verification cycle...[/]");
                await Task.Delay(LiteLimits.VERIFICATION_DELAY_MS, _cancellationTokenSource.Token);

                // Reset counters
                _validCount = 0;
                _invalidCount = 0;
                _verifiedCount = 0;
            }
            catch (OperationCanceledException)
            {
                break;
            }
            catch (Exception ex)
            {
                AnsiConsole.MarkupLine($"[red]Error during verification: {Markup.Escape(ex.Message)}[/]");
                logger?.LogError(ex, "Verification cycle error");
                await Task.Delay(5000, _cancellationTokenSource.Token);
            }
        }

        AnsiConsole.MarkupLine("[green]Verifier stopped.[/]");
    }

    private async Task RunVerificationCycleAsync()
    {
        // Count current valid keys
        var currentValidCount = await dbContext.APIKeys
            .CountAsync(k => k.Status == ApiStatusEnum.Valid, _cancellationTokenSource!.Token);

        AnsiConsole.MarkupLine($"[dim]Current valid keys: [yellow]{currentValidCount}[/] / [yellow]{LiteLimits.MAX_VALID_KEYS}[/][/]");

        if (currentValidCount >= LiteLimits.MAX_VALID_KEYS)
        {
            // Re-verify existing valid keys to ensure they're still valid
            await ReVerifyExistingKeysAsync();
        }
        else
        {
            // Verify unverified keys until we reach the limit
            await VerifyNewKeysAsync(LiteLimits.MAX_VALID_KEYS - currentValidCount);
        }

        // Display summary
        var table = new Table()
            .Border(TableBorder.Rounded)
            .AddColumn("[bold]Metric[/]")
            .AddColumn("[bold]Value[/]");

        table.AddRow("Keys Verified", _verifiedCount.ToString());
        table.AddRow("Now Valid", $"[green]{_validCount}[/]");
        table.AddRow("Now Invalid", $"[red]{_invalidCount}[/]");

        var newValidCount = await dbContext.APIKeys
            .CountAsync(k => k.Status == ApiStatusEnum.Valid, _cancellationTokenSource!.Token);
        table.AddRow("Total Valid", $"[yellow]{newValidCount}[/] / [yellow]{LiteLimits.MAX_VALID_KEYS}[/]");

        AnsiConsole.Write(table);
    }

    private async Task ReVerifyExistingKeysAsync()
    {
        AnsiConsole.MarkupLine("[dim]Re-verifying existing valid keys...[/]");

        // Get oldest verified keys first
        var keysToReVerify = await dbContext.APIKeys
            .Where(k => k.Status == ApiStatusEnum.Valid)
            .OrderBy(k => k.LastCheckedUTC)
            .Take(LiteLimits.VERIFICATION_BATCH_SIZE)
            .ToListAsync(_cancellationTokenSource!.Token);

        await AnsiConsole.Progress()
            .StartAsync(async ctx =>
            {
                var task = ctx.AddTask("[green]Re-verifying keys[/]", maxValue: keysToReVerify.Count);

                foreach (var key in keysToReVerify)
                {
                    if (_cancellationTokenSource.Token.IsCancellationRequested)
                        break;

                    await VerifyKeyAsync(key);
                    task.Increment(1);
                }
            });

        await dbContext.SaveChangesAsync(_cancellationTokenSource.Token);
    }

    private async Task VerifyNewKeysAsync(int neededCount)
    {
        AnsiConsole.MarkupLine($"[dim]Verifying unverified keys (need {neededCount} more valid)...[/]");

        // Get unverified keys
        var keysToVerify = await dbContext.APIKeys
            .Where(k => k.Status == ApiStatusEnum.Unverified)
            .OrderBy(k => k.FirstFoundUTC)
            .Take(Math.Max(neededCount * 2, LiteLimits.VERIFICATION_BATCH_SIZE))
            .ToListAsync(_cancellationTokenSource!.Token);

        if (keysToVerify.Count == 0)
        {
            AnsiConsole.MarkupLine("[yellow]No unverified keys available.[/]");
            return;
        }

        await AnsiConsole.Progress()
            .StartAsync(async ctx =>
            {
                var task = ctx.AddTask("[green]Verifying new keys[/]", maxValue: keysToVerify.Count);
                var validFound = 0;

                foreach (var key in keysToVerify)
                {
                    if (_cancellationTokenSource.Token.IsCancellationRequested)
                        break;

                    // Stop if we've reached our target
                    if (validFound >= neededCount)
                    {
                        task.Value = keysToVerify.Count;
                        break;
                    }

                    var wasValid = await VerifyKeyAsync(key);
                    if (wasValid)
                        validFound++;

                    task.Increment(1);
                }
            });

        await dbContext.SaveChangesAsync(_cancellationTokenSource.Token);
    }

    private async Task<bool> VerifyKeyAsync(APIKey key)
    {
        Interlocked.Increment(ref _verifiedCount);

        // Build list of providers to try, starting with the assigned one
        var providersToTry = GetProvidersToTry(key);

        if (providersToTry.Count == 0)
        {
            key.Status = ApiStatusEnum.Error;
            key.LastCheckedUTC = DateTime.UtcNow;
            AnsiConsole.MarkupLine($"[yellow]No matching providers for key[/]");
            return false;
        }

        // Try each matching provider until one succeeds
        foreach (var provider in providersToTry)
        {
            try
            {
                var result = await provider.ValidateKeyAsync(key.ApiKey, httpClientFactory);
                key.LastCheckedUTC = DateTime.UtcNow;

                switch (result.Status)
                {
                    case Providers.Common.ValidationAttemptStatus.Valid:
                        // Update the key's API type if a different provider validated it
                        if (key.ApiType != provider.ApiType)
                        {
                            AnsiConsole.MarkupLine($"[dim]Reclassified from {key.ApiType} to {provider.ApiType}[/]");
                            key.ApiType = provider.ApiType;
                        }
                        key.Status = ApiStatusEnum.Valid;
                        key.ErrorCount = 0;
                        Interlocked.Increment(ref _validCount);
                        AnsiConsole.MarkupLine($"[green]Valid: {Markup.Escape(provider.ProviderName)} key[/]");
                        return true;

                    case Providers.Common.ValidationAttemptStatus.HttpError:
                        // Check if it's a quota/credits issue based on detail
                        if (result.Detail?.Contains("quota", StringComparison.OrdinalIgnoreCase) == true ||
                            result.Detail?.Contains("credit", StringComparison.OrdinalIgnoreCase) == true ||
                            result.Detail?.Contains("billing", StringComparison.OrdinalIgnoreCase) == true)
                        {
                            // Update the key's API type if a different provider validated it
                            if (key.ApiType != provider.ApiType)
                            {
                                AnsiConsole.MarkupLine($"[dim]Reclassified from {key.ApiType} to {provider.ApiType}[/]");
                                key.ApiType = provider.ApiType;
                            }
                            key.Status = ApiStatusEnum.ValidNoCredits;
                            key.ErrorCount = 0;
                            Interlocked.Increment(ref _validCount);
                            AnsiConsole.MarkupLine($"[yellow]Valid [[no credits]]: {Markup.Escape(provider.ProviderName)} key[/]");
                            return true;
                        }
                        // HTTP error but not quota - try next provider
                        continue;

                    case Providers.Common.ValidationAttemptStatus.Unauthorized:
                        // This provider explicitly rejected it - try next provider
                        continue;

                    case Providers.Common.ValidationAttemptStatus.NetworkError:
                        // Network error - don't try other providers, just increment error count
                        key.ErrorCount++;
                        if (key.ErrorCount >= 3)
                        {
                            key.Status = ApiStatusEnum.Error;
                        }
                        return false;

                    default:
                        // Provider-specific error - try next provider
                        continue;
                }
            }
            catch (Exception ex)
            {
                logger?.LogWarning(ex, "Error verifying key {KeyId} with provider {Provider}", key.Id, provider.ProviderName);
                // Continue to next provider on exception
                continue;
            }
        }

        // All providers failed - mark as invalid
        key.Status = ApiStatusEnum.Invalid;
        Interlocked.Increment(ref _invalidCount);
        return false;
    }

    /// <summary>
    /// Gets providers to try for a key, ordered by: assigned provider first, then other matching providers.
    /// </summary>
    private List<IApiKeyProvider> GetProvidersToTry(APIKey key)
    {
        var result = new List<IApiKeyProvider>();

        // First, add the assigned provider (if it exists)
        var assignedProvider = _providers.FirstOrDefault(p => p.ApiType == key.ApiType);
        if (assignedProvider != null)
        {
            result.Add(assignedProvider);
        }

        // Then add other providers whose patterns match this key
        foreach (var provider in _providers)
        {
            // Skip the already-added assigned provider
            if (provider.ApiType == key.ApiType)
                continue;

            // Check if any of this provider's patterns match the key
            foreach (var pattern in provider.RegexPatterns)
            {
                try
                {
                    var regex = new System.Text.RegularExpressions.Regex(pattern);
                    if (regex.IsMatch(key.ApiKey))
                    {
                        result.Add(provider);
                        break; // One match is enough for this provider
                    }
                }
                catch
                {
                    // Invalid regex pattern - skip it
                }
            }
        }

        return result;
    }
}


================================================
FILE: UnsecuredAPIKeys.CLI/UnsecuredAPIKeys.CLI.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <AssemblyName>unsecuredapikeys</AssemblyName>
    <RootNamespace>UnsecuredAPIKeys.CLI</RootNamespace>
    <Product>UnsecuredAPIKeys - Open Source</Product>
    <Authors>https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/</Authors>
    <Company>https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/</Company>
    <PackageProjectUrl>https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/</PackageProjectUrl>
    <RepositoryUrl>https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/</RepositoryUrl>
    <RepositoryType>git</RepositoryType>
    <EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
    <AnalysisLevel>latest-recommended</AnalysisLevel>
    <Version>1.0.0</Version>

    <!-- Single-file publishing settings -->
    <PublishSingleFile>true</PublishSingleFile>
    <SelfContained>true</SelfContained>
    <EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
    <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Spectre.Console" Version="0.54.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
    <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.1" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\UnsecuredAPIKeys.Data\UnsecuredAPIKeys.Data.csproj" />
    <ProjectReference Include="..\UnsecuredAPIKeys.Providers\UnsecuredAPIKeys.Providers.csproj" />
  </ItemGroup>

  <ItemGroup>
    <None Update="appsettings.example.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>

</Project>


================================================
FILE: UnsecuredAPIKeys.CLI/appsettings.example.json
================================================
{
  "// README": "Copy this file to appsettings.json and configure your GitHub token",
  "// IMPORTANT": "Do NOT commit appsettings.json to version control",

  "GitHub": {
    "Token": "ghp_YOUR_GITHUB_TOKEN_HERE",
    "// TokenInfo": "Create at https://github.com/settings/tokens - requires 'public_repo' scope"
  },

  "Database": {
    "Path": "unsecuredapikeys.db",
    "// PathInfo": "SQLite database file path. Can be absolute or relative."
  },

  "Limits": {
    "MaxValidKeys": 50,
    "// MaxValidKeysInfo": "Lite version limit. For higher limits, use www.UnsecuredAPIKeys.com"
  }
}


================================================
FILE: UnsecuredAPIKeys.Data/Common/ApiProviderAttribute.cs
================================================
namespace UnsecuredAPIKeys.Data.Common;

[AttributeUsage(AttributeTargets.Class)]
public class ApiProviderAttribute : Attribute
{
    /// <summary>
    /// Whether this provider should be used by the Scraper bot
    /// </summary>
    public bool ScraperUse { get; set; } = true;

    /// <summary>
    /// Whether this provider should be used by the Verifier bot
    /// </summary>
    public bool VerificationUse { get; set; } = true;

    /// <summary>
    /// Creates an ApiProvider attribute with default usage (enabled for both scraper and verifier)
    /// </summary>
    public ApiProviderAttribute()
    {
    }

    /// <summary>
    /// Creates an ApiProvider attribute with specific usage flags
    /// </summary>
    /// <param name="scraperUse">Enable for scraper bot</param>
    /// <param name="verificationUse">Enable for verifier bot</param>
    public ApiProviderAttribute(bool scraperUse, bool verificationUse)
    {
        ScraperUse = scraperUse;
        VerificationUse = verificationUse;
    }
}


================================================
FILE: UnsecuredAPIKeys.Data/Common/CommonEnums.cs
================================================
namespace UnsecuredAPIKeys.Data.Common
{
    /// <summary>
    /// Search provider for finding API keys.
    /// Lite version: GitHub only.
    /// Full version: www.UnsecuredAPIKeys.com
    /// </summary>
    public enum SearchProviderEnum
    {
        Unknown = -99,
        GitHub = 1
    }

    /// <summary>
    /// Status of an API key in the system.
    /// </summary>
    public enum ApiStatusEnum
    {
        /// <summary>The key was found but not yet checked for validity.</summary>
        Unverified = -99,

        /// <summary>The key was checked and is valid/working.</summary>
        Valid = 1,

        /// <summary>The key was checked and is not working (invalid, expired, revoked, etc.).</summary>
        Invalid = 0,

        /// <summary>The key is valid but has no credits/quota.</summary>
        ValidNoCredits = 7,

        /// <summary>The key was checked and is erroring out for some reason.</summary>
        Error = 6
    }

    /// <summary>
    /// Type of API provider.
    /// Lite version: OpenAI, Anthropic, Google only.
    /// Full version with all providers: www.UnsecuredAPIKeys.com
    /// </summary>
    public enum ApiTypeEnum
    {
        Unknown = -99,

        // AI Services - Lite version only supports these 3
        OpenAI = 100,
        AnthropicClaude = 120,
        GoogleAI = 130
    }
}


================================================
FILE: UnsecuredAPIKeys.Data/DBContext.cs
================================================
using Microsoft.EntityFrameworkCore;
using UnsecuredAPIKeys.Data.Models;

namespace UnsecuredAPIKeys.Data
{
    /// <summary>
    /// SQLite database context for UnsecuredAPIKeys Lite.
    /// Full version with PostgreSQL: www.UnsecuredAPIKeys.com
    /// </summary>
    public class DBContext : DbContext
    {
        private readonly string _dbPath;

        public DBContext(DbContextOptions<DBContext> options) : base(options)
        {
            _dbPath = "unsecuredapikeys.db";
        }

        public DBContext(string dbPath = "unsecuredapikeys.db")
        {
            _dbPath = dbPath;
        }

        // Core entities
        public DbSet<APIKey> APIKeys { get; set; } = null!;
        public DbSet<RepoReference> RepoReferences { get; set; } = null!;
        public DbSet<SearchQuery> SearchQueries { get; set; } = null!;
        public DbSet<SearchProviderToken> SearchProviderTokens { get; set; } = null!;
        public DbSet<ApplicationSetting> ApplicationSettings { get; set; } = null!;

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.UseSqlite($"Data Source={_dbPath}");
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // APIKey indexes for performance
            modelBuilder.Entity<APIKey>()
                .HasIndex(k => k.ApiKey)
                .IsUnique()
                .HasDatabaseName("IX_APIKeys_ApiKey");

            modelBuilder.Entity<APIKey>()
                .HasIndex(k => new { k.Status, k.ApiType })
                .HasDatabaseName("IX_APIKeys_Status_ApiType");

            modelBuilder.Entity<APIKey>()
                .HasIndex(k => k.LastCheckedUTC)
                .HasDatabaseName("IX_APIKeys_LastCheckedUTC");

            modelBuilder.Entity<APIKey>()
                .HasIndex(k => k.Status)
                .HasDatabaseName("IX_APIKeys_Status");

            // RepoReference indexes
            modelBuilder.Entity<RepoReference>()
                .HasIndex(r => r.APIKeyId)
                .HasDatabaseName("IX_RepoReferences_ApiKeyId");

            // SearchQuery indexes
            modelBuilder.Entity<SearchQuery>()
                .HasIndex(q => new { q.IsEnabled, q.LastSearchUTC })
                .HasDatabaseName("IX_SearchQueries_IsEnabled_LastSearchUTC");

            // SearchProviderToken indexes
            modelBuilder.Entity<SearchProviderToken>()
                .HasIndex(t => t.SearchProvider)
                .HasDatabaseName("IX_SearchProviderTokens_SearchProvider");

            // Relationships
            modelBuilder.Entity<RepoReference>()
                .HasOne(r => r.APIKey)
                .WithMany(k => k.References)
                .HasForeignKey(r => r.APIKeyId)
                .OnDelete(DeleteBehavior.Cascade);
        }
    }
}


================================================
FILE: UnsecuredAPIKeys.Data/DesignTimeDbContextFactory.cs
================================================
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;

namespace UnsecuredAPIKeys.Data
{
    /// <summary>
    /// Factory for creating DBContext during EF Core design-time operations (migrations).
    /// Uses SQLite for the lite version.
    /// </summary>
    public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<DBContext>
    {
        public DBContext CreateDbContext(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder<DBContext>();

            // Use SQLite for the lite version
            optionsBuilder.UseSqlite("Data Source=unsecuredapikeys.db");

            return new DBContext(optionsBuilder.Options);
        }
    }
}


================================================
FILE: UnsecuredAPIKeys.Data/Models/APIKey.cs
================================================
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using UnsecuredAPIKeys.Data.Common;
using System.Text.Json.Serialization; // <-- Add this using directive

namespace UnsecuredAPIKeys.Data.Models
{
    public class APIKey
    {
        [Key]
        public long Id { get; set; }

        [Required]
        public required string ApiKey { get; set; }

        [JsonConverter(typeof(JsonStringEnumConverter))]
        public ApiStatusEnum Status { get; set; }

        [JsonConverter(typeof(JsonStringEnumConverter))]
        public ApiTypeEnum ApiType { get; set; } = ApiTypeEnum.Unknown;

        public SearchProviderEnum SearchProvider { get; set; }

        public DateTime? LastCheckedUTC { get; set; }
        public DateTime FirstFoundUTC { get; set; }
        public DateTime LastFoundUTC { get; set; }

        public int TimesDisplayed { get; set; }
        
        // Error tracking for verification failures
        public int ErrorCount { get; set; } = 0;

        // Navigation property to where this key was found
        public virtual ICollection<RepoReference> References { get; set; } = [];
    }
}


================================================
FILE: UnsecuredAPIKeys.Data/Models/ApplicationSetting.cs
================================================
using System.ComponentModel.DataAnnotations;

namespace UnsecuredAPIKeys.Data.Models
{
    public class ApplicationSetting
    {
        [Key] public required string Key { get; init; }
        public required string Value { get; init; }
        public string? Description { get; init; }
    }
}


================================================
FILE: UnsecuredAPIKeys.Data/Models/RepoReference.cs
================================================
using System.ComponentModel.DataAnnotations;

namespace UnsecuredAPIKeys.Data.Models
{
    public class RepoReference
    {
        [Key]
        public long Id { get; set; }
        public long APIKeyId { get; set; }  // Foreign key to APIKey
        public virtual APIKey? APIKey { get; set; }  // Navigation property

        // Repository information
        [Required]
        public string? RepoURL { get; set; }  // Just base repo URL
        public string? RepoOwner { get; set; }  // Owner username/organization
        public string? RepoName { get; set; }  // Repository name
        public string? RepoDescription { get; set; }
        public long RepoId { get; set; }  // GitHub's repo ID

        // File information
        [Required]
        public string? FileURL { get; set; }  // Full path with commit hash
        public string? FileName { get; set; }
        public string? FilePath { get; set; }  // Path within repo
        public string? FileSHA { get; set; }
        public string? ApiContentUrl { get; set; } // URL to fetch raw content via API

        // Context information
        public string? CodeContext { get; set; }  // Surrounding code
        public int LineNumber { get; set; }

        // Discovery metadata
        public long SearchQueryId { get; set; }  // Which query found this
        public DateTime FoundUTC { get; set; }
        public string? Provider { get; set; } // e.g., GitHub, GitLab
        public string? Branch { get; set; } // Branch where the file was found
    }
}


================================================
FILE: UnsecuredAPIKeys.Data/Models/SearchProviderToken.cs
================================================
using System.ComponentModel.DataAnnotations;
using UnsecuredAPIKeys.Data.Common;

namespace UnsecuredAPIKeys.Data.Models
{
    public class SearchProviderToken
    {
        [Key] public int Id { get; set; }
        public string Token { get; set; } = string.Empty;
        public SearchProviderEnum SearchProvider { get; set; } = SearchProviderEnum.Unknown;
        public bool IsEnabled { get; set; }

        // Make it nullable so we can identify never-used tokens  
        public DateTime? LastUsedUTC { get; set; }
    }
}


================================================
FILE: UnsecuredAPIKeys.Data/Models/SearchQuery.cs
================================================
using System.ComponentModel.DataAnnotations;

namespace UnsecuredAPIKeys.Data.Models
{
    public class SearchQuery
    {
        [Key] public long Id { get; set; }
        public string Query { get; set; } = string.Empty;
        public bool IsEnabled { get; set; }
        public int SearchResultsCount { get; set; }

        public DateTime LastSearchUTC { get; set; }
    }
}


================================================
FILE: UnsecuredAPIKeys.Data/UnsecuredAPIKeys.Data.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

</Project>


================================================
FILE: UnsecuredAPIKeys.Providers/AI Providers/AnthropicProvider.cs
================================================
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

using Microsoft.Extensions.Logging;

using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Providers._Base;
using UnsecuredAPIKeys.Providers.Common;

namespace UnsecuredAPIKeys.Providers.AI_Providers
{
    /// <summary>
    /// Provider implementation for handling Anthropic (Claude) API keys with enhanced validation.
    /// </summary>
    [ApiProvider]
    public class AnthropicProvider : BaseApiKeyProvider
    {
        private const string API_ENDPOINT = "https://api.anthropic.com/v1/messages";
        private const string ANTHROPIC_VERSION = "2023-06-01";
        private const string DEFAULT_MODEL = "claude-sonnet-4-20250514";
        private const int MAX_RETRIES = 3;
        private const int TIMEOUT_SECONDS = 30;

        // Anthropic-specific response keywords (additional to base class)
        private static readonly HashSet<string> InvalidKeyIndicators = new(StringComparer.OrdinalIgnoreCase)
        {
            "invalid_api_key",
            "authentication_error",
            "invalid x-api-key",
            "unauthorized"
        };

        public override string ProviderName => "Anthropic";

        public override ApiTypeEnum ApiType => ApiTypeEnum.AnthropicClaude;

        // Enhanced regex patterns with compiled regex for better performance
        public override IEnumerable<string> RegexPatterns =>
        [
            @"sk-ant-api\d{0,2}-[a-zA-Z0-9\-_]{40,120}",
            @"sk-ant-[a-zA-Z0-9\-_]{40,95}",
            @"sk-ant-v\d+-[a-zA-Z0-9\-_]{40,95}",
            @"sk-ant-[a-zA-Z0-9]+-[a-zA-Z0-9\-_]{20,120}",
            @"sk-ant-[a-zA-Z0-9]{40,64}",
            @"\bsk-ant-[a-zA-Z0-9\-_]{20,120}\b"
        ];

        public AnthropicProvider() : base()
        {
        }

        public AnthropicProvider(ILogger<AnthropicProvider>? logger) : base(logger)
        {
        }

        protected override async Task<ValidationResult> ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient)
        {
            using var request = CreateValidationRequest(apiKey);

            var response = await httpClient.SendAsync(request);
            var responseBody = await response.Content.ReadAsStringAsync();

            _logger?.LogDebug("Anthropic API response: Status={StatusCode}, Body={Body}",
                response.StatusCode, responseBody.Length > 200 ? responseBody.Substring(0, 200) + "..." : responseBody);

            return InterpretResponse(response.StatusCode, responseBody);
        }

        private HttpRequestMessage CreateValidationRequest(string apiKey)
        {
            var request = new HttpRequestMessage(HttpMethod.Post, API_ENDPOINT);

            // Set headers
            request.Headers.Add("x-api-key", apiKey);
            request.Headers.Add("anthropic-version", ANTHROPIC_VERSION);
            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            // Ultra-minimal payload for lowest cost
            var payload = new
            {
                model = DEFAULT_MODEL,
                max_tokens = 1,
                messages = new[]
                {
                    new { role = "user", content = "1" }
                },
                temperature = 0,
                stop_sequences = new[] { "1", "2", "3", "4", "5" }
            };

            var jsonContent = JsonSerializer.Serialize(payload);
            request.Content = new StringContent(jsonContent, Encoding.UTF8, "application/json");

            return request;
        }

        private ValidationResult InterpretResponse(HttpStatusCode statusCode, string responseBody)
        {
            // Success cases
            if (IsSuccessStatusCode(statusCode))
            {
                return ValidationResult.Success(statusCode);
            }

            var bodyLower = responseBody.ToLowerInvariant();

            switch (statusCode)
            {
                case HttpStatusCode.Unauthorized: // 401
                    if (ContainsAny(bodyLower, InvalidKeyIndicators))
                    {
                        return ValidationResult.IsUnauthorized(statusCode);
                    }
                    return ValidationResult.IsUnauthorized(statusCode);

                case HttpStatusCode.Forbidden: // 403
                    if (ContainsAny(bodyLower, PermissionIndicators))
                    {
                        _logger?.LogInformation("API key has permission restrictions but is valid");
                        return ValidationResult.Success(statusCode);
                    }
                    return ValidationResult.HasHttpError(statusCode, $"Forbidden: {TruncateResponse(responseBody)}");

                case HttpStatusCode.BadRequest: // 400
                    if (ContainsAny(bodyLower, QuotaIndicators))
                    {
                        _logger?.LogInformation("API key is valid but has quota/billing issues");
                        return ValidationResult.Success(statusCode);
                    }
                    return ValidationResult.HasHttpError(statusCode, $"Bad request: {TruncateResponse(responseBody)}");

                case HttpStatusCode.PaymentRequired: // 402
                case HttpStatusCode.TooManyRequests: // 429
                    return ValidationResult.Success(statusCode);

                case HttpStatusCode.ServiceUnavailable: // 503
                case HttpStatusCode.GatewayTimeout: // 504
                    return ValidationResult.HasNetworkError($"Service unavailable: {statusCode}");

                default:
                    if (ContainsAny(bodyLower, QuotaIndicators))
                    {
                        return ValidationResult.Success(statusCode);
                    }

                    return ValidationResult.HasHttpError(statusCode,
                        $"API request failed with status {statusCode}. Response: {TruncateResponse(responseBody)}");
            }
        }

        protected override bool IsValidKeyFormat(string apiKey)
        {
            if (string.IsNullOrWhiteSpace(apiKey) || apiKey.Length < 20)
                return false;

            if (!apiKey.StartsWith("sk-ant-", StringComparison.Ordinal))
                return false;

            return apiKey.All(c => char.IsLetterOrDigit(c) || c == '-' || c == '_');
        }
    }
}

================================================
FILE: UnsecuredAPIKeys.Providers/AI Providers/GoogleProvider.cs
================================================
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Providers._Base;
using UnsecuredAPIKeys.Providers.Common;

namespace UnsecuredAPIKeys.Providers.AI_Providers
{
    /// <summary>
    /// Provider implementation for handling Google AI API keys.
    /// </summary>
    [ApiProvider]
    public class GoogleProvider : BaseApiKeyProvider
    {
        public override string ProviderName => "Google";
        public override ApiTypeEnum ApiType => ApiTypeEnum.GoogleAI;

        // Regex patterns specific to Google AI keys (from Scraper_Program.cs)
        public override IEnumerable<string> RegexPatterns =>
        [
            @"AIza[0-9A-Za-z\-_]{35}",  // Standard length is exactly 39 characters total
            @"AIza[0-9A-Za-z\-_]{35,40}" // Allow for some variation in newer keys
        ];

        public GoogleProvider() : base()
        {
        }

        public GoogleProvider(ILogger<GoogleProvider>? logger) : base(logger)
        {
        }

        protected override async Task<ValidationResult> ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient)
        {
            // Use Google's models endpoint for lightweight validation
            using var modelRequest = new HttpRequestMessage(HttpMethod.Get, "https://generativelanguage.googleapis.com/v1beta/models");
            // Google uses x-goog-api-key header
            modelRequest.Headers.Add("x-goog-api-key", apiKey);
            
            var modelResponse = await httpClient.SendAsync(modelRequest);
            string responseBody = await modelResponse.Content.ReadAsStringAsync();

            _logger?.LogDebug("Google AI models API response: Status={StatusCode}, Body={Body}",
                modelResponse.StatusCode, TruncateResponse(responseBody));

            if (IsSuccessStatusCode(modelResponse.StatusCode))
            {
                // Parse the models from the response
                var models = ParseGoogleModels(responseBody);
                return ValidationResult.Success(modelResponse.StatusCode, models);
            }
            else if (modelResponse.StatusCode == HttpStatusCode.Unauthorized || 
                     modelResponse.StatusCode == HttpStatusCode.Forbidden)
            {
                // Google often uses 403 for invalid keys
                if (ContainsAny(responseBody, UnauthorizedIndicators))
                {
                    return ValidationResult.IsUnauthorized(modelResponse.StatusCode);
                }
                return ValidationResult.IsUnauthorized(modelResponse.StatusCode);
            }
            else if (modelResponse.StatusCode == HttpStatusCode.BadRequest)
            {
                // Check specific error types
                if (ContainsAny(responseBody, UnauthorizedIndicators))
                {
                    return ValidationResult.IsUnauthorized(modelResponse.StatusCode);
                }
                return ValidationResult.HasHttpError(modelResponse.StatusCode, $"Bad request. Response: {TruncateResponse(responseBody)}");
            }
            else if ((int)modelResponse.StatusCode == 429)
            {
                // Rate limited means the key is valid
                return ValidationResult.Success(modelResponse.StatusCode);
            }
            else
            {
                // Check for quota/billing issues
                if (ContainsAny(responseBody, QuotaIndicators))
                {
                    return ValidationResult.Success(modelResponse.StatusCode);
                }
                
                return ValidationResult.HasHttpError(modelResponse.StatusCode, 
                    $"API request failed with status {modelResponse.StatusCode}. Response: {TruncateResponse(responseBody)}");
            }
        }

        protected override bool IsValidKeyFormat(string apiKey)
        {
            return !string.IsNullOrWhiteSpace(apiKey) && 
                   apiKey.StartsWith("AIza") && 
                   apiKey.Length >= 39; // AIza + 35 chars
        }

        private List<ModelInfo>? ParseGoogleModels(string jsonResponse)
        {
            try
            {
                using var doc = JsonDocument.Parse(jsonResponse);
                if (!doc.RootElement.TryGetProperty("models", out var modelsArray))
                {
                    return null;
                }

                var models = new List<ModelInfo>();
                foreach (var modelElement in modelsArray.EnumerateArray())
                {
                    var model = new ModelInfo
                    {
                        ModelId = modelElement.GetProperty("name").GetString() ?? "",
                        DisplayName = modelElement.TryGetProperty("displayName", out var displayName) ? displayName.GetString() : null,
                        Description = modelElement.TryGetProperty("description", out var description) ? description.GetString() : null,
                        Version = modelElement.TryGetProperty("version", out var version) ? version.GetString() : null,
                        InputTokenLimit = modelElement.TryGetProperty("inputTokenLimit", out var inputLimit) ? inputLimit.GetInt64() : null,
                        OutputTokenLimit = modelElement.TryGetProperty("outputTokenLimit", out var outputLimit) ? outputLimit.GetInt64() : null,
                        Temperature = modelElement.TryGetProperty("temperature", out var temp) ? (float?)temp.GetDouble() : null,
                        TopP = modelElement.TryGetProperty("topP", out var topP) ? (float?)topP.GetDouble() : null,
                        TopK = modelElement.TryGetProperty("topK", out var topK) ? topK.GetInt32() : null,
                        MaxTemperature = modelElement.TryGetProperty("maxTemperature", out var maxTemp) ? (float?)maxTemp.GetDouble() : null
                    };

                    // Parse supported methods
                    if (modelElement.TryGetProperty("supportedGenerationMethods", out var methods))
                    {
                        model.SupportedMethods = new List<string>();
                        foreach (var method in methods.EnumerateArray())
                        {
                            if (method.GetString() is string methodStr)
                            {
                                model.SupportedMethods.Add(methodStr);
                            }
                        }
                    }

                    // Extract model group from the display name
                    if (model.DisplayName != null)
                    {
                        // Extract model family (e.g., "Gemini 1.5", "Gemini 2.0", etc.)
                        if (model.DisplayName.Contains("Gemini"))
                        {
                            var parts = model.DisplayName.Split(' ');
                            if (parts.Length >= 2)
                            {
                                model.ModelGroup = $"{parts[0]} {parts[1]}";
                            }
                        }
                    }

                    models.Add(model);
                }

                return models;
            }
            catch (Exception ex)
            {
                _logger?.LogError(ex, "Error parsing Google models response");
                return null;
            }
        }
    }
}


================================================
FILE: UnsecuredAPIKeys.Providers/AI Providers/OpenAIProvider.cs
================================================
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Providers._Base;
using UnsecuredAPIKeys.Providers.Common;

namespace UnsecuredAPIKeys.Providers.AI_Providers
{
    /// <summary>
    /// Provider implementation for handling OpenAI API keys.
    /// </summary>
    [ApiProvider]
    public class OpenAIProvider : BaseApiKeyProvider
    {
        public override string ProviderName => "OpenAI";
        public override ApiTypeEnum ApiType => ApiTypeEnum.OpenAI;

        // Enhanced regex patterns for OpenAI keys
        public override IEnumerable<string> RegexPatterns =>
        [
            @"sk-[A-Za-z0-9\-]{20,}",
            @"sk-proj-[A-Za-z0-9\-]{20,}",
            @"sk-svcacct-[A-Za-z0-9\-]{20,}",
            @"sk-[A-Za-z0-9]{48}",  // Standard format
            @"Bearer sk-[A-Za-z0-9\-]{20,}"  // Keys in auth headers
        ];

        public OpenAIProvider() : base()
        {
        }

        public OpenAIProvider(ILogger<OpenAIProvider>? logger) : base(logger)
        {
        }

        protected override async Task<ValidationResult> ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient)
        {
            // First, try a lightweight model listing endpoint
            using var modelRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.openai.com/v1/models");
            modelRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
            
            var modelResponse = await httpClient.SendAsync(modelRequest);
            string responseBody = await modelResponse.Content.ReadAsStringAsync();

            _logger?.LogDebug("OpenAI models API response: Status={StatusCode}, Body={Body}",
                modelResponse.StatusCode, TruncateResponse(responseBody));

            if (IsSuccessStatusCode(modelResponse.StatusCode))
            {
                // Parse the models from the response
                var models = ParseOpenAIModels(responseBody);
                return ValidationResult.Success(modelResponse.StatusCode, models);
            }
            else if (modelResponse.StatusCode == HttpStatusCode.Unauthorized)
            {
                return ValidationResult.IsUnauthorized(modelResponse.StatusCode);
            }
            else if ((int)modelResponse.StatusCode == 429)
            {
                // Rate limited means the key is valid
                return ValidationResult.Success(modelResponse.StatusCode);
            }
            else if (modelResponse.StatusCode == HttpStatusCode.PaymentRequired)
            {
                // Payment required means valid key but no credits
                return ValidationResult.Success(modelResponse.StatusCode);
            }
            else
            {
                // Check response body for quota/billing issues
                if (ContainsAny(responseBody, QuotaIndicators))
                {
                    return ValidationResult.Success(modelResponse.StatusCode);
                }
                
                return ValidationResult.HasHttpError(modelResponse.StatusCode, 
                    $"API request failed with status {modelResponse.StatusCode}. Response: {TruncateResponse(responseBody)}");
            }
        }

        protected override bool IsValidKeyFormat(string apiKey)
        {
            return !string.IsNullOrWhiteSpace(apiKey) && 
                   apiKey.StartsWith("sk-") && 
                   apiKey.Length >= 23; // sk- + at least 20 chars
        }

        private List<ModelInfo>? ParseOpenAIModels(string jsonResponse)
        {
            try
            {
                using var doc = JsonDocument.Parse(jsonResponse);
                if (!doc.RootElement.TryGetProperty("data", out var dataArray))
                {
                    return null;
                }

                var models = new List<ModelInfo>();
                foreach (var modelElement in dataArray.EnumerateArray())
                {
                    var model = new ModelInfo
                    {
                        ModelId = modelElement.GetProperty("id").GetString() ?? "",
                        DisplayName = modelElement.GetProperty("id").GetString() ?? "", // OpenAI uses id as display name
                        Description = modelElement.TryGetProperty("description", out var desc) ? desc.GetString() : null
                    };

                    // Extract model group from the ID
                    if (!string.IsNullOrEmpty(model.ModelId))
                    {
                        // Group models by family (e.g., "gpt-4", "gpt-3.5", "text-embedding")
                        if (model.ModelId.StartsWith("gpt-4"))
                        {
                            model.ModelGroup = "GPT-4";
                            
                            // Check for specific capabilities
                            if (model.ModelId.Contains("turbo"))
                            {
                                model.Description = "GPT-4 Turbo model with enhanced capabilities";
                            }
                            else if (model.ModelId.Contains("vision"))
                            {
                                model.Description = "GPT-4 model with vision capabilities";
                            }
                        }
                        else if (model.ModelId.StartsWith("gpt-3.5"))
                        {
                            model.ModelGroup = "GPT-3.5";
                        }
                        else if (model.ModelId.StartsWith("o1"))
                        {
                            model.ModelGroup = "O1";
                            model.Description = "OpenAI's reasoning model";
                        }
                        else if (model.ModelId.StartsWith("text-embedding"))
                        {
                            model.ModelGroup = "Embeddings";
                            model.Description = "Text embedding model";
                        }
                        else if (model.ModelId.StartsWith("dall-e"))
                        {
                            model.ModelGroup = "DALL-E";
                            model.Description = "Image generation model";
                        }
                        else if (model.ModelId.StartsWith("whisper"))
                        {
                            model.ModelGroup = "Whisper";
                            model.Description = "Speech recognition model";
                        }
                        else if (model.ModelId.StartsWith("tts"))
                        {
                            model.ModelGroup = "TTS";
                            model.Description = "Text-to-speech model";
                        }
                        else
                        {
                            model.ModelGroup = "Other";
                        }
                    }

                    models.Add(model);
                }

                return models;
            }
            catch (Exception ex)
            {
                _logger?.LogError(ex, "Error parsing OpenAI models response");
                return null;
            }
        }
    }
}


================================================
FILE: UnsecuredAPIKeys.Providers/ApiProviderRegistry.cs
================================================
using System.Reflection;
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Providers._Interfaces;

namespace UnsecuredAPIKeys.Providers
{
    public static class ApiProviderRegistry
    {
        private static readonly Lazy<List<IApiKeyProvider>> _allProviders = new(() =>
        {
            return [.. Assembly.GetExecutingAssembly()
                .GetTypes()
                .Where(type => type.GetCustomAttribute<ApiProviderAttribute>() != null
                               && typeof(IApiKeyProvider).IsAssignableFrom(type)
                               && !type.IsInterface
                               && !type.IsAbstract)
                .Select(type => (IApiKeyProvider)Activator.CreateInstance(type)!)];
        });

        private static readonly Lazy<List<IApiKeyProvider>> _scraperProviders = new(() =>
        {
            return [.. Assembly.GetExecutingAssembly()
                .GetTypes()
                .Where(type => {
                    var attr = type.GetCustomAttribute<ApiProviderAttribute>();
                    return attr != null
                           && attr.ScraperUse
                           && typeof(IApiKeyProvider).IsAssignableFrom(type)
                           && !type.IsInterface
                           && !type.IsAbstract;
                })
                .Select(type => (IApiKeyProvider)Activator.CreateInstance(type)!)];
        });

        private static readonly Lazy<List<IApiKeyProvider>> _verifierProviders = new(() =>
        {
            return [.. Assembly.GetExecutingAssembly()
                .GetTypes()
                .Where(type => {
                    var attr = type.GetCustomAttribute<ApiProviderAttribute>();
                    return attr != null
                           && attr.VerificationUse
                           && typeof(IApiKeyProvider).IsAssignableFrom(type)
                           && !type.IsInterface
                           && !type.IsAbstract;
                })
                .Select(type => (IApiKeyProvider)Activator.CreateInstance(type)!)];
        });

        /// <summary>
        /// Gets all providers with ApiProvider attribute (backward compatibility)
        /// </summary>
        public static IReadOnlyList<IApiKeyProvider> Providers => _allProviders.Value;

        /// <summary>
        /// Gets providers that are enabled for scraper use
        /// </summary>
        public static IReadOnlyList<IApiKeyProvider> ScraperProviders => _scraperProviders.Value;

        /// <summary>
        /// Gets providers that are enabled for verifier use
        /// </summary>
        public static IReadOnlyList<IApiKeyProvider> VerifierProviders => _verifierProviders.Value;

        /// <summary>
        /// Gets providers for a specific bot type
        /// </summary>
        /// <param name="botType">The type of bot (Scraper or Verifier)</param>
        /// <returns>List of providers enabled for the specified bot type</returns>
        public static IReadOnlyList<IApiKeyProvider> GetProvidersForBot(BotType botType)
        {
            return botType switch
            {
                BotType.Scraper => ScraperProviders,
                BotType.Verifier => VerifierProviders,
                _ => Providers
            };
        }
    }

    /// <summary>
    /// Enumeration of bot types for provider filtering
    /// </summary>
    public enum BotType
    {
        Scraper,
        Verifier
    }
}


================================================
FILE: UnsecuredAPIKeys.Providers/Common/ValidationResult.cs
================================================
using System.Net;

namespace UnsecuredAPIKeys.Providers.Common
{
    public enum ValidationAttemptStatus
    {
        Valid,                 // Key is valid and working.
        Unauthorized,          // Key is explicitly unauthorized (e.g., HTTP 401).
        HttpError,             // An HTTP error occurred (e.g., 403, 404, 429, 5xx).
        NetworkError,          // A network-level error occurred (e.g., DNS, timeout, connection refused).
        ProviderSpecificError  // An unexpected error within the provider's logic.
    }

    public class ModelInfo
    {
        public string ModelId { get; set; } = string.Empty;
        public string? DisplayName { get; set; }
        public string? Description { get; set; }
        public string? Version { get; set; }
        public long? InputTokenLimit { get; set; }
        public long? OutputTokenLimit { get; set; }
        public List<string>? SupportedMethods { get; set; }
        public float? Temperature { get; set; }
        public float? TopP { get; set; }
        public int? TopK { get; set; }
        public float? MaxTemperature { get; set; }
        public string? ModelGroup { get; set; } // For grouping similar models
    }

    public class ValidationResult
    {
        public ValidationAttemptStatus Status { get; set; }
        public HttpStatusCode? HttpStatusCode { get; set; } // Null if not an HTTP-related error.
        public string? Detail { get; set; } = string.Empty; // Optional error message or detail.
        
        // Model information discovered during validation
        public List<ModelInfo>? AvailableModels { get; set; }

        // Helper factory methods for convenience
        public static ValidationResult Success(HttpStatusCode statusCode, List<ModelInfo>? models = null) =>
            new() { Status = ValidationAttemptStatus.Valid, HttpStatusCode = statusCode, AvailableModels = models };

        public static ValidationResult IsUnauthorized(HttpStatusCode statusCode, string? detail = null) =>
            new() { Status = ValidationAttemptStatus.Unauthorized, HttpStatusCode = statusCode, Detail = detail };

        public static ValidationResult HasHttpError(HttpStatusCode statusCode, string? detail = null) =>
            new() { Status = ValidationAttemptStatus.HttpError, HttpStatusCode = statusCode, Detail = detail };

        public static ValidationResult HasNetworkError(string detail) =>
            new() { Status = ValidationAttemptStatus.NetworkError, Detail = detail };
        
        public static ValidationResult HasProviderSpecificError(string detail) =>
            new() { Status = ValidationAttemptStatus.ProviderSpecificError, Detail = detail };
    }
}


================================================
FILE: UnsecuredAPIKeys.Providers/Search Providers/GitHubSearchProvider.cs
================================================
using Microsoft.Extensions.Logging;
using Octokit;
using UnsecuredAPIKeys.Data;
using UnsecuredAPIKeys.Data.Models;
using UnsecuredAPIKeys.Providers._Interfaces;
// Assuming logging might be needed later

namespace UnsecuredAPIKeys.Providers.Search_Providers
{
    /// <summary>
    /// Implements the ISearchProvider interface for searching code on GitHub.
    /// </summary>
    public class GitHubSearchProvider(DBContext dbContext, ILogger<GitHubSearchProvider>? logger = null) : ISearchProvider
    {
        /// <inheritdoc />
        public string ProviderName => "GitHub";

        /// <inheritdoc />
        public async Task<IEnumerable<RepoReference>> SearchAsync(SearchQuery query, SearchProviderToken? token)
        {
            if (token == null || string.IsNullOrWhiteSpace(token.Token))
            {
                logger?.LogError("GitHub token is missing or invalid."); // Use _logger field
                throw new ArgumentNullException(nameof(token), "A valid GitHub token is required.");
            }

            if (query == null || string.IsNullOrWhiteSpace(query.Query))
            {
                logger?.LogError("Search query is missing or invalid."); // Use _logger field
                throw new ArgumentNullException(nameof(query), "A valid search query is required.");
            }

            var client = new GitHubClient(new ProductHeaderValue("UnsecuredAPIKeys-Scraper"))
            {
                Credentials = new Credentials(token.Token)
            };

            var results = new List<RepoReference>();
            int page = 1;
            const int perPage = 100; // Max allowed by GitHub API

            try
            {
                logger?.LogInformation("Starting GitHub search for query: {Query}", query.Query); // Use _logger field

                while (true) // Loop to handle pagination
                {
                    var request = new SearchCodeRequest(query.Query)
                    {
                        // Consider adding filters like language, user, repo if needed
                        Page = page,
                        PerPage = perPage
                        // Order = SortDirection.Descending
                    };

                    SearchCodeResult searchResult;
                    try
                    {
                        searchResult = await client.Search.SearchCode(request);

                        if (page == 1)
                        {
                            query.SearchResultsCount = searchResult.TotalCount;
                            dbContext.SearchQueries.Update(query);
                            await dbContext.SaveChangesAsync();
                        }
                    }
                    catch (RateLimitExceededException ex)
                    {
                        logger?.LogWarning("GitHub API rate limit exceeded. Waiting until {ResetTime}.", ex.Reset.ToString("o")); // Use _logger field

                        // Wait until the rate limit resets
                        var delay = ex.Reset - DateTimeOffset.UtcNow;
                        if (delay > TimeSpan.Zero)
                        {
                            if (delay.TotalMinutes > 1)
                            {
                                Environment.Exit(200);
                            }

                            await Task.Delay(delay);
                        }
                        continue; // Retry the same page
                    }
                    catch (ApiException apiEx)
                    {
                        logger?.LogError(apiEx, "GitHub API error during search on page {Page}. Status: {StatusCode}", page, apiEx.StatusCode); // Use _logger field
                        // Decide how to handle API errors (e.g., stop, retry after delay)
                        break; // Stop searching on API error for now
                    }

                    if (searchResult?.Items == null || !searchResult.Items.Any())
                    {
                        logger?.LogInformation("No more results found for query '{Query}' on page {Page}.", query.Query, page); // Use _logger field
                        break; // No more results
                    }

                    logger?.LogDebug("Found {Count} results on page {Page} for query '{Query}'.", searchResult.Items.Count, page, query.Query); // Use _logger field

                    foreach (var item in searchResult.Items)
                    {
                        results.Add(new RepoReference
                        {
                            SearchQueryId = query.Id,
                            Provider = ProviderName,
                            RepoOwner = item.Repository?.Owner?.Login, // Corrected field name
                            RepoName = item.Repository?.Name, // Corrected field name
                            FilePath = item.Path,
                            FileURL = item.HtmlUrl, // HTML URL for viewing in browser
                            ApiContentUrl = item.Url, // API URL for fetching content
                            Branch = item.Repository?.DefaultBranch, // Assuming default branch, might need refinement
                            FileSHA = item.Sha, // Corrected field name (SHA of the file blob)
                            FoundUTC = DateTime.UtcNow, // Corrected field name (Record when this specific reference was found)
                            RepoURL = item.Repository?.HtmlUrl,
                            RepoDescription = item.Repository?.Description,
                            FileName = item.Name
                        });
                    }

                    // Basic check to prevent infinite loops if API behaves unexpectedly
                    if (searchResult.Items.Count < perPage || results.Count >= 1000) // GitHub limits code search results to 1000
                    {
                        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
                        break;
                    }

                    page++; // Move to the next page
                    await Task.Delay(TimeSpan.FromSeconds(2)); // Add a small delay to be polite to the API
                }
            }
            catch (Exception ex)
            {
                logger?.LogError(ex, "An unexpected error occurred during GitHub search for query: {Query}", query.Query); // Use _logger field
            }

            logger?.LogInformation("Completed GitHub search for query '{Query}'. Found {Count} potential references.", query.Query, results.Count); // Use _logger field
            return results;
        }
    }
}


================================================
FILE: UnsecuredAPIKeys.Providers/UnsecuredAPIKeys.Providers.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <ProjectReference Include="..\UnsecuredAPIKeys.Data\UnsecuredAPIKeys.Data.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
    <PackageReference Include="Octokit" Version="14.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" />
  </ItemGroup>

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>


================================================
FILE: UnsecuredAPIKeys.Providers/_Base/BaseApiKeyProvider.cs
================================================
using System.Net;
using Microsoft.Extensions.Logging;
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Providers._Interfaces;
using UnsecuredAPIKeys.Providers.Common;

namespace UnsecuredAPIKeys.Providers._Base
{
    /// <summary>
    /// Base class for API key providers with common functionality and retry logic.
    /// Lite version: OpenAI, Anthropic, Google only.
    /// Full version with all providers: www.UnsecuredAPIKeys.com
    /// </summary>
    public abstract class BaseApiKeyProvider(ILogger? logger = null) : IApiKeyProvider
    {
        protected const int DEFAULT_MAX_RETRIES = 3;
        protected const int DEFAULT_TIMEOUT_SECONDS = 30;

        protected readonly ILogger? _logger = logger;

        public abstract string ProviderName { get; }
        public abstract ApiTypeEnum ApiType { get; }
        public abstract IEnumerable<string> RegexPatterns { get; }

        /// <summary>
        /// Validates an API key with retry logic and proper resource management.
        /// </summary>
        public async Task<ValidationResult> ValidateKeyAsync(string apiKey, IHttpClientFactory httpClientFactory)
        {
            if (string.IsNullOrWhiteSpace(apiKey))
            {
                return ValidationResult.HasProviderSpecificError("API key is null or whitespace.");
            }

            // Clean the API key
            apiKey = CleanApiKey(apiKey);

            // Validate format if implemented
            if (!IsValidKeyFormat(apiKey))
            {
                return ValidationResult.HasProviderSpecificError("API key format is invalid.");
            }

            Exception? lastException = null;

            for (int retry = 0; retry < GetMaxRetries(); retry++)
            {
                if (retry > 0)
                {
                    var delay = TimeSpan.FromSeconds(Math.Pow(2, retry - 1));
                    _logger?.LogDebug("Retrying {Provider} validation after {Delay}ms (attempt {Retry}/{MaxRetries})",
                        ProviderName, delay.TotalMilliseconds, retry + 1, GetMaxRetries());
                    await Task.Delay(delay);
                }

                try
                {
                    using var httpClient = CreateHttpClient(httpClientFactory);
                    var result = await ValidateKeyWithHttpClientAsync(apiKey, httpClient);

                    if (result.Status != ValidationAttemptStatus.NetworkError)
                    {
                        return result;
                    }

                    // Continue retrying on network errors
                    lastException = new Exception(result.Detail);
                }
                catch (HttpRequestException ex)
                {
                    lastException = ex;
                    _logger?.LogWarning(ex, "HTTP request failed on attempt {Retry}/{MaxRetries} for {Provider}",
                        retry + 1, GetMaxRetries(), ProviderName);

                    if (retry == GetMaxRetries() - 1)
                    {
                        return ValidationResult.HasNetworkError($"HTTP request failed after {GetMaxRetries()} retries: {ex.Message}");
                    }
                }
                catch (TaskCanceledException ex)
                {
                    lastException = ex;
                    _logger?.LogWarning(ex, "Request timeout on attempt {Retry}/{MaxRetries} for {Provider}",
                        retry + 1, GetMaxRetries(), ProviderName);

                    if (retry == GetMaxRetries() - 1)
                    {
                        return ValidationResult.HasNetworkError($"Request timeout after {GetMaxRetries()} retries: {ex.Message}");
                    }
                }
                catch (Exception ex)
                {
                    _logger?.LogError(ex, "Unexpected error during {Provider} key validation", ProviderName);
                    return ValidationResult.HasProviderSpecificError($"Unexpected error: {ex.Message}");
                }
            }

            return ValidationResult.HasNetworkError($"Failed after {GetMaxRetries()} retries. Last error: {lastException?.Message ?? "Unknown error"}");
        }

        /// <summary>
        /// Abstract method for provider-specific validation logic.
        /// </summary>
        protected abstract Task<ValidationResult> ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient);

        /// <summary>
        /// Creates an HttpClient with proper configuration.
        /// </summary>
        protected virtual HttpClient CreateHttpClient(IHttpClientFactory httpClientFactory)
        {
            try
            {
                var client = httpClientFactory.CreateClient(ProviderName.ToLowerInvariant().Replace(" ", ""));
                client.Timeout = TimeSpan.FromSeconds(GetTimeoutSeconds());
                return client;
            }
            catch
            {
                // Fall back to manual creation if factory fails
                return new HttpClient
                {
                    Timeout = TimeSpan.FromSeconds(GetTimeoutSeconds())
                };
            }
        }

        /// <summary>
        /// Cleans the API key by removing common prefixes and whitespace.
        /// </summary>
        protected virtual string CleanApiKey(string apiKey)
        {
            apiKey = apiKey.Trim();

            // Remove common prefixes
            if (apiKey.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
            {
                apiKey = apiKey.Substring(7).Trim();
            }
            else if (apiKey.StartsWith("x-api-key:", StringComparison.OrdinalIgnoreCase))
            {
                apiKey = apiKey.Substring(10).Trim();
            }

            return apiKey;
        }

        /// <summary>
        /// Validates the API key format. Override in derived classes for specific validation.
        /// </summary>
        protected virtual bool IsValidKeyFormat(string apiKey)
        {
            return !string.IsNullOrWhiteSpace(apiKey) && apiKey.Length >= 10;
        }

        /// <summary>
        /// Gets the maximum number of retries. Override in derived classes if needed.
        /// </summary>
        protected virtual int GetMaxRetries() => DEFAULT_MAX_RETRIES;

        /// <summary>
        /// Gets the timeout in seconds. Override in derived classes if needed.
        /// </summary>
        protected virtual int GetTimeoutSeconds() => DEFAULT_TIMEOUT_SECONDS;

        /// <summary>
        /// Common method to check if response body contains any of the specified indicators.
        /// </summary>
        protected static bool ContainsAny(string text, HashSet<string> indicators)
        {
            return indicators.Any(indicator => text.Contains(indicator, StringComparison.OrdinalIgnoreCase));
        }

        /// <summary>
        /// Truncates response text for logging purposes.
        /// </summary>
        protected static string TruncateResponse(string response, int maxLength = 200)
        {
            if (string.IsNullOrEmpty(response))
                return string.Empty;

            return response.Length > maxLength
                ? response.Substring(0, maxLength) + "..."
                : response;
        }

        /// <summary>
        /// Checks if the status code indicates success.
        /// </summary>
        protected static bool IsSuccessStatusCode(HttpStatusCode statusCode)
        {
            return (int)statusCode >= 200 && (int)statusCode < 300;
        }

        /// <summary>
        /// Common quota/billing indicators across providers.
        /// </summary>
        protected static readonly HashSet<string> QuotaIndicators = new(StringComparer.OrdinalIgnoreCase)
        {
            "credit", "quota", "billing", "insufficient_funds", "payment", "exceeded", "balance", "limit",
            "insufficient_quota", "exceeded_quota", "rate_limit", "rate_limit_exceeded", "RESOURCE_EXHAUSTED"
        };

        /// <summary>
        /// Common unauthorized indicators across providers.
        /// </summary>
        protected static readonly HashSet<string> UnauthorizedIndicators = new(StringComparer.OrdinalIgnoreCase)
        {
            "invalid_api_key", "authentication_error", "unauthorized", "invalid x-api-key", "API_KEY_INVALID",
            "API key not valid", "API key expired", "invalid token", "authentication failed"
        };

        /// <summary>
        /// Common permission indicators across providers.
        /// </summary>
        protected static readonly HashSet<string> PermissionIndicators = new(StringComparer.OrdinalIgnoreCase)
        {
            "permission", "access", "not_authorized_for_model", "forbidden", "read-only", "Pro service"
        };
    }
}


================================================
FILE: UnsecuredAPIKeys.Providers/_Interfaces/IApiKeyProvider.cs
================================================
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Providers.Common;

namespace UnsecuredAPIKeys.Providers._Interfaces
{
    /// <summary>
    /// Defines the contract for an API key provider, responsible for
    /// identifying and validating keys for a specific service.
    /// Lite version: OpenAI, Anthropic, Google only.
    /// Full version: www.UnsecuredAPIKeys.com
    /// </summary>
    public interface IApiKeyProvider
    {
        /// <summary>
        /// Gets the unique name of the provider (e.g., "OpenAI", "Anthropic").
        /// </summary>
        string ProviderName { get; }

        /// <summary>
        /// Gets the corresponding ApiTypeEnum value for this provider.
        /// </summary>
        ApiTypeEnum ApiType { get; }

        /// <summary>
        /// Gets the list of regex patterns used to identify potential keys for this provider.
        /// </summary>
        IEnumerable<string> RegexPatterns { get; }

        /// <summary>
        /// Asynchronously validates the given API key against the provider's service.
        /// </summary>
        /// <param name="apiKey">The API key string to validate.</param>
        /// <param name="httpClientFactory">The IHttpClientFactory for creating HttpClient instances.</param>
        /// <returns>A ValidationResult indicating the outcome of the validation attempt.</returns>
        Task<ValidationResult> ValidateKeyAsync(string apiKey, IHttpClientFactory httpClientFactory);
    }
}


================================================
FILE: UnsecuredAPIKeys.Providers/_Interfaces/ISearchProvider.cs
================================================
using UnsecuredAPIKeys.Data.Models;

namespace UnsecuredAPIKeys.Providers._Interfaces
{
    /// <summary>
    /// Defines the contract for a search provider used to find potential API keys.
    /// </summary>
    public interface ISearchProvider
    {
        /// <summary>
        /// Gets the name of the search provider (e.g., "GitHub", "GitLab").
        /// </summary>
        string ProviderName { get; }

        /// <summary>
        /// Executes a search based on the provided query.
        /// </summary>
        /// <param name="query">The search query details.</param>
        /// <param name="token">The API token to use for the search.</param>
        /// <returns>A collection of RepoReference objects representing potential findings.</returns>
        Task<IEnumerable<RepoReference>> SearchAsync(SearchQuery query, SearchProviderToken? token);
    }
}
Download .txt
gitextract_966uldie/

├── .gitattributes
├── .github/
│   └── workflows/
│       ├── claude-code-review.yml
│       └── claude.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── UnsecuredAPIKeys-OpenSource.sln
├── UnsecuredAPIKeys.CLI/
│   ├── Constants.cs
│   ├── Program.cs
│   ├── Services/
│   │   ├── DatabaseService.cs
│   │   ├── ScraperService.cs
│   │   └── VerifierService.cs
│   ├── UnsecuredAPIKeys.CLI.csproj
│   └── appsettings.example.json
├── UnsecuredAPIKeys.Data/
│   ├── Common/
│   │   ├── ApiProviderAttribute.cs
│   │   └── CommonEnums.cs
│   ├── DBContext.cs
│   ├── DesignTimeDbContextFactory.cs
│   ├── Models/
│   │   ├── APIKey.cs
│   │   ├── ApplicationSetting.cs
│   │   ├── RepoReference.cs
│   │   ├── SearchProviderToken.cs
│   │   └── SearchQuery.cs
│   └── UnsecuredAPIKeys.Data.csproj
└── UnsecuredAPIKeys.Providers/
    ├── AI Providers/
    │   ├── AnthropicProvider.cs
    │   ├── GoogleProvider.cs
    │   └── OpenAIProvider.cs
    ├── ApiProviderRegistry.cs
    ├── Common/
    │   └── ValidationResult.cs
    ├── Search Providers/
    │   └── GitHubSearchProvider.cs
    ├── UnsecuredAPIKeys.Providers.csproj
    ├── _Base/
    │   └── BaseApiKeyProvider.cs
    └── _Interfaces/
        ├── IApiKeyProvider.cs
        └── ISearchProvider.cs
Download .txt
SYMBOL INDEX (90 symbols across 22 files)

FILE: UnsecuredAPIKeys.CLI/Constants.cs
  class LiteLimits (line 7) | public static class LiteLimits
  class AppInfo (line 39) | public static class AppInfo

FILE: UnsecuredAPIKeys.CLI/Services/DatabaseService.cs
  class DatabaseService (line 12) | public class DatabaseService(string dbPath = "unsecuredapikeys.db")
    method InitializeDatabaseAsync (line 14) | public async Task<DBContext> InitializeDatabaseAsync()
    method SeedDefaultDataAsync (line 27) | private async Task SeedDefaultDataAsync(DBContext dbContext)
    method GetStatisticsAsync (line 76) | public async Task<Statistics> GetStatisticsAsync(DBContext dbContext)
    method SaveGitHubTokenAsync (line 95) | public async Task SaveGitHubTokenAsync(DBContext dbContext, string token)
    method ResetDatabaseAsync (line 118) | public async Task ResetDatabaseAsync()
    method ExportKeysAsync (line 129) | public async Task ExportKeysAsync(DBContext dbContext, string filePath...
    method ExportAsJsonAsync (line 152) | private async Task ExportAsJsonAsync(List<APIKey> keys, string filePath)
    method ExportAsCsvAsync (line 180) | private async Task ExportAsCsvAsync(List<APIKey> keys, string filePath)
  class Statistics (line 197) | public class Statistics

FILE: UnsecuredAPIKeys.CLI/Services/ScraperService.cs
  class ScraperService (line 18) | public class ScraperService
    method ScraperService (line 29) | public ScraperService(DBContext dbContext, IHttpClientFactory httpClie...
    method RunAsync (line 37) | public async Task RunAsync(CancellationToken cancellationToken)
    method RunScrapingCycleAsync (line 93) | private async Task RunScrapingCycleAsync(SearchProviderToken token)
    method ProcessResultAsync (line 166) | private async Task ProcessResultAsync(RepoReference repoRef, SearchPro...
    method FetchFileContentAsync (line 229) | private async Task<string?> FetchFileContentAsync(RepoReference repoRe...

FILE: UnsecuredAPIKeys.CLI/Services/VerifierService.cs
  class VerifierService (line 18) | public class VerifierService(
    method RunAsync (line 30) | public async Task RunAsync(CancellationToken cancellationToken)
    method RunVerificationCycleAsync (line 77) | private async Task RunVerificationCycleAsync()
    method ReVerifyExistingKeysAsync (line 113) | private async Task ReVerifyExistingKeysAsync()
    method VerifyNewKeysAsync (line 142) | private async Task VerifyNewKeysAsync(int neededCount)
    method VerifyKeyAsync (line 188) | private async Task<bool> VerifyKeyAsync(APIKey key)
    method GetProvidersToTry (line 282) | private List<IApiKeyProvider> GetProvidersToTry(APIKey key)

FILE: UnsecuredAPIKeys.Data/Common/ApiProviderAttribute.cs
  class ApiProviderAttribute (line 3) | [AttributeUsage(AttributeTargets.Class)]
    method ApiProviderAttribute (line 19) | public ApiProviderAttribute()
    method ApiProviderAttribute (line 28) | public ApiProviderAttribute(bool scraperUse, bool verificationUse)

FILE: UnsecuredAPIKeys.Data/Common/CommonEnums.cs
  type SearchProviderEnum (line 8) | public enum SearchProviderEnum
  type ApiStatusEnum (line 17) | public enum ApiStatusEnum
  type ApiTypeEnum (line 40) | public enum ApiTypeEnum

FILE: UnsecuredAPIKeys.Data/DBContext.cs
  class DBContext (line 10) | public class DBContext : DbContext
    method DBContext (line 14) | public DBContext(DbContextOptions<DBContext> options) : base(options)
    method DBContext (line 19) | public DBContext(string dbPath = "unsecuredapikeys.db")
    method OnConfiguring (line 31) | protected override void OnConfiguring(DbContextOptionsBuilder optionsB...
    method OnModelCreating (line 39) | protected override void OnModelCreating(ModelBuilder modelBuilder)

FILE: UnsecuredAPIKeys.Data/DesignTimeDbContextFactory.cs
  class DesignTimeDbContextFactory (line 10) | public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<DB...
    method CreateDbContext (line 12) | public DBContext CreateDbContext(string[] args)

FILE: UnsecuredAPIKeys.Data/Models/APIKey.cs
  class APIKey (line 8) | public class APIKey

FILE: UnsecuredAPIKeys.Data/Models/ApplicationSetting.cs
  class ApplicationSetting (line 5) | public class ApplicationSetting

FILE: UnsecuredAPIKeys.Data/Models/RepoReference.cs
  class RepoReference (line 5) | public class RepoReference

FILE: UnsecuredAPIKeys.Data/Models/SearchProviderToken.cs
  class SearchProviderToken (line 6) | public class SearchProviderToken

FILE: UnsecuredAPIKeys.Data/Models/SearchQuery.cs
  class SearchQuery (line 5) | public class SearchQuery

FILE: UnsecuredAPIKeys.Providers/AI Providers/AnthropicProvider.cs
  class AnthropicProvider (line 17) | [ApiProvider]
    method AnthropicProvider (line 50) | public AnthropicProvider() : base()
    method AnthropicProvider (line 54) | public AnthropicProvider(ILogger<AnthropicProvider>? logger) : base(lo...
    method ValidateKeyWithHttpClientAsync (line 58) | protected override async Task<ValidationResult> ValidateKeyWithHttpCli...
    method CreateValidationRequest (line 71) | private HttpRequestMessage CreateValidationRequest(string apiKey)
    method InterpretResponse (line 99) | private ValidationResult InterpretResponse(HttpStatusCode statusCode, ...
    method IsValidKeyFormat (line 153) | protected override bool IsValidKeyFormat(string apiKey)

FILE: UnsecuredAPIKeys.Providers/AI Providers/GoogleProvider.cs
  class GoogleProvider (line 14) | [ApiProvider]
    method GoogleProvider (line 27) | public GoogleProvider() : base()
    method GoogleProvider (line 31) | public GoogleProvider(ILogger<GoogleProvider>? logger) : base(logger)
    method ValidateKeyWithHttpClientAsync (line 35) | protected override async Task<ValidationResult> ValidateKeyWithHttpCli...
    method IsValidKeyFormat (line 91) | protected override bool IsValidKeyFormat(string apiKey)
    method ParseGoogleModels (line 98) | private List<ModelInfo>? ParseGoogleModels(string jsonResponse)

FILE: UnsecuredAPIKeys.Providers/AI Providers/OpenAIProvider.cs
  class OpenAIProvider (line 14) | [ApiProvider]
    method OpenAIProvider (line 30) | public OpenAIProvider() : base()
    method OpenAIProvider (line 34) | public OpenAIProvider(ILogger<OpenAIProvider>? logger) : base(logger)
    method ValidateKeyWithHttpClientAsync (line 38) | protected override async Task<ValidationResult> ValidateKeyWithHttpCli...
    method IsValidKeyFormat (line 83) | protected override bool IsValidKeyFormat(string apiKey)
    method ParseOpenAIModels (line 90) | private List<ModelInfo>? ParseOpenAIModels(string jsonResponse)

FILE: UnsecuredAPIKeys.Providers/ApiProviderRegistry.cs
  class ApiProviderRegistry (line 7) | public static class ApiProviderRegistry
    method GetProvidersForBot (line 70) | public static IReadOnlyList<IApiKeyProvider> GetProvidersForBot(BotTyp...
  type BotType (line 84) | public enum BotType

FILE: UnsecuredAPIKeys.Providers/Common/ValidationResult.cs
  type ValidationAttemptStatus (line 5) | public enum ValidationAttemptStatus
  class ModelInfo (line 14) | public class ModelInfo
  class ValidationResult (line 30) | public class ValidationResult
    method Success (line 40) | public static ValidationResult Success(HttpStatusCode statusCode, List...
    method IsUnauthorized (line 43) | public static ValidationResult IsUnauthorized(HttpStatusCode statusCod...
    method HasHttpError (line 46) | public static ValidationResult HasHttpError(HttpStatusCode statusCode,...
    method HasNetworkError (line 49) | public static ValidationResult HasNetworkError(string detail) =>
    method HasProviderSpecificError (line 52) | public static ValidationResult HasProviderSpecificError(string detail) =>

FILE: UnsecuredAPIKeys.Providers/Search Providers/GitHubSearchProvider.cs
  class GitHubSearchProvider (line 13) | public class GitHubSearchProvider(DBContext dbContext, ILogger<GitHubSea...
    method SearchAsync (line 19) | public async Task<IEnumerable<RepoReference>> SearchAsync(SearchQuery ...

FILE: UnsecuredAPIKeys.Providers/_Base/BaseApiKeyProvider.cs
  class BaseApiKeyProvider (line 14) | public abstract class BaseApiKeyProvider(ILogger? logger = null) : IApiK...
    method ValidateKeyAsync (line 28) | public async Task<ValidationResult> ValidateKeyAsync(string apiKey, IH...
    method ValidateKeyWithHttpClientAsync (line 104) | protected abstract Task<ValidationResult> ValidateKeyWithHttpClientAsy...
    method CreateHttpClient (line 109) | protected virtual HttpClient CreateHttpClient(IHttpClientFactory httpC...
    method CleanApiKey (line 130) | protected virtual string CleanApiKey(string apiKey)
    method IsValidKeyFormat (line 150) | protected virtual bool IsValidKeyFormat(string apiKey)
    method GetMaxRetries (line 158) | protected virtual int GetMaxRetries() => DEFAULT_MAX_RETRIES;
    method GetTimeoutSeconds (line 163) | protected virtual int GetTimeoutSeconds() => DEFAULT_TIMEOUT_SECONDS;
    method ContainsAny (line 168) | protected static bool ContainsAny(string text, HashSet<string> indicat...
    method TruncateResponse (line 176) | protected static string TruncateResponse(string response, int maxLengt...
    method IsSuccessStatusCode (line 189) | protected static bool IsSuccessStatusCode(HttpStatusCode statusCode)

FILE: UnsecuredAPIKeys.Providers/_Interfaces/IApiKeyProvider.cs
  type IApiKeyProvider (line 12) | public interface IApiKeyProvider
    method ValidateKeyAsync (line 35) | Task<ValidationResult> ValidateKeyAsync(string apiKey, IHttpClientFact...

FILE: UnsecuredAPIKeys.Providers/_Interfaces/ISearchProvider.cs
  type ISearchProvider (line 8) | public interface ISearchProvider
    method SearchAsync (line 21) | Task<IEnumerable<RepoReference>> SearchAsync(SearchQuery query, Search...
Condensed preview — 35 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (146K chars).
[
  {
    "path": ".gitattributes",
    "chars": 2518,
    "preview": "###############################################################################\n# Set default behavior to automatically "
  },
  {
    "path": ".github/workflows/claude-code-review.yml",
    "chars": 1952,
    "preview": "name: Claude Code Review\n\non:\n  pull_request:\n    types: [opened, synchronize]\n    # Optional: Only run on specific file"
  },
  {
    "path": ".github/workflows/claude.yml",
    "chars": 1886,
    "preview": "name: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issue"
  },
  {
    "path": ".gitignore",
    "chars": 6517,
    "preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## G"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 8688,
    "preview": "# Changelog\n\nAll notable changes to UnsecuredAPIKeys Open Source.\n\n## [1.0.0] - 2025-12-09 - Lite Version Release\n\nThis "
  },
  {
    "path": "LICENSE",
    "chars": 3745,
    "preview": "UnsecuredAPIKeys Open Source License\nBased on MIT License with Attribution Requirements\n\nCopyright (c) 2025 TSCarterJr\n\n"
  },
  {
    "path": "README.md",
    "chars": 7197,
    "preview": "# UnsecuredAPIKeys Lite\n\n[![GitHub Stars](https://img.shields.io/github/stars/TSCarterJr/UnsecuredAPIKeys-OpenSource?sty"
  },
  {
    "path": "UnsecuredAPIKeys-OpenSource.sln",
    "chars": 4153,
    "preview": "\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.13.359"
  },
  {
    "path": "UnsecuredAPIKeys.CLI/Constants.cs",
    "chars": 1366,
    "preview": "namespace UnsecuredAPIKeys.CLI;\n\n/// <summary>\n/// Constants for the lite version of UnsecuredAPIKeys.\n/// Full version "
  },
  {
    "path": "UnsecuredAPIKeys.CLI/Program.cs",
    "chars": 11314,
    "preview": "using Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Spectre.Console;\nusing Unsecur"
  },
  {
    "path": "UnsecuredAPIKeys.CLI/Services/DatabaseService.cs",
    "chars": 6636,
    "preview": "using Microsoft.EntityFrameworkCore;\nusing Spectre.Console;\nusing UnsecuredAPIKeys.Data;\nusing UnsecuredAPIKeys.Data.Com"
  },
  {
    "path": "UnsecuredAPIKeys.CLI/Services/ScraperService.cs",
    "chars": 10071,
    "preview": "using Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.Logging;\nusing Spectre.Console;\nusing UnsecuredAPIKeys.D"
  },
  {
    "path": "UnsecuredAPIKeys.CLI/Services/VerifierService.cs",
    "chars": 12549,
    "preview": "using Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.Logging;\nusing Spectre.Console;\nusing UnsecuredAPIKeys.D"
  },
  {
    "path": "UnsecuredAPIKeys.CLI/UnsecuredAPIKeys.CLI.csproj",
    "chars": 2149,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>net10.0</Tar"
  },
  {
    "path": "UnsecuredAPIKeys.CLI/appsettings.example.json",
    "chars": 595,
    "preview": "{\n  \"// README\": \"Copy this file to appsettings.json and configure your GitHub token\",\n  \"// IMPORTANT\": \"Do NOT commit "
  },
  {
    "path": "UnsecuredAPIKeys.Data/Common/ApiProviderAttribute.cs",
    "chars": 1022,
    "preview": "namespace UnsecuredAPIKeys.Data.Common;\n\n[AttributeUsage(AttributeTargets.Class)]\npublic class ApiProviderAttribute : A"
  },
  {
    "path": "UnsecuredAPIKeys.Data/Common/CommonEnums.cs",
    "chars": 1348,
    "preview": "namespace UnsecuredAPIKeys.Data.Common\n{\n    /// <summary>\n    /// Search provider for finding API keys.\n    /// Lite ve"
  },
  {
    "path": "UnsecuredAPIKeys.Data/DBContext.cs",
    "chars": 2997,
    "preview": "using Microsoft.EntityFrameworkCore;\nusing UnsecuredAPIKeys.Data.Models;\n\nnamespace UnsecuredAPIKeys.Data\n{\n    /// <sum"
  },
  {
    "path": "UnsecuredAPIKeys.Data/DesignTimeDbContextFactory.cs",
    "chars": 716,
    "preview": "using Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Design;\n\nnamespace UnsecuredAPIKeys.Data\n{\n    "
  },
  {
    "path": "UnsecuredAPIKeys.Data/Models/APIKey.cs",
    "chars": 1167,
    "preview": "using System.ComponentModel.DataAnnotations;\nusing System.ComponentModel.DataAnnotations.Schema;\nusing UnsecuredAPIKeys"
  },
  {
    "path": "UnsecuredAPIKeys.Data/Models/ApplicationSetting.cs",
    "chars": 296,
    "preview": "using System.ComponentModel.DataAnnotations;\n\nnamespace UnsecuredAPIKeys.Data.Models\n{\n    public class ApplicationSett"
  },
  {
    "path": "UnsecuredAPIKeys.Data/Models/RepoReference.cs",
    "chars": 1528,
    "preview": "using System.ComponentModel.DataAnnotations;\n\nnamespace UnsecuredAPIKeys.Data.Models\n{\n    public class RepoReference\n "
  },
  {
    "path": "UnsecuredAPIKeys.Data/Models/SearchProviderToken.cs",
    "chars": 531,
    "preview": "using System.ComponentModel.DataAnnotations;\nusing UnsecuredAPIKeys.Data.Common;\n\nnamespace UnsecuredAPIKeys.Data.Model"
  },
  {
    "path": "UnsecuredAPIKeys.Data/Models/SearchQuery.cs",
    "chars": 381,
    "preview": "using System.ComponentModel.DataAnnotations;\n\nnamespace UnsecuredAPIKeys.Data.Models\n{\n    public class SearchQuery\n   "
  },
  {
    "path": "UnsecuredAPIKeys.Data/UnsecuredAPIKeys.Data.csproj",
    "chars": 663,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <ImplicitUsings"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/AI Providers/AnthropicProvider.cs",
    "chars": 6422,
    "preview": "using System.Net;\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\n\nusing Microsoft.Extensions."
  },
  {
    "path": "UnsecuredAPIKeys.Providers/AI Providers/GoogleProvider.cs",
    "chars": 7479,
    "preview": "using System.Net;\nusing System.Net.Http.Headers;\nusing System.Text.Json;\nusing Microsoft.Extensions.Logging;\nusing Unsec"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/AI Providers/OpenAIProvider.cs",
    "chars": 7328,
    "preview": "using System.Net;\nusing System.Net.Http.Headers;\nusing System.Text.Json;\nusing Microsoft.Extensions.Logging;\nusing Unsec"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/ApiProviderRegistry.cs",
    "chars": 3464,
    "preview": "using System.Reflection;\nusing UnsecuredAPIKeys.Data.Common;\nusing UnsecuredAPIKeys.Providers._Interfaces;\n\nnamespace U"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/Common/ValidationResult.cs",
    "chars": 2696,
    "preview": "using System.Net;\n\nnamespace UnsecuredAPIKeys.Providers.Common\n{\n    public enum ValidationAttemptStatus\n    {\n        V"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/Search Providers/GitHubSearchProvider.cs",
    "chars": 6785,
    "preview": "using Microsoft.Extensions.Logging;\nusing Octokit;\nusing UnsecuredAPIKeys.Data;\nusing UnsecuredAPIKeys.Data.Models;\nusin"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/UnsecuredAPIKeys.Providers.csproj",
    "chars": 588,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\UnsecuredAPIKeys.Data\\UnsecuredAPIKe"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/_Base/BaseApiKeyProvider.cs",
    "chars": 8819,
    "preview": "using System.Net;\nusing Microsoft.Extensions.Logging;\nusing UnsecuredAPIKeys.Data.Common;\nusing UnsecuredAPIKeys.Provide"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/_Interfaces/IApiKeyProvider.cs",
    "chars": 1479,
    "preview": "using UnsecuredAPIKeys.Data.Common;\nusing UnsecuredAPIKeys.Providers.Common;\n\nnamespace UnsecuredAPIKeys.Providers._Inte"
  },
  {
    "path": "UnsecuredAPIKeys.Providers/_Interfaces/ISearchProvider.cs",
    "chars": 870,
    "preview": "using UnsecuredAPIKeys.Data.Models;\n\nnamespace UnsecuredAPIKeys.Providers._Interfaces\n{\n    /// <summary>\n    /// Define"
  }
]

About this extraction

This page contains the full source code of the TSCarterJr/UnsecuredAPIKeys-OpenSource GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 35 files (134.7 KB), approximately 31.7k tokens, and a symbol index with 90 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!