From 63bfeca740ab72a5b3c870f336666dbe0bfe47d5 Mon Sep 17 00:00:00 2001 From: dela Date: Tue, 13 Jan 2026 11:28:37 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=A3=E6=B7=B7=E6=B7=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 48 ++ CONTRIBUTING.md | 145 +++++ LICENSE | 22 + README.md | 546 +++++++++++++++++ background.js | 550 ++++++++++++++++++ content.js | 880 ++++++++++++++++++++++++++++ icon128.png | Bin 0 -> 15885 bytes icon16.png | Bin 0 -> 703 bytes icon48.png | Bin 0 -> 3080 bytes manifest.json | 43 ++ popup.html | 249 ++++++++ popup.js | 627 ++++++++++++++++++++ styles.css | 1486 +++++++++++++++++++++++++++++++++++++++++++++++ 13 files changed, 4596 insertions(+) create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 background.js create mode 100644 content.js create mode 100644 icon128.png create mode 100644 icon16.png create mode 100644 icon48.png create mode 100644 manifest.json create mode 100644 popup.html create mode 100644 popup.js create mode 100644 styles.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f97e45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# IDE and Editor Files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# OS Files +Thumbs.db +desktop.ini + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Extension Build Files +*.zip +*.crx +*.pem + +# Temporary Files +*.tmp +*.temp +.cache/ + +# Node modules (if added in future) +node_modules/ +package-lock.json +yarn.lock + +# Test Files +test-results/ +coverage/ + +# Private Keys and Certificates +*.key +*.pem +*.p12 +secrets.json + +# Environment Files +.env +.env.local +.env.*.local + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9efaa5c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,145 @@ +# Contributing to cardbingenerator - Stripe Auto Fill + +First off, thank you for considering contributing to cardbingenerator! It's people like you that make cardbingenerator such a great tool. + +## Code of Conduct + +This project and everyone participating in it is governed by our Code of Conduct. By participating, you are expected to uphold this code. + +## How Can I Contribute? + +### Reporting Bugs + +Before creating bug reports, please check the existing issues as you might find out that you don't need to create one. When you are creating a bug report, please include as many details as possible: + +* **Use a clear and descriptive title** +* **Describe the exact steps which reproduce the problem** +* **Provide specific examples to demonstrate the steps** +* **Describe the behavior you observed after following the steps** +* **Explain which behavior you expected to see instead and why** +* **Include screenshots and animated GIFs** if possible +* **Include your browser version and OS** + +### Suggesting Enhancements + +Enhancement suggestions are tracked as GitHub issues. When creating an enhancement suggestion, please include: + +* **Use a clear and descriptive title** +* **Provide a step-by-step description of the suggested enhancement** +* **Provide specific examples to demonstrate the steps** +* **Describe the current behavior** and **explain which behavior you expected to see instead** +* **Explain why this enhancement would be useful** + +### Pull Requests + +* Fill in the required template +* Do not include issue numbers in the PR title +* Include screenshots and animated GIFs in your pull request whenever possible +* Follow the JavaScript style guide +* End all files with a newline +* Avoid platform-dependent code + +## Development Process + +### Setup + +1. Fork the repository +2. Clone your fork: +```bash +git clone https://github.com/your-username/cardbingenerator-extension.git +``` +3. Create a branch: +```bash +git checkout -b feature/your-feature-name +``` + +### Making Changes + +1. Make your changes +2. Test your changes thoroughly: + - Load extension in developer mode + - Test on multiple Stripe checkout pages + - Verify all functionality works + - Check console for errors + +3. Commit your changes: +```bash +git add . +git commit -m "Add: description of your changes" +``` + +### Commit Message Guidelines + +* Use the present tense ("Add feature" not "Added feature") +* Use the imperative mood ("Move cursor to..." not "Moves cursor to...") +* Limit the first line to 72 characters or less +* Reference issues and pull requests liberally after the first line +* Consider starting the commit message with an applicable emoji: + * 🎨 `:art:` when improving the format/structure of the code + * 🐛 `:bug:` when fixing a bug + * ✨ `:sparkles:` when adding a new feature + * 📝 `:memo:` when writing docs + * 🚀 `:rocket:` when improving performance + * 🔒 `:lock:` when dealing with security + * ⬆️ `:arrow_up:` when upgrading dependencies + * ⬇️ `:arrow_down:` when downgrading dependencies + * 🔧 `:wrench:` when changing configuration files + +### Coding Style + +* Use 2 spaces for indentation +* Use camelCase for variable and function names +* Use PascalCase for class names +* Use UPPER_CASE for constants +* Add comments for complex logic +* Keep functions small and focused +* Use meaningful variable names +* Avoid magic numbers +* Use template literals for string concatenation + +### Testing Checklist + +Before submitting your PR, ensure: + +- [ ] Code follows the style guidelines +- [ ] No console errors +- [ ] Extension loads without errors +- [ ] All existing features still work +- [ ] New features work as expected +- [ ] Tested on multiple Stripe pages +- [ ] No performance degradation +- [ ] Code is commented where necessary +- [ ] No hardcoded values (use constants) + +### Pull Request Process + +1. Update the README.md with details of changes if applicable +2. Update the CHANGELOG.md with a note describing your changes +3. The PR will be merged once you have the sign-off of a maintainer + +## File Structure + +``` +cardbingenerator-extension/ +├── manifest.json # Extension configuration +├── popup.html # Popup UI +├── popup.js # Popup logic +├── styles.css # Styles +├── background.js # Service worker +├── content.js # Content script +├── icons/ # Extension icons +├── README.md # Documentation +├── LICENSE # License file +└── CONTRIBUTING.md # This file +``` + +## Questions? + +Feel free to open an issue with your question or contact the maintainers directly. + +## Recognition + +Contributors will be recognized in the README.md file. + +Thank you for contributing! 🎉 + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4c61eb1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 cardbingenerator - Stripe Auto Fill + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the 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. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..a887723 --- /dev/null +++ b/README.md @@ -0,0 +1,546 @@ +# 💳 Cardbingenerator - Stripe Auto Fill + +[![Version](https://img.shields.io/badge/version-1.0.0-blue.svg)](https://github.com/yourusername/cardbingenerator-extension) +[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) +[![Chrome Extension](https://img.shields.io/badge/Chrome-Extension-green.svg)](https://developer.chrome.com/docs/extensions/) + +一个功能强大的 Chrome 浏览器扩展,用于自动生成测试信用卡号并自动填充 Stripe 支付表单。支持 Luhn 算法验证、自定义地址管理和现代化暗黑主题界面。 + +[English](#english-version) | [中文](#中文版本) + +--- + +## 中文版本 + +## ⚠️ 免责声明 + +**本工具仅用于测试和教育目的** + +- 仅可用于开发/测试环境或演示支付表单 +- 请勿在生产环境的支付系统中使用 +- 请勿用于欺诈或任何非法活动 +- 开发者对工具的滥用不承担任何责任 +- 测试卡号无法在真实支付处理器上使用 + +## 🌟 核心特性 + +### 💳 智能卡号生成 +- **Luhn 算法验证**:生成数学上有效的卡号 +- **BIN 支持**:使用任意银行识别号码(BIN)模式 +- **多卡种识别**:自动识别 Visa、Mastercard、Amex、Discover 等 +- **智能生成**:包含有效的到期日期和 CVV 安全码 + +### 🚀 自动填充能力 +- **智能字段检测**:自动查找并填充卡片字段 +- **Shadow DOM 支持**:兼容现代 Web 组件 +- **地址自动填充**:填充账单地址、城市、州、邮编 +- **仿人类输入**:模拟自然的打字模式 +- **框架兼容**:绕过 React/Vue 值设置器 + +### 🛠️ 高度定制化 +- **自定义 BIN 历史**:保存并重用你喜欢的 BIN 模式 +- **多地址管理**:管理自定义账单地址 +- **姓名生成器**:内置身份池用于持卡人姓名 +- **数据源选择**:在静态、手动或自动生成数据之间切换 + +### 🔒 隐私保护功能 +- **数据清理**:深度清除 cookies、缓存和存储 +- **反指纹追踪**:清除所有 Stripe 相关的浏览数据 +- **隔离存储**:所有数据本地存储在浏览器中 + +## 📦 安装方法 + +### 从源码安装 + +1. **克隆仓库** + ```bash + git clone https://github.com/yourusername/cardbingenerator.git + cd cardbingenerator + ``` + +2. **在 Chrome 中加载扩展** + - 打开 Chrome 并访问 `chrome://extensions/` + - 启用"开发者模式"(右上角的开关) + - 点击"加载已解压的扩展程序" + - 选择项目文件夹 + +3. **固定扩展图标** + - 点击 Chrome 工具栏中的拼图图标 + - 找到"cardbingenerator"并点击图钉图标 + +### 手动安装 + +1. 下载最新版本 +2. 解压 ZIP 文件 +3. 按照"从源码安装"中的步骤 2-3 操作 + +## 🚀 使用指南 + +### 基本使用 + +1. **打开扩展** + - 点击 Chrome 工具栏中的扩展图标 + - 你会看到主界面,包含标签页:General、BIN List、Settings + +2. **输入 BIN 模式** + - 在"BIN Template"字段中输入 BIN(例如:`552461`) + - 扩展会自动用 'x' 填充到 16 位 + - 示例:`552461` 会变成 `552461xxxxxxxxxx` + +3. **生成并填充** + - 导航到 Stripe 结账页面 + - 点击"Fill Everything"按钮 + - 扩展将自动: + - 生成有效的卡号 + - 填充卡片详情(卡号、到期日、CVV) + - 填充账单地址信息 + - 自动提交表单 + +### 高级功能 + +#### BIN 历史记录 +- 点击 BIN 输入框旁边的 `+` 按钮保存到历史 +- 点击任意已保存的 BIN 快速选择 +- 点击 `×` 从历史中删除 + +#### 自定义地址 +1. 进入 **Settings** → **Addresses** 标签 +2. 填写地址表单字段 +3. 点击"Add"保存 +4. 在 Address Source 下拉菜单中选择"Manual (Custom)" + +#### 设置配置 + +**卡号验证开关** +- 启用:生成通过 Luhn 算法验证的卡号 +- 禁用:生成随机卡号(更快,较宽松) + +**地址来源** +- **Static**:使用内置默认地址 +- **Manual**:使用你自定义保存的地址 +- **Auto**:生成随机地址(即将推出) + +**姓名来源** +- 控制是使用内置姓名还是自定义条目 +- 姓名从地址条目中提取 + +## 🎯 工作原理 + +### 卡号生成算法 + +扩展使用 **Luhn 算法**(模 10 校验和)生成有效卡号: + +1. 接收你的 BIN 模式(例如:`552461xxxxxxxxxx`) +2. 为每个 `x` 填充随机数字 +3. 计算最后一位的 Luhn 校验位 +4. 验证整个卡号 + +```javascript +// 生成示例 +BIN 输入: 552461xxxxxxxxxx +生成结果: 5524610123456789 +验证状态: ✅ 通过 Luhn 验证 +卡片类型: Mastercard +``` + +### 自动填充流程 + +1. **字段检测**:扫描页面查找卡片和地址字段 + - 使用 CSS 选择器和 autocomplete 属性 + - 穿透 Shadow DOM 边界 + - 根据相关性为字段评分 + +2. **智能填充**:按最佳顺序填充字段 + - 首先填充国家(可能会刷新表单结构) + - 使用模拟打字填充卡片详情 + - 适当延迟填充地址字段 + +3. **事件触发**:触发适当的 DOM 事件 + - 原生值设置器绕过框架锁定 + - Input、change 和 blur 事件 + - 焦点管理触发验证 + +## 🔧 技术细节 + +### 支持的 Stripe 版本 +- Stripe Elements v3 +- Stripe Checkout +- 自定义 Stripe 集成 + +### 浏览器兼容性 +- Chrome 88+ +- Edge 88+ +- Brave(基于 Chromium) +- Opera(基于 Chromium) + +### 使用的权限 + +| 权限 | 用途 | +|------------|---------| +| `storage` | 保存 BIN 历史和自定义地址 | +| `activeTab` | 与当前 Stripe 结账页面交互 | +| `tabs` | 查找和管理 Stripe 标签页 | +| `scripting` | 注入自动填充脚本 | +| `cookies` | 清除会话数据用于测试 | +| `browsingData` | 深度清除 Stripe 指纹数据 | + +### 文件结构 + +``` +cardbingenerator/ +├── manifest.json # 扩展配置 +├── background.js # Service Worker(卡号生成逻辑) +├── content.js # Content Script(自动填充逻辑) +├── popup.html # 扩展用户界面 +├── popup.js # UI 逻辑和设置 +├── styles.css # 扩展样式 +├── icon16.png # 扩展图标(16x16) +├── icon48.png # 扩展图标(48x48) +├── icon128.png # 扩展图标(128x128) +└── README.md # 本文件 +``` + +## 🎨 支持的卡片类型 + +扩展自动检测并生成以下卡片: + +- ✅ Visa(以 4 开头) +- ✅ Mastercard(以 51-55 开头) +- ✅ American Express(以 34、37 开头) +- ✅ Discover(以 6011、65 开头) +- ✅ JCB(以 35 开头) +- ✅ Diners Club(以 30、36、38 开头) +- ✅ Maestro(以 50、56-58、6304、6390、67 开头) +- ✅ UnionPay(以 62 开头) + +## 🐛 故障排除 + +### 扩展未填充表单 +- **解决方案**:确保你在 Stripe 结账页面上 +- 检查页面是否已完全加载 +- 尝试刷新页面并再次点击"Fill Everything" + +### 生成的卡号被拒绝 +- **解决方案**:在设置中启用"Card Validation" +- 某些测试环境需要 Luhn 有效的卡号 +- 如果一个 BIN 不起作用,尝试使用不同的 BIN + +### 字段填充不正确 +- **解决方案**:页面可能有自定义字段名称 +- 尝试点击"Clear All Data"按钮并刷新 +- 报告问题时请提供页面 URL + +### 扩展图标未出现 +- **解决方案**:检查扩展是否在 `chrome://extensions/` 中启用 +- 尝试重新加载扩展 +- 检查浏览器控制台是否有错误 + +## 🔐 安全说明 + +### 仅本地存储 +- 所有数据存储在 Chrome 本地存储中 +- 敏感数据不调用外部 API +- 无遥测或跟踪 + +### 数据清理 +使用"Clear All Data"按钮清除: +- Cookies(包括 HttpOnly) +- LocalStorage 和 SessionStorage +- IndexedDB 数据库 +- 缓存和 Service Workers +- Stripe 指纹数据 + +## 📚 资源链接 + +- [Stripe 测试文档](https://stripe.com/docs/testing) +- [Luhn 算法详解](https://zh.wikipedia.org/wiki/Luhn%E7%AE%97%E6%B3%95) +- [Chrome 扩展开发](https://developer.chrome.com/docs/extensions/) + +## 🤝 贡献指南 + +欢迎贡献!请遵循以下指南: + +1. Fork 仓库 +2. 创建特性分支 (`git checkout -b feature/amazing-feature`) +3. 提交更改 (`git commit -m 'Add amazing feature'`) +4. 推送到分支 (`git push origin feature/amazing-feature`) +5. 打开 Pull Request + +### 开发设置 + +```bash +# 克隆你的 fork +git clone https://github.com/yourusername/cardbingenerator.git + +# 创建分支 +git checkout -b my-feature + +# 进行更改并在 Chrome 中测试 +# 从 chrome://extensions 加载未打包的扩展 + +# 提交并推送 +git add . +git commit -m "你的功能描述" +git push origin my-feature +``` + +## 📄 许可证 + +本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件 + +## ⚠️ 法律声明 + +本软件按"原样"提供,不提供任何形式的保证。作者和贡献者: + +- 不支持欺诈活动 +- 不鼓励绕过支付安全 +- 对滥用不承担责任 +- 建议仅在授权测试环境中使用 + +**使用风险和责任自负** + +## 👨‍💻 作者 + +由 **Blackhat_bullet** 创建 + +- 📺 [YouTube 频道](https://www.youtube.com/@Blackhat_bullet) +- 💬 [Telegram 群组](https://t.me/+VJJt9csJoEUxZTA1) +- 📞 [临时短信服务](https://t.me/Tempotpsms_bot) +- 💳 [BIN 生成器网站](https://cardbingenerator.com/) + +## 🙏 致谢 + +- Stripe 提供的优秀测试文档 +- Chrome 扩展社区 +- 所有贡献者和测试人员 + +--- + +
+ 记住:本工具仅用于测试目的。请始终尊重法律和你正在测试的平台的服务条款。 +
+ +--- + +## English Version + +## ⚠️ Disclaimer + +**This tool is for TESTING and EDUCATIONAL purposes only.** + +- Use only on development/test environments or demo payment forms +- Do NOT use on production payment systems +- Do NOT use for fraud or any illegal activities +- The developers assume NO responsibility for misuse of this tool +- Test cards will not work on real payment processors + +## 🌟 Features + +### 💳 Smart Card Generation +- **Luhn Algorithm Validation**: Generates mathematically valid card numbers +- **BIN Support**: Use any Bank Identification Number (BIN) pattern +- **Multiple Card Types**: Automatically detects Visa, Mastercard, Amex, Discover, etc. +- **Smart Generation**: Includes valid expiry dates and CVV codes + +### 🚀 Auto-Fill Capabilities +- **Intelligent Field Detection**: Automatically finds and fills card fields +- **Shadow DOM Support**: Works with modern web components +- **Address Auto-Fill**: Fills billing address, city, state, ZIP code +- **Human-Like Input**: Simulates natural typing patterns +- **Framework Compatible**: Bypasses React/Vue value setters + +### 🛠️ Customization +- **Custom BIN History**: Save and reuse your favorite BIN patterns +- **Multiple Addresses**: Manage custom billing addresses +- **Name Generator**: Built-in identity pool for cardholder names +- **Source Selection**: Choose between static, manual, or auto-generated data + +### 🔒 Privacy Features +- **Data Clearing**: Deep clean cookies, cache, and storage +- **Anti-Fingerprinting**: Clear all Stripe-related browsing data +- **Isolated Storage**: All data stored locally in your browser + +## 📦 Installation + +### From Source + +1. **Clone the repository** + ```bash + git clone https://github.com/yourusername/cardbingenerator.git + cd cardbingenerator + ``` + +2. **Load the extension in Chrome** + - Open Chrome and go to `chrome://extensions/` + - Enable "Developer mode" (toggle in top right) + - Click "Load unpacked" + - Select the project folder + +3. **Pin the extension** + - Click the puzzle icon in Chrome toolbar + - Find "cardbingenerator" and click the pin icon + +## 🚀 Usage + +### Basic Usage + +1. **Open the Extension** + - Click the extension icon in your Chrome toolbar + - You'll see the main interface with tabs: General, BIN List, Settings + +2. **Enter a BIN Pattern** + - In the "BIN Template" field, enter a BIN (e.g., `552461`) + - The extension auto-fills with 'x' to create a 16-digit pattern + - Example: `552461` becomes `552461xxxxxxxxxx` + +3. **Generate & Fill** + - Navigate to a Stripe checkout page + - Click "Fill Everything" button + - The extension will: + - Generate a valid card number + - Fill card details (number, expiry, CVV) + - Fill billing address information + +### Advanced Features + +#### BIN History +- Click the `+` button next to BIN input to save to history +- Click any saved BIN to quickly select it +- Click `×` to remove from history + +#### Custom Addresses +1. Go to **Settings** → **Addresses** tab +2. Fill in the address form fields +3. Click "Add" to save +4. Select "Manual (Custom)" in the Address Source dropdown + +#### Settings Configuration + +**Card Validation Toggle** +- Enable: Generates cards that pass Luhn algorithm validation +- Disable: Generates random cards (faster, less strict) + +**Address Source** +- **Static**: Use built-in default addresses +- **Manual**: Use your custom saved addresses +- **Auto**: Generate random addresses (coming soon) + +**Name Source** +- Controls whether to use built-in names or custom entries +- Names are taken from the address entries + +## 🎯 How It Works + +### Card Number Generation + +The extension uses the **Luhn Algorithm** (mod-10 checksum) to generate valid card numbers: + +1. Takes your BIN pattern (e.g., `552461xxxxxxxxxx`) +2. Fills random digits for each `x` +3. Calculates the Luhn check digit for the last position +4. Validates the entire card number + +```javascript +// Example generated card +BIN Input: 552461xxxxxxxxxx +Generated: 5524610123456789 +Valid: ✅ Passes Luhn validation +Type: Mastercard +``` + +### Auto-Fill Process + +1. **Field Detection**: Scans the page for card and address fields + - Uses CSS selectors and autocomplete attributes + - Penetrates Shadow DOM boundaries + - Scores fields by relevance + +2. **Smart Filling**: Fills fields in optimal order + - Country first (may refresh form structure) + - Card details with simulated typing + - Address fields with proper delays + +3. **Event Triggering**: Fires proper DOM events + - Native value setters bypass framework locks + - Input, change, and blur events + - Focus management for validation triggers + +## 🎨 Supported Card Types + +The extension automatically detects and generates cards for: + +- ✅ Visa (starts with 4) +- ✅ Mastercard (starts with 51-55) +- ✅ American Express (starts with 34, 37) +- ✅ Discover (starts with 6011, 65) +- ✅ JCB (starts with 35) +- ✅ Diners Club (starts with 30, 36, 38) +- ✅ Maestro (starts with 50, 56-58, 6304, 6390, 67) +- ✅ UnionPay (starts with 62) + +## 🐛 Troubleshooting + +### Extension doesn't fill the form +- **Solution**: Make sure you're on a Stripe checkout page +- Check that the page has loaded completely +- Try refreshing the page and clicking "Fill Everything" again + +### Generated cards are rejected +- **Solution**: Enable "Card Validation" in settings +- Some test environments require Luhn-valid cards +- Use a different BIN if one doesn't work + +### Fields are filled incorrectly +- **Solution**: The page may have custom field names +- Try clicking the "Clear All Data" button and refresh +- Report the issue with the page URL + +### Extension icon doesn't appear +- **Solution**: Check if extension is enabled in `chrome://extensions/` +- Try reloading the extension +- Check browser console for errors + +## 🔐 Security Notes + +### Local Storage Only +- All data stored locally in Chrome storage +- No external API calls for sensitive data +- No telemetry or tracking + +### Data Clearing +Use the "Clear All Data" button to remove: +- Cookies (including HttpOnly) +- LocalStorage and SessionStorage +- IndexedDB databases +- Cache and Service Workers +- Stripe fingerprinting data + +## 👨‍💻 Author + +Created by **Blackhat_bullet** + +- 📺 [YouTube Channel](https://www.youtube.com/@Blackhat_bullet) +- 💬 [Telegram Group](https://t.me/+VJJt9csJoEUxZTA1) +- 📞 [Temp SMS Service](https://t.me/Tempotpsms_bot) +- 💳 [BIN Generator Website](https://cardbingenerator.com/) + +## 📄 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## ⚠️ Legal Notice + +This software is provided "as is" without warranty of any kind. The authors and contributors: + +- Do NOT endorse fraudulent activities +- Do NOT encourage bypassing payment security +- Do NOT take responsibility for misuse +- Recommend using only in authorized test environments + +**Use at your own risk and responsibility.** + +--- + +
+ Remember: This tool is for testing purposes only. Always respect the law and terms of service of the platforms you're testing on. +
diff --git a/background.js b/background.js new file mode 100644 index 0000000..d3cb7f7 --- /dev/null +++ b/background.js @@ -0,0 +1,550 @@ +/** + * 虚拟身份池 + * 用于生成随机持卡人姓名 + */ +const FIRST_NAMES = ["John", "Michael", "David", "James", "Robert", "William", "Richard", "Joseph", "Charles", "Thomas", "Christopher", "Daniel", "Matthew", "Anthony", "Mark", "Donald", "Steven", "Paul", "Andrew", "Joshua", "Kenneth", "Kevin", "Brian", "Mary", "Patricia", "Jennifer", "Linda", "Barbara", "Elizabeth", "Susan", "Jessica", "Sarah", "Karen", "Nancy", "Lisa", "Betty", "Margaret", "Sandra"]; +const LAST_NAMES = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin", "Lee", "Thompson", "White", "Harris", "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson", "Walker", "Young"]; + +/** + * 默认地址配置 + */ +const DEFAULT_ADDRESSES = [{ + name: "John Smith", + firstName: "John", + lastName: "Smith", + address1: "69 Adams Street", + address2: "", + city: "Brooklyn", + state: "New York", + stateCode: "NY", + postal: "11201", + countryText: "United States", + countryValue: "US" +}, { + name: "Michael Johnson", + firstName: "Michael", + lastName: "Johnson", + address1: "3511 Carlisle Avenue", + address2: "", + city: "Covington", + state: "Kentucky", + stateCode: "KY", + postal: "41015", + countryText: "United States", + countryValue: "US" +}]; + +// ========================================== +// 算法工具函数 (Luhn Algorithm) +// ========================================== + +function randomChoice(array) { + return array[Math.floor(Math.random() * array.length)]; +} + +/** + * 根据卡号前缀识别卡组织类型 + */ +function getCardType(cardNumber) { + const patterns = { + Visa: /^4/, + Mastercard: /^5[1-5]/, + "American Express": /^3[47]/, + Discover: /^6(?:011|5)/, + JCB: /^35/, + "Diners Club": /^3(?:0[0-5]|[68])/, + Maestro: /^(?:5[0678]\d\d|6304|6390|67\d\d)/, + UnionPay: /^62/ + }; + for (const [type, regex] of Object.entries(patterns)) { + if (regex.test(cardNumber)) { + return type; + } + } + return "Unknown"; +} + +/** + * 计算 Luhn 校验位 (模10算法) + * 用于确保生成的卡号在数学上是有效的 + */ +function calculateLuhnCheckDigit(digits) { + let sum = 0; + let shouldDouble = true; + for (let i = digits.length - 1; i >= 0; i--) { + let digit = parseInt(digits[i]); + if (shouldDouble) { + digit *= 2; + if (digit > 9) { + digit -= 9; + } + } + sum += digit; + shouldDouble = !shouldDouble; + } + return (10 - sum % 10) % 10; +} + +/** + * 验证卡号是否符合 Luhn 算法 + */ +function validateLuhn(number) { + const digits = number.replace(/\D/g, ""); + let sum = 0; + let shouldDouble = false; + for (let i = digits.length - 1; i >= 0; i--) { + let digit = parseInt(digits[i]); + if (shouldDouble) { + digit *= 2; + if (digit > 9) { + digit -= 9; + } + } + sum += digit; + shouldDouble = !shouldDouble; + } + return sum % 10 === 0; +} + +/** + * 生成符合校验规则的卡号 + * @param {string} binPattern - BIN 格式 (如 "552461xxxxxxxxxx") + */ +function generateValidCardNumber(binPattern) { + let rawNumber = ""; + // 1. 填充随机数 + for (let i = 0; i < binPattern.length - 1; i++) { + if (binPattern[i] === "x" || binPattern[i] === "X") { + rawNumber += Math.floor(Math.random() * 10); + } else { + rawNumber += binPattern[i]; + } + } + // 2. 计算最后一位校验码 + const checkDigit = calculateLuhnCheckDigit(rawNumber); + rawNumber += checkDigit; + return rawNumber; +} + +function generateExpiryDate() { + const now = new Date(); + const currentYear = now.getFullYear(); + const currentMonth = now.getMonth() + 1; + + // 随机生成 1 到 60 个月后的日期 + const randomMonths = Math.floor(Math.random() * 60) + 1; + let targetMonth = currentMonth + randomMonths; + let targetYear = currentYear; + + while (targetMonth > 12) { + targetMonth -= 12; + targetYear += 1; + } + + return { + month: targetMonth.toString().padStart(2, "0"), + year: targetYear.toString() + }; +} + +function generateCVV(length = 3) { + let cvv = ""; + for (let i = 0; i < length; i++) { + cvv += Math.floor(Math.random() * 10); + } + return cvv; +} + +// ========================================== +// 核心生成逻辑 +// ========================================== + +/** + * 本地生成模式:使用内置算法生成卡号 + */ +function generateCardsLocally(bin, amount = 10) { + const cards = []; + const uniqueSet = new Set(); + console.log(`🎲 Generating ${amount} valid cards from BIN: ${bin}`); + + let attempts = 0; + const maxAttempts = amount * 10; + + while (cards.length < amount && attempts < maxAttempts) { + attempts++; + const number = generateValidCardNumber(bin); + + if (uniqueSet.has(number)) continue; + if (!validateLuhn(number)) { + console.warn("⚠️ Generated invalid card (should not happen):", number); + continue; + } + + uniqueSet.add(number); + const expiry = generateExpiryDate(); + const cvv = generateCVV(3); + const type = getCardType(number); + + cards.push({ + serial_number: cards.length + 1, + card_number: number, + expiry_month: expiry.month, + expiry_year: expiry.year, + cvv: cvv, + card_type: type, + full_format: `${number}|${expiry.month}|${expiry.year}|${cvv}`, + luhn_valid: true + }); + } + + console.log(`[cardbingenerator] Successfully generated ${cards.length} valid cards`); + + // 最后的完整性检查 + const invalid = cards.filter(c => !validateLuhn(c.card_number)); + if (invalid.length > 0) { + console.error(`❌ Found ${invalid.length} invalid cards!`); + } else { + console.log("[cardbingenerator] All cards passed Luhn validation"); + } + + // 统计卡种分布 + const stats = {}; + cards.forEach(c => { + stats[c.card_type] = (stats[c.card_type] || 0) + 1; + }); + console.log("📊 Card types:", stats); + + return cards; +} + +/** + * 简单生成模式:仅生成随机数,不进行 Luhn 校验 (用于某些不需要校验的测试场景) + */ +function generateCardsSimple(bin, amount = 10) { + const cards = []; + const uniqueSet = new Set(); + console.log(`🎲 Generating ${amount} cards (no validation) from BIN: ${bin}`); + + for (let i = 0; i < amount; i++) { + let number = ""; + for (let j = 0; j < bin.length; j++) { + if (bin[j] === "x" || bin[j] === "X") { + number += Math.floor(Math.random() * 10); + } else { + number += bin[j]; + } + } + + if (uniqueSet.has(number)) { + i--; + continue; + } + uniqueSet.add(number); + const expiry = generateExpiryDate(); + const cvv = generateCVV(3); + const type = getCardType(number); + + cards.push({ + serial_number: i + 1, + card_number: number, + expiry_month: expiry.month, + expiry_year: expiry.year, + cvv: cvv, + card_type: type, + full_format: `${number}|${expiry.month}|${expiry.year}|${cvv}`, + luhn_valid: false + }); + } + console.log(`[cardbingenerator] Generated ${cards.length} cards (simple mode)`); + return cards; +} + +// ========================================== +// 远程生成逻辑 (AKR-Gen) +// ========================================== + +/** + * 远程生成模式:操控第三方网站 (akr-gen.bigfk.com) 生成卡号 + * 当本地算法不满足需求时使用 + */ +async function generateCardsFromAKR(bin, onComplete) { + let tab = null; + try { + console.log("[cardbingenerator] Opening AKR-gen tab..."); + // 创建一个不激活的标签页(后台静默打开) + tab = await chrome.tabs.create({ + url: "https://akr-gen.bigfk.com/", + active: false + }); + + console.log("[cardbingenerator] Waiting for page load..."); + await new Promise(resolve => setTimeout(resolve, 4000)); + + console.log("[cardbingenerator] Filling BIN and generating cards..."); + // 注入脚本:自动填入BIN并点击生成 + const fillResult = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: remotePage_fillAndClick, + args: [bin] + }); + console.log("Fill result:", fillResult[0]?.result); + + console.log("⏳ Waiting a moment before checking results..."); + await new Promise(resolve => setTimeout(resolve, 2000)); + + console.log("📥 Getting generated cards (will wait up to 10 seconds)..."); + // 注入脚本:抓取结果 + const scrapeResult = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: remotePage_scrapeResults + }); + + console.log("[cardbingenerator] Closing AKR-gen tab..."); + await chrome.tabs.remove(tab.id); + tab = null; + + if (scrapeResult && scrapeResult[0] && scrapeResult[0].result) { + const parsedCards = parseScrapedCards(scrapeResult[0].result); + console.log(`[cardbingenerator] Generated ${parsedCards.length} cards`); + + if (parsedCards.length > 0) { + const randomAddr = await getRandomAddress(); + // 存入 storage 供 Content Script 使用 + chrome.storage.local.set({ + generatedCards: parsedCards, + randomData: randomAddr + }); + onComplete({ success: true, cards: parsedCards }); + } else { + console.error("❌ No cards generated from AKR"); + onComplete({ success: false, error: "No cards generated from AKR-gen" }); + } + } else { + console.error("❌ Failed to retrieve cards from result"); + onComplete({ success: false, error: "Failed to retrieve cards from page" }); + } + + } catch (err) { + console.error("❌ Error in generateCardsFromAKR:", err); + if (tab) { + try { await chrome.tabs.remove(tab.id); } catch (e) {} + } + onComplete({ success: false, error: err.message }); + } +} + +// -- 以下函数会被注入到目标页面执行,不能使用外部变量 -- + +function remotePage_fillAndClick(bin) { + return new Promise(resolve => { + function waitForElement(selector, retries = 10, delay = 300) { + return new Promise(res => { + let count = 0; + const check = () => { + const el = document.querySelector(selector) || document.getElementById(selector.replace("#", "")); + if (el) { + res(el); + } else if (count < retries) { + count++; + setTimeout(check, delay); + } else { + res(null); + } + }; + check(); + }); + } + + waitForElement("bin").then(input => { + if (input) { + console.log("[cardbingenerator] Found BIN input, filling with:", bin); + input.value = bin; + input.dispatchEvent(new Event("input", { bubbles: true })); + input.dispatchEvent(new Event("change", { bubbles: true })); + + setTimeout(() => { + waitForElement("button[type=\"submit\"]").then(btn => { + if (btn) { + console.log("[cardbingenerator] Found generate button, clicking..."); + btn.click(); + resolve(true); + } else { + console.error("❌ Generate button not found"); + resolve(false); + } + }); + }, 500); + } else { + console.error("❌ BIN input not found"); + resolve(false); + } + }); + }); +} + +function remotePage_scrapeResults() { + return new Promise(resolve => { + function waitLoop(retries = 20, delay = 500) { + let count = 0; + const check = () => { + const resultArea = document.getElementById("result"); + if (resultArea && resultArea.value.trim()) { + console.log("[cardbingenerator] Found generated cards:", resultArea.value.split("\n").length, "lines"); + resolve(resultArea.value); + } else if (count < retries) { + count++; + console.log(`[cardbingenerator] Waiting for cards... attempt ${count}/${retries}`); + setTimeout(check, delay); + } else { + console.error("❌ Timeout waiting for cards"); + resolve(""); + } + }; + check(); + } + waitLoop(); + }); +} + +// ---------------------------------------------------- + +function parseScrapedCards(text) { + if (!text) return []; + const lines = text.trim().split("\n"); + const cards = []; + + lines.forEach((line, index) => { + if (line.trim()) { + const parts = line.trim().split("|"); + if (parts.length === 4) { + cards.push({ + serial_number: index + 1, + card_number: parts[0], + expiry_month: parts[1], + expiry_year: parts[2], + cvv: parts[3], + full_format: line.trim() + }); + } + } + }); + return cards; +} + +/** + * 辅助:获取随机地址 + */ +async function getRandomAddress() { + return new Promise(resolve => { + chrome.storage.local.get(["customAddresses"], data => { + const custom = data.customAddresses || []; + const pool = [...custom, ...DEFAULT_ADDRESSES]; + if (pool.length === 0) { + resolve(DEFAULT_ADDRESSES[0]); + } else { + const selected = randomChoice(pool); + resolve(selected); + } + }); + }); +} + +// ========================================== +// 数据清理逻辑 (Browsing Data API) +// ========================================== + +async function clearStripeBrowsingData(sendResponse) { + try { + const domains = ["stripe.com", "checkout.stripe.com", "js.stripe.com", "hooks.stripe.com"]; + + // 1. 深度清理 Cookies (针对特定域) + for (const domain of domains) { + const cookies = await chrome.cookies.getAll({ domain: domain }); + for (const cookie of cookies) { + await chrome.cookies.remove({ + url: "https://" + cookie.domain + cookie.path, + name: cookie.name + }); + } + } + + // 2. 清理浏览器缓存和存储 (针对特定 Origin) + await chrome.browsingData.remove({ + origins: domains.map(d => "https://" + d) + }, { + cache: true, + cookies: true, + localStorage: true, + indexedDB: true, + serviceWorkers: true, + cacheStorage: true + }); + + console.log("[cardbingenerator] Deep clear completed for Stripe domains"); + if (sendResponse) sendResponse({ success: true }); + + } catch (err) { + console.error("Error in deep clear:", err); + if (sendResponse) sendResponse({ success: false, error: err.message }); + } +} + +// ========================================== +// 消息监听与路由 +// ========================================== + +// 安装时初始化默认 BIN +chrome.runtime.onInstalled.addListener(() => { + chrome.storage.local.get(["currentBin", "binHistory"], data => { + if (!data.currentBin) { + chrome.storage.local.set({ + currentBin: "552461xxxxxxxxxx", + binHistory: ["552461xxxxxxxxxx"] + }); + } + }); +}); + +// 主消息处理器 +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === "generateCards") { + // 路由到生成逻辑 + generateCardsHandler(request.bin, request.useValidation, sendResponse); + return true; // 保持消息通道打开以进行异步响应 + } + if (request.action === "clearBrowsingData") { + // 路由到清理逻辑 + clearStripeBrowsingData(sendResponse); + return true; + } +}); + +async function generateCardsHandler(bin, useValidation = true, sendResponse) { + try { + console.log(`[cardbingenerator] Starting card generation... (Luhn: ${useValidation ? "ON" : "OFF"})`); + + // 根据配置决定是验证 Luhn 还是简单生成 + const cards = useValidation ? + generateCardsLocally(bin, 10) : + generateCardsSimple(bin, 10); + + if (cards.length > 0) { + const address = await getRandomAddress(); + + // 将结果存入 storage,这样 Content Script (第一部分代码) 就能读到了 + chrome.storage.local.set({ + generatedCards: cards, + randomData: address + }); + + console.log(`[cardbingenerator] Generated and saved ${cards.length} cards`); + sendResponse({ success: true, cards: cards }); + } else { + console.error("❌ No cards generated"); + sendResponse({ success: false, error: "Failed to generate cards" }); + } + } catch (err) { + console.error("❌ Error in generateCardsHandler:", err); + sendResponse({ success: false, error: err.message }); + } +} diff --git a/content.js b/content.js new file mode 100644 index 0000000..029e53a --- /dev/null +++ b/content.js @@ -0,0 +1,880 @@ +/** + * 全局状态标记 + */ +let isProcessing = false; +let fillButton = null; +let clearButton = null; + +/** + * 默认地址数据源 + */ +const DEFAULT_ADDRESSES = [{ + name: "John Smith", + firstName: "John", + lastName: "Smith", + address1: "69 Adams Street", + address2: "", + city: "Brooklyn", + state: "New York", + stateCode: "NY", + postal: "11201", + countryText: "United States", + countryValue: "US" +}, { + name: "Michael Johnson", + firstName: "Michael", + lastName: "Johnson", + address1: "3511 Carlisle Avenue", + address2: "", + city: "Covington", + state: "Kentucky", + stateCode: "KY", + postal: "41015", + countryText: "United States", + countryValue: "US" +}]; + +/** + * 字段匹配同义词词典 + * 用于启发式识别表单字段类型 + */ +const FIELD_SYNONYMS = { + fullName: ["full name", "name", "cardholder name", "card name", "cc-name"], + firstName: ["first name", "given-name"], + lastName: ["last name", "family-name", "surname"], + address1: ["address", "address line 1", "street", "addressline1", "address-line1", "address-line-1"], + address2: ["address line 2", "apt", "apartment", "addressline2", "address-line2", "suite"], + city: ["city", "locality", "address-level2"], + state: ["state", "region", "province", "administrative area", "address-level1", "address level 1"], + postal: ["postal", "zip", "postcode", "postal-code"], + country: ["country", "country or region"] +}; + +const CARD_FIELD_WORDS = ["card", "cvc", "cvv", "expiry", "expiration", "valid thru", "month", "year"]; + +// ========================================== +// 核心工具函数 +// ========================================== + +/** + * 获取随机地址 + * 优先从 storage 读取配置,支持 static/manual/auto 模式 + */ +async function getRandomAddress() { + return new Promise(resolve => { + chrome.storage.local.get(["customAddresses", "addressSource"], data => { + const customList = data.customAddresses || []; + const sourceMode = data.addressSource || "static"; + let addressPool = []; + + switch (sourceMode) { + case "static": + addressPool = DEFAULT_ADDRESSES; + break; + case "manual": + addressPool = customList.length > 0 ? customList : DEFAULT_ADDRESSES; + break; + case "auto": + addressPool = DEFAULT_ADDRESSES; + break; + default: + addressPool = DEFAULT_ADDRESSES; + } + + if (addressPool.length === 0) { + resolve(DEFAULT_ADDRESSES[0]); + } else { + const selected = addressPool[Math.floor(Math.random() * addressPool.length)]; + console.log(`[cardbingenerator] Using ${sourceMode} address:`, selected.name); + resolve(selected); + } + }); + }); +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function randomDelay(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +/** + * 模拟人类输入文本 + * @param {HTMLElement} element - 目标输入框 + * @param {string} text - 要输入的文本 + * @param {boolean} clearFirst - 是否先清空输入框 (默认 false) + */ +async function typeText(element, text, clearFirst = false) { + if (!element || !text) return; + + element.focus(); + element.scrollIntoView({ behavior: "smooth", block: "center" }); + await sleep(randomDelay(150, 300)); + + if (clearFirst && text.length < 50) { + // 模拟逐字删除或清空 + element.value = ""; + for (let i = 0; i < text.length; i++) { + element.value += text[i]; + element.dispatchEvent(new Event("input", { bubbles: true })); + await sleep(randomDelay(30, 80)); // 模拟击键间隔 + } + element.dispatchEvent(new Event("change", { bubbles: true })); + } else { + // 直接赋值模式 + element.value = text; + element.dispatchEvent(new Event("input", { bubbles: true })); + element.dispatchEvent(new Event("change", { bubbles: true })); + } + + await sleep(randomDelay(100, 200)); + element.blur(); + await sleep(randomDelay(200, 400)); +} + +/** + * 收集页面所有根节点(包括 Shadow DOM) + * 用于穿透 Shadow DOM 查找元素 + */ +function collectRoots() { + const roots = [document]; + const queue = [document.documentElement]; + + while (queue.length) { + const node = queue.pop(); + if (!node) continue; + + if (node.shadowRoot) { + roots.push(node.shadowRoot); + } + + const children = node.children || []; + for (let i = 0; i < children.length; i++) { + queue.push(children[i]); + } + } + return roots; +} + +/** + * 检测元素是否可见且可交互 + */ +function isVisible(el) { + if (!el) return false; + const rect = el.getBoundingClientRect(); + const style = window.getComputedStyle(el); + + if (style.visibility === "hidden" || style.display === "none") return false; + if (el.disabled) return false; + if (rect.width <= 0 || rect.height <= 0) return false; + if (el.type === "hidden") return false; + + return true; +} + +/** + * 收集页面所有可见的表单元素 (input, select, textarea) + */ +function collectFormElements() { + const elements = []; + for (const root of collectRoots()) { + const nodes = root.querySelectorAll("input, select, textarea"); + nodes.forEach(node => { + if (isVisible(node)) { + elements.push(node); + } + }); + } + return elements; +} + +/** + * 使用原生 Setter 设置值并触发事件 + * 绕过 React/Vue 等框架的状态绑定限制 + */ +async function setNativeValueAndDispatch(element, value, simulateTyping = false) { + if (!element) return; + + try { + element.focus(); + element.scrollIntoView({ behavior: "smooth", block: "center" }); + await sleep(randomDelay(150, 300)); + + const tagName = element.tagName; + + if (tagName === "INPUT" || tagName === "TEXTAREA") { + const proto = tagName === "INPUT" ? window.HTMLInputElement.prototype : window.HTMLTextAreaElement.prototype; + const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value").set; + + if (simulateTyping && value && value.length < 30) { + // 模拟打字效果 + element.value = ""; + for (let i = 0; i < value.length; i++) { + nativeSetter.call(element, element.value + value[i]); + element.dispatchEvent(new Event("input", { bubbles: true })); + await sleep(randomDelay(50, 120)); + } + } else { + // 快速填充 + nativeSetter.call(element, value); + element.dispatchEvent(new Event("input", { bubbles: true })); + } + + element.dispatchEvent(new Event("change", { bubbles: true })); + await sleep(randomDelay(150, 250)); + element.blur(); + + } else if (tagName === "SELECT") { + element.value = value; + element.dispatchEvent(new Event("change", { bubbles: true })); + await sleep(randomDelay(200, 350)); + element.blur(); + } + + await sleep(randomDelay(300, 500)); + + } catch (err) { + // 降级处理:直接赋值 + try { + element.focus(); + await sleep(randomDelay(150, 300)); + element.value = value; + element.dispatchEvent(new Event("input", { bubbles: true })); + element.dispatchEvent(new Event("change", { bubbles: true })); + await sleep(randomDelay(150, 250)); + element.blur(); + await sleep(randomDelay(300, 500)); + } catch (ignored) {} + } +} + +/** + * 处理 Select 下拉框的值选择 + * 尝试匹配 value, textContent 或模糊匹配 + */ +function pickSelectValue(selectEl, valueOptions, textOptions) { + if (!selectEl || selectEl.tagName !== "SELECT") return null; + + const options = Array.from(selectEl.options || []); + const normalize = str => (str || "").toLowerCase().replace(/\s+/g, " ").trim(); + const includesText = (text, query) => normalize(text).includes(normalize(query)); + + // 1. 尝试匹配 value + for (const val of valueOptions || []) { + const found = options.find(opt => normalize(opt.value) === normalize(val)); + if (found) return found.value; + } + + // 2. 尝试匹配 textContent (精确) + for (const txt of textOptions || []) { + const found = options.find(opt => normalize(opt.textContent) === normalize(txt)); + if (found) return found.value; + } + + // 3. 尝试匹配 textContent (包含) + for (const txt of textOptions || []) { + const found = options.find(opt => includesText(opt.textContent, txt)); + if (found) return found.value; + } + + return null; +} + +// ========================================== +// 字段识别逻辑 +// ========================================== + +function getElementTextAttributes(el) { + const attrs = [ + el.getAttribute("name"), + el.getAttribute("id"), + el.getAttribute("placeholder"), + el.getAttribute("aria-label"), + el.getAttribute("autocomplete"), + el.getAttribute("data-testid"), + el.getAttribute("data-qa") + ].filter(Boolean); + return attrs.join(" ").toLowerCase(); +} + +function matchesAny(text, keywords) { + const lowerText = text.toLowerCase(); + return keywords.some(kw => lowerText.includes(kw.toLowerCase())); +} + +function isCardField(el) { + const text = getElementTextAttributes(el); + if (!text) return false; + return matchesAny(text, CARD_FIELD_WORDS); +} + +/** + * 计算字段与特定类型的匹配分数 + */ +function scoreForSynonyms(el, synonyms) { + const text = getElementTextAttributes(el); + let score = 0; + if (!text) return score; + + const autocomplete = (el.getAttribute("autocomplete") || "").toLowerCase(); + + // 权重最高:autocomplete 属性匹配 + for (const syn of synonyms) { + if (autocomplete.split(/\s+/).includes(syn.toLowerCase())) { + score += 6; + } + } + + // 权重中等:name 或 id 包含关键词 + const nameId = [(el.getAttribute("name") || "").toLowerCase(), (el.getAttribute("id") || "").toLowerCase()].join(" "); + for (const syn of synonyms) { + if (nameId.includes(syn.toLowerCase())) { + score += 4; + } + } + + // 权重最低:placeholder 或 aria-label 包含关键词 + const desc = [(el.getAttribute("placeholder") || "").toLowerCase(), (el.getAttribute("aria-label") || "").toLowerCase()].join(" "); + for (const syn of synonyms) { + if (desc.includes(syn.toLowerCase())) { + score += 2; + } + } + + return score; +} + +/** + * 寻找最佳匹配字段 + */ +function findBestField(elements, synonyms, validator) { + let bestEl = null; + let bestScore = 0; + + for (const el of elements) { + if (validator && !validator(el)) continue; + + const score = scoreForSynonyms(el, synonyms); + if (score > bestScore) { + bestEl = el; + bestScore = score; + } + } + return bestEl; +} + +/** + * 识别所有非卡号类的普通表单字段 (姓名、地址等) + */ +function detectAddressFields() { + const formElements = collectFormElements().filter(el => !isCardField(el)); + const fields = {}; + + fields.firstName = findBestField(formElements, FIELD_SYNONYMS.firstName, el => el.tagName !== "SELECT"); + fields.lastName = findBestField(formElements, FIELD_SYNONYMS.lastName, el => el.tagName !== "SELECT"); + fields.fullName = findBestField(formElements, FIELD_SYNONYMS.fullName, el => el.tagName !== "SELECT"); + fields.address1 = findBestField(formElements, FIELD_SYNONYMS.address1, el => el.tagName !== "SELECT"); + fields.address2 = findBestField(formElements, FIELD_SYNONYMS.address2, el => el.tagName !== "SELECT"); + fields.city = findBestField(formElements, FIELD_SYNONYMS.city, el => el.tagName !== "SELECT"); + fields.state = findBestField(formElements, FIELD_SYNONYMS.state, el => el.tagName !== "SELECT"); + fields.postal = findBestField(formElements, FIELD_SYNONYMS.postal, el => el.tagName !== "SELECT"); + fields.country = findBestField(formElements, FIELD_SYNONYMS.country, () => true); // Country 可以是 select + + // 逻辑修正:避免全名和名/姓重复匹配 + if (fields.fullName) { + if (fields.firstName === fields.fullName) fields.firstName = null; + if (fields.lastName === fields.fullName) fields.lastName = null; + } + if (fields.firstName && fields.lastName && fields.firstName === fields.lastName) { + fields.fullName = fields.firstName; + fields.firstName = null; + fields.lastName = null; + } + + return fields; +} + +/** + * 专门识别信用卡相关字段 + * 使用 CSS 选择器优先匹配 + */ +function detectCardFields() { + const roots = collectRoots(); + let numEl, expEl, cvcEl; + + const numSelectors = ["input[autocomplete=\"cc-number\"]", "input[name*=\"cardnumber\" i]", "input[id*=\"cardnumber\" i]", "input[name=\"cardNumber\"]", "input[placeholder*=\"1234\"]", "#cardNumber"]; + const expSelectors = ["input[autocomplete=\"cc-exp\"]", "input[name*=\"exp\" i]", "input[id*=\"exp\" i]", "input[placeholder*=\"MM\"]", "input[name=\"cardExpiry\"]", "#cardExpiry"]; + const cvcSelectors = ["input[autocomplete=\"cc-csc\"]", "input[name*=\"cvc\" i]", "input[name*=\"cvv\" i]", "input[id*=\"cvc\" i]", "input[placeholder*=\"CVC\"]", "input[name=\"cardCvc\"]", "#cardCvc"]; + + function findInRoots(selectors) { + for (const root of roots) { + for (const sel of selectors) { + const el = root.querySelector(sel); + if (el && isVisible(el)) return el; + } + } + return null; + } + + numEl = findInRoots(numSelectors); + expEl = findInRoots(expSelectors); + cvcEl = findInRoots(cvcSelectors); + + if (numEl || expEl || cvcEl) { + return { number: numEl, exp: expEl, cvc: cvcEl }; + } + return null; +} + +/** + * 自动点击“手动输入地址”按钮(如果有的话) + * 常见于 Google Places Autocomplete 覆盖了原生输入框的情况 + */ +function clickManualAddressIfPresent() { + const keywords = ["enter address manually", "manually enter address", "ввести адрес вручную", "введите адрес вручную", "адрес вручную"]; + try { + const roots = collectRoots(); + const tagSelectors = ["button", "[role=\"button\"]", "a", ".Button", ".Link", "span[role=\"button\"]", "div[role=\"button\"]"]; + + for (const root of roots) { + for (const selector of tagSelectors) { + const elements = root.querySelectorAll(selector); + for (let i = 0; i < elements.length; i++) { + const el = elements[i]; + if (!isVisible(el)) continue; + + const text = [ + el.textContent || "", + el.getAttribute("aria-label") || "", + el.getAttribute("title") || "", + el.getAttribute("data-testid") || "" + ].join(" ").toLowerCase(); + + if (!text) continue; + + if (keywords.some(kw => text.includes(kw.toLowerCase()))) { + const clickable = el.closest("button, [role=\"button\"], a, [role=\"link\"]") || el; + console.log("[cardbingenerator] Clicking manual address button:", el.textContent); + clickable.click(); + return true; + } + } + } + } + } catch (err) {} + return false; +} + +// ========================================== +// 主业务逻辑:自动填充流程 +// ========================================== + +async function autofillAll() { + if (isProcessing) return; + isProcessing = true; + + try { + showNotification("🔄 Starting auto-fill...", "info"); + await sleep(randomDelay(500, 1000)); + + // 1. 生成新卡片 + showNotification("🔄 Generating fresh cards...", "info"); + const storage = await chrome.storage.local.get(["currentBin"]); + const currentBin = storage.currentBin || "552461xxxxxxxxxx"; + + // 发送消息给 background script 生成卡片 + const genResult = await new Promise(resolve => { + chrome.runtime.sendMessage({ + action: "generateCards", + bin: currentBin, + stripeTabId: null + }, response => resolve(response)); + }); + + if (!genResult || !genResult.success) { + showNotification("❌ Failed to generate cards: " + (genResult?.error || "Unknown error"), "error"); + isProcessing = false; + return; + } + + await sleep(2000); + + // 2. 获取生成的卡片数据 + const cardStorage = await chrome.storage.local.get(["generatedCards"]); + if (!cardStorage.generatedCards || cardStorage.generatedCards.length === 0) { + showNotification("❌ No cards were generated", "error"); + isProcessing = false; + return; + } + + const selectedCard = cardStorage.generatedCards[Math.floor(Math.random() * cardStorage.generatedCards.length)]; + const addressData = await getRandomAddress(); + + showNotification("💳 Filling card details...", "info"); + await sleep(randomDelay(400, 700)); + + // 3. 处理 Stripe 常见的折叠卡片按钮 + const accordionBtn = document.querySelector("[data-testid=\"card-accordion-item-button\"]"); + if (accordionBtn && isVisible(accordionBtn)) { + console.log("[cardbingenerator] Clicking card button..."); + accordionBtn.scrollIntoView({ behavior: "smooth", block: "center" }); + await sleep(randomDelay(300, 500)); + accordionBtn.click(); + await sleep(randomDelay(800, 1200)); + } + + // 4. 填充卡片信息 + const cardFields = detectCardFields(); + if (cardFields) { + if (cardFields.number) { + console.log("[cardbingenerator] Filling card number..."); + await setNativeValueAndDispatch(cardFields.number, selectedCard.card_number); + } + if (cardFields.exp) { + console.log("[cardbingenerator] Filling expiry date..."); + const expStr = selectedCard.expiry_month + " / " + selectedCard.expiry_year.slice(-2); + await setNativeValueAndDispatch(cardFields.exp, expStr); + } + if (cardFields.cvc) { + console.log("[cardbingenerator] Filling CVC..."); + await setNativeValueAndDispatch(cardFields.cvc, selectedCard.cvv); + } + } + + // 5. 填充地址信息 + showNotification("📝 Filling address...", "info"); + const addrFields = detectAddressFields(); + + // 优先处理国家选择,因为这可能会刷新表单格式 + if (addrFields.country && addrFields.country.tagName === "SELECT") { + console.log("[cardbingenerator] Filling country..."); + const countryVal = pickSelectValue(addrFields.country, [addressData.countryValue], [addressData.countryText]); + if (countryVal) { + await setNativeValueAndDispatch(addrFields.country, countryVal); + } + await sleep(300); + } + + // 检查是否有“手动输入地址”按钮并点击 + const clickedManual = clickManualAddressIfPresent(); + if (clickedManual) { + console.log("[cardbingenerator] Manual address button clicked"); + await sleep(randomDelay(800, 1200)); + } + + // 填充详细地址字段 + const fillAddressDetails = async () => { + const currentFields = detectAddressFields(); // 重新检测,因为 DOM 可能已变化 + + // 名字处理逻辑 + if (currentFields.firstName && currentFields.lastName && currentFields.firstName !== currentFields.lastName) { + console.log("[cardbingenerator] Filling first name..."); + await setNativeValueAndDispatch(currentFields.firstName, addressData.firstName, true); + console.log("[cardbingenerator] Filling last name..."); + await setNativeValueAndDispatch(currentFields.lastName, addressData.lastName, true); + } else { + const nameField = currentFields.fullName || currentFields.firstName || currentFields.lastName; + if (nameField) { + console.log("[cardbingenerator] Filling full name..."); + await setNativeValueAndDispatch(nameField, addressData.name, true); + } + } + + if (currentFields.address1) { + console.log("[cardbingenerator] Filling address line 1..."); + await setNativeValueAndDispatch(currentFields.address1, addressData.address1, false); + } + if (currentFields.address2) { + console.log("[cardbingenerator] Filling address line 2..."); + await setNativeValueAndDispatch(currentFields.address2, addressData.address2, false); + } + if (currentFields.city) { + console.log("[cardbingenerator] Filling city..."); + await setNativeValueAndDispatch(currentFields.city, addressData.city, true); + } + if (currentFields.postal) { + console.log("[cardbingenerator] Filling postal code..."); + await setNativeValueAndDispatch(currentFields.postal, addressData.postal, false); + } + }; + + await fillAddressDetails(); + + // 延迟填充 State/Province,因为有些表单需要先填 Country/Zip 才会出现 State + const fillState = async () => { + await sleep(randomDelay(400, 600)); + const fieldsNow = detectAddressFields(); + if (!fieldsNow.state) return; + + if (fieldsNow.state.tagName === "SELECT") { + console.log("📍 Filling state (select)..."); + const stateVal = pickSelectValue(fieldsNow.state, [addressData.stateCode], [addressData.state]); + if (stateVal) { + await setNativeValueAndDispatch(fieldsNow.state, stateVal); + } + } else { + console.log("📍 Filling state (input)..."); + await setNativeValueAndDispatch(fieldsNow.state, addressData.stateCode || addressData.state, true); + } + }; + + await fillState(); + await sleep(randomDelay(600, 1000)); + + // 清理已使用的卡片数据 + chrome.storage.local.remove(["generatedCards"], () => { + console.log("[cardbingenerator] Cleared used cards from storage"); + }); + + console.log("[cardbingenerator] Auto-fill completed!"); + showNotification("✅ All fields filled successfully!", "success"); + + } catch (err) { + chrome.storage.local.remove(["generatedCards"]); + showNotification("❌ Error: " + err.message, "error"); + console.error("Autofill error:", err); + } + isProcessing = false; +} + +// ========================================== +// UI 辅助功能:通知和清理按钮 +// ========================================== + +function showNotification(message, type = "info") { + const existing = document.getElementById("auto-card-filler-notification"); + if (existing) existing.remove(); + + const notif = document.createElement("div"); + notif.id = "auto-card-filler-notification"; + notif.textContent = message; + + const colors = { + info: "#3498db", + success: "#2ecc71", + warning: "#f39c12", + error: "#e74c3c" + }; + + Object.assign(notif.style, { + position: "fixed", + top: "20px", + right: "20px", + background: colors[type] || colors.info, + color: "white", + padding: "15px 20px", + borderRadius: "8px", + boxShadow: "0 4px 12px rgba(0,0,0,0.3)", + zIndex: "9999999", + fontSize: "14px", + fontWeight: "600", + maxWidth: "300px", + animation: "slideIn 0.3s ease-out" + }); + + const styleEl = document.createElement("style"); + styleEl.textContent = ` + @keyframes slideIn { + from { transform: translateX(400px); opacity: 0; } + to { transform: translateX(0); opacity: 1; } + } + `; + + if (!document.getElementById("autofill-notification-style")) { + styleEl.id = "autofill-notification-style"; + document.head.appendChild(styleEl); + } + + document.body.appendChild(notif); + + setTimeout(() => { + notif.style.transition = "all 0.3s ease-out"; + notif.style.transform = "translateX(400px)"; + notif.style.opacity = "0"; + setTimeout(() => notif.remove(), 300); + }, 5000); +} + +function findPaymentMethodHeader() { + const roots = collectRoots(); + for (const root of roots) { + const headers = root.querySelectorAll("h1, h2, h3, h4, .Header, [class*=\"header\"], [class*=\"title\"], [class*=\"Title\"]"); + for (const h of headers) { + const text = h.textContent.toLowerCase().trim(); + if (["payment method", "payment", "метод оплаты", "способ оплаты"].includes(text) || text.includes("payment method")) { + return h; + } + } + } + return null; +} + +function createFillButton() { + if (clearButton || document.getElementById("stripe-clear-btn")) return; + + const header = findPaymentMethodHeader(); + clearButton = document.createElement("button"); + clearButton.id = "stripe-clear-btn"; + clearButton.innerHTML = "🗑️ Clear All Data"; + + // 注入按钮样式 + clearButton.style.cssText = ` + background: #dc3545; + color: white; + border: none; + border-radius: 8px; + padding: 8px 16px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3); + transition: all 0.2s ease; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + margin-left: 8px; + vertical-align: middle; + white-space: nowrap; + `; + + clearButton.addEventListener("mouseenter", () => { + clearButton.style.transform = "translateY(-1px)"; + clearButton.style.boxShadow = "0 4px 12px rgba(220, 53, 69, 0.4)"; + }); + + clearButton.addEventListener("mouseleave", () => { + clearButton.style.transform = "translateY(0)"; + clearButton.style.boxShadow = "0 2px 8px rgba(220, 53, 69, 0.3)"; + }); + + clearButton.addEventListener("click", async () => { + if (confirm(`⚠️ Clear all Stripe data? This will:\n\n• Delete all cookies\n• Clear localStorage\n• Clear sessionStorage\n• Clear cache\n• Reload the page\n\nContinue?`)) { + clearButton.disabled = true; + clearButton.innerHTML = "⏳ Clearing..."; + await clearAllStripeData(); + showNotification("✅ All data cleared! Reloading...", "success"); + setTimeout(() => { + location.reload(); + }, 1000); + } + }); + + // 尝试将按钮插入到支付标题旁边,如果找不到标题则悬浮显示 + if (header) { + if (header.parentElement) { + const container = document.createElement("div"); + container.style.cssText = "display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;"; + const headerClone = header.cloneNode(true); + headerClone.style.margin = "0"; + const btnGroup = document.createElement("div"); + btnGroup.style.cssText = "display: flex; gap: 8px;"; + btnGroup.appendChild(clearButton); + container.appendChild(headerClone); + container.appendChild(btnGroup); + header.parentElement.replaceChild(container, header); + } + } else { + clearButton.style.cssText += ` + position: fixed; + top: 20px; + right: 20px; + z-index: 999999; + padding: 12px 20px; + font-size: 14px; + `; + document.body.appendChild(clearButton); + } +} + +/** + * 彻底清理浏览器数据 (Stripe 反指纹追踪) + */ +async function clearAllStripeData() { + try { + localStorage.clear(); + sessionStorage.clear(); + + // 清理 IndexedDB + if (window.indexedDB) { + const dbs = await window.indexedDB.databases(); + for (const db of dbs) { + window.indexedDB.deleteDatabase(db.name); + } + } + + // 暴力清理 Cookies + const cookies = document.cookie.split(";"); + for (let cookie of cookies) { + const eqPos = cookie.indexOf("="); + const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim(); + document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/"; + document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=" + location.hostname; + document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=." + location.hostname; + } + + if ("caches" in window) { + const keys = await caches.keys(); + for (const key of keys) { + await caches.delete(key); + } + } + + chrome.runtime.sendMessage({ action: "clearBrowsingData" }); + console.log("[cardbingenerator] All Stripe data cleared"); + } catch (err) { + console.error("Error clearing data:", err); + showNotification("⚠️ Partial clear - some data may remain", "warning"); + } +} + +function shouldShowButton() { + const hasCardFields = detectCardFields(); + const hasAddrFields = detectAddressFields(); + return hasCardFields || hasAddrFields.fullName || hasAddrFields.address1; +} + +function initButton() { + if (shouldShowButton()) { + createFillButton(); + } +} + +// ========================================== +// 初始化与事件监听 +// ========================================== + +const observer = new MutationObserver(() => { + if (!fillButton && shouldShowButton()) { + createFillButton(); + } +}); + +if (document.body) { + observer.observe(document.body, { childList: true, subtree: true }); +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initButton); +} else { + initButton(); +} + +// 多次尝试初始化,应对动态加载的 SPA +setTimeout(initButton, 1000); +setTimeout(initButton, 2000); +setTimeout(initButton, 3000); + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === "fillForm") { + autofillAll(); + } +}); + +window.addEventListener("beforeunload", () => { + chrome.storage.local.remove(["generatedCards"]); + console.log("[cardbingenerator] Cleared cards on page unload"); +}); + +chrome.storage.local.remove(["generatedCards"], () => { + console.log("[cardbingenerator] Cleared old cards on page load"); +}); diff --git a/icon128.png b/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..4b6dd90a9b40ecf1c1aef45a3a56ba668ced8030 GIT binary patch literal 15885 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 S%XGuQD9EUOto~pc{C@x^&T^9g literal 0 HcmV?d00001 diff --git a/icon16.png b/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..91428a5702f904384bec2004eaa866e41c1cc13f GIT binary patch literal 703 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 W`Fw;jjHA#10000 + + + + + cardbingenerator + + + + + + +
+
+
+ +

cardbingenerator

+
+
+
Blackhat_bullet
+ +
+
+ +
+ + + +
+ +
+ +
+
+
+ 💳 +

Card Generator

+
+ +
+ +
+ + + +
+ + +
+
+ +
+ +
+ +
+
🏠 Address Source:
+ +
+ +
+
👤 Name Source:
+ +
Names are taken from addresses
+
+
+ + + Auto-fills with 'x' to 16 digits • Max 19 digits for long cards + +
+ + + +
+
+ +
+
+ 📜 +

History

+
+
+
+
+ + +
+
+
+ 💎 +

BIN Generator

+
+
+
💎
+
+ Powered by cardbingenerator +
+
+ Visit cardbingenerator.com for advanced BIN generation with multiple card networks, formats, and validation options. +
+ + Visit BIN Generator + +
+
+ +
+ + +
+
+ + +
+ + +
+
+ + + +
+ + +
+ + +
+ +
+
+
+
+ + +
+
+
+ + +
+ +
+ +
+
+
+
+
+ + + +
+
+ + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..9eeb6eb --- /dev/null +++ b/popup.js @@ -0,0 +1,627 @@ +/** + * 界面元素引用 (UI References) + */ +const tabs = document.querySelectorAll(".tab-btn"); +const tabContents = document.querySelectorAll(".tab-content"); +const binInput = document.getElementById("binInput"); +const addBinBtn = document.getElementById("addBinBtn"); +const generateCardsBtn = document.getElementById("generateCardsBtn"); +const statusMessage = document.getElementById("statusMessage"); +const binHistoryList = document.getElementById("binHistoryList"); + +// 地址管理相关元素 +const nameInput = document.getElementById("nameInput"); +const address1Input = document.getElementById("address1Input"); +const address2Input = document.getElementById("address2Input"); +const cityInput = document.getElementById("cityInput"); +const stateInput = document.getElementById("stateInput"); +const zipInput = document.getElementById("zipInput"); +const addAddressBtn = document.getElementById("addAddressBtn"); +const addressesList = document.getElementById("addressesList"); + +// 姓名管理相关元素 +const firstNameInput = document.getElementById("firstNameInput"); +const lastNameInput = document.getElementById("lastNameInput"); +const addNameBtn = document.getElementById("addNameBtn"); +const namesList = document.getElementById("namesList"); + +// 设置相关元素 +const useLuhnValidation = document.getElementById("useLuhnValidation"); +const DEFAULT_BIN = "552461xxxxxxxxxx"; + +/** + * 格式化输入:BIN 输入框 + * 自动转大写,移除非数字/X字符 + */ +binInput.addEventListener("input", (e) => { + let val = e.target.value.toUpperCase(); + val = val.replace(/[^0-9X]/g, ""); + if (val.length > 19) { + val = val.substring(0, 19); + } + e.target.value = val; +}); + +/** + * 格式化输入:粘贴处理 + * 自动补全 X,确保格式符合 16 位卡号长度 + */ +binInput.addEventListener("blur", (e) => { + let val = e.target.value.trim().replace(/[^0-9X]/g, ""); + // 如果长度不足且非空,自动补 X + if (val.length > 0 && val.length < 16) { + const currentLen = val.replace(/X/g, "").length; + const needed = 16 - currentLen; + val = val + "X".repeat(needed); + } + e.target.value = val; +}); + +// ========================================== +// 设置菜单逻辑 +// ========================================== +const settingsToggleBtn = document.getElementById("settingsToggleBtn"); +const miniSettings = document.getElementById("miniSettings"); + +if (settingsToggleBtn && miniSettings) { + settingsToggleBtn.addEventListener("click", (e) => { + e.stopPropagation(); + miniSettings.classList.toggle("show"); + settingsToggleBtn.classList.toggle("active"); + }); + + // 点击外部关闭菜单 + document.addEventListener("click", (e) => { + if (!miniSettings.contains(e.target) && e.target !== settingsToggleBtn && !settingsToggleBtn.contains(e.target)) { + miniSettings.classList.remove("show"); + settingsToggleBtn.classList.remove("active"); + } + }); +} + +// Luhn 校验开关 +if (useLuhnValidation) { + useLuhnValidation.addEventListener("change", () => { + chrome.storage.local.set({ + useLuhnValidation: useLuhnValidation.checked + }); + }); + // 初始化状态 + chrome.storage.local.get(["useLuhnValidation"], (data) => { + if (data.useLuhnValidation !== undefined) { + useLuhnValidation.checked = data.useLuhnValidation; + } + }); +} + +// 数据源选择器逻辑 +const addressSourceSelect = document.getElementById("addressSourceSelect"); +const nameSourceSelect = document.getElementById("nameSourceSelect"); + +if (addressSourceSelect) { + addressSourceSelect.addEventListener("change", () => { + const val = addressSourceSelect.value; + chrome.storage.local.set({ addressSource: val }); + console.log("✅ Address source changed to:", val); + }); + chrome.storage.local.get(["addressSource"], (data) => { + if (data.addressSource) { + addressSourceSelect.value = data.addressSource; + } else { + // 默认设置为 static + addressSourceSelect.value = "static"; + chrome.storage.local.set({ addressSource: "static" }); + } + }); +} + +if (nameSourceSelect) { + nameSourceSelect.addEventListener("change", () => { + const val = nameSourceSelect.value; + chrome.storage.local.set({ nameSource: val }); + console.log("✅ Name source changed to:", val); + }); + chrome.storage.local.get(["nameSource"], (data) => { + if (data.nameSource) { + nameSourceSelect.value = data.nameSource; + } else { + nameSourceSelect.value = "static"; + chrome.storage.local.set({ nameSource: "static" }); + } + }); +} + +// 初始化加载所有数据 +loadData(); + +// ========================================== +// 标签页切换逻辑 (Tabs) +// ========================================== +tabs.forEach(tab => { + tab.addEventListener("click", () => { + const targetId = tab.dataset.tab; + tabs.forEach(t => t.classList.remove("active")); + tabContents.forEach(c => c.classList.remove("active")); + + tab.classList.add("active"); + document.getElementById(targetId + "-tab").classList.add("active"); + }); +}); + +const subTabs = document.querySelectorAll(".sub-tab-btn"); +const subTabContents = document.querySelectorAll(".sub-tab-content"); + +subTabs.forEach(tab => { + tab.addEventListener("click", () => { + const targetId = tab.dataset.subtab; + subTabs.forEach(t => t.classList.remove("active")); + subTabContents.forEach(c => c.classList.remove("active")); + + tab.classList.add("active"); + document.getElementById(targetId + "-subtab").classList.add("active"); + }); +}); + +// ========================================== +// 核心功能:添加 BIN 和 生成卡片 +// ========================================== + +addBinBtn.addEventListener("click", () => { + const bin = binInput.value.trim(); + if (!bin) return; + + chrome.storage.local.get(["binHistory"], (data) => { + let history = data.binHistory || []; + // 去重并添加到头部 + history = history.filter(b => b !== bin); + history.unshift(bin); + // 限制历史记录数量 + if (history.length > 20) history = history.slice(0, 20); + + chrome.storage.local.set({ + binHistory: history, + currentBin: bin + }, () => { + loadBinHistory(); + showToast("BIN added to history"); + }); + }); +}); + +generateCardsBtn.addEventListener("click", async () => { + const bin = binInput.value.trim(); + if (!bin) { + showStatus("Please enter a BIN number", "error"); + return; + } + if (bin.length < 6) { + showStatus("BIN must be at least 6 digits", "error"); + return; + } + + const useLuhn = useLuhnValidation.checked; + + // UI 状态更新:处理中 + generateCardsBtn.disabled = true; + generateCardsBtn.innerHTML = "Processing..."; + + if (useLuhn) { + showStatus("🔐 Generating cards with Luhn validation...", "loading"); + } else { + showStatus("⚡ Generating cards...", "loading"); + } + + // 保存 BIN 历史 + chrome.storage.local.get(["binHistory"], (data) => { + let history = data.binHistory || []; + history = history.filter(b => b !== bin); + history.unshift(bin); + if (history.length > 20) history = history.slice(0, 20); + chrome.storage.local.set({ + binHistory: history, + currentBin: bin + }, () => { + loadBinHistory(); + }); + }); + + // 发送消息给后台生成卡片 + chrome.runtime.sendMessage({ + action: "generateCards", + bin: bin, + useValidation: useLuhn, + stripeTabId: null + }, (response) => { + if (response && response.success) { + const luhnMsg = useLuhn ? " (Luhn validated)" : ""; + showStatus("✅ Generated " + response.cards.length + " cards" + luhnMsg + ". Filling form...", "loading"); + + // 查找 Stripe 标签页并注入填充指令 + chrome.tabs.query({ + url: ["https://checkout.stripe.com/*", "https://*.stripe.com/*"] + }, (tabs) => { + if (chrome.runtime.lastError) { + resetGenerateButton(); + showStatus("❌ Error: " + chrome.runtime.lastError.message, "error"); + return; + } + + if (tabs.length > 0) { + // 优先选择当前激活的标签页 + const targetTab = tabs.find(t => t.active) || tabs[0]; + chrome.tabs.sendMessage(targetTab.id, { + action: "fillForm" + }, (fillResponse) => { + resetGenerateButton(); + if (chrome.runtime.lastError) { + showStatus("❌ No Stripe checkout page found. Please open one first.", "error"); + } else { + showStatus("Form filled!", "success"); + showToast("✅ Form filled successfully!"); + } + }); + } else { + resetGenerateButton(); + showStatus("❌ No Stripe checkout page found. Please open one first.", "error"); + } + }); + } else { + resetGenerateButton(); + showStatus("Failed to generate cards", "error"); + showToast("❌ Failed to generate cards. Try again.", "error"); + } + }); +}); + +function resetGenerateButton() { + generateCardsBtn.disabled = false; + generateCardsBtn.innerHTML = "🚀Fill Everything"; +} + +// ========================================== +// 历史记录管理 +// ========================================== + +function loadBinHistory() { + chrome.storage.local.get(["binHistory", "currentBin"], (data) => { + const history = data.binHistory || []; + const current = data.currentBin || DEFAULT_BIN; + + binInput.value = current; + binHistoryList.innerHTML = ""; + + if (history.length === 0) { + binHistoryList.innerHTML = "
No BINs saved yet
"; + return; + } + + history.forEach(bin => { + const item = document.createElement("div"); + item.className = "history-item"; + + const binText = document.createElement("span"); + binText.textContent = bin; + binText.className = "history-bin"; + binText.addEventListener("click", () => { + binInput.value = bin; + chrome.storage.local.set({ currentBin: bin }); + showToast("BIN selected"); + }); + + const delBtn = document.createElement("button"); + delBtn.textContent = "×"; + delBtn.className = "delete-btn"; + delBtn.addEventListener("click", (e) => { + e.stopPropagation(); + deleteBin(bin); + }); + + item.appendChild(binText); + item.appendChild(delBtn); + binHistoryList.appendChild(item); + }); + }); +} + +function deleteBin(bin) { + chrome.storage.local.get(["binHistory"], (data) => { + let history = data.binHistory || []; + history = history.filter(b => b !== bin); + chrome.storage.local.set({ binHistory: history }, () => { + loadBinHistory(); + showToast("BIN deleted"); + }); + }); +} + +// ========================================== +// 地址管理 +// ========================================== + +addAddressBtn.addEventListener("click", () => { + const name = nameInput.value.trim(); + const addr1 = address1Input.value.trim(); + const addr2 = address2Input.value.trim(); + const city = cityInput.value.trim(); + const state = stateInput.value.trim(); + const zip = zipInput.value.trim(); + + if (!name || !addr1 || !city || !state || !zip) { + showToast("Please fill all required fields", "error"); + return; + } + + // 简单的姓名拆分逻辑 + const nameParts = name.split(" "); + const first = nameParts[0] || name; + const last = nameParts.slice(1).join(" ") || nameParts[0]; + + const newAddr = { + id: Date.now(), + name: name, + firstName: first, + lastName: last, + address1: addr1, + address2: addr2, + city: city, + state: state, + stateCode: getStateCode(state), // 转换州简写 (如 California -> CA) + postal: zip, + countryText: "United States", + countryValue: "US" + }; + + chrome.storage.local.get(["customAddresses"], (data) => { + const list = data.customAddresses || []; + list.push(newAddr); + chrome.storage.local.set({ customAddresses: list }, () => { + clearAddressInputs(); + loadAddresses(); + showToast("Address added"); + }); + }); +}); + +function clearAddressInputs() { + nameInput.value = ""; + address1Input.value = ""; + address2Input.value = ""; + cityInput.value = ""; + stateInput.value = ""; + zipInput.value = ""; +} + +function loadAddresses() { + chrome.storage.local.get(["customAddresses"], (data) => { + const list = data.customAddresses || []; + addressesList.innerHTML = ""; + + if (list.length === 0) { + addressesList.innerHTML = "
No addresses saved yet
"; + return; + } + + list.forEach(addr => { + const item = document.createElement("div"); + item.className = "list-item"; + + const info = document.createElement("div"); + info.className = "item-info"; + info.innerHTML = ` + ${addr.name}
+ ${addr.address1}${addr.address2 ? ", " + addr.address2 : ""}
+ ${addr.city}, ${addr.state} ${addr.postal}
+ `; + + const delBtn = document.createElement("button"); + delBtn.textContent = "×"; + delBtn.className = "delete-btn"; + delBtn.addEventListener("click", () => deleteAddress(addr.id)); + + item.appendChild(info); + item.appendChild(delBtn); + addressesList.appendChild(item); + }); + }); +} + +function deleteAddress(id) { + chrome.storage.local.get(["customAddresses"], (data) => { + let list = data.customAddresses || []; + list = list.filter(item => item.id !== id); + chrome.storage.local.set({ customAddresses: list }, () => { + loadAddresses(); + showToast("Address deleted"); + }); + }); +} + +// ========================================== +// 姓名管理 +// ========================================== + +addNameBtn.addEventListener("click", () => { + const first = firstNameInput.value.trim(); + const last = lastNameInput.value.trim(); + + if (!first || !last) { + showToast("Please enter both first and last name", "error"); + return; + } + + const newName = { + id: Date.now(), + firstName: first, + lastName: last, + fullName: first + " " + last + }; + + chrome.storage.local.get(["customNames"], (data) => { + const list = data.customNames || []; + list.push(newName); + chrome.storage.local.set({ customNames: list }, () => { + firstNameInput.value = ""; + lastNameInput.value = ""; + loadNames(); + showToast("Name added"); + }); + }); +}); + +function loadNames() { + chrome.storage.local.get(["customNames"], (data) => { + const list = data.customNames || []; + namesList.innerHTML = ""; + + if (list.length === 0) { + namesList.innerHTML = "
No names saved yet
"; + return; + } + + list.forEach(item => { + const div = document.createElement("div"); + div.className = "list-item"; + + const info = document.createElement("div"); + info.className = "item-info"; + info.innerHTML = `${item.fullName}`; + + const delBtn = document.createElement("button"); + delBtn.textContent = "×"; + delBtn.className = "delete-btn"; + delBtn.addEventListener("click", () => deleteName(item.id)); + + div.appendChild(info); + div.appendChild(delBtn); + namesList.appendChild(div); + }); + }); +} + +function deleteName(id) { + chrome.storage.local.get(["customNames"], (data) => { + let list = data.customNames || []; + list = list.filter(item => item.id !== id); + chrome.storage.local.set({ customNames: list }, () => { + loadNames(); + showToast("Name deleted"); + }); + }); +} + +// ========================================== +// 辅助工具函数 +// ========================================== + +/** + * 将州全称转换为两字母缩写 (用于表单匹配) + */ +function getStateCode(stateName) { + const map = { + Alabama: "AL", Alaska: "AK", Arizona: "AZ", Arkansas: "AR", California: "CA", + Colorado: "CO", Connecticut: "CT", Delaware: "DE", Florida: "FL", Georgia: "GA", + Hawaii: "HI", Idaho: "ID", Illinois: "IL", Indiana: "IN", Iowa: "IA", + Kansas: "KS", Kentucky: "KY", Louisiana: "LA", Maine: "ME", Maryland: "MD", + Massachusetts: "MA", Michigan: "MI", Minnesota: "MN", Mississippi: "MS", Missouri: "MO", + Montana: "MT", Nebraska: "NE", Nevada: "NV", "New Hampshire": "NH", "New Jersey": "NJ", + "New Mexico": "NM", "New York": "NY", "North Carolina": "NC", "North Dakota": "ND", Ohio: "OH", + Oklahoma: "OK", Oregon: "OR", Pennsylvania: "PA", "Rhode Island": "RI", "South Carolina": "SC", + "South Dakota": "SD", Tennessee: "TN", Texas: "TX", Utah: "UT", Vermont: "VT", + Virginia: "VA", Washington: "WA", "West Virginia": "WV", Wisconsin: "WI", Wyoming: "WY" + }; + return map[stateName] || stateName.substring(0, 2).toUpperCase(); +} + +function showStatus(msg, type = "") { + statusMessage.textContent = msg; + statusMessage.className = "status-message " + type; + statusMessage.style.display = "block"; + + if (type === "success" || type === "error") { + setTimeout(() => { + statusMessage.style.display = "none"; + }, 5000); + } +} + +function showToast(msg, type = "success") { + const toast = document.createElement("div"); + toast.className = "toast " + type; + toast.textContent = msg; + document.body.appendChild(toast); + + // 动画显示 + setTimeout(() => { + toast.classList.add("show"); + }, 10); + + // 自动消失 + setTimeout(() => { + toast.classList.remove("show"); + setTimeout(() => toast.remove(), 300); + }, 2000); +} + +function loadData() { + loadBinHistory(); + loadAddresses(); + loadNames(); +} + +// ========================================== +// Telegram 广告弹窗逻辑 +// ========================================== + +const telegramModal = document.getElementById("telegramModal"); +const modalCloseBtn = document.getElementById("modalCloseBtn"); +const modalTimer = document.getElementById("modalTimer"); +let countdownInterval = null; +let autoCloseTimeout = null; + +function showTelegramModal() { + telegramModal.classList.add("show"); + let seconds = 5; + modalTimer.textContent = seconds; + + // 倒计时 + countdownInterval = setInterval(() => { + seconds--; + if (seconds > 0) { + modalTimer.textContent = seconds; + } else { + modalTimer.textContent = "0"; + clearInterval(countdownInterval); + } + }, 1000); + + // 5秒后自动关闭 + autoCloseTimeout = setTimeout(() => { + closeTelegramModal(); + }, 5000); +} + +function closeTelegramModal() { + telegramModal.classList.remove("show"); + if (countdownInterval) { + clearInterval(countdownInterval); + countdownInterval = null; + } + if (autoCloseTimeout) { + clearTimeout(autoCloseTimeout); + autoCloseTimeout = null; + } +} + +if (modalCloseBtn) { + modalCloseBtn.addEventListener("click", (e) => { + e.preventDefault(); + closeTelegramModal(); + }); +} + +// 延迟显示广告,避免一打开就弹 +if (telegramModal && modalCloseBtn && modalTimer) { + setTimeout(() => { + showTelegramModal(); + }, 300); +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..230e49c --- /dev/null +++ b/styles.css @@ -0,0 +1,1486 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + /* Smooth Black & White Color Scheme */ + --bg-primary: #0a0a0a; + --bg-secondary: #1a1a1a; + --bg-tertiary: #2a2a2a; + --bg-glass: rgba(42, 42, 42, 0.7); + --accent-primary: #ffffff; + --accent-secondary: #e0e0e0; + --accent-tertiary: #c0c0c0; + --accent-gradient: linear-gradient(135deg, #ffffff 0%, #e0e0e0 50%, #ffffff 100%); + --accent-gradient-hover: linear-gradient(135deg, #f5f5f5 0%, #ffffff 50%, #f5f5f5 100%); + --text-primary: #ffffff; + --text-secondary: #d0d0d0; + --text-muted: #888888; + --border-color: #333333; + --border-light: #444444; + --border-glow: rgba(255, 255, 255, 0.1); + --success: #00ff88; + --error: #ff4444; + --warning: #ffaa00; + --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.5); + --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.6); + --shadow-xl: 0 12px 48px rgba(0, 0, 0, 0.7); + --glow-white: 0 0 20px rgba(255, 255, 255, 0.1); +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; + background: var(--bg-primary); + background-image: + radial-gradient(at 0% 0%, rgba(255, 255, 255, 0.03) 0px, transparent 50%), + radial-gradient(at 100% 100%, rgba(255, 255, 255, 0.03) 0px, transparent 50%); + color: var(--text-primary); + min-width: 420px; + max-width: 600px; + min-height: 500px; + max-height: 700px; + margin: 0; + padding: 0; + overflow: hidden; +} + +.container { + background: var(--bg-primary); + width: 100%; + min-height: 100vh; + display: flex; + flex-direction: column; + position: relative; +} + +/* Header */ +.header { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--border-color); + padding: 18px 20px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + position: relative; + box-shadow: var(--shadow-md); + min-height: 70px; +} + +.header-left { + display: flex; + align-items: center; + gap: 12px; + flex: 1; + min-width: 0; +} + +.logo { + font-size: 32px; + filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.3)); + animation: float 3s ease-in-out infinite; + flex-shrink: 0; +} + +@keyframes float { + 0%, 100% { + transform: translateY(0px) scale(1); + filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.3)); + } + 50% { + transform: translateY(-4px) scale(1.05); + filter: drop-shadow(0 0 12px rgba(255, 255, 255, 0.5)); + } +} + +.header h2 { + font-size: 16px; + font-weight: 800; + color: var(--text-primary); + margin: 0; + letter-spacing: 1.5px; + text-transform: uppercase; + text-shadow: 0 0 10px rgba(255, 255, 255, 0.3); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; +} + +.branding { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 6px; + flex-shrink: 0; + margin-left: auto; + padding-left: 12px; +} + +.brand-name { + font-size: 10px; + font-weight: 700; + color: var(--text-primary); + letter-spacing: 1px; + text-transform: uppercase; + text-shadow: 0 0 8px rgba(255, 255, 255, 0.4); + white-space: nowrap; +} + +.brand-links { + display: flex; + gap: 6px; + font-size: 9px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.brand-link { + color: var(--text-secondary); + text-decoration: none; + transition: all 0.3s ease; + padding: 3px 8px; + border-radius: 4px; + white-space: nowrap; + border: 1px solid transparent; +} + +.brand-link:hover { + color: var(--text-primary); + background: rgba(255, 255, 255, 0.1); + border-color: var(--border-light); + text-shadow: 0 0 6px rgba(255, 255, 255, 0.5); +} + +.version { + display: none; +} + +/* Tabs */ +.tabs { + display: flex; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + padding: 0 12px; + gap: 4px; + box-shadow: inset 0 -1px 4px rgba(0, 0, 0, 0.3); +} + +.tab-btn { + flex: 1; + padding: 14px 16px; + background: transparent; + border: none; + cursor: pointer; + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: 12px 12px 0 0; +} + +.tab-btn::before { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%) scaleX(0); + width: 80%; + height: 2px; + background: var(--accent-primary); + border-radius: 2px 2px 0 0; + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: var(--glow-white); +} + +.tab-btn:hover { + background: rgba(255, 255, 255, 0.05); + color: var(--text-primary); + transform: translateY(-2px); +} + +.tab-btn.active { + color: var(--accent-primary); + background: var(--bg-primary); +} + +.tab-btn.active::before { + transform: translateX(-50%) scaleX(1); +} + +.tab-icon { + font-size: 18px; + filter: drop-shadow(0 0 4px rgba(255, 255, 255, 0.3)); +} + +/* Content */ +.content { + flex: 1; + padding: 20px; + overflow-y: auto; + overflow-x: hidden; + background: var(--bg-primary); +} + +.tab-content { + display: none; + animation: fadeInUp 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +.tab-content.active { + display: block; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Sections */ +.section { + background: var(--bg-glass); + backdrop-filter: blur(20px); + border: 1px solid var(--border-color); + border-radius: 16px; + padding: 20px; + margin-bottom: 16px; + transition: all 0.3s ease; + box-shadow: var(--shadow-md); + position: relative; + overflow: hidden; +} + +.section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, var(--accent-primary), transparent); + opacity: 0; + transition: opacity 0.3s ease; +} + +.section:hover { + border-color: var(--border-light); + box-shadow: var(--shadow-lg); + transform: translateY(-2px); +} + +.section:hover::before { + opacity: 1; +} + +.section-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border-color); +} + +.section-icon { + font-size: 20px; + filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.3)); +} + +.section-header h3 { + font-size: 16px; + font-weight: 700; + color: var(--text-primary); + margin: 0; + letter-spacing: 0.5px; +} + +/* Sub Tabs (for Settings) */ +.sub-tabs { + display: flex; + gap: 10px; + margin-bottom: 16px; +} + +.sub-tab-btn { + flex: 1; + padding: 12px 16px; + background: var(--bg-tertiary); + border: 2px solid var(--border-color); + border-radius: 12px; + cursor: pointer; + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + position: relative; + overflow: hidden; +} + +.sub-tab-btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.1); + transition: left 0.3s ease; + z-index: 0; +} + +.sub-tab-btn span { + position: relative; + z-index: 1; +} + +.sub-tab-btn:hover { + border-color: var(--border-light); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.sub-tab-btn.active { + background: rgba(255, 255, 255, 0.1); + color: var(--text-primary); + border-color: var(--accent-primary); + box-shadow: var(--shadow-lg), var(--glow-white); +} + +.sub-tab-btn.active::before { + left: 0; +} + +.sub-tab-content { + display: none; + animation: fadeInUp 0.3s ease; +} + +.sub-tab-content.active { + display: block; +} + +/* Input Groups */ +.input-group { + margin-bottom: 16px; + position: relative; +} + +.input-group label { + display: block; + margin-bottom: 10px; + font-weight: 700; + color: var(--text-secondary); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.input-with-btn { + display: flex; + gap: 10px; +} + +.input-with-btn input { + flex: 1; +} + +input.form-input, +.input-group input { + width: 100%; + padding: 14px 16px; + background: var(--bg-tertiary); + border: 2px solid var(--border-color); + border-radius: 12px; + font-size: 14px; + color: var(--text-primary); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-family: 'Courier New', monospace; + box-shadow: var(--shadow-sm); +} + +input.form-input:focus, +.input-group input:focus { + outline: none; + border-color: var(--accent-primary); + background: var(--bg-secondary); + box-shadow: 0 0 0 4px var(--border-glow), var(--shadow-md); + transform: translateY(-1px); +} + +input::placeholder { + color: var(--text-muted); +} + +.input-row { + display: flex; + gap: 12px; +} + +.input-row input { + flex: 1; +} + +/* Checkbox */ +.checkbox-group { + margin: 16px 0; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + padding: 14px; + background: var(--bg-tertiary); + border: 2px solid var(--border-color); + border-radius: 12px; + transition: all 0.3s ease; +} + +.checkbox-label:hover { + border-color: var(--border-light); + background: var(--bg-secondary); + box-shadow: var(--shadow-md); +} + +.checkbox-label input[type="checkbox"] { + display: none; +} + +.checkbox-custom { + width: 22px; + height: 22px; + border: 2px solid var(--border-color); + border-radius: 6px; + position: relative; + transition: all 0.3s ease; + flex-shrink: 0; + background: var(--bg-secondary); +} + +.checkbox-label input[type="checkbox"]:checked + .checkbox-custom { + background: var(--accent-primary); + border-color: var(--accent-primary); + box-shadow: 0 0 12px rgba(255, 255, 255, 0.3); +} + +.checkbox-label input[type="checkbox"]:checked + .checkbox-custom::after { + content: '✓'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--bg-primary); + font-weight: bold; + font-size: 14px; +} + +.checkbox-text { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; +} + +.checkbox-text small { + color: var(--text-muted); + font-size: 12px; +} + +/* Mini Settings Dropdown */ +.mini-settings { + display: none; + margin-top: 12px; + background: var(--bg-glass); + backdrop-filter: blur(20px); + border: 2px solid var(--border-color); + border-radius: 16px; + padding: 16px; + box-shadow: var(--shadow-xl); + animation: slideDown 0.3s cubic-bezier(0.4, 0, 0.2, 1); + max-height: 400px; + overflow-y: auto; +} + +.mini-settings.show { + display: block; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.mini-settings-item { + padding: 0; + margin-bottom: 12px; +} + +.mini-settings-item:last-child { + margin-bottom: 0; +} + +.mini-settings-divider { + height: 1px; + background: linear-gradient(90deg, transparent, var(--border-color), transparent); + margin: 12px 0; +} + +.mini-settings-label { + font-size: 11px; + color: var(--text-secondary); + margin-bottom: 8px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; +} + +.mini-settings-hint { + font-size: 11px; + color: var(--text-muted); + margin-top: 6px; + font-style: italic; +} + +.mini-select { + width: 100%; + padding: 10px 12px; + background: var(--bg-tertiary); + border: 2px solid var(--border-color); + border-radius: 10px; + color: var(--text-primary); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + outline: none; + font-family: inherit; +} + +.mini-select:hover { + border-color: var(--border-light); + background: var(--bg-secondary); + box-shadow: var(--shadow-sm); +} + +.mini-select:focus { + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px var(--border-glow); +} + +.mini-select option { + background: var(--bg-secondary); + color: var(--text-primary); + padding: 10px; +} + +.mini-select option:disabled { + color: var(--text-muted); +} + +.mini-checkbox-label { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + padding: 12px 14px; + border-radius: 10px; + transition: all 0.3s ease; + user-select: none; +} + +.mini-checkbox-label:hover { + background: var(--bg-tertiary); +} + +.mini-checkbox-label:hover .mini-checkbox-custom { + border-color: var(--border-light); + box-shadow: 0 0 8px rgba(255, 255, 255, 0.2); +} + +.mini-checkbox-label input[type="checkbox"] { + display: none; +} + +.mini-checkbox-custom { + width: 20px; + height: 20px; + min-width: 20px; + min-height: 20px; + border: 2px solid var(--border-color); + border-radius: 5px; + position: relative; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-tertiary); +} + +.mini-checkbox-label input[type="checkbox"]:checked + .mini-checkbox-custom { + background: var(--accent-primary); + border-color: var(--accent-primary); + box-shadow: 0 0 12px rgba(255, 255, 255, 0.4); +} + +.mini-checkbox-label input[type="checkbox"]:checked + .mini-checkbox-custom::after { + content: '✓'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(1); + color: var(--bg-primary); + font-weight: bold; + font-size: 14px; + line-height: 1; + animation: checkmarkPop 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +@keyframes checkmarkPop { + 0% { + transform: translate(-50%, -50%) scale(0); + opacity: 0; + } + 50% { + transform: translate(-50%, -50%) scale(1.3); + } + 100% { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + } +} + +.mini-checkbox-text { + color: var(--text-primary); + font-size: 14px; + font-weight: 500; + flex: 1; +} + +.mini-checkbox-label input[type="checkbox"]:checked ~ .mini-checkbox-text { + color: var(--accent-primary); +} + +/* Buttons */ +.icon-btn { + width: 48px; + height: 48px; + background: rgba(255, 255, 255, 0.1); + color: white; + border: 2px solid var(--border-color); + border-radius: 12px; + font-size: 22px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + box-shadow: var(--shadow-md); + position: relative; + overflow: hidden; +} + +.icon-btn::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; +} + +.icon-btn:hover::before { + width: 300px; + height: 300px; +} + +.icon-btn:hover { + transform: translateY(-3px) scale(1.05); + box-shadow: var(--shadow-xl); + border-color: var(--accent-primary); + background: rgba(255, 255, 255, 0.15); +} + +.icon-btn:active { + transform: translateY(-1px) scale(1); +} + +.icon-btn.settings-btn { + background: var(--bg-tertiary); + border: 2px solid var(--border-color); + font-size: 20px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.icon-btn.settings-btn:hover { + background: rgba(255, 255, 255, 0.15); + border-color: var(--accent-primary); + transform: translateY(-3px) rotate(90deg) scale(1.05); + box-shadow: var(--shadow-xl); +} + +.icon-btn.settings-btn.active { + background: rgba(255, 255, 255, 0.2); + border-color: var(--accent-primary); + box-shadow: var(--shadow-xl), var(--glow-white); + transform: rotate(180deg); +} + +.btn { + width: 100%; + padding: 16px 24px; + border: none; + border-radius: 12px; + font-size: 15px; + font-weight: 700; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + position: relative; + overflow: hidden; + letter-spacing: 0.5px; +} + +.btn::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; +} + +.btn:hover::before { + width: 400px; + height: 400px; +} + +.btn-primary { + background: rgba(255, 255, 255, 0.1); + color: white; + border: 2px solid var(--accent-primary); + box-shadow: var(--shadow-lg), var(--glow-white); +} + +.btn-primary:hover { + transform: translateY(-3px); + box-shadow: var(--shadow-xl), 0 0 30px rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.15); + border-color: var(--accent-primary); +} + +.btn-primary:active { + transform: translateY(-1px); +} + +.btn-primary:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.btn-secondary { + background: var(--bg-tertiary); + color: var(--text-primary); + border: 2px solid var(--border-color); + box-shadow: var(--shadow-sm); +} + +.btn-secondary:hover { + border-color: var(--border-light); + background: var(--bg-secondary); + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} + +.btn-icon { + font-size: 20px; + filter: drop-shadow(0 0 4px rgba(255, 255, 255, 0.3)); +} + +/* Form Groups */ +.form-group { + display: flex; + flex-direction: column; + gap: 14px; +} + +.form-group-compact { + display: flex; + flex-direction: column; + gap: 10px; +} + +.form-group-compact input { + padding: 11px 14px; + font-size: 13px; +} + +.btn-sm { + padding: 12px 20px; + font-size: 14px; +} + +/* History and Lists */ +.history-list { + max-height: 220px; + overflow-y: auto; +} + +.list-container { + max-height: 320px; + overflow-y: auto; +} + +.list-container-compact { + max-height: 280px; + overflow-y: auto; +} + +.history-item, +.list-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 12px; + margin-bottom: 8px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.history-item::before, +.list-item::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: var(--accent-primary); + transform: scaleY(0); + transition: transform 0.3s ease; + box-shadow: var(--glow-white); +} + +.history-item:hover::before, +.list-item:hover::before { + transform: scaleY(1); +} + +.history-item:hover, +.list-item:hover { + background: var(--bg-secondary); + border-color: var(--border-light); + transform: translateX(6px); + box-shadow: var(--shadow-md); +} + +.history-bin { + font-family: 'Courier New', monospace; + font-size: 14px; + color: var(--text-primary); + cursor: pointer; + flex: 1; + font-weight: 600; +} + +.history-bin:hover { + color: var(--accent-primary); +} + +.item-info { + flex: 1; + font-size: 13px; + line-height: 1.7; +} + +.item-info strong { + color: var(--text-primary); + font-size: 15px; + font-weight: 700; +} + +.item-info small { + color: var(--text-secondary); +} + +.delete-btn { + width: 32px; + height: 32px; + background: rgba(255, 68, 68, 0.2); + color: var(--error); + border: 2px solid var(--error); + border-radius: 8px; + cursor: pointer; + font-size: 18px; + line-height: 1; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + box-shadow: var(--shadow-sm); +} + +.delete-btn:hover { + background: var(--error); + color: white; + transform: scale(1.15) rotate(90deg); + box-shadow: var(--shadow-md), 0 0 15px rgba(255, 68, 68, 0.5); +} + +.empty { + text-align: center; + padding: 50px 20px; + color: var(--text-muted); + font-size: 14px; + font-style: italic; +} + +/* Status Message */ +.status-message { + display: none; + padding: 14px 18px; + border-radius: 12px; + margin-top: 18px; + font-size: 13px; + text-align: center; + font-weight: 600; + border: 2px solid; + backdrop-filter: blur(10px); +} + +.status-message.loading { + background: rgba(255, 170, 0, 0.1); + color: var(--warning); + border-color: var(--warning); + box-shadow: 0 0 16px rgba(255, 170, 0, 0.2); +} + +.status-message.success { + background: rgba(0, 255, 136, 0.1); + color: var(--success); + border-color: var(--success); + box-shadow: 0 0 16px rgba(0, 255, 136, 0.2); +} + +.status-message.error { + background: rgba(255, 68, 68, 0.1); + color: var(--error); + border-color: var(--error); + box-shadow: 0 0 16px rgba(255, 68, 68, 0.2); +} + +/* Toast Notifications */ +.toast { + position: fixed; + bottom: 24px; + right: 24px; + background: var(--bg-glass); + backdrop-filter: blur(20px); + border: 2px solid var(--border-color); + color: var(--text-primary); + padding: 14px 24px; + border-radius: 12px; + font-size: 13px; + font-weight: 700; + box-shadow: var(--shadow-xl); + opacity: 0; + transform: translateY(30px) scale(0.9); + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + z-index: 10000; +} + +.toast.show { + opacity: 1; + transform: translateY(0) scale(1); +} + +.toast.success { + border-color: var(--success); + box-shadow: 0 8px 32px rgba(0, 255, 136, 0.3); +} + +.toast.error { + border-color: var(--error); + box-shadow: 0 8px 32px rgba(255, 68, 68, 0.3); +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: var(--bg-tertiary); + border-radius: 10px; +} + +::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 10px; + transition: background 0.3s ease; + border: 2px solid var(--bg-tertiary); +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} + +/* Animations */ +@keyframes shimmer { + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; + } +} + +/* BIN List Styles */ +.bin-search { + margin-bottom: 16px; +} + +.bin-categories { + display: flex; + gap: 10px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.bin-cat-btn { + padding: 10px 16px; + background: var(--bg-tertiary); + border: 2px solid var(--border-color); + border-radius: 10px; + color: var(--text-secondary); + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.bin-cat-btn:hover { + background: var(--bg-secondary); + color: var(--text-primary); + border-color: var(--border-light); +} + +.bin-cat-btn.active { + background: rgba(255, 255, 255, 0.1); + color: white; + border-color: var(--accent-primary); + box-shadow: var(--shadow-md); +} + +.bin-list-grid { + display: grid; + grid-template-columns: 1fr; + gap: 10px; + max-height: 420px; + overflow-y: auto; +} + +.bin-card { + background: var(--bg-glass); + backdrop-filter: blur(20px); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 14px; + cursor: pointer; + transition: all 0.3s ease; +} + +.bin-card:hover { + background: var(--bg-secondary); + border-color: var(--border-light); + transform: translateX(4px); + box-shadow: var(--shadow-md); +} + +.bin-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.bin-number { + font-family: 'Courier New', monospace; + font-size: 15px; + font-weight: 700; + color: var(--accent-primary); + text-shadow: 0 0 8px rgba(255, 255, 255, 0.3); +} + +.bin-type { + font-size: 11px; + padding: 4px 10px; + background: var(--bg-tertiary); + border-radius: 6px; + color: var(--text-secondary); + font-weight: 600; +} + +.bin-card-body { + display: flex; + flex-direction: column; + gap: 6px; +} + +.bin-info { + font-size: 12px; + color: var(--text-secondary); +} + +.bin-bank { + font-weight: 600; + color: var(--text-primary); +} + +.bin-country { + display: flex; + align-items: center; + gap: 8px; + font-size: 11px; + color: var(--text-muted); +} + +/* Footer/Branding Section */ +.footer-branding { + margin-top: 20px; + padding: 16px; + background: var(--bg-glass); + backdrop-filter: blur(20px); + border: 1px solid var(--border-color); + border-radius: 12px; + text-align: center; +} + +.footer-branding-title { + font-size: 12px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 8px; + letter-spacing: 1px; + text-transform: uppercase; +} + +.footer-branding-links { + display: flex; + justify-content: center; + gap: 12px; + flex-wrap: wrap; + margin-top: 8px; +} + +.footer-branding-link { + color: var(--text-secondary); + text-decoration: none; + font-size: 11px; + padding: 6px 12px; + border: 1px solid var(--border-color); + border-radius: 6px; + transition: all 0.3s ease; +} + +.footer-branding-link:hover { + color: var(--text-primary); + border-color: var(--accent-primary); + background: rgba(255, 255, 255, 0.05); + transform: translateY(-2px); +} + +/* Responsive adjustments */ +@media (min-width: 500px) { + body { + min-width: 480px; + } + + .content { + padding: 24px; + } + + .section { + padding: 24px; + } +} + +@media (max-height: 600px) { + .header { + padding: 14px 18px; + } + + .tab-btn { + padding: 12px 14px; + } + + .content { + padding: 16px; + } + + .section { + padding: 16px; + margin-bottom: 12px; + } + + .history-list { + max-height: 140px; + } + + .list-container-compact { + max-height: 180px; + } +} + +/* Telegram Modal Styles */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(8px); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + opacity: 0; + visibility: hidden; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.modal-overlay.show { + opacity: 1; + visibility: visible; +} + +.modal-content { + background: var(--bg-glass); + backdrop-filter: blur(20px); + border: 2px solid var(--border-color); + border-radius: 20px; + padding: 24px; + max-width: 400px; + width: 90%; + position: relative; + box-shadow: var(--shadow-xl), 0 0 40px rgba(255, 255, 255, 0.1); + transform: scale(0.9) translateY(20px); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.modal-overlay.show .modal-content { + transform: scale(1) translateY(0); +} + +.modal-close { + position: absolute; + top: 12px; + right: 12px; + width: 32px; + height: 32px; + background: rgba(255, 255, 255, 0.1); + border: 2px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-size: 20px; + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + line-height: 1; + padding: 0; +} + +.modal-close:hover { + background: rgba(255, 255, 255, 0.2); + border-color: var(--accent-primary); + transform: rotate(90deg) scale(1.1); + box-shadow: var(--shadow-md); +} + +.modal-header { + text-align: center; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid var(--border-color); +} + +.modal-icon { + font-size: 48px; + margin-bottom: 12px; + filter: drop-shadow(0 0 12px rgba(255, 255, 255, 0.3)); + animation: float 3s ease-in-out infinite; +} + +.modal-title { + font-size: 20px; + font-weight: 700; + color: var(--text-primary); + margin: 0; + letter-spacing: 1px; + text-transform: uppercase; + text-shadow: 0 0 10px rgba(255, 255, 255, 0.3); +} + +.modal-body { + display: flex; + flex-direction: column; + gap: 16px; +} + +.modal-description { + font-size: 13px; + color: var(--text-secondary); + text-align: center; + margin: 0; + line-height: 1.6; +} + +.modal-links { + display: flex; + flex-direction: column; + gap: 12px; +} + +.modal-link { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 18px; + background: var(--bg-tertiary); + border: 2px solid var(--border-color); + border-radius: 12px; + text-decoration: none; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.modal-link::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.05); + transition: left 0.3s ease; +} + +.modal-link:hover::before { + left: 0; +} + +.modal-link:hover { + border-color: var(--accent-primary); + background: var(--bg-secondary); + transform: translateX(4px); + box-shadow: var(--shadow-md), 0 0 20px rgba(255, 255, 255, 0.1); +} + +.modal-link-icon { + font-size: 28px; + flex-shrink: 0; + filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.3)); +} + +.modal-link-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.modal-link-title { + font-size: 14px; + font-weight: 700; + color: var(--text-primary); + letter-spacing: 0.5px; +} + +.modal-link-subtitle { + font-size: 11px; + color: var(--text-secondary); + font-weight: 500; +} + +.telegram-link:hover .modal-link-icon { + animation: pulse 1s ease-in-out infinite; +} + +.temp-link:hover .modal-link-icon { + animation: pulse 1s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.1); + } +} + +.modal-timer { + text-align: center; + padding-top: 12px; + border-top: 1px solid var(--border-color); + font-size: 12px; + color: var(--text-muted); + font-weight: 600; +} + +.modal-timer span { + color: var(--accent-primary); + font-size: 14px; + font-weight: 700; + text-shadow: 0 0 8px rgba(255, 255, 255, 0.3); +}