commit 660fae261fd9fe03cfb8859a5377c0e0357727ae
Author: Dmitry Ng <19asdek91@gmail.com>
Date: Sat Sep 27 04:02:03 2025 +0300
initial state
diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.yml b/.github/ISSUE_TEMPLATE/1-bug-report.yml
new file mode 100644
index 0000000..3b41ee4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/1-bug-report.yml
@@ -0,0 +1,125 @@
+name: "\U0001F41B Bug report"
+description: "Report a bug in VXControl Cloud SDK"
+title: "[Bug]: "
+labels: ["bug"]
+assignees:
+ - asdek
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this bug report! Please provide as much information as possible to help us diagnose and fix the SDK issue.
+ - type: dropdown
+ id: component
+ attributes:
+ label: Affected Component
+ description: Which component of VXControl Cloud SDK is affected by this bug?
+ multiple: true
+ options:
+ - SDK Core (Call patterns, Transport, Configuration)
+ - Security Framework (PoW, Encryption, Signatures)
+ - Data Models (Request/Response types, Validation)
+ - Anonymizer Engine (PII/Secrets masking, Pattern recognition)
+ - License System (Validation, Introspection, Tier management)
+ - Examples (Integration patterns, Usage demonstrations)
+ - Documentation (README, API reference, Package docs)
+ - Other (please specify in the description)
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Describe the bug
+ description: Please provide a clear and concise description of the bug, including expected and actual behavior.
+ placeholder: |
+ What happened:
+ - Actual behavior: When using SDK function [function_name], the API call [behavior]
+
+ What should happen:
+ - Expected behavior: The SDK should [expected outcome]
+
+ Additional context:
+ - Target service: [Update/Package/Support/AI Investigation]
+ - Error messages: [any error messages from SDK or cloud services]
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Steps to Reproduce
+ description: Please provide detailed steps to reproduce the bug.
+ placeholder: |
+ 1. Initialize SDK with configuration: [provide CallConfig details]
+ 2. Call SDK function: [specific function call with parameters]
+ 3. Observe behavior: [what happens vs what should happen]
+ 4. Error occurs when: [specific conditions or inputs]
+ 5. Minimal code example: [provide minimal reproducible example]
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: System Configuration
+ description: Please provide details about your setup
+ placeholder: |
+ SDK Version: [e.g., latest from GitHub releases]
+ Integration Type:
+ - [ ] Update Service Integration
+ - [ ] Package Management Integration
+ - [ ] Support Service Integration
+ - [ ] AI Investigation Integration
+
+ Environment:
+ - Go Version: [output of `go version`]
+ - Host OS: [e.g., Ubuntu 22.04, macOS 14.0, Windows 11]
+ - Target Architecture: [amd64/arm64]
+
+ License Configuration:
+ - [ ] No License (Free tier)
+ - [ ] Professional License
+ - [ ] Enterprise License
+
+ SDK Configuration:
+ - Host: [e.g., update.pentagi.com, support.pentagi.com]
+ - Methods: [CallMethodPOST/GET/etc]
+ - Call Types: [CallReqBytesRespBytes/etc]
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Logs and Artifacts
+ description: |
+ Please provide relevant logs and error information:
+ - SDK error messages and stack traces
+ - Go application logs with SDK debug information
+ - Network errors or connection issues
+ - PoW solving timeouts or failures
+ placeholder: |
+ ```
+ Paste error messages and relevant logs here
+ ```
+
+ For large logs or full stack traces, please use GitHub Gist and provide the link.
+ validations:
+ required: false
+ - type: textarea
+ attributes:
+ label: Screenshots or Recordings
+ description: |
+ If applicable, add screenshots or recordings to help explain your problem.
+ - For integration issues: Code screenshots showing the problem
+ - For error dialogs: Screenshots of error messages
+ - For network issues: Terminal output or debug logs
+ placeholder: Drag and drop images here, or paste links to external storage.
+ validations:
+ required: false
+ - type: checkboxes
+ id: verification
+ attributes:
+ label: Verification
+ description: Please verify the following before submitting
+ options:
+ - label: I have checked that this issue hasn't been already reported
+ - label: I have provided SDK configuration details (with sensitive data removed)
+ - label: I have included relevant error messages and stack traces
+ - label: I am using a supported Go version (1.24.0+)
+ - label: I have tested with the latest SDK version
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/2-enhancement.yml b/.github/ISSUE_TEMPLATE/2-enhancement.yml
new file mode 100644
index 0000000..fac0e73
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/2-enhancement.yml
@@ -0,0 +1,116 @@
+name: "\U0001F680 Enhancement"
+description: "Suggest an enhancement for VXControl Cloud SDK"
+title: "[Enhancement]: "
+labels: ["enhancement"]
+assignees:
+ - asdek
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thank you for suggesting an enhancement to make VXControl Cloud SDK better! Please provide as much detail as possible to help us understand your suggestion.
+ - type: dropdown
+ id: component
+ attributes:
+ label: Target Component
+ description: Which component of VXControl Cloud SDK would this enhancement affect?
+ multiple: true
+ options:
+ - SDK Core (Call patterns, Transport, Configuration)
+ - Security Framework (PoW, Encryption, Signatures)
+ - Data Models (Request/Response types, Validation)
+ - Anonymizer Engine (PII/Secrets masking, Pattern recognition)
+ - License System (Validation, Introspection, Tier management)
+ - Examples (Integration patterns, Usage demonstrations)
+ - Documentation (README, API reference, Package docs)
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Enhancement Description
+ description: Please describe the enhancement you would like to see.
+ placeholder: |
+ Problem Statement:
+ - Current Limitation: [describe what's currently missing from SDK or could be improved]
+ - Use Case: [describe how you use VXControl Cloud SDK and why this enhancement would help]
+
+ Proposed Solution:
+ - Feature Description: [detailed description of the SDK enhancement]
+ - Expected Benefits: [how this would improve SDK integration and developer experience]
+
+ Example Scenario:
+ [Provide a concrete code example of how this enhancement would be used in security tool integration]
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Technical Details
+ description: If you have technical suggestions for implementation, please share them.
+ placeholder: |
+ Implementation Approach:
+ - SDK Changes: [changes needed to current SDK architecture]
+ - New Function Types: [any new call patterns needed]
+ - Dependencies: [new Go modules or libraries required]
+
+ Integration Points:
+ - Call Patterns: [how it affects existing function types]
+ - Data Models: [new request/response models needed]
+ - Security Framework: [PoW, encryption, or signature changes]
+ - Anonymization: [any new pattern recognition requirements]
+
+ Security Considerations:
+ - [Any cryptographic or anonymization implications to consider]
+ validations:
+ required: false
+ - type: textarea
+ attributes:
+ label: Designs and Mockups
+ description: |
+ If applicable, provide diagrams or examples to illustrate your enhancement.
+ - For API changes: sequence diagrams or function signatures
+ - For architecture changes: component diagrams or data flow
+ - For integration patterns: code examples or workflow diagrams
+ placeholder: |
+ Drag and drop images here, or provide links to external design tools.
+
+ For API flows, you can use Mermaid syntax:
+ ```mermaid
+ sequenceDiagram
+ Client->>SDK: API Call
+ SDK->>CloudAPI: Encrypted Request
+ CloudAPI->>SDK: Response
+ SDK->>Client: Processed Data
+ ```
+ validations:
+ required: false
+ - type: textarea
+ attributes:
+ label: Alternative Solutions
+ description: |
+ Please describe any alternative solutions or features you've considered.
+ placeholder: |
+ Alternative Approaches:
+ 1. [First alternative approach]
+ - Pros: [benefits]
+ - Cons: [drawbacks]
+
+ 2. [Second alternative approach]
+ - Pros: [benefits]
+ - Cons: [drawbacks]
+
+ Reason for Preferred Solution:
+ [Explain why your main proposal is better than these alternatives]
+ validations:
+ required: false
+ - type: checkboxes
+ id: verification
+ attributes:
+ label: Verification
+ description: Please verify the following before submitting
+ options:
+ - label: I have checked that this enhancement hasn't been already proposed
+ - label: This enhancement aligns with VXControl Cloud SDK's goal of secure cloud integration
+ - label: I have considered the security and performance implications
+ - label: I have provided clear use cases and benefits for cybersecurity applications
+ validations:
+ required: true
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..2cc4931
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,126 @@
+
+
+### Description of the Change
+
+
+#### Problem
+
+
+#### Solution
+
+
+
+Closes #
+
+### Type of Change
+
+
+- [ ] ๐ Bug fix (non-breaking change which fixes an issue)
+- [ ] ๐ New feature (non-breaking change which adds functionality)
+- [ ] ๐ฅ Breaking change (fix or feature that would cause existing functionality to not work as expected)
+- [ ] ๐ Documentation update
+- [ ] ๐ง Configuration change
+- [ ] ๐งช Test update
+- [ ] ๐ก๏ธ Security update
+
+### Areas Affected
+
+
+- [ ] SDK Core (Call patterns, Transport, Configuration)
+- [ ] Security Framework (PoW, Encryption, Signatures)
+- [ ] Data Models (Request/Response types, Validation)
+- [ ] Anonymizer Engine (PII/Secrets masking, Pattern recognition)
+- [ ] License System (Validation, Introspection, Tier management)
+- [ ] Examples (Integration patterns, Usage demonstrations)
+- [ ] Documentation (README, API reference, Package docs)
+- [ ] Testing (Unit tests, Benchmarks, Integration tests)
+
+### Testing and Verification
+
+
+#### Test Configuration
+```yaml
+Go Version:
+SDK Version:
+Host OS:
+Target Services: [Update/Package/Support/AI]
+License Type: [Free/Professional/Enterprise]
+```
+
+#### Test Steps
+1.
+2.
+3.
+
+#### Test Results
+
+
+### Security Considerations
+
+
+### Performance Impact
+
+
+### Documentation Updates
+
+
+- [ ] README.md updates
+- [ ] API.md documentation updates
+- [ ] Package documentation (doc.go files)
+- [ ] Example code updates
+- [ ] CONTRIBUTING.md updates
+- [ ] Other:
+
+### Integration Notes
+
+
+### Checklist
+
+
+#### Code Quality
+- [ ] My code follows Go coding standards and project conventions
+- [ ] I have added/updated necessary documentation (doc.go, README, API.md)
+- [ ] I have added comprehensive tests covering new functionality
+- [ ] All new and existing tests pass (`go test ./...`)
+- [ ] I have run `go fmt`, `go vet`, and `go mod tidy`
+- [ ] Benchmarks added for performance-critical changes
+
+#### Security
+- [ ] I have considered security implications of changes
+- [ ] Cryptographic operations follow established patterns
+- [ ] Sensitive data anonymization implemented where applicable
+- [ ] No sensitive information exposed in logs or error messages
+- [ ] PoW algorithm changes reviewed for FPGA/GPU resistance
+
+#### Compatibility
+- [ ] Changes are backward compatible with existing SDK users
+- [ ] Breaking changes are clearly marked and documented
+- [ ] Dependencies are properly updated and verified
+- [ ] License validation compatibility maintained
+
+#### Documentation
+- [ ] Package documentation (doc.go) updated for public APIs
+- [ ] Code comments follow project standards (lowercase, focus on why/how)
+- [ ] Examples updated to demonstrate new functionality
+- [ ] API.md updated for new endpoints or models
+
+### Additional Notes
+
diff --git a/.github/SAVED_REPLIES.md b/.github/SAVED_REPLIES.md
new file mode 100644
index 0000000..b8aca10
--- /dev/null
+++ b/.github/SAVED_REPLIES.md
@@ -0,0 +1,176 @@
+# Saved Replies
+
+These are standardized responses for the VXControl Cloud SDK Development Team to use when responding to Issues and Pull Requests. Using these templates helps maintain consistency in our communications and saves time.
+
+Since GitHub currently does not support repository-wide saved replies, team members should maintain these individually. All responses are versioned for easier updates.
+
+While these are templates, please customize them to fit the specific context and:
+- Welcome new contributors
+- Thank them for their contribution
+- Provide context for your response
+- Outline next steps
+
+You can add these saved replies to [your personal GitHub account here](https://github.com/settings/replies).
+
+## Issue Responses
+
+### Issue: Already Fixed (v1)
+```
+Thank you for reporting this issue! This has been resolved in a recent release. Please update to the latest version (see our releases page) and verify if the issue persists.
+
+If you continue experiencing problems after updating, please:
+1. Check your configuration against our documentation (README.md, API.md)
+2. Verify your Go version compatibility (Go 1.24.0+)
+3. Test with our working examples in the examples/ directory
+4. Include SDK version, license type, and relevant error logs
+```
+
+### Issue: Need More Information (v1)
+```
+Thank you for your report! To help us better understand and address your issue, please provide additional information:
+
+1. VXControl Cloud SDK version and Go version
+2. Which SDK components are affected (sdk, models, anonymizer)
+3. Target cloud services (Update, Package, Support, AI Investigation)
+4. License type and any relevant error messages
+5. Minimal code example demonstrating the issue
+6. Expected vs actual behavior
+
+Please update your issue using our bug report template for consistency.
+```
+
+### Issue: Cannot Reproduce (v1)
+```
+Thank you for reporting this issue! Unfortunately, I cannot reproduce the problem with the provided information. To help us investigate:
+
+1. Verify you're using the latest SDK version
+2. Provide your complete SDK configuration (CallConfig, Options)
+3. Share relevant error logs and stack traces
+4. Include step-by-step reproduction instructions with minimal code example
+5. Specify which cloud services are involved (Update/Package/Support/AI)
+6. Include Go version and target OS
+
+Please update your issue with these details so we can better assist you.
+```
+
+### Issue: Expected Behavior (v1)
+```
+Thank you for your report! This appears to be the expected behavior because:
+
+[Explanation of why this is working as designed]
+
+If you believe this behavior should be different, please:
+1. Describe your integration use case in detail
+2. Explain why the current SDK behavior doesn't meet your needs
+3. Suggest alternative API behavior that would work better
+4. Consider whether this affects other SDK users
+
+We're always open to improving VXControl Cloud SDK functionality.
+```
+
+### Issue: Missing Template (v1)
+```
+Thank you for reporting this! To help us process your issue efficiently, please use our issue templates:
+
+- Bug Report Template for SDK issues, integration problems, or security concerns
+- Enhancement Template for new features, API improvements, or performance optimizations
+
+Please edit your issue to include the template information. This helps ensure we have all necessary details to assist you effectively.
+```
+
+### Issue: PR Welcome (v1)
+```
+Thank you for raising this issue! We welcome contributions from the cybersecurity community.
+
+If you'd like to implement this yourself:
+1. Check our [contribution guidelines](CONTRIBUTING.md)
+2. Review the SDK architecture documentation
+3. Consider security implications (especially for cryptographic modifications)
+4. Include comprehensive tests and documentation
+5. Ensure backward compatibility with existing integrations
+
+Feel free to ask questions if you need guidance. We're here to help!
+```
+
+## PR Responses
+
+### PR: Ready to Merge (v1)
+```
+Excellent work! This PR meets our quality standards and I'll proceed with merging it.
+
+If you're interested in further contributions, check our:
+- Open issues for enhancement opportunities
+- Examples directory for integration pattern improvements
+- Documentation that could benefit from additional clarity
+
+Thank you for improving VXControl Cloud SDK for the cybersecurity community!
+```
+
+### PR: Needs Work (v1)
+```
+Thank you for your contribution! A few items need attention before we can merge:
+
+[List specific items that need addressing]
+
+Common requirements:
+- Comprehensive tests with unique scenarios
+- Package documentation (doc.go) updates for public APIs
+- Security considerations for cryptographic changes
+- Performance benchmarks for critical path modifications
+- Anonymization implementation for AI-related features
+
+Please update your PR addressing these points. Let us know if you need any clarification.
+```
+
+### PR: Missing Template (v1)
+```
+Thank you for your contribution! Please update your PR to use our PR template.
+
+The template helps ensure we have:
+- Clear description of changes and affected SDK components
+- Testing information and benchmark results
+- Security considerations for cryptographic modifications
+- Documentation updates (doc.go, README.md, API.md)
+- Integration notes for SDK users
+
+This helps us review your changes effectively and ensure quality.
+```
+
+### PR: Missing Issue (v1)
+```
+Thank you for your contribution! We require an associated issue for each PR to:
+- Discuss approach before implementation
+- Track related changes and dependencies
+- Maintain clear project history
+
+Please:
+1. Create an issue describing the problem or enhancement
+2. Link it to this PR using "Closes #issue-number"
+3. Update the PR description with the issue reference
+
+This helps us maintain good project organization and review quality.
+```
+
+### PR: Inactive (v1)
+```
+This PR has been inactive for a while. To keep our review process efficient:
+
+1. If you're still working on this:
+ - Let us know your timeline
+ - Update with latest main branch
+ - Address any existing feedback
+
+2. If you're no longer working on this:
+ - We can close it
+ - Someone else can pick it up
+
+Please let us know your preference within the next week.
+```
+
+### General: Need Help (v1)
+```
+I need additional expertise on this. Pinging:
+- @asdek for technical review and architecture decisions
+
+[Specific questions or concerns that need addressing]
+```
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..09cba64
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,98 @@
+name: Go SDK CI
+
+on:
+ push:
+ branches:
+ - "**"
+ tags:
+ - "v[0-9]+.[0-9]+.[0-9]+"
+ pull_request:
+ branches:
+ - main
+ workflow_dispatch:
+
+jobs:
+ test:
+ name: Test and Lint
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '1.24'
+ cache: true
+ cache-dependency-path: go.sum
+
+ - name: Cache Go modules
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cache/go-build
+ ~/go/pkg/mod
+ key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-go-
+
+ - name: Download dependencies
+ run: go mod download
+
+ - name: Verify dependencies
+ run: go mod verify
+
+ - name: Format check
+ run: |
+ gofmt -s -l . | tee /tmp/gofmt.out
+ test ! -s /tmp/gofmt.out
+
+ - name: Go vet
+ run: go vet ./...
+
+ - name: Lint
+ uses: golangci/golangci-lint-action@v4
+ with:
+ version: latest
+ args: --timeout=5m
+ continue-on-error: true
+
+ - name: Unit tests
+ run: go test ./... -v -race -coverprofile=coverage.out
+
+ - name: Test examples
+ run: |
+ echo "Testing examples build..."
+ cd examples/check-update && go build .
+ cd ../download-installer && go build .
+ cd ../report-errors && go build .
+ echo "โ All examples build successfully"
+
+ - name: Cross-platform build test
+ env:
+ CGO_ENABLED: 0
+ run: |
+ echo "Testing cross-platform builds..."
+ # Test key platforms
+ GOOS=linux GOARCH=amd64 go build ./...
+ GOOS=linux GOARCH=arm64 go build ./...
+ GOOS=darwin GOARCH=amd64 go build ./...
+ GOOS=darwin GOARCH=arm64 go build ./...
+ GOOS=windows GOARCH=amd64 go build ./...
+ echo "โ Cross-platform builds successful"
+
+ - name: Security scan
+ run: |
+ echo "Running security checks..."
+ go install golang.org/x/vuln/cmd/govulncheck@latest
+ govulncheck ./...
+ echo "โ Security scan completed"
+ continue-on-error: true
+
+ - name: Upload coverage
+ uses: codecov/codecov-action@v4
+ with:
+ file: ./coverage.out
+ fail_ci_if_error: false
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..4274249
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,9 @@
+{
+ "go.testFlags": [
+ "-count=1",
+ "-benchtime=3s",
+ ],
+ "go.testTimeout": "300s",
+ "go.testExplorer.enable": true,
+ "search.useIgnoreFiles": true,
+}
diff --git a/API.md b/API.md
new file mode 100644
index 0000000..eaf8e2b
--- /dev/null
+++ b/API.md
@@ -0,0 +1,516 @@
+# VXControl Cloud Platform API Reference
+
+## Overview
+
+The VXControl Cloud Platform provides a comprehensive suite of cybersecurity services accessible through secure, PoW-protected APIs. The platform serves as the backbone for advanced security operations, threat intelligence, vulnerability management, and AI-powered security analysis.
+
+## API Request Flow
+
+```mermaid
+sequenceDiagram
+ participant App as Security Application
+ participant SDK as VXControl SDK
+ participant Anon as Data Anonymizer
+ participant API as Cloud API
+ participant PoW as PoW Service
+
+ App->>SDK: Configure endpoints & Build()
+ App->>SDK: Support API call request
+
+ alt Support/AI Services
+ SDK->>Anon: Anonymize sensitive data
+ Anon-->>SDK: PII/secrets masked
+ Note over Anon: Mandatory for AI requests
300+ pattern recognition
+ end
+
+ SDK->>PoW: Request challenge ticket
+ PoW-->>SDK: Challenge (nonce, difficulty)
+ SDK->>SDK: Solve PoW puzzle (12-1024KB)
+
+ SDK->>API: HTTP request with PoW signature
+ Note over SDK,API: AES-GCM encrypted
Ed25519 signed
Anonymized data
+
+ API->>API: Validate PoW & license
+ API->>API: Process request
+ API-->>SDK: Encrypted response
+
+ SDK->>SDK: Decrypt & validate
+ SDK-->>App: Typed response model
+
+ Note over App,API: All data validated
with Go models
+```
+
+### Technical Components
+
+- **PoW System**: Memory-hard challenges (12-1024KB, 800-4000 AES iterations) with dynamic parameters for FPGA resistance
+- **Data Anonymization**: Comprehensive PII/secrets masking before AI troubleshooting transmission
+- **Cryptographic Validation**: Ed25519 signatures with SHA-512 hashing ensure data integrity
+- **Type Safety**: 24 strongly-typed call patterns with built-in Go model validation
+- **Streaming Architecture**: Memory-efficient processing with AES-GCM chunk encryption
+
+## Authentication & Security
+
+All API endpoints require:
+- **PoW Challenge**: Memory-hard proof-of-work with more than 206M parameter combinations
+- **License Validation**: Cryptographic license verification with tier-based access control
+- **End-to-End Encryption**: AES-GCM streaming encryption with 1KB chunks
+- **Forward Secrecy**: Daily server key rotation with deterministic derivation
+- **Data Anonymization**: Mandatory PII/secrets masking for all AI troubleshooting requests
+
+## API Services
+
+| Service | Host | Endpoint | Method | Models | Status | Description |
+|---------|------|----------|--------|--------|--------|-------------|
+| **Update Service** | `update.pentagi.com` | `/api/v1/updates/check` | POST | [CheckUpdatesRequest](models/update.go) โ [CheckUpdatesResponse](models/update.go) | โ Production | Check for component updates with changelogs |
+| **Package Service** | `update.pentagi.com` | `/api/v1/packages/info` | GET | [PackageInfoRequest](models/package.go) โ [PackageInfoResponse](models/package.go) | โ Production | Get package metadata and signatures |
+| **Package Service** | `update.pentagi.com` | `/api/v1/packages/download` | GET | [DownloadPackageRequest](models/package.go) โ Binary stream | โ Production | Download signed packages with validation |
+| **Support Service** | `support.pentagi.com` | `/api/v1/errors/report` | POST | [SupportErrorRequest](models/support.go) โ [SupportErrorResponse](models/support.go) | โ Production | Automated error reporting with data anonymization |
+| **Support Service** | `support.pentagi.com` | `/api/v1/issues/create` | POST | [SupportIssueRequest](models/support.go) โ [SupportIssueResponse](models/support.go) | โ Production | Create support issues with AI assistance and PII protection |
+| **AI Investigation** | `support.pentagi.com` | `/api/v1/issues/investigate` | POST | [SupportInvestigationRequest](models/support.go) โ [SupportInvestigationResponse](models/support.go) | โ Production | AI-powered troubleshooting with secure data handling |
+| **Threat Intelligence** | `ti.vxcontrol.com` | `/api/v1/threats/query` | POST | ThreatQueryRequest โ ThreatIntelResponse | ๐ง Development | Access real-time threat intelligence and IOC databases |
+| **Vulnerability Assessment** | `ti.vxcontrol.com` | `/api/v1/vulns/scan` | POST | VulnScanRequest โ VulnScanResponse | ๐ง Development | Vulnerability assessment and exploit database queries |
+| **Knowledge Base** | `kb.vxcontrol.com` | `/api/v1/knowledge/search` | GET | Query params โ KnowledgeResponse | ๐ง Development | Search cybersecurity knowledge base (MITRE ATT&CK, IOCs, vulnerabilities) |
+| **Computational Resources** | `compute.pentagi.com` | `/api/v1/compute/submit` | POST | ComputeTaskRequest โ ComputeTaskResponse | ๐ง Development | Submit intensive computational tasks (password cracking, analysis) |
+| **Computational Resources** | `compute.pentagi.com` | `/api/v1/compute/results/:taskId` | GET | Path args โ ComputeResultResponse | ๐ง Development | Retrieve computational task results |
+
+## SDK Architecture
+
+### Request Lifecycle
+
+1. **SDK Configuration**: Define endpoints with `CallConfig` structs
+2. **Function Generation**: SDK creates typed functions for each endpoint
+3. **Data Anonymization**: Mandatory PII/secrets masking for support services
+4. **PoW Challenge**: Automatic challenge solving before each request
+5. **Request Signing**: Ed25519 signature generation with installation ID
+6. **Encryption**: AES-GCM encryption of request/response bodies
+7. **Type Validation**: Go models ensure data integrity throughout
+
+### Core Components
+
+- **Call Patterns**: 24 function types handle different request/response scenarios
+- **Data Anonymizer**: Mandatory PII/secrets masking engine with 300+ pattern recognition
+- **Transport Layer**: HTTP/2 with connection pooling and custom TLS configuration
+- **Cryptographic Engine**: Ed25519 + AES-GCM for signatures and encryption
+- **PoW Solver**: Memory-hard algorithm implementation with configurable timeout
+- **License Manager**: Cryptographic license validation and tier enforcement
+
+## Data Models
+
+All API requests and responses use strongly-typed Go models with built-in validation.
+
+### Available Models
+
+- **Component Management**: [models/types.go](models/types.go) - Component types, statuses, OS/architecture enums
+- **Update Service**: [models/update.go](models/update.go) - Update checking and component information
+- **Package Service**: [models/package.go](models/package.go) - Package metadata, downloads, signatures
+- **Support Service**: [models/support.go](models/support.go) - Error reporting, AI-powered issue creation and investigation
+- **Signature Validation**: [models/signature.go](models/signature.go) - Ed25519 cryptographic signature validation
+- **Data Anonymization**: [anonymizer/](anonymizer/) - PII/secrets masking with pattern recognition
+- **System Utilities**: [system/](system/) - Cross-platform installation ID generation and machine identification
+
+### Model Features
+
+- **IValid Interface**: All models implement validation with comprehensive rules
+- **IQuery Interface**: GET endpoints support automatic query parameter generation
+- **Database Integration**: SQL driver support with Scan() and Value() methods
+- **Type Safety**: Enum validation prevents invalid API calls
+
+## SDK Call Patterns
+
+The SDK provides 24 strongly-typed function patterns covering all request/response scenarios:
+
+### Request Types
+- **None**: Simple requests without parameters
+- **Query**: URL query parameters (`?limit=10&offset=20`)
+- **Args**: Path arguments (`/users/:id/posts/:postId`)
+- **QueryWithArgs**: Combined path arguments and query parameters
+
+### Request Body Types
+- **None**: GET requests without body
+- **Bytes**: `[]byte` request body for JSON/binary data
+- **Reader**: `io.Reader` for streaming large uploads
+
+### Response Types
+- **Bytes**: `[]byte` response for JSON/binary data
+- **Reader**: `io.Reader` for streaming large downloads
+- **Writer**: `io.Writer` for direct output streaming
+
+### Complete Pattern Matrix
+
+| Request | Body | Response | Function Type | Use Case |
+|---------|------|----------|---------------|----------|
+| None | None | Bytes | `CallReqRespBytes` | Simple data retrieval |
+| None | None | Reader | `CallReqRespReader` | Large file downloads |
+| None | None | Writer | `CallReqRespWriter` | Direct output streaming |
+| Query | None | Bytes | `CallReqQueryRespBytes` | Filtered data queries |
+| Args | None | Bytes | `CallReqWithArgsRespBytes` | Resource-specific requests |
+| None | Bytes | Bytes | `CallReqBytesRespBytes` | JSON API calls |
+| None | Reader | Bytes | `CallReqReaderRespBytes` | Large file uploads |
+| Args | Bytes | Bytes | `CallReqBytesWithArgsRespBytes` | Resource updates |
+
+## Data Anonymization
+
+### Mandatory PII Protection
+
+All support and AI troubleshooting requests require data anonymization before transmission. The process is automatic and comprehensive:
+
+**Protected Data Categories**:
+- **Credentials**: API keys, tokens, passwords, database connections
+- **PII**: Email addresses, phone numbers, SSNs, credit cards, personal identifiers
+- **Network Data**: IP addresses, domains, URLs, network configurations
+- **Cloud Secrets**: AWS/Azure/GCP credentials, service tokens, certificates
+- **System Data**: File paths, configuration values, session tokens
+
+**Anonymization Process**:
+```go
+// Before transmission to AI services
+if err := anonymizer.Anonymize(&errorDetails); err != nil {
+ return fmt.Errorf("failed to anonymize data: %w", err)
+}
+if err := anonymizer.Anonymize(&logs); err != nil {
+ return fmt.Errorf("failed to anonymize logs: %w", err)
+}
+```
+
+**Technical Features**:
+- **Structure Preservation**: `admin@company.com` โ `ยง**SSH Connection**ยง` (maintains analytical value)
+- **Comprehensive Coverage**: Multiple pattern databases (General, PII, Secrets) with extensive pattern recognition
+- **Reflection-Based**: Deep anonymization of complex Go structures and nested data
+- **Performance Optimized**: High throughput for production workloads
+- **Memory Efficient**: Fixed memory footprint with chunk-based streaming
+
+## Rate Limiting & PoW Protection
+
+The platform uses a sophisticated memory-hard proof-of-work system designed for GPU and FPGA resistance:
+
+### PoW Algorithm Specifications
+
+- **Memory Requirement**: Dynamic allocation with step variation (prevents FPGA optimization)
+- **Sequential Operations**: Variable AES iterations with configurable key rotation intervals
+- **Memory Access**: Cryptographically derived offsets with uniform distribution
+- **Parameter Combinations**: Millions of configurations prevent hardware specialization
+- **Performance**: Exponential difficulty scaling based on server load and threat level
+
+### Dynamic Anti-FPGA Protection
+
+```go
+type TicketSettings struct {
+ MemorySize uint16 // Dynamic memory allocation
+ AESIterations uint16 // Variable iteration count
+ KeyUpdateInterval uint8 // Configurable rotation frequency
+ MemoryReads uint16 // Memory access operations
+ ChunkSize uint8 // Variable chunk sizes
+ RetryDelaySeconds uint16 // Client backoff time
+ AllowedRPM uint16 // Rate limit threshold
+}
+```
+
+### Security Properties
+
+- **Multi-Threading Resistance**: High difficulty levels provide significant delays even with multi-core attacks
+- **GPU Resistance**: Memory-hard properties substantially reduce GPU optimization advantages
+- **Daily Key Isolation**: Regular key rotation prevents long-term cryptanalysis attacks
+- **Deterministic Synchronization**: All servers use identical daily keys through cryptographic derivation
+
+## Error Handling
+
+All endpoints return structured error responses:
+
+```json
+{
+ "status": "error",
+ "code": "RATE_LIMIT_EXCEEDED",
+ "message": "Request rate limit exceeded",
+ "details": {
+ "current_usage": "exceeded",
+ "limit": "tier_based",
+ "reset_time": "2025-09-17T15:30:00Z"
+ }
+}
+```
+
+Common error codes:
+- `INVALID_LICENSE`: License validation failed
+- `POW_REQUIRED`: Proof-of-work challenge not solved
+- `RATE_LIMIT_EXCEEDED`: Request rate limit exceeded
+- `INSUFFICIENT_TIER`: Feature requires higher access tier
+- `INVALID_REQUEST`: Malformed request data
+
+## SDK Integration
+
+Use the VXControl Cloud SDK for seamless integration with the platform:
+
+```go
+import (
+ "github.com/vxcontrol/cloud/sdk"
+ "github.com/vxcontrol/cloud/system"
+)
+
+// Configure endpoints for multiple services
+configs := []sdk.CallConfig{
+ {
+ Calls: []any{&checkUpdates},
+ Host: "update.pentagi.com",
+ Name: "check-updates",
+ Path: "/api/v1/updates/check",
+ Method: sdk.CallMethodPOST,
+ },
+ {
+ Calls: []any{&reportError},
+ Host: "support.pentagi.com",
+ Name: "report-error",
+ Path: "/api/v1/errors/report",
+ Method: sdk.CallMethodPOST,
+ },
+ {
+ Calls: []any{&queryThreats},
+ Host: "ti.vxcontrol.com",
+ Name: "threat-intelligence",
+ Path: "/api/v1/threats/query",
+ Method: sdk.CallMethodPOST,
+ },
+}
+
+// Build SDK with stable installation ID
+err := sdk.Build(configs,
+ sdk.WithClient("MySecTool", "1.0.0"),
+ sdk.WithInstallationID(system.GetInstallationID()),
+ sdk.WithLicenseKey("XXXX-XXXX-XXXX-XXXX"),
+)
+```
+
+### Working Examples
+
+Production-ready examples are available in the [examples/](examples/) directory:
+
+- **[examples/check-update/](examples/check-update/)** - Update service integration with component management
+- **[examples/download-installer/](examples/download-installer/)** - Package downloads with streaming signature validation
+- **[examples/report-errors/](examples/report-errors/)** - Support workflow with automated data anonymization
+
+### Integration Example
+
+```go
+// Complete multi-service integration
+type Client struct {
+ UpdatesCheck sdk.CallReqBytesRespBytes
+ PackageDownload sdk.CallReqQueryRespWriter
+ ErrorReport sdk.CallReqBytesRespBytes
+ IssueInvestigate sdk.CallReqBytesRespReader // Steam response support
+}
+
+configs := []sdk.CallConfig{
+ {
+ Calls: []any{&client.UpdatesCheck},
+ Host: "update.pentagi.com",
+ Name: "updates_check",
+ Path: "/api/v1/updates/check",
+ Method: sdk.CallMethodPOST,
+ },
+ {
+ Calls: []any{&client.ErrorReport},
+ Host: "support.pentagi.com",
+ Name: "error_report",
+ Path: "/api/v1/errors/report",
+ Method: sdk.CallMethodPOST,
+ },
+}
+
+// SDK automatically handles:
+// - PoW ticket generation and solving
+// - AES-GCM streaming encryption
+// - Retry logic with exponential backoff
+
+// Client must initialize anonymizer for support services:
+// - Mandatory PII/secrets masking before AI transmission
+// - Comprehensive pattern recognition (credentials, emails, IPs, etc.)
+// - Structure-preserving anonymization maintains analytical value
+anonymizer, _ := anonymizer.NewAnonymizer(nil)
+
+err := sdk.Build(configs, options...)
+```
+
+## Performance Characteristics
+
+### Architecture Benefits
+
+**Server Performance**:
+- **High-Throughput**: Optimized ticket generation with microsecond latency
+- **Efficient Validation**: Millions of PoW validations per second
+- **Scalable Proxy**: Throughput scales based on security validation requirements
+
+**Client Performance**:
+- **Dynamic Scaling**: PoW solving scales exponentially with difficulty for effective rate limiting
+- **Optimized Processing**: High-performance path templates and function generation
+- **Efficient Cryptography**: Sub-millisecond license and signature validation
+- **Streaming Architecture**: Memory-efficient processing for large data transfers
+
+**Memory Efficiency**:
+- **Minimal Footprint**: Optimized memory usage per request and SDK instance
+- **Dynamic Allocation**: PoW memory reused across multiple attempts
+- **Streaming Processing**: Fixed memory usage regardless of data size
+- **Connection Pooling**: Efficient transport layer resource management
+
+## Production Deployment
+
+### Security Requirements
+- **TLS 1.2+**: All API communications encrypted
+- **Certificate validation**: Verify server certificates in production
+- **License management**: Secure storage of license keys
+- **Signature verification**: Validate all downloaded packages
+
+### Performance Tuning
+- **PoW timeout**: Adjust based on hardware capabilities and security requirements
+- **Connection pooling**: Optimized connection limits per host and total idle connections
+- **HTTP/2 optimization**: Automatic protocol negotiation with multiplexing
+- **Transport layer**: Configurable response timeouts for backend processing
+
+### Monitoring Integration
+- **Endpoint Statistics**: Per-endpoint request/error/timing metrics with atomic counters
+- **PoW Performance**: Solve time distribution and retry rate monitoring
+- **Cryptographic Operations**: Signature validation and encryption performance tracking
+- **Backend Health**: Response time and error rate tracking for proxy services
+
+## Proxy Architecture
+
+The platform uses a dynamic reverse proxy for unified API management:
+
+### Components
+- **Protocol Proxy**: Acts as API gateway with integrated PoW validation
+- **Dynamic Routing**: Configuration-driven endpoint generation
+- **Middleware Chain**: Unified security validation across all backends
+- **Statistics Collection**: Real-time performance and error metrics
+
+### Benefits
+- **Backend Isolation**: Services receive only validated, decrypted requests
+- **Unified Security**: Single point for PoW validation and rate limiting
+- **Transparent Encryption**: No security complexity for backend services
+- **Health Monitoring**: Built-in `/api/health` endpoint for load balancers
+
+## Technical Details
+
+### Request Headers
+
+All API requests include these headers:
+```
+X-Client-Name: YourApp/1.0.0
+X-Installation-ID: stable-machine-uuid (from system.GetInstallationID())
+X-Request-ID: challenge-request-id
+X-Request-Sign: base64-encoded-pow-signature
+X-License-Key: encrypted-license-key (optional)
+Content-Type: application/json
+```
+
+### Installation ID Generation
+
+The SDK uses cross-platform machine identification for stable installation tracking:
+
+**Implementation**:
+```go
+// Automatic stable ID generation
+installationID := system.GetInstallationID()
+// Returns same UUID for same machine across application restarts
+```
+
+**Platform Support**:
+- **Linux**: `/var/lib/dbus/machine-id` + SMBIOS data (when available)
+- **macOS**: IOPlatformUUID from hardware registry
+- **Windows**: Registry MachineGuid + system product information
+
+**Features**:
+- **Deterministic**: Same machine always generates same UUID
+- **Cross-Platform**: Works on Linux, macOS, Windows
+- **Fallback Logic**: Uses hostname when machine ID unavailable
+- **UUID Format**: RFC4122 compliant UUID v3 (MD5-based)
+
+### Response Format
+
+Successful responses return JSON data:
+```json
+{
+ "status": "success",
+ "data": { /* model-specific response */ }
+}
+```
+
+Error responses include structured details:
+```json
+{
+ "status": "error",
+ "code": "RATE_LIMIT_EXCEEDED",
+ "message": "Request rate limit exceeded",
+ "details": {
+ "retry_after": "server_defined",
+ "quota_reset": "2025-09-26T15:30:00Z"
+ }
+}
+```
+
+### Streaming Encryption
+
+Request and response bodies use AES-GCM chunk encryption:
+
+```
+โโโโโโโโโโโโโโโโโโโฌโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
+โ Length (4 bytes)โ Nonce โ GCM Ciphertext โ
+โ (Big Endian) โ โ (Data + Auth) โ
+โโโโโโโโโโโโโโโโโโโดโโโโโโโโดโโโโโโโโโโโโโโโโโโ
+```
+
+**Features**:
+- **Configurable Chunks**: Optimized chunk size for different workloads
+- **Authenticated Encryption**: Tampering detection per chunk
+- **Streaming Processing**: No memory accumulation for large files
+- **Random Nonces**: Unique nonce per chunk for GCM security
+
+### PoW Signature Format
+
+**Validation Process**:
+1. **Nonce Verification**: XOR unmask with installation ID
+2. **Timestamp Check**: Configurable time window validation
+3. **Content Length**: Match actual request body size
+4. **Integrity Check**: CRC32 validation of signature payload
+5. **AES Decryption**: CBC decrypt with PoW-derived key
+
+### Client-Side Anonymization
+
+All support and AI investigation requests undergo mandatory data anonymization before transmission:
+
+**Implementation**:
+```go
+type Client struct {
+ errorReport sdk.CallReqBytesRespBytes
+ issueCreate sdk.CallReqBytesRespBytes
+ anonymizer anonymizer.Anonymizer // required for support services
+}
+
+// Mandatory anonymization - requests fail if anonymization fails
+func (c *Client) ReportError(ctx context.Context,
+ component models.ComponentType, errorDetails map[string]any) error {
+
+ if err := c.anonymizer.Anonymize(&errorDetails); err != nil {
+ return fmt.Errorf("failed to anonymize error details: %w", err)
+ }
+ // ... proceed with anonymized data
+}
+```
+
+**Pattern Recognition**:
+- **Regex Engine**: go-re2 with experimental.Set for efficient multi-pattern matching
+- **Pattern Database**: 3 categories (General, PII, Secrets) with comprehensive coverage
+- **Structural Processing**: Reflection-based deep anonymization of complex Go structures
+- **Tag-Based Control**: `anonymizer:"skip"` struct tags preserve critical system identifiers
+
+**Anonymization Examples**:
+```go
+// before anonymization:
+"message": "Failed to connect ssh://admin@company.com with API key sk-1234567890abcdef"
+"database_url": "postgres://user:password123@db.internal:5432/app"
+"target_host": "192.168.1.100"
+
+// after anonymization:
+"message": "Failed to connect ยง*SSH Connection*ยง with API key ยง*****api_key*****ยง"
+"database_url": "ยง*****Database Connection Generic*****ยง"
+"target_host": "ยง***ipv4****ยง"
+```
+
+For complete implementation examples, see the [examples/](examples/) directory.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..ea298b8
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,400 @@
+# Contributing to VXControl Cloud SDK
+
+Thank you for your interest in contributing to the VXControl Cloud SDK! We welcome contributions from the cybersecurity community and value your help in making this SDK more robust and useful for everyone.
+
+## Table of Contents
+
+- [Code of Conduct](#code-of-conduct)
+- [How to Contribute](#how-to-contribute)
+- [Development Setup](#development-setup)
+- [Coding Standards](#coding-standards)
+- [Testing Guidelines](#testing-guidelines)
+- [Documentation](#documentation)
+- [Security Considerations](#security-considerations)
+- [Submitting Changes](#submitting-changes)
+
+## Code of Conduct
+
+This project adheres to ethical cybersecurity practices and responsible disclosure principles. By participating, you agree to:
+
+- Use the SDK and related tools only for legitimate security research and defensive purposes
+- Follow responsible disclosure practices for any vulnerabilities discovered
+- Respect the intellectual property rights of VXControl and other contributors
+- Maintain professional conduct in all interactions with the community
+
+## How to Contribute
+
+### Types of Contributions Welcome
+
+1. **Bug Reports**: Help us identify and fix issues in the SDK
+2. **Feature Requests**: Suggest new capabilities or improvements
+3. **Code Contributions**: Submit bug fixes, optimizations, or new features
+4. **Documentation**: Improve existing documentation or add new guides
+5. **Examples**: Create practical examples demonstrating SDK usage
+6. **Testing**: Enhance test coverage and identify edge cases
+
+### Before You Start
+
+- Check existing issues to avoid duplicating work
+- For major changes, create an issue to discuss your proposal first
+- Ensure your contribution aligns with the project's security and ethical standards
+
+## Development Setup
+
+### Prerequisites
+
+- Go 1.24.0 or later
+- Git
+- Linux, macOS, or Windows (for development)
+- Access to internet (for dependency downloads and testing)
+
+### Environment Setup
+
+1. **Fork and clone the repository**:
+ ```bash
+ git clone https://github.com/your-username/cloud.git
+ cd cloud
+ ```
+
+2. **Install dependencies**:
+ ```bash
+ go mod download
+ go mod tidy
+ ```
+
+3. **Run tests to verify setup**:
+ ```bash
+ go test ./...
+ go test -v ./models -run TestSignature # Test signature validation
+ go test -bench=. ./anonymizer # Benchmark anonymizer performance
+ ```
+
+4. **Create a feature branch**:
+ ```bash
+ git checkout -b feature/your-feature-name
+ ```
+
+## Coding Standards
+
+### Go Style Guidelines
+
+Follow standard Go conventions and best practices:
+
+- Use `gofmt` for code formatting
+- Follow [Effective Go](https://golang.org/doc/effective_go.html) guidelines
+- Use meaningful variable and function names
+- Keep functions focused and concise
+- Add comments for complex logic, not obvious operations
+
+### Project-Specific Standards
+
+- **Error handling**: Use typed errors and wrap with context using fmt.Errorf
+- **Logging**: Use structured logging with lowercase message starts (project standard)
+- **Security**: Never log sensitive data (keys, tokens, credentials, PII)
+- **Performance**: Optimize for minimal memory allocations and CPU overhead
+- **Testing**: Write comprehensive tests with unique scenarios avoiding duplication
+- **Comments**: Start with lowercase letters, focus on why/how rather than what
+- **Anonymization**: Use anonymizer package for all data sent to AI services
+
+### Code Organization
+
+```
+cloud/
+โโโ sdk/ # Main SDK package
+โ โโโ sdk.go # Core SDK structure and configuration
+โ โโโ calls.go # Call type definitions and function generation
+โ โโโ transport.go # HTTP transport and PoW integration
+โ โโโ cypher.go # Streaming encryption/decryption
+โ โโโ pow.go # Memory-hard proof-of-work algorithm
+โ โโโ license.go # License validation and introspection
+โ โโโ logger.go # Structured logging abstraction
+โ โโโ mock_test.go # Mock server for testing
+โ โโโ *_test.go # Comprehensive test files
+โ โโโ testdata/ # Test fixtures and data
+โโโ models/ # Type-safe data models with validation
+โ โโโ types.go # Component, OS, and architecture enums
+โ โโโ update.go # Update service models
+โ โโโ package.go # Package service models
+โ โโโ support.go # Support service models
+โ โโโ signature.go # Ed25519 signature validation
+โ โโโ *_test.go # Model validation tests
+โโโ anonymizer/ # PII/secrets masking engine
+โ โโโ anonymizer.go # Core anonymization interface
+โ โโโ replacer.go # Pattern replacement engine
+โ โโโ wrapper.go # Streaming anonymization wrapper
+โ โโโ patterns/ # Pattern recognition database
+โ โโโ testdata/ # Anonymization test datasets
+โโโ system/ # Cross-platform system utilities
+โ โโโ installation_id.go # Stable machine-specific UUID generation
+โ โโโ utils*.go # Platform-specific machine identification
+โ โโโ *_test.go # System utility tests
+โโโ examples/ # Production-ready integration examples
+```
+
+## Testing Guidelines
+
+### Test Requirements
+
+All contributions must include appropriate tests:
+
+- **Unit tests**: Test individual functions and methods
+- **Integration tests**: Test component interactions
+- **Performance tests**: Benchmark critical paths
+- **Security tests**: Validate cryptographic operations
+
+### Test Structure
+
+```go
+func TestFeatureName(t *testing.T) {
+ tests := []struct {
+ name string
+ input InputType
+ expected OutputType
+ wantErr bool
+ }{
+ {
+ name: "valid input scenario",
+ input: validInput,
+ expected: expectedOutput,
+ wantErr: false,
+ },
+ // Add edge cases and error scenarios
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := functionUnderTest(tt.input)
+
+ if tt.wantErr && err == nil {
+ t.Error("expected error but got nil")
+ return
+ }
+ if !tt.wantErr && err != nil {
+ t.Errorf("unexpected error: %v", err)
+ return
+ }
+ if !tt.wantErr && result != tt.expected {
+ t.Errorf("expected %v, got %v", tt.expected, result)
+ }
+ })
+ }
+}
+```
+
+### Running Tests
+
+```bash
+# Run all tests
+go test ./...
+
+# Run tests with coverage
+go test -cover ./...
+
+# Run specific package tests
+go test -v ./sdk # SDK functionality tests
+go test -v ./models -run TestSignature # Signature validation tests
+go test -v ./anonymizer # Anonymization tests
+go test -v ./system # System utilities tests
+
+# Run benchmarks
+go test -bench=. ./sdk # SDK performance benchmarks
+go test -bench=. ./anonymizer # Anonymizer performance benchmarks
+go test -bench=BenchmarkSignature ./models # Signature performance benchmarks
+go test -bench=BenchmarkGetInstallationID ./system # Installation ID benchmarks
+
+# Run examples
+cd examples/check-update && go build .
+cd examples/download-installer && go build .
+cd examples/report-errors && go build .
+```
+
+### Test Data
+
+- Store test data in `testdata/` directories
+- Use appropriate formats: JSON for API data, YAML for patterns, TXT for datasets
+- Never include real credentials or sensitive information
+- Use realistic but fabricated data for testing anonymization
+- Document test scenarios clearly with descriptive names
+
+## Documentation
+
+### Documentation Standards
+
+- Write clear, concise documentation
+- Include practical examples
+- Use proper Go documentation format
+- Update relevant documentation when making changes
+
+### Documentation Types
+
+1. **Code Comments**: Document public APIs and complex logic
+2. **README Updates**: Update main README for significant changes
+3. **API Documentation**: Update API.md for new endpoints
+4. **Examples**: Create or update examples for new features
+
+### Example Documentation
+
+```go
+// ReportError sends an automated error report to support with data anonymization.
+// All sensitive data is automatically masked before transmission to AI services.
+//
+// Example:
+// client := &Client{anonymizer: anon, errorReport: errorReportFunc}
+//
+// errorDetails := map[string]any{
+// "message": "Connection failed to admin@company.com",
+// "api_key": "sk-1234567890abcdef",
+// }
+//
+// err := client.ReportError(ctx, models.ComponentTypePentagi, errorDetails)
+// if err != nil {
+// return fmt.Errorf("failed to report error: %w", err)
+// }
+func (c *Client) ReportError(ctx context.Context,
+ component models.ComponentType, errorDetails map[string]any) error {
+ // anonymize sensitive data before transmission
+ if err := c.anonymizer.Anonymize(&errorDetails); err != nil {
+ return fmt.Errorf("failed to anonymize error details: %w", err)
+ }
+ // Implementation...
+}
+```
+
+## Security Considerations
+
+### Security Review Process
+
+All contributions undergo security review:
+
+- **Cryptographic changes**: Require thorough review and testing
+- **Network code**: Must follow secure communication practices
+- **Input validation**: All user inputs must be properly validated
+- **Error handling**: Avoid leaking sensitive information in errors
+
+### Security Testing
+
+- Test error conditions and edge cases
+- Validate input sanitization
+- Test cryptographic operations with known vectors
+- Verify that sensitive data is properly protected
+
+### Responsible Disclosure
+
+If you discover security vulnerabilities:
+
+1. **Do not** create public issues for security vulnerabilities
+2. Email info@vxcontrol.com with "SECURITY" in subject line
+3. Allow reasonable time for assessment and remediation
+4. Follow coordinated disclosure practices
+
+## Submitting Changes
+
+### Pull Request Process
+
+1. **Ensure your code follows all guidelines above**
+2. **Update documentation** for any user-facing changes
+3. **Add or update tests** for your changes
+4. **Run the full test suite** and ensure all tests pass
+5. **Use conventional commit format** for commit messages
+6. **Create a clear pull request description**
+
+### Commit Message Format
+
+Use [Conventional Commits](https://www.conventionalcommits.org/) format:
+
+```
+type(scope): description
+
+[optional body]
+
+[optional footer]
+```
+
+**Types:**
+- `feat`: New features
+- `fix`: Bug fixes
+- `docs`: Documentation changes
+- `test`: Adding or updating tests
+- `refactor`: Code refactoring without functionality changes
+- `perf`: Performance improvements
+- `security`: Security fixes or improvements
+
+**Examples:**
+```bash
+git commit -m "feat(sdk): add streaming encryption support"
+git commit -m "fix(anonymizer): resolve pattern matching edge case"
+git commit -m "docs(readme): update installation instructions"
+git commit -m "test(models): add comprehensive signature validation tests"
+```
+
+### Pull Request Template
+
+```markdown
+## Description
+Brief description of changes and motivation.
+
+## Type of Change
+- [ ] Bug fix (non-breaking change)
+- [ ] New feature (non-breaking change)
+- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+- [ ] Documentation update
+
+## Testing
+- [ ] Unit tests added/updated
+- [ ] Integration tests added/updated
+- [ ] All tests pass locally
+- [ ] Performance impact assessed
+
+## Security Review
+- [ ] No sensitive data exposed in logs or errors
+- [ ] Input validation implemented where applicable
+- [ ] Cryptographic operations follow established patterns
+- [ ] No new attack surfaces introduced
+
+## Documentation
+- [ ] Code comments added for complex logic
+- [ ] API documentation updated (if applicable)
+- [ ] Examples updated (if applicable)
+- [ ] README updated (if applicable)
+```
+
+### Review Process
+
+1. **Automated checks**: CI/CD pipeline runs tests and security scans
+2. **Code review**: Maintainers review code quality and security
+3. **Security review**: Additional review for security-sensitive changes
+4. **Documentation review**: Ensure documentation is complete and accurate
+
+### Merge Criteria
+
+Pull requests are merged when:
+- All automated checks pass
+- Code review is approved by maintainers
+- Security review is completed (if applicable)
+- Documentation is complete and accurate
+- Changes align with project goals and standards
+
+## Getting Help
+
+### Community Support
+
+- **Documentation**: Check existing documentation first (README.md, API.md)
+- **Examples**: Review production-ready examples in examples/ directory
+- **Discord Community**: [Join our Discord](https://discord.gg/2xrMh7qX6m) for real-time support
+- **Telegram Channel**: [Join our Telegram](https://t.me/+Ka9i6CNwe71hMWQy) for updates
+
+### Maintainer Contact
+
+For complex contributions or questions:
+- Create an issue for discussion
+- Email: info@vxcontrol.com with "SDK Contribution" in subject line
+- For security issues: Email info@vxcontrol.com with "SECURITY" in subject line
+
+## Recognition
+
+Contributors are recognized through:
+- Release notes for significant contributions
+- Special recognition for security improvements and vulnerability reports
+- Community acknowledgment in Discord and Telegram channels
+
+Thank you for helping make VXControl Cloud SDK better for the entire cybersecurity community!
diff --git a/EULA.md b/EULA.md
new file mode 100644
index 0000000..d25f2f8
--- /dev/null
+++ b/EULA.md
@@ -0,0 +1,251 @@
+# End User License Agreement (EULA)
+## VXControl Cloud SDK and Intelligence Platform
+
+**Effective Date:** September 17, 2025
+**Version:** 1.0
+
+---
+
+## 1. Introduction and Scope
+
+This End User License Agreement ("EULA") governs your use of the VXControl Cloud SDK and access to the VXControl Cloud Intelligence Platform (collectively, the "Software" and "Services"). This agreement is between you ("User," "you," or "your") and VXControl LLC ("VXControl," "we," "us," or "our").
+
+**By downloading, installing, or using this Software, you acknowledge that you have read, understood, and agree to be bound by the terms of this EULA.** If you do not agree to these terms, you must not use the Software or Services.
+
+The Software provides access to advanced cybersecurity tools, threat intelligence, vulnerability databases, and computational resources designed for legitimate security research, penetration testing, and defensive operations. The dual-use nature of these capabilities requires strict adherence to ethical and legal standards.
+
+---
+
+## 2. License Grant and Permitted Uses
+
+### 2.1 License Grant
+VXControl hereby grants you a limited, non-exclusive, non-transferable, revocable license to use the Software solely for the purposes and under the conditions specified in this EULA.
+
+### 2.2 Permitted Uses
+The Software and Services may ONLY be used for:
+
+- **Authorized Security Testing**: Penetration testing and security assessments of systems you own or have explicit written permission to test
+- **Defensive Cybersecurity**: Protecting and monitoring systems under your control or management
+- **Security Research**: Academic or professional research conducted in accordance with responsible disclosure principles
+- **Educational Purposes**: Learning and training in cybersecurity practices within controlled environments
+- **Incident Response**: Investigating and responding to actual security incidents affecting your systems
+- **Compliance Assessment**: Evaluating security posture for regulatory or standard compliance requirements
+
+### 2.3 Authorization Requirements
+You must obtain explicit written authorization before conducting any security testing activities that involve:
+- Systems not owned or controlled by you
+- Networks or infrastructure belonging to third parties
+- Any testing that could potentially impact system availability or data integrity
+
+---
+
+## 3. Prohibited Uses and Restrictions
+
+### 3.1 Strictly Prohibited Activities
+You are expressly prohibited from using the Software or Services for:
+
+- **Unauthorized Access**: Accessing systems, networks, or data without proper authorization
+- **Malicious Activities**: Creating, distributing, or deploying malware, exploits, or attack tools
+- **Criminal Conduct**: Any activities that violate applicable laws, regulations, or legal obligations
+- **Harassment or Harm**: Targeting individuals, organizations, or systems with intent to cause harm
+- **Critical Infrastructure**: Unauthorized testing of critical infrastructure, emergency services, or essential services
+- **Privacy Violations**: Accessing, collecting, or processing personal data without proper authorization and legal basis
+- **Commercial Exploitation**: Using discovered vulnerabilities for financial gain without proper disclosure
+- **Circumventing Security**: Bypassing or disabling security measures in production systems without authorization
+
+### 3.2 Technical Restrictions
+You may not:
+- Reverse engineer, decompile, or disassemble the Software (except as permitted by applicable law)
+- Modify, adapt, or create derivative works based on the Software
+- Remove, alter, or obscure any proprietary notices or labels
+- Distribute, sublicense, or transfer the Software to third parties
+- Use the Software to develop competing products or services
+
+### 3.3 Export Control Compliance
+You acknowledge that the Software may be subject to export control laws and regulations. You agree to comply with all applicable export control laws and regulations, including but not limited to the U.S. Export Administration Regulations (EAR) and International Traffic in Arms Regulations (ITAR).
+
+---
+
+## 4. Ethical Use and Professional Standards
+
+### 4.1 Professional Conduct
+You agree to use the Software in accordance with established cybersecurity professional standards, including:
+
+- Following responsible disclosure practices for discovered vulnerabilities
+- Respecting the confidentiality of information encountered during security testing
+- Minimizing potential disruption to systems and services during testing activities
+- Maintaining appropriate documentation of testing activities and findings
+- Cooperating with system owners and administrators when issues are discovered
+
+### 4.2 Responsible Disclosure
+When vulnerabilities are discovered through use of the Software, you agree to:
+
+- Report vulnerabilities to affected parties in a timely and responsible manner
+- Allow reasonable time for remediation before public disclosure
+- Avoid public disclosure of vulnerabilities that could cause immediate harm
+- Coordinate with relevant authorities when vulnerabilities affect critical infrastructure
+
+### 4.3 Data Protection and Privacy
+You must ensure compliance with applicable data protection and privacy laws, including:
+
+- Obtaining proper consent for processing personal data
+- Implementing appropriate safeguards for sensitive information
+- Minimizing collection and retention of personal data
+- Respecting individual privacy rights and data subject requests
+
+---
+
+## 5. Access Tiers and Service Limitations
+
+### 5.1 Service Tiers
+The Software provides access to different service tiers with varying capabilities and limitations:
+
+- **Free Tier**: Basic functionality with usage limitations
+- **Professional Tier**: Enhanced capabilities for professional security practitioners
+- **Enterprise Tier**: Full access to all features and computational resources
+
+### 5.2 Usage Limitations
+Each service tier includes specific usage limitations, rate limits, and access restrictions. Exceeding these limitations may result in temporary or permanent suspension of access.
+
+### 5.3 Service Availability
+VXControl does not guarantee continuous availability of the Services. Planned maintenance, security updates, and emergency situations may result in temporary service interruptions.
+
+---
+
+## 6. Intellectual Property Rights
+
+### 6.1 Ownership
+VXControl retains all right, title, and interest in and to the Software and Services, including all intellectual property rights. No ownership rights are transferred to you under this EULA.
+
+### 6.2 User-Generated Content
+Any data, configurations, or content you create using the Software remains your property, subject to the rights granted to VXControl under this EULA for service operation and improvement.
+
+### 6.3 Feedback and Improvements
+Any feedback, suggestions, or improvements you provide regarding the Software may be used by VXControl without compensation or attribution.
+
+---
+
+## 7. Disclaimers and Warranties
+
+### 7.1 Disclaimer of Warranties
+THE SOFTWARE AND SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE.
+
+VXControl specifically disclaims all warranties, including but not limited to:
+- Warranties of merchantability, fitness for a particular purpose, and non-infringement
+- Warranties that the Software will be error-free, secure, or continuously available
+- Warranties regarding the accuracy, completeness, or reliability of threat intelligence or vulnerability data
+- Warranties that the Software will detect all security threats or prevent all security incidents
+
+### 7.2 Security Limitations
+You acknowledge that:
+- No security tool can guarantee complete protection against all threats
+- The Software may produce false positives or false negatives in threat detection
+- Security effectiveness depends on proper configuration and implementation
+- The Software is not a substitute for comprehensive security policies and procedures
+
+---
+
+## 8. Limitation of Liability
+
+### 8.1 Liability Limitations
+TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, VXCONTROL SHALL NOT BE LIABLE FOR:
+
+- **Consequential Damages**: Loss of profits, business interruption, loss of data, or other indirect damages
+- **Security Incidents**: Damages resulting from security breaches, attacks, or vulnerabilities not detected by the Software
+- **Third-Party Actions**: Damages caused by third parties using information obtained through the Software
+- **User Misconduct**: Damages resulting from your violation of this EULA or applicable laws
+- **Service Interruptions**: Damages caused by temporary or permanent service unavailability
+
+### 8.2 Maximum Liability
+In no event shall VXControl's total liability exceed the amount paid by you for the Software in the twelve (12) months preceding the claim.
+
+### 8.3 Essential Purpose
+These limitations are essential elements of the agreement between you and VXControl and will apply even if any remedy fails of its essential purpose.
+
+---
+
+## 9. Indemnification
+
+You agree to indemnify, defend, and hold harmless VXControl, its officers, directors, employees, and agents from and against any and all claims, damages, losses, costs, and expenses (including reasonable attorneys' fees) arising from or relating to:
+
+- Your use or misuse of the Software or Services
+- Your violation of this EULA or applicable laws
+- Your violation of any third-party rights
+- Any security testing activities conducted using the Software
+- Any unauthorized access or activities performed using the Software
+
+---
+
+## 10. Termination
+
+### 10.1 Termination Rights
+This EULA is effective until terminated. VXControl may terminate this EULA immediately without notice if you breach any provision of this agreement.
+
+### 10.2 Effect of Termination
+Upon termination:
+- Your right to use the Software immediately ceases
+- You must discontinue all use of the Software and Services
+- You must destroy all copies of the Software in your possession
+- Provisions regarding liability, indemnification, and intellectual property shall survive termination
+
+### 10.3 Suspension of Services
+VXControl reserves the right to suspend or terminate your access to the Services at any time for violation of this EULA, suspected illegal activity, or protection of the platform and other users.
+
+---
+
+## 11. Compliance and Legal Requirements
+
+### 11.1 Legal Compliance
+You are solely responsible for ensuring that your use of the Software complies with all applicable laws, regulations, and legal requirements in your jurisdiction.
+
+### 11.2 Regulatory Requirements
+If you operate in a regulated industry, you are responsible for ensuring compliance with applicable regulatory requirements and standards.
+
+### 11.3 Audit Rights
+VXControl reserves the right to audit your use of the Software to ensure compliance with this EULA, subject to reasonable notice and confidentiality protections.
+
+---
+
+## 12. Governing Law and Dispute Resolution
+
+### 12.1 Governing Law
+This EULA shall be governed by and construed in accordance with the laws of the United Arab Emirates, without regard to conflict of law principles.
+
+### 12.2 Dispute Resolution
+Any disputes arising under this EULA shall be resolved through binding arbitration in accordance with the arbitration rules of the Dubai International Arbitration Centre (DIAC).
+
+### 12.3 Injunctive Relief
+Notwithstanding the arbitration provision, VXControl may seek injunctive relief in any court of competent jurisdiction to protect its intellectual property rights or prevent ongoing violations of this EULA.
+
+---
+
+## 13. General Provisions
+
+### 13.1 Entire Agreement
+This EULA constitutes the entire agreement between you and VXControl regarding the Software and supersedes all prior agreements and understandings.
+
+### 13.2 Modifications
+VXControl reserves the right to modify this EULA at any time. Updated versions will be posted on our website and will become effective upon posting for new users and upon your next use of the Software for existing users.
+
+### 13.3 Severability
+If any provision of this EULA is found to be unenforceable, the remainder shall continue in full force and effect.
+
+### 13.4 Waiver
+No waiver of any provision of this EULA shall be deemed or shall constitute a waiver of any other provision.
+
+---
+
+## 14. Contact Information
+
+For questions about this EULA or to report violations, contact:
+
+**VXControl LLC**
+Email: info@vxcontrol.com
+Website: https://vxcontrol.com
+Subject Line: "Cloud SDK EULA Inquiry"
+
+---
+
+**IMPORTANT NOTICE:** This software is designed for legitimate cybersecurity purposes only. Misuse of this software for illegal activities may result in criminal prosecution. Users are solely responsible for ensuring their activities comply with all applicable laws and ethical standards.
+
+By using this software, you acknowledge your understanding of these responsibilities and agree to use the software only for lawful, ethical purposes.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..acfad01
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,134 @@
+VXControl Cloud SDK License
+==========================
+
+Copyright (c) 2025 VXControl LLC. All rights reserved.
+
+ABOUT THIS SOFTWARE
+
+The VXControl Cloud SDK provides secure access to the VXControl Cloud Intelligence
+Platform, enabling integration of advanced cybersecurity services including threat
+intelligence, vulnerability databases, AI-powered troubleshooting, and computational
+resources into security tools and applications. This SDK is designed to enhance
+the capabilities of security platforms including PentAGI and other cybersecurity
+solutions developed by the community.
+
+DUAL LICENSING TERMS
+
+This software is available under two separate licensing options:
+
+1. NON-COMMERCIAL LICENSE (Default)
+2. COMMERCIAL LICENSE (Available separately)
+
+NON-COMMERCIAL LICENSE
+----------------------
+
+Permission is hereby granted, free of charge, to any person or organization
+obtaining a copy of this software and associated documentation files (the "Software"),
+to use, copy, modify, and distribute the Software for NON-COMMERCIAL purposes only,
+subject to the following conditions:
+
+PERMITTED USES:
+- Personal projects and learning
+- Open source projects (with compatible licenses)
+- Academic research and education
+- Non-profit organizations
+- Internal evaluation and testing
+- Integration with PentAGI and other open-source security tools
+- Development of non-commercial cybersecurity research tools
+
+PROHIBITED USES:
+- Commercial products or services
+- Revenue-generating applications
+- Consulting or professional services
+- Redistribution in commercial products
+- Any use that generates direct or indirect financial benefit
+
+CONDITIONS:
+1. The above copyright notice and this license notice must be included in all
+ copies or substantial portions of the Software.
+
+2. Any derivative work must also be licensed under the same non-commercial terms.
+
+3. You must clearly indicate if changes were made to the original Software.
+
+4. You may not use the names "VXControl", "PentAGI", or any related trademarks
+ without explicit written permission from VXControl LLC.
+
+COMMERCIAL LICENSE
+------------------
+
+For commercial use, including but not limited to:
+- Integration into commercial security products or services
+- Use in revenue-generating cybersecurity applications
+- Commercial penetration testing or security consulting services
+- Redistribution as part of commercial security offerings
+- Integration with commercial SOC (Security Operations Center) platforms
+- Development of commercial Red Team or Blue Team tools
+
+A separate Commercial License is required. Commercial licenses are available
+from VXControl LLC and include:
+
+- Unrestricted commercial usage rights
+- Priority technical support
+- Enhanced SLA guarantees
+- Custom feature development options
+- Indemnification coverage
+- Access to premium cloud services and higher API quotas
+
+To obtain a Commercial License, contact: info@vxcontrol.com
+
+CLOUD SERVICES ACCESS
+
+Access to VXControl Cloud Intelligence Platform services requires:
+- Valid license key (obtained from VXControl LLC)
+- Compliance with service tier limitations (Free/Professional/Enterprise)
+- Adherence to acceptable use policies outlined in the EULA
+- Compliance with export control regulations for cybersecurity tools
+
+DISCLAIMER OF WARRANTIES
+------------------------
+
+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.
+
+LIMITATION OF LIABILITY
+-----------------------
+
+IN NO EVENT SHALL VXCONTROL LLC BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+TERMINATION
+-----------
+
+This license is automatically terminated if you violate any of its terms.
+Upon termination, you must immediately cease all use of the Software and
+destroy all copies in your possession.
+
+GOVERNING LAW
+-------------
+
+This license shall be governed by and construed in accordance with the laws
+of the United Arab Emirates, without regard to its conflict of law provisions.
+
+CONTACT INFORMATION
+-------------------
+
+For questions about this license or to obtain a Commercial License:
+
+Email: info@vxcontrol.com
+Website: https://vxcontrol.com
+Subject: "Cloud SDK Commercial Licensing"
+
+================================================================================
+
+Last updated: September 2025
+License version: 1.0
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ce99117
--- /dev/null
+++ b/README.md
@@ -0,0 +1,647 @@
+# VXControl Cloud SDK
+
+
+ Enterprise-grade Go SDK for secure integration with VXControl Cloud Intelligence Platform and Services.
+
+
+
+
+> ๐ **Join the Community!** Connect with security researchers, AI enthusiasts, and fellow ethical hackers. Get support, share insights, and stay updated with the latest PentAGI developments.
+
+[](https://discord.gg/2xrMh7qX6m)โ [](https://t.me/+Ka9i6CNwe71hMWQy)
+
+
+
+## Overview
+
+The VXControl Cloud SDK enables developers to integrate their security tools and applications with the VXControl Cloud Intelligence Platform, providing access to advanced cybersecurity services including threat intelligence, vulnerability databases, computational resources, AI-powered troubleshooting, and automated update systems.
+
+## Key Features
+
+- **Type-Safe API**: 24 strongly-typed function patterns covering all request/response scenarios
+- **Transparent Security**: Automatic proof-of-work solving and end-to-end encryption
+- **Performance Optimized**: HTTP/2 support, connection pooling, streaming encryption
+- **Enterprise Ready**: Comprehensive error handling, retry logic, and production monitoring
+- **License Integration**: Built-in premium feature validation and tier management
+
+## Quick Start
+
+### Installation
+
+```bash
+go get github.com/vxcontrol/cloud/sdk
+```
+
+### Basic Usage
+
+```go
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "log"
+
+ "github.com/vxcontrol/cloud/models"
+ "github.com/vxcontrol/cloud/sdk"
+ "github.com/vxcontrol/cloud/system"
+
+ "github.com/sirupsen/logrus"
+)
+
+type Client struct {
+ UpdatesCheck sdk.CallReqBytesRespBytes
+ ReportError sdk.CallReqBytesRespBytes
+}
+
+func main() {
+ var client Client
+
+ // Configure endpoints
+ configs := []sdk.CallConfig{
+ {
+ Calls: []any{&client.UpdatesCheck},
+ Host: "update.pentagi.com",
+ Name: "updates_check",
+ Path: "/api/v1/updates/check",
+ Method: sdk.CallMethodPOST,
+ },
+ {
+ Calls: []any{&client.ReportError},
+ Host: "support.pentagi.com",
+ Name: "error_report",
+ Path: "/api/v1/errors/report",
+ Method: sdk.CallMethodPOST,
+ },
+ }
+
+ // Initialize SDK
+ err := sdk.Build(configs,
+ sdk.WithClient("MySecTool", "1.0.0"),
+ sdk.WithInstallationID(system.GetInstallationID()),
+ sdk.WithLogger(sdk.WrapLogrus(logrus.StandardLogger())),
+ sdk.WithLicenseKey("XXXX-XXXX-XXXX-XXXX"),
+ )
+ if err != nil {
+ log.Fatal("SDK initialization failed:", err)
+ }
+
+ // Check for updates
+ updateReq := models.CheckUpdatesRequest{
+ InstallerVersion: "1.0.0",
+ InstallerOS: models.OSTypeLinux,
+ InstallerArch: models.ArchTypeAMD64,
+ }
+
+ data, _ := json.Marshal(updateReq)
+ response, err := client.UpdatesCheck(context.Background(), data)
+ if err != nil {
+ log.Fatal("Update check failed:", err)
+ }
+
+ var updateResp models.CheckUpdatesResponse
+ json.Unmarshal(response, &updateResp)
+ log.Printf("Available updates: %+v", updateResp.Updates)
+}
+```
+
+## Architecture
+
+```mermaid
+graph TD
+ A[Your Security Application] --> B[VXControl Cloud SDK]
+ B --> C[PoW Challenge System]
+ B --> D[Encrypted Transport Layer]
+ D --> E[VXControl Cloud Platform]
+
+ E --> F[Update Services]
+ E --> G[Package Management]
+ E --> H[Error Reporting & AI Support]
+ E --> I[Threat Intelligence Hub]
+ E --> J[Vulnerability Database]
+ E --> K[Computational Resources]
+ E --> L[Knowledge Base]
+
+ A --> M[PentAGI]
+ A --> N[Security Tools]
+ A --> O[SOC Systems]
+ A --> P[Red Team Tools]
+ A --> Q[Custom Applications]
+
+ C --> R[Memory-Hard Algorithm]
+ C --> S[Rate Limiting]
+ D --> T[End-to-End Encryption]
+ D --> V[Forward Secrecy]
+ D --> X[Ed25519 Signatures]
+```
+
+## Cloud Services Integration
+
+### Update Management
+Keep PentAGI with automated update checking:
+
+```go
+import "github.com/vxcontrol/cloud/models"
+
+// Check for component updates
+updateReq := models.CheckUpdatesRequest{
+ InstallerVersion: "1.0.0",
+ InstallerOS: models.OSTypeLinux,
+ InstallerArch: models.ArchTypeAMD64,
+ Components: []models.ComponentInfo{
+ {
+ Component: models.ComponentTypePentagi,
+ Status: models.ComponentStatusRunning,
+ Version: &[]string{"1.2.0"}[0],
+ },
+ },
+}
+
+data, _ := json.Marshal(updateReq)
+response, err := client.UpdatesCheck(ctx, data)
+
+var updateResp models.CheckUpdatesResponse
+json.Unmarshal(response, &updateResp)
+```
+
+### Error Reporting & AI Support
+Get intelligent assistance for troubleshooting:
+
+```go
+// Report an error for analysis
+errorReq := models.SupportErrorRequest{
+ Component: models.ComponentTypePentagi,
+ Version: "1.0.0",
+ OS: models.OSTypeLinux,
+ Arch: models.ArchTypeAMD64,
+ ErrorDetails: map[string]any{
+ "error_type": "connection_timeout",
+ "message": "Failed to connect to target",
+ "context": map[string]string{"target": "192.168.1.1", "port": "443"},
+ },
+}
+
+data, _ := json.Marshal(errorReq)
+response, err := client.ReportError(ctx, data)
+```
+
+### Package Management
+Download and validate software packages:
+
+```go
+// Get package information
+packageReq := models.PackageInfoRequest{
+ Component: models.ComponentTypePentagi,
+ Version: "1.0.0",
+ OS: models.OSTypeLinux,
+ Arch: models.ArchTypeAMD64,
+}
+
+// Validate package integrity with signatures
+signature := models.SignatureValue("base64-encoded-signature")
+fileData, _ := os.ReadFile("package.tar.gz")
+if err := signature.ValidateData(fileData); err != nil {
+ log.Fatal("Package signature validation failed:", err)
+}
+```
+
+### AI-Powered Troubleshooting
+Interactive support with investigation capabilities:
+
+```go
+// Create support issue
+issueReq := models.SupportIssueRequest{
+ Component: models.ComponentTypeEngine,
+ Version: "2.0.0",
+ OS: models.OSTypeDarwin,
+ Arch: models.ArchTypeARM64,
+ ErrorDetails: "Scanner fails to detect specific vulnerability patterns",
+ Logs: []models.SupportLogs{
+ {
+ Component: models.ComponentTypeEngine,
+ Logs: []string{"ERROR: Pattern matching timeout", "WARN: Memory usage high"},
+ },
+ },
+}
+
+// Investigate with AI assistance
+investigationReq := models.SupportInvestigationRequest{
+ IssueID: receivedIssueID,
+ UserInput: "The scanner works fine with other patterns but fails on this specific CVE",
+}
+```
+
+## Call Function Types
+
+The SDK supports 24 function patterns to handle different request/response scenarios:
+
+| Pattern | Request | Response | Use Case |
+|---------|---------|----------|----------|
+| `CallReqRespBytes` | None | Bytes | Simple data retrieval |
+| `CallReqQueryRespBytes` | Query params | Bytes | Filtered data queries |
+| `CallReqWithArgsRespBytes` | Path args | Bytes | Resource-specific requests |
+| `CallReqBytesRespBytes` | Body data | Bytes | Data submission/processing |
+| `CallReqReaderRespReader` | Stream | Stream | Large file processing |
+| `CallReqReaderRespWriter` | Stream | Writer | Direct output streaming |
+
+[Complete function reference](https://pkg.go.dev/github.com/vxcontrol/cloud/sdk)
+
+## Configuration Options
+
+### Basic Configuration
+
+```go
+err := sdk.Build(configs,
+ // Required: Client identification
+ sdk.WithClient("MyApp", "1.0.0"),
+
+ // Optional: Premium features
+ sdk.WithLicenseKey("XXXX-XXXX-XXXX-XXXX"),
+
+ // Optional: Performance tuning
+ sdk.WithPowTimeout(30*time.Second),
+ sdk.WithMaxRetries(3),
+)
+```
+
+### Advanced Configuration
+
+```go
+// Custom transport for proxies/certificates
+transport := sdk.DefaultTransport()
+transport.TLSClientConfig = &tls.Config{
+ MinVersion: tls.VersionTLS12,
+ // custom certificate validation
+}
+transport.Proxy = http.ProxyURL(proxyURL)
+
+// Custom structured logging
+logger := logrus.New()
+logger.SetLevel(logrus.InfoLevel)
+
+err := sdk.Build(configs,
+ sdk.WithTransport(transport),
+ sdk.WithLogger(sdk.WrapLogrus(logger)),
+ sdk.WithInstallationID(system.GetInstallationID()),
+)
+```
+
+## Security Model
+
+### Proof-of-Work Protection
+All API calls require solving computational challenges to prevent abuse and DDoS attacks. The SDK automatically:
+- Requests challenge tickets from the server
+- Solves memory-hard proof-of-work puzzles
+- Includes cryptographic signatures with requests
+
+### End-to-End Encryption
+- **Session Keys**: Ephemeral AES keys for each request
+- **NaCL Encryption**: Secure key exchange using Curve25519
+- **Streaming Cipher**: AES-GCM for large data transfers
+- **Forward Secrecy**: Cypher key rotation
+
+### Rate Limiting Integration
+- **Adaptive Difficulty**: PoW complexity scales with server load
+- **Tier-Based Access**: License validation determines API quotas
+- **Intelligent Retry**: Automatic backoff with server-provided timing
+
+## Error Handling
+
+### Automatic Retry Logic
+
+```go
+// Temporary errors (automatically retried):
+// - Server overload (sdk.ErrBadGateway, sdk.ErrServerInternal)
+// - Rate limits (sdk.ErrTooManyRequests, sdk.ErrTooManyRequestsRPM)
+// - PoW timeouts (sdk.ErrExperimentTimeout)
+
+// Fatal errors (no retry):
+// - Invalid requests (sdk.ErrBadRequest, sdk.ErrForbidden)
+// - Missing resources (sdk.ErrNotFound)
+// - Long-term limits (sdk.ErrTooManyRequestsRPH, sdk.ErrTooManyRequestsRPD)
+```
+
+### Custom Error Handling
+
+```go
+data, err := api.QueryThreats(ctx, []byte(threatQuery))
+if err != nil {
+ switch {
+ case errors.Is(err, sdk.ErrTooManyRequestsRPM):
+ // Wait and retry with exponential backoff
+ time.Sleep(60 * time.Second)
+
+ case errors.Is(err, sdk.ErrForbidden):
+ // Check license validity or authentication
+ log.Error("access denied - verify license key")
+
+ case errors.Is(err, sdk.ErrExperimentTimeout):
+ // Increase PoW timeout for slower systems
+ // Reconfigure with sdk.WithPowTimeout(60*time.Second)
+
+ default:
+ log.Error("unexpected error:", err)
+ }
+}
+```
+
+## Performance Characteristics
+
+### Benchmarks
+- **License validation**: ~334,000 operations/sec
+- **Function generation**: ~2M path templates/sec
+- **Streaming encryption**: ~50MB/sec throughput
+- **Connection pooling**: 300 connections/host, 50 total idle
+
+### Memory Usage
+- **Per request**: ~300 bytes (context + headers + keys)
+- **Per SDK instance**: ~200KB (connection pools + crypto keys)
+- **PoW solving**: 20-1024KB (reused across attempts)
+
+### Optimization Tips
+
+```go
+// Reuse SDK instances across requests
+err := sdk.Build(configs, options...)
+
+// Use streaming for large data
+reader, err := api.ProcessLargeDataset(ctx, dataStream, dataSize)
+
+// Configure connection pooling for high throughput
+transport := sdk.DefaultTransport()
+transport.MaxConnsPerHost = 500
+sdk.WithTransport(transport)
+```
+
+## Production Deployment
+
+### Required Configuration
+
+```go
+// Minimum production setup
+err := sdk.Build(configs,
+ sdk.WithClient("YourApp", version), // Required: Identification
+ sdk.WithLicenseKey(licenseKey), // Optional: Authentication
+ sdk.WithLogger(productionLogger), // Recommended: Monitoring
+)
+```
+
+### Monitoring Integration
+
+```go
+// Custom logger for metrics collection
+type MetricsLogger struct {
+ *logrus.Logger
+ metrics MetricsCollector
+}
+
+func (m *MetricsLogger) WithError(err error) sdk.Entry {
+ // Track error rates by type
+ m.metrics.IncrementErrorCounter(err)
+ return m.Logger.WithError(err)
+}
+
+// Integration
+logger := &MetricsLogger{Logger: logrus.New(), metrics: yourMetrics}
+sdk.WithLogger(logger)
+```
+
+### Security Considerations
+
+- **Certificate Validating**: Validate server certificates in production
+- **Proxy Support**: Configure corporate proxy settings if required
+- **Timeout Tuning**: Adjust PoW timeouts based on hardware capabilities
+- **Rate Limit Monitoring**: Track API quota usage and plan capacity
+
+## Use Cases
+
+### Security Tool Integration
+
+```go
+// Integrate update checking into security tools
+func checkSecurityToolUpdates(components []models.ComponentInfo) error {
+ updateReq := models.CheckUpdatesRequest{
+ InstallerVersion: getCurrentVersion(),
+ InstallerOS: getCurrentOS(),
+ InstallerArch: getCurrentArch(),
+ Components: components,
+ }
+
+ data, _ := json.Marshal(updateReq)
+ response, err := client.UpdatesCheck(context.Background(), data)
+ if err != nil {
+ return err
+ }
+
+ var updateResp models.CheckUpdatesResponse
+ json.Unmarshal(response, &updateResp)
+
+ for _, update := range updateResp.Updates {
+ if update.HasUpdate {
+ log.Printf("Update available for %s: %s -> %s",
+ update.Stack, *update.CurrentVersion, *update.LatestVersion)
+ }
+ }
+
+ return nil
+}
+```
+
+### Automated Error Reporting
+
+```go
+// Integrate error reporting into application error handling
+func reportSecurityToolError(component models.ComponentType, err error) error {
+ errorReq := models.SupportErrorRequest{
+ Component: component,
+ Version: getComponentVersion(component),
+ OS: getCurrentOS(),
+ Arch: getCurrentArch(),
+ ErrorDetails: map[string]any{
+ "error_message": err.Error(),
+ "stack_trace": getStackTrace(),
+ "context": getCurrentContext(),
+ },
+ }
+
+ data, _ := json.Marshal(errorReq)
+ _, reportErr := client.ReportError(context.Background(), data)
+ return reportErr
+}
+```
+
+### Package Integrity Validation
+
+```go
+// Validate downloaded packages before installation
+func validatePackageIntegrity(packagePath, signatureStr string) error {
+ signature := models.SignatureValue(signatureStr)
+
+ // Validate file signature
+ if err := signature.ValidateFile(packagePath); err != nil {
+ return fmt.Errorf("package signature validation failed: %w", err)
+ }
+
+ log.Println("Package integrity verified successfully")
+ return nil
+}
+
+// Validate data integrity in memory
+func validateDataIntegrity(data []byte, signatureStr string) error {
+ signature := models.SignatureValue(signatureStr)
+
+ if err := signature.ValidateData(data); err != nil {
+ return fmt.Errorf("data signature validation failed: %w", err)
+ }
+
+ return nil
+}
+```
+
+## Advanced Features
+
+### Multiple Service Endpoints
+
+```go
+// Connect to different service clusters
+type FullClient struct {
+ UpdatesCheck sdk.CallReqBytesRespBytes
+ ErrorReport sdk.CallReqBytesRespBytes
+ PackageInfo sdk.CallReqBytesRespBytes
+ SupportIssue sdk.CallReqBytesRespBytes
+}
+
+configs := []sdk.CallConfig{
+ {
+ Calls: []any{&client.UpdatesCheck},
+ Host: "update.pentagi.com",
+ Name: "check_updates",
+ Path: "/api/v1/updates/check",
+ Method: sdk.CallMethodPOST,
+ },
+ {
+ Calls: []any{&client.ErrorReport},
+ Host: "support.pentagi.com",
+ Name: "error_report",
+ Path: "/api/v1/errors/report",
+ Method: sdk.CallMethodPOST,
+ },
+ {
+ Calls: []any{&client.PackageInfo},
+ Host: "update.pentagi.com",
+ Name: "package_info",
+ Path: "/api/v1/packages/info",
+ Method: sdk.CallMethodPOST,
+ },
+}
+```
+
+### Timeout and Retry Configuration
+
+```go
+// Configure timeouts for different operation types
+err := sdk.Build(configs,
+ sdk.WithPowTimeout(30*time.Second), // For slower systems (max 60s, default 10s)
+ sdk.WithMaxRetries(5), // For rate limiting and network issues
+ sdk.WithTransport(customTransport), // Custom HTTP configuration
+)
+
+// Per-request timeouts
+ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
+defer cancel()
+
+response, err := client.UpdatesCheck(ctx, requestData)
+```
+
+## Error Reference
+
+| Error | Type | Retry | Description |
+|-------|------|-------|-------------|
+| `sdk.ErrBadGateway` | Temporary | Yes (3s) | Server maintenance/overload |
+| `sdk.ErrTooManyRequestsRPM` | Temporary | Yes (server-defined) | Rate limit exceeded |
+| `sdk.ErrExperimentTimeout` | Temporary | Yes (10s) | PoW solving timeout |
+| `sdk.ErrForbidden` | Fatal | No | Invalid license or authentication |
+| `sdk.ErrBadRequest` | Fatal | No | Invalid request format |
+| `sdk.ErrNotFound` | Fatal | No | Unknown endpoint or resource |
+
+## Available Models
+
+The SDK provides strongly-typed models for all API interactions:
+
+### Component Management
+- `ComponentType`: pentagi, scraper, langfuse-worker, langfuse-web, grafana, otelcol, worker, installer, engine
+- `ComponentStatus`: unused, connected, installed, running
+- `ProductStack`: pentagi, langfuse, observability, worker, installer, engine
+- `OSType`: windows, linux, darwin
+- `ArchType`: amd64, arm64
+
+### Update Service Models
+- `CheckUpdatesRequest` / `CheckUpdatesResponse`: Check for component updates
+- `ComponentInfo`: Information about installed components
+- `UpdateInfo`: Available update details with changelog
+
+### Package Service Models
+- `PackageInfoRequest` / `PackageInfoResponse`: Get package metadata
+- `DownloadPackageRequest`: Request package downloads
+- `SignatureValue`: Cryptographic signature validation
+
+### Support Service Models
+- `SupportErrorRequest` / `SupportErrorResponse`: Automated error reporting
+- `SupportIssueRequest` / `SupportIssueResponse`: Manual issue creation with AI
+- `SupportLogs`: Component log collection
+- `SupportInvestigationRequest` / `SupportInvestigationResponse`: AI-powered troubleshooting
+
+### System Utilities
+- `system.GetInstallationID()`: Generates stable, machine-specific UUID for installation tracking
+
+## Service Tiers
+
+### Free Tier
+- **Basic error reporting**: Automated error submission
+- **Package validation**: Ed25519 signature verification
+- **Rate limiting**: Standard PoW difficulty
+
+### Professional Tier
+- **AI troubleshooting**: x5 investigation sessions/day
+- **Package downloads**: Access to all packages
+- **Rate limiting**: Reduced PoW difficulty
+
+### Enterprise Tier
+- **Advanced AI troubleshooting**: x50 investigation sessions/day
+- **Custom integrations**: Specialized endpoints and workflows
+- **Priority processing**: Minimal PoW difficulty and fast-track handling
+
+## Future Roadmap
+
+The VXControl Cloud Platform is actively expanding. Future releases may include:
+
+- **Threat Intelligence Services**: IOC/IOA database access and threat analysis
+- **Vulnerability Assessment**: CVE database integration and security scanning
+- **Computational Resources**: Cloud-based intensive task processing
+- **Advanced Analytics**: Security metrics and reporting dashboards
+- **Custom Workflows**: Specialized security automation pipelines
+
+*Note: These features are in development and not yet available in the current SDK version.*
+
+## Contributing
+
+1. Fork the repository
+2. Create a feature branch (`git checkout -b feature/amazing-feature`)
+3. Commit your changes (`git commit -m 'feat: add amazing feature'`)
+4. Push to the branch (`git push origin feature/amazing-feature`)
+5. Open a Pull Request
+
+## Support
+
+- **Documentation**: [API Reference](API.md) | [Examples](examples/)
+- **Issues**: [GitHub Issues](https://github.com/vxcontrol/cloud/issues)
+- **Enterprise Support**: support@vxcontrol.com
+- **Community**: [Discord](https://discord.gg/2xrMh7qX6m) and [Telegram](https://t.me/+Ka9i6CNwe71hMWQy)
+
+## License
+
+Copyright (c) 2025 VXControl LLC. All rights reserved.
+
+This software is provided under a proprietary license. See [LICENSE](LICENSE) file for details.
+
+Commercial licenses available for enterprise use. Contact info@vxcontrol.com for pricing.
diff --git a/anonymizer/anonymizer.go b/anonymizer/anonymizer.go
new file mode 100644
index 0000000..becc23d
--- /dev/null
+++ b/anonymizer/anonymizer.go
@@ -0,0 +1,242 @@
+package anonymizer
+
+import (
+ "io"
+ "reflect"
+
+ "github.com/vxcontrol/cloud/anonymizer/patterns"
+)
+
+type Anonymizer interface {
+ Anonymize(any) error
+ ReplaceString(string) string
+ ReplaceBytes([]byte) []byte
+ WrapReader(io.Reader) io.Reader
+}
+
+type anonymizer struct {
+ Replacer
+}
+
+func NewAnonymizer(pts []patterns.Pattern) (Anonymizer, error) {
+ patterns, err := patterns.LoadPatterns(patterns.PatternListTypeAll)
+ if err != nil {
+ return nil, err
+ }
+
+ patterns.Patterns = append(patterns.Patterns, pts...)
+
+ replacer, err := NewReplacer(patterns.Regexes(), patterns.Names())
+ if err != nil {
+ return nil, err
+ }
+
+ return &anonymizer{Replacer: replacer}, nil
+}
+
+func (a *anonymizer) Anonymize(v any) error {
+ rv := reflect.ValueOf(v)
+
+ if !isMutable(rv) {
+ return ErrObjectImmutable
+ }
+
+ // use visited map to prevent infinite recursion
+ visited := make(map[uintptr]bool)
+ return a.anonymizeValue(rv, visited)
+}
+
+// anonymizeValue recursively anonymizes a reflect.Value with cycle detection
+func (a *anonymizer) anonymizeValue(v reflect.Value, visited map[uintptr]bool) error {
+ if !v.IsValid() {
+ return nil
+ }
+
+ // check for cycles in pointer types
+ if v.Kind() == reflect.Pointer && !v.IsNil() {
+ ptr := v.Pointer()
+ if visited[ptr] {
+ return nil // already visited, skip to prevent cycles
+ }
+ visited[ptr] = true
+ }
+
+ switch v.Kind() {
+ case reflect.Pointer:
+ if v.IsNil() {
+ return nil
+ }
+ // for pointer to string, anonymize the string value directly
+ elem := v.Elem()
+ if elem.Kind() == reflect.String && elem.CanSet() {
+ original := elem.String()
+ anonymized := a.ReplaceString(original)
+ elem.SetString(anonymized)
+ return nil
+ }
+ // for other types, recurse
+ return a.anonymizeValue(elem, visited)
+
+ case reflect.Interface:
+ if v.IsNil() {
+ return nil
+ }
+ elem := v.Elem()
+
+ // if the interface contains a struct value, we need special handling
+ if elem.Kind() == reflect.Struct {
+ // create a new struct value that can be modified
+ newStruct := reflect.New(elem.Type()).Elem()
+ newStruct.Set(elem)
+
+ if err := a.anonymizeValue(newStruct.Addr(), visited); err != nil {
+ return err
+ }
+
+ // set the modified struct back to the interface
+ v.Set(newStruct)
+ return nil
+ }
+
+ // for other types, try to modify in place if possible
+ if elem.CanSet() {
+ return a.anonymizeValue(elem, visited)
+ }
+
+ // if we can't modify in place, create a new value
+ newVal := reflect.New(elem.Type()).Elem()
+ newVal.Set(elem)
+
+ if err := a.anonymizeValue(newVal, visited); err != nil {
+ return err
+ }
+
+ v.Set(newVal)
+ return nil
+
+ case reflect.Map:
+ return a.anonymizeMap(v, visited)
+
+ case reflect.Slice, reflect.Array:
+ return a.anonymizeSlice(v, visited)
+
+ case reflect.Struct:
+ return a.anonymizeStruct(v, visited)
+
+ case reflect.String:
+ if v.CanSet() {
+ original := v.String()
+ anonymized := a.ReplaceString(original)
+ v.SetString(anonymized)
+ }
+ return nil
+
+ default:
+ // for other types (numbers, bools, etc.) do nothing
+ return nil
+ }
+}
+
+// anonymizeMap anonymizes all values in a map with cycle detection
+func (a *anonymizer) anonymizeMap(v reflect.Value, visited map[uintptr]bool) error {
+ if v.IsNil() {
+ return nil
+ }
+
+ iter := v.MapRange()
+ for iter.Next() {
+ key := iter.Key()
+ val := iter.Value()
+
+ // handle different value types appropriately
+ switch val.Kind() {
+ case reflect.Pointer:
+ // for pointers, work directly with the original pointer
+ if err := a.anonymizeValue(val, visited); err != nil {
+ return err
+ }
+ // no need to set back to map - we modified the pointed-to value
+
+ case reflect.Struct:
+ // for struct values in map, create a copy and work with its address
+ newVal := reflect.New(val.Type()).Elem()
+ newVal.Set(val)
+ if err := a.anonymizeValue(newVal.Addr(), visited); err != nil {
+ return err
+ }
+ v.SetMapIndex(key, newVal)
+
+ default:
+ // for other types (strings, primitives, slices, maps), create a copy
+ newVal := reflect.New(val.Type()).Elem()
+ newVal.Set(val)
+ if err := a.anonymizeValue(newVal, visited); err != nil {
+ return err
+ }
+ v.SetMapIndex(key, newVal)
+ }
+ }
+
+ return nil
+}
+
+// anonymizeSlice anonymizes all elements in a slice or array with cycle detection
+func (a *anonymizer) anonymizeSlice(v reflect.Value, visited map[uintptr]bool) error {
+ for i := 0; i < v.Len(); i++ {
+ elem := v.Index(i)
+ if err := a.anonymizeValue(elem, visited); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// anonymizeStruct anonymizes struct fields based on their tags and types with cycle detection
+func (a *anonymizer) anonymizeStruct(v reflect.Value, visited map[uintptr]bool) error {
+ structType := v.Type()
+
+ // get struct information from cache or parse it
+ info, err := getStructInfo(structType)
+ if err != nil {
+ return err
+ }
+
+ // iterate through all fields
+ for _, field := range info.Fields {
+ // skip fields marked with skip tag
+ if field.Skip {
+ continue
+ }
+
+ // get field value using index path
+ fieldValue := v
+ for _, idx := range field.Index {
+ if fieldValue.Kind() == reflect.Pointer {
+ if fieldValue.IsNil() {
+ // skip nil pointers - don't create new objects
+ fieldValue = reflect.Value{}
+ break
+ }
+ fieldValue = fieldValue.Elem()
+ }
+
+ if !fieldValue.IsValid() || idx >= fieldValue.NumField() {
+ // field index out of range or invalid, skip
+ fieldValue = reflect.Value{}
+ break
+ }
+
+ fieldValue = fieldValue.Field(idx)
+ }
+
+ // anonymize the field value only if it's valid and not marked as skip
+ if fieldValue.IsValid() {
+ if err := a.anonymizeValue(fieldValue, visited); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/anonymizer/anonymizer_bench_test.go b/anonymizer/anonymizer_bench_test.go
new file mode 100644
index 0000000..274d7b2
--- /dev/null
+++ b/anonymizer/anonymizer_bench_test.go
@@ -0,0 +1,888 @@
+package anonymizer
+
+import (
+ "fmt"
+ "reflect"
+ "sync"
+ "sync/atomic"
+ "testing"
+ "unsafe"
+
+ "github.com/vxcontrol/cloud/anonymizer/patterns"
+)
+
+// benchmark test structures
+type BenchmarkStruct struct {
+ Name string `json:"name"`
+ Email string `json:"email"`
+ Token string `json:"token"`
+ Skip string `json:"skip" anonymizer:"skip"`
+ Age int `json:"age"`
+ Active bool `json:"active"`
+ Metadata map[string]string `json:"metadata"`
+ Tags []string `json:"tags"`
+}
+
+type ComplexBenchmarkStruct struct {
+ ID string `json:"id"`
+ Data *BenchmarkStruct `json:"data"`
+ Items []*BenchmarkStruct `json:"items"`
+ Groups map[string]*BenchmarkStruct `json:"groups"`
+ Any map[string]any `json:"any"`
+ Skip string `json:"skip" anonymizer:"skip"`
+}
+
+type SpecialTypesBenchmark struct {
+ Mutex sync.Mutex `json:"mutex"`
+ RWMutex sync.RWMutex `json:"rwmutex"`
+ AtomicInt atomic.Int64 `json:"atomic_int"`
+ AtomicBool atomic.Bool `json:"atomic_bool"`
+ UnsafePtr unsafe.Pointer `json:"unsafe_ptr"`
+ String string `json:"string"`
+ Skip string `json:"skip" anonymizer:"skip"`
+}
+
+// benchmark data generators
+func generateBenchmarkStruct() *BenchmarkStruct {
+ return &BenchmarkStruct{
+ Name: "John Doe",
+ Email: "john.doe@example.com",
+ Token: "secret_token_12345",
+ Skip: "do_not_anonymize",
+ Age: 30,
+ Active: true,
+ Metadata: map[string]string{
+ "ip": "192.168.1.1",
+ "user_id": "user_12345",
+ "session": "session_abc123",
+ },
+ Tags: []string{"admin", "privileged", "api_access"},
+ }
+}
+
+func generateComplexBenchmarkStruct() *ComplexBenchmarkStruct {
+ items := make([]*BenchmarkStruct, 10)
+ for i := range items {
+ items[i] = generateBenchmarkStruct()
+ items[i].Name = fmt.Sprintf("User_%d", i)
+ }
+
+ groups := make(map[string]*BenchmarkStruct)
+ for i := 0; i < 5; i++ {
+ groups[fmt.Sprintf("group_%d", i)] = generateBenchmarkStruct()
+ }
+
+ return &ComplexBenchmarkStruct{
+ ID: "complex_struct_id",
+ Data: generateBenchmarkStruct(),
+ Items: items,
+ Groups: groups,
+ Any: map[string]any{
+ "string": "any_string_value",
+ "int": 42,
+ "struct": *generateBenchmarkStruct(),
+ "ptr": generateBenchmarkStruct(),
+ "slice": []string{"a", "b", "c"},
+ "nil": nil,
+ },
+ Skip: "complex_skip_value",
+ }
+}
+
+func generateSpecialTypesBenchmark() *SpecialTypesBenchmark {
+ s := &SpecialTypesBenchmark{
+ String: "test_string",
+ Skip: "skip_value",
+ }
+ s.AtomicInt.Store(123)
+ s.AtomicBool.Store(true)
+ return s
+}
+
+// benchmarks for different structure types
+func BenchmarkAnonymize_SimpleStruct(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ data := generateBenchmarkStruct()
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func BenchmarkAnonymize_ComplexStruct(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ data := generateComplexBenchmarkStruct()
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func BenchmarkAnonymize_SpecialTypes(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ data := generateSpecialTypesBenchmark()
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+// benchmarks for different collection types
+func BenchmarkAnonymize_MapStringString(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ // generate test data
+ data := make(map[string]string)
+ for i := 0; i < 100; i++ {
+ data[fmt.Sprintf("key_%d", i)] = fmt.Sprintf("value_%d", i)
+ }
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // create fresh copy for each iteration
+ testData := make(map[string]string)
+ for k, v := range data {
+ testData[k] = v
+ }
+
+ err := anonymizer.Anonymize(testData)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func BenchmarkAnonymize_SliceStrings(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ // generate test data
+ data := make([]string, 100)
+ for i := range data {
+ data[i] = fmt.Sprintf("string_value_%d", i)
+ }
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // create fresh copy for each iteration
+ testData := make([]string, len(data))
+ copy(testData, data)
+
+ err := anonymizer.Anonymize(testData)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func BenchmarkAnonymize_MapStringAny(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ data := map[string]any{
+ "string": "test_string",
+ "int": 42,
+ "struct": *generateBenchmarkStruct(),
+ "ptr_struct": generateBenchmarkStruct(),
+ "slice": []string{"a", "b", "c"},
+ "map": map[string]string{"nested": "value"},
+ "nil": nil,
+ }
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+// benchmarks for struct caching efficiency
+func BenchmarkAnonymize_StructCaching_SameType(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // same struct type - should hit cache after first parse
+ data := generateBenchmarkStruct()
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func BenchmarkAnonymize_StructCaching_DifferentTypes(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for i := 0; b.Loop(); i++ {
+ // different struct types - will require parsing each time
+ switch i % 3 {
+ case 0:
+ data := generateBenchmarkStruct()
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ case 1:
+ data := generateComplexBenchmarkStruct()
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ case 2:
+ data := generateSpecialTypesBenchmark()
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ }
+}
+
+// benchmarks for memory allocation patterns
+func BenchmarkAnonymize_LargeMap(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ sizes := []int{100, 500, 1000, 5000}
+
+ for _, size := range sizes {
+ b.Run(fmt.Sprintf("size_%d", size), func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ data := make(map[string]string)
+ for i := 0; i < size; i++ {
+ data[fmt.Sprintf("key_%d", i)] = fmt.Sprintf("sensitive_value_%d", i)
+ }
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ }
+}
+
+func BenchmarkAnonymize_LargeSlice(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ sizes := []int{100, 500, 1000, 5000}
+
+ for _, size := range sizes {
+ b.Run(fmt.Sprintf("size_%d", size), func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ data := make([]string, size)
+ for i := range data {
+ data[i] = fmt.Sprintf("sensitive_string_%d", i)
+ }
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ }
+}
+
+func BenchmarkAnonymize_DeepNesting(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ depths := []int{5, 10, 20, 50}
+
+ for _, depth := range depths {
+ b.Run(fmt.Sprintf("depth_%d", depth), func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // create deeply nested structure
+ data := &struct {
+ Value string `json:"value"`
+ Next interface{} `json:"next"`
+ }{Value: "root"}
+
+ current := data
+ for i := 0; i < depth; i++ {
+ next := &struct {
+ Value string `json:"value"`
+ Next interface{} `json:"next"`
+ }{Value: fmt.Sprintf("level_%d", i)}
+ current.Next = next
+ current = next
+ }
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ }
+}
+
+// benchmarks comparing with naive implementations
+func BenchmarkAnonymize_vs_Naive_SimpleStruct(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ b.Run("optimized", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ data := generateBenchmarkStruct()
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+
+ b.Run("naive_reflection", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ data := generateBenchmarkStruct()
+ // naive implementation without caching
+ naiveAnonymizeStruct(data)
+ }
+ })
+}
+
+// naive implementation for comparison (without caching)
+func naiveAnonymizeStruct(v any) {
+ rv := reflect.ValueOf(v)
+ if rv.Kind() != reflect.Pointer || rv.IsNil() {
+ return
+ }
+
+ elem := rv.Elem()
+ if elem.Kind() != reflect.Struct {
+ return
+ }
+
+ // parse struct every time (no caching)
+ for i := 0; i < elem.NumField(); i++ {
+ field := elem.Field(i)
+ if field.Kind() == reflect.String && field.CanSet() {
+ field.SetString(maskedString)
+ }
+ }
+}
+
+// benchmark for pointer handling efficiency
+func BenchmarkAnonymize_PointerHandling(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ b.Run("many_pointers", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // structure with many pointer fields
+ str1, str2, str3 := "value1", "value2", "value3"
+ data := &struct {
+ Ptr1 *string `json:"ptr1"`
+ Ptr2 *string `json:"ptr2"`
+ Ptr3 *string `json:"ptr3"`
+ Nil1 *string `json:"nil1"`
+ Nil2 *string `json:"nil2"`
+ }{
+ Ptr1: &str1,
+ Ptr2: &str2,
+ Ptr3: &str3,
+ Nil1: nil,
+ Nil2: nil,
+ }
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+
+ b.Run("deep_pointers", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // deeply nested pointer structure
+ level3String := "deep_value"
+ data := &struct {
+ Level1 *struct {
+ Level2 *struct {
+ Level3 *struct {
+ Value *string `json:"value"`
+ } `json:"level3"`
+ } `json:"level2"`
+ } `json:"level1"`
+ }{
+ Level1: &struct {
+ Level2 *struct {
+ Level3 *struct {
+ Value *string `json:"value"`
+ } `json:"level3"`
+ } `json:"level2"`
+ }{
+ Level2: &struct {
+ Level3 *struct {
+ Value *string `json:"value"`
+ } `json:"level3"`
+ }{
+ Level3: &struct {
+ Value *string `json:"value"`
+ }{
+ Value: &level3String,
+ },
+ },
+ },
+ }
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
+
+// benchmark for circular reference handling
+func BenchmarkAnonymize_CircularReferences(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ type CircularNode struct {
+ Value string `json:"value"`
+ Next *CircularNode `json:"next"`
+ }
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // create circular structure
+ node1 := &CircularNode{Value: "node1"}
+ node2 := &CircularNode{Value: "node2"}
+ node3 := &CircularNode{Value: "node3"}
+
+ node1.Next = node2
+ node2.Next = node3
+ node3.Next = node1 // circular reference
+
+ err := anonymizer.Anonymize(node1)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+// benchmark for real-world production patterns
+func BenchmarkAnonymize_ProductionPatterns(b *testing.B) {
+ // create anonymizer with real patterns
+ anonymizer, err := NewAnonymizer([]patterns.Pattern{})
+ if err != nil {
+ b.Skip("failed to create production anonymizer")
+ }
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ data := generateComplexBenchmarkStruct()
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+// benchmark for struct parsing vs caching
+func BenchmarkStructParsing_vs_Caching(b *testing.B) {
+ type TestStruct struct {
+ Field1 string `json:"field1"`
+ Field2 string `json:"field2"`
+ Field3 string `json:"field3"`
+ Skip string `json:"skip" anonymizer:"skip"`
+ }
+
+ b.Run("with_caching", func(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ data := &TestStruct{
+ Field1: "value1",
+ Field2: "value2",
+ Field3: "value3",
+ Skip: "skip_value",
+ }
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+
+ b.Run("without_caching", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ data := &TestStruct{
+ Field1: "value1",
+ Field2: "value2",
+ Field3: "value3",
+ Skip: "skip_value",
+ }
+ // naive implementation - parse struct every time
+ naiveAnonymizeStruct(data)
+ }
+ })
+}
+
+// benchmark for memory allocation patterns
+func BenchmarkAnonymize_AllocationPatterns(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ b.Run("struct_reuse", func(b *testing.B) {
+ // reuse the same struct instance
+ data := generateBenchmarkStruct()
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // reset values
+ data.Name = "John Doe"
+ data.Email = "john.doe@example.com"
+ data.Token = "secret_token_12345"
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+
+ b.Run("struct_recreation", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // create new struct each time
+ data := generateBenchmarkStruct()
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
+
+// benchmark for concurrent usage
+func BenchmarkAnonymize_Concurrent(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ b.Run("parallel_simple_structs", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ data := generateBenchmarkStruct()
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ })
+
+ b.Run("parallel_complex_structs", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ data := generateComplexBenchmarkStruct()
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ })
+}
+
+// benchmark for edge case performance
+func BenchmarkAnonymize_EdgeCases(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ b.Run("many_nil_pointers", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ data := &struct {
+ Ptr1 *string `json:"ptr1"`
+ Ptr2 *string `json:"ptr2"`
+ Ptr3 *string `json:"ptr3"`
+ Ptr4 *string `json:"ptr4"`
+ Ptr5 *string `json:"ptr5"`
+ Value string `json:"value"`
+ }{
+ // all pointers are nil
+ Value: "only_this_value",
+ }
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+
+ b.Run("many_skip_fields", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ data := &struct {
+ Skip1 string `json:"skip1" anonymizer:"skip"`
+ Skip2 string `json:"skip2" anonymizer:"skip"`
+ Skip3 string `json:"skip3" anonymizer:"skip"`
+ Skip4 string `json:"skip4" anonymizer:"skip"`
+ Skip5 string `json:"skip5" anonymizer:"skip"`
+ Value string `json:"value"`
+ }{
+ Skip1: "skip1",
+ Skip2: "skip2",
+ Skip3: "skip3",
+ Skip4: "skip4",
+ Skip5: "skip5",
+ Value: "process_this",
+ }
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+
+ b.Run("empty_collections", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ data := &struct {
+ EmptyMap map[string]string `json:"empty_map"`
+ EmptySlice []string `json:"empty_slice"`
+ NilMap map[string]string `json:"nil_map"`
+ NilSlice []string `json:"nil_slice"`
+ Value string `json:"value"`
+ }{
+ EmptyMap: make(map[string]string),
+ EmptySlice: make([]string, 0),
+ NilMap: nil,
+ NilSlice: nil,
+ Value: "process_this",
+ }
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
+
+// benchmark for worst-case scenarios
+func BenchmarkAnonymize_WorstCase(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ b.Run("all_strings_need_processing", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // structure where every field is a string that needs processing
+ data := &struct {
+ F1 string `json:"f1"`
+ F2 string `json:"f2"`
+ F3 string `json:"f3"`
+ F4 string `json:"f4"`
+ F5 string `json:"f5"`
+ F6 string `json:"f6"`
+ F7 string `json:"f7"`
+ F8 string `json:"f8"`
+ F9 string `json:"f9"`
+ F10 string `json:"f10"`
+ }{
+ F1: "value1", F2: "value2", F3: "value3", F4: "value4", F5: "value5",
+ F6: "value6", F7: "value7", F8: "value8", F9: "value9", F10: "value10",
+ }
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+
+ b.Run("mixed_with_many_allocations", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // structure that requires many allocations for map[string]any
+ data := map[string]any{
+ "s1": "string1", "s2": "string2", "s3": "string3",
+ "i1": 1, "i2": 2, "i3": 3,
+ "struct1": BenchmarkStruct{Name: "n1", Email: "e1"},
+ "struct2": BenchmarkStruct{Name: "n2", Email: "e2"},
+ "slice1": []string{"a", "b", "c"},
+ "slice2": []string{"d", "e", "f"},
+ "map1": map[string]string{"k1": "v1"},
+ "map2": map[string]string{"k2": "v2"},
+ }
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
+
+// benchmark for best-case scenarios
+func BenchmarkAnonymize_BestCase(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ b.Run("no_strings_to_process", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // structure with no strings to anonymize
+ data := &struct {
+ Int1 int `json:"int1"`
+ Int2 int `json:"int2"`
+ Bool1 bool `json:"bool1"`
+ Bool2 bool `json:"bool2"`
+ Float1 float64 `json:"float1"`
+ Float2 float64 `json:"float2"`
+ }{
+ Int1: 1, Int2: 2, Bool1: true, Bool2: false, Float1: 1.1, Float2: 2.2,
+ }
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+
+ b.Run("all_skip_fields", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // structure where all string fields are marked as skip
+ data := &struct {
+ Skip1 string `json:"skip1" anonymizer:"skip"`
+ Skip2 string `json:"skip2" anonymizer:"skip"`
+ Skip3 string `json:"skip3" anonymizer:"skip"`
+ Skip4 string `json:"skip4" anonymizer:"skip"`
+ Skip5 string `json:"skip5" anonymizer:"skip"`
+ }{
+ Skip1: "value1", Skip2: "value2", Skip3: "value3", Skip4: "value4", Skip5: "value5",
+ }
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
+
+// benchmark for memory efficiency
+func BenchmarkAnonymize_MemoryEfficiency(b *testing.B) {
+ anonymizer := createMockAnonymizer()
+
+ b.Run("reuse_visited_map", func(b *testing.B) {
+ // test if visited map is efficiently managed
+ data := generateComplexBenchmarkStruct()
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // reset string values
+ data.ID = "complex_struct_id"
+ data.Data.Name = "John Doe"
+ data.Data.Email = "john.doe@example.com"
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+
+ b.Run("large_visited_map", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ // create structure with many unique pointers
+ data := make(map[string]*BenchmarkStruct)
+ for i := 0; i < 100; i++ {
+ data[fmt.Sprintf("key_%d", i)] = generateBenchmarkStruct()
+ }
+
+ err := anonymizer.Anonymize(data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
diff --git a/anonymizer/anonymizer_test.go b/anonymizer/anonymizer_test.go
new file mode 100644
index 0000000..5ba9689
--- /dev/null
+++ b/anonymizer/anonymizer_test.go
@@ -0,0 +1,1396 @@
+package anonymizer
+
+import (
+ "fmt"
+ "io"
+ "reflect"
+ "sync"
+ "sync/atomic"
+ "testing"
+ "unsafe"
+)
+
+// test constants
+const (
+ maskedString = "***MASKED***"
+)
+
+// testMockReplacer replaces any non-empty string with a constant for predictable testing
+type testMockReplacer struct{}
+
+func (m *testMockReplacer) ReplaceString(s string) string {
+ if s == "" {
+ return s
+ }
+ return maskedString
+}
+
+func (m *testMockReplacer) ReplaceBytes(b []byte) []byte {
+ if len(b) == 0 {
+ return b
+ }
+ return []byte(maskedString)
+}
+
+func (m *testMockReplacer) WrapReader(r io.Reader) io.Reader {
+ return r // not used in struct anonymization tests
+}
+
+// test helper structures
+type SimpleStruct struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+ Skip string `json:"skip" anonymizer:"skip"`
+}
+
+type NestedStruct struct {
+ ID string `json:"id"`
+ Simple SimpleStruct `json:"simple"`
+}
+
+type EmbeddedStruct struct {
+ SimpleStruct
+ Extra string `json:"extra"`
+}
+
+type ComplexStruct struct {
+ Name string `json:"name"`
+ Tags []string `json:"tags"`
+ Metadata map[string]string `json:"metadata"`
+ Any map[string]any `json:"any"`
+ Skip string `json:"skip" anonymizer:"skip"`
+}
+
+// test structures for pointer and special type handling
+type PointerTestStruct struct {
+ StringPtr *string `json:"string_ptr"`
+ IntPtr *int `json:"int_ptr"`
+ NilStringPtr *string `json:"nil_string_ptr"`
+ NilIntPtr *int `json:"nil_int_ptr"`
+ StructPtr *SimpleStruct `json:"struct_ptr"`
+ NilStructPtr *SimpleStruct `json:"nil_struct_ptr"`
+ Skip *string `json:"skip" anonymizer:"skip"`
+}
+
+type SpecialTypesStruct struct {
+ Mutex sync.Mutex `json:"mutex"`
+ RWMutex sync.RWMutex `json:"rwmutex"`
+ AtomicInt atomic.Int64 `json:"atomic_int"`
+ AtomicBool atomic.Bool `json:"atomic_bool"`
+ UnsafePtr unsafe.Pointer `json:"unsafe_ptr"`
+ String string `json:"string"`
+ Skip string `json:"skip" anonymizer:"skip"`
+}
+
+type DeepPointerStruct struct {
+ Level1 *struct {
+ StringPtr *string `json:"string_ptr"`
+ Level2 *struct {
+ StringPtr *string `json:"string_ptr"`
+ NilPtr *string `json:"nil_ptr"`
+ } `json:"level2"`
+ } `json:"level1"`
+}
+
+// test case structure
+type testCase struct {
+ name string
+ object any
+ check func(any) error
+}
+
+// helper function to create anonymizer with mock replacer
+func createMockAnonymizer() Anonymizer {
+ return &anonymizer{Replacer: &testMockReplacer{}}
+}
+
+func checkSimpleStruct(name, value, skip string) func(any) error {
+ return func(obj any) error {
+ s, ok := obj.(*SimpleStruct)
+ if !ok {
+ return fmt.Errorf("object is not *SimpleStruct: %T", obj)
+ }
+ if s.Name != name {
+ return fmt.Errorf("Name: expected %q, got %q", name, s.Name)
+ }
+ if s.Value != value {
+ return fmt.Errorf("Value: expected %q, got %q", value, s.Value)
+ }
+ if s.Skip != skip {
+ return fmt.Errorf("Skip: expected %q, got %q", skip, s.Skip)
+ }
+ return nil
+ }
+}
+
+func checkMapStringString(expected map[string]string) func(any) error {
+ return func(obj any) error {
+ m, ok := obj.(map[string]string)
+ if !ok {
+ return fmt.Errorf("object is not map[string]string: %T", obj)
+ }
+ if len(m) != len(expected) {
+ return fmt.Errorf("map length: expected %d, got %d", len(expected), len(m))
+ }
+ for k, v := range expected {
+ if actual, exists := m[k]; !exists {
+ return fmt.Errorf("key %q not found", k)
+ } else if actual != v {
+ return fmt.Errorf("key %q: expected %q, got %q", k, v, actual)
+ }
+ }
+ return nil
+ }
+}
+
+func checkSliceStrings(expected []string) func(any) error {
+ return func(obj any) error {
+ s, ok := obj.([]string)
+ if !ok {
+ return fmt.Errorf("object is not []string: %T", obj)
+ }
+ if len(s) != len(expected) {
+ return fmt.Errorf("slice length: expected %d, got %d", len(expected), len(s))
+ }
+ for i, exp := range expected {
+ if s[i] != exp {
+ return fmt.Errorf("index %d: expected %q, got %q", i, exp, s[i])
+ }
+ }
+ return nil
+ }
+}
+
+// helper to check pointer identity (same address)
+func checkPointerIdentity(original, current any, fieldName string) error {
+ origPtr := fmt.Sprintf("%p", original)
+ currPtr := fmt.Sprintf("%p", current)
+ if origPtr != currPtr {
+ return fmt.Errorf("%s pointer identity changed: original %s, current %s", fieldName, origPtr, currPtr)
+ }
+ return nil
+}
+
+// helper to check that nil pointers remain nil
+func checkNilPointer(ptr any, fieldName string) error {
+ if ptr == nil {
+ return nil // truly nil
+ }
+
+ // check for typed nil using reflection
+ v := reflect.ValueOf(ptr)
+ if v.Kind() == reflect.Pointer && v.IsNil() {
+ return nil // typed nil pointer
+ }
+
+ return fmt.Errorf("%s should be nil, got %v (type: %T)", fieldName, ptr, ptr)
+}
+
+// helper to check string pointer value
+func checkStringPointer(ptr *string, expected string, fieldName string) error {
+ if ptr == nil {
+ return fmt.Errorf("%s should not be nil", fieldName)
+ }
+ if *ptr != expected {
+ return fmt.Errorf("%s: expected %q, got %q", fieldName, expected, *ptr)
+ }
+ return nil
+}
+
+// comprehensive test cases
+func TestAnonymizeComprehensive(t *testing.T) {
+ anonymizer := createMockAnonymizer()
+
+ testCases := []testCase{
+ // basic types
+ {
+ name: "simple_struct_pointer",
+ object: &SimpleStruct{Name: "test", Value: "value", Skip: "keep"},
+ check: checkSimpleStruct(maskedString, maskedString, "keep"),
+ },
+
+ // nested structures
+ {
+ name: "nested_struct",
+ object: &NestedStruct{
+ ID: "outer",
+ Simple: SimpleStruct{Name: "inner", Value: "data", Skip: "preserve"},
+ },
+ check: func(obj any) error {
+ n := obj.(*NestedStruct)
+ if n.ID != maskedString {
+ return fmt.Errorf("ID: expected %q, got %q", maskedString, n.ID)
+ }
+ return checkSimpleStruct(maskedString, maskedString, "preserve")(&n.Simple)
+ },
+ },
+
+ // embedded structures
+ {
+ name: "embedded_struct",
+ object: &EmbeddedStruct{
+ SimpleStruct: SimpleStruct{Name: "embedded", Value: "data", Skip: "keep"},
+ Extra: "additional",
+ },
+ check: func(obj any) error {
+ e := obj.(*EmbeddedStruct)
+ if e.Extra != maskedString {
+ return fmt.Errorf("Extra: expected %q, got %q", maskedString, e.Extra)
+ }
+ return checkSimpleStruct(maskedString, maskedString, "keep")(&e.SimpleStruct)
+ },
+ },
+
+ // maps
+ {
+ name: "map_string_string",
+ object: map[string]string{
+ "key1": "value1",
+ "key2": "value2",
+ },
+ check: checkMapStringString(map[string]string{
+ "key1": maskedString,
+ "key2": maskedString,
+ }),
+ },
+
+ // slices
+ {
+ name: "slice_strings",
+ object: []string{"first", "second", "third"},
+ check: checkSliceStrings([]string{maskedString, maskedString, maskedString}),
+ },
+
+ // map of structs
+ {
+ name: "map_of_structs",
+ object: map[string]*SimpleStruct{
+ "struct1": {Name: "name1", Value: "value1", Skip: "skip1"},
+ "struct2": {Name: "name2", Value: "value2", Skip: "skip2"},
+ },
+ check: func(obj any) error {
+ m := obj.(map[string]*SimpleStruct)
+ if len(m) != 2 {
+ return fmt.Errorf("expected 2 structs, got %d", len(m))
+ }
+ if err := checkSimpleStruct(maskedString, maskedString, "skip1")(m["struct1"]); err != nil {
+ return fmt.Errorf("struct1: %w", err)
+ }
+ if err := checkSimpleStruct(maskedString, maskedString, "skip2")(m["struct2"]); err != nil {
+ return fmt.Errorf("struct2: %w", err)
+ }
+ return nil
+ },
+ },
+
+ // slice of structs
+ {
+ name: "slice_of_structs",
+ object: []*SimpleStruct{
+ {Name: "first", Value: "val1", Skip: "keep1"},
+ {Name: "second", Value: "val2", Skip: "keep2"},
+ },
+ check: func(obj any) error {
+ s := obj.([]*SimpleStruct)
+ if len(s) != 2 {
+ return fmt.Errorf("expected 2 structs, got %d", len(s))
+ }
+ if err := checkSimpleStruct(maskedString, maskedString, "keep1")(s[0]); err != nil {
+ return fmt.Errorf("index 0: %w", err)
+ }
+ if err := checkSimpleStruct(maskedString, maskedString, "keep2")(s[1]); err != nil {
+ return fmt.Errorf("index 1: %w", err)
+ }
+ return nil
+ },
+ },
+
+ // map of slice of structs
+ {
+ name: "map_of_slice_of_structs",
+ object: map[string][]*SimpleStruct{
+ "group1": {
+ {Name: "g1s1", Value: "v1", Skip: "k1"},
+ {Name: "g1s2", Value: "v2", Skip: "k2"},
+ },
+ },
+ check: func(obj any) error {
+ m := obj.(map[string][]*SimpleStruct)
+ group := m["group1"]
+ if len(group) != 2 {
+ return fmt.Errorf("expected 2 structs in group1, got %d", len(group))
+ }
+ if err := checkSimpleStruct(maskedString, maskedString, "k1")(group[0]); err != nil {
+ return fmt.Errorf("group1[0]: %w", err)
+ }
+ if err := checkSimpleStruct(maskedString, maskedString, "k2")(group[1]); err != nil {
+ return fmt.Errorf("group1[1]: %w", err)
+ }
+ return nil
+ },
+ },
+
+ // slice of maps of structs
+ {
+ name: "slice_of_maps_of_structs",
+ object: []map[string]*SimpleStruct{
+ {
+ "item1": {Name: "name1", Value: "value1", Skip: "skip1"},
+ },
+ {
+ "item2": {Name: "name2", Value: "value2", Skip: "skip2"},
+ },
+ },
+ check: func(obj any) error {
+ s := obj.([]map[string]*SimpleStruct)
+ if len(s) != 2 {
+ return fmt.Errorf("expected 2 maps, got %d", len(s))
+ }
+ if err := checkSimpleStruct(maskedString, maskedString, "skip1")(s[0]["item1"]); err != nil {
+ return fmt.Errorf("map[0][item1]: %w", err)
+ }
+ if err := checkSimpleStruct(maskedString, maskedString, "skip2")(s[1]["item2"]); err != nil {
+ return fmt.Errorf("map[1][item2]: %w", err)
+ }
+ return nil
+ },
+ },
+
+ // struct with slice of strings
+ {
+ name: "struct_with_slice_of_strings",
+ object: &struct {
+ Name string `json:"name"`
+ Items []string `json:"items"`
+ }{
+ Name: "container",
+ Items: []string{"item1", "item2", "item3"},
+ },
+ check: func(obj any) error {
+ s := obj.(*struct {
+ Name string `json:"name"`
+ Items []string `json:"items"`
+ })
+ if s.Name != maskedString {
+ return fmt.Errorf("Name: expected %q, got %q", maskedString, s.Name)
+ }
+ return checkSliceStrings([]string{maskedString, maskedString, maskedString})(s.Items)
+ },
+ },
+
+ // struct with slice of maps of strings
+ {
+ name: "struct_with_slice_of_maps_of_strings",
+ object: &struct {
+ Name string `json:"name"`
+ Data []map[string]string `json:"data"`
+ }{
+ Name: "complex",
+ Data: []map[string]string{
+ {"key1": "val1", "key2": "val2"},
+ {"key3": "val3", "key4": "val4"},
+ },
+ },
+ check: func(obj any) error {
+ s := obj.(*struct {
+ Name string `json:"name"`
+ Data []map[string]string `json:"data"`
+ })
+ if s.Name != maskedString {
+ return fmt.Errorf("Name: expected %q, got %q", maskedString, s.Name)
+ }
+ if len(s.Data) != 2 {
+ return fmt.Errorf("expected 2 maps, got %d", len(s.Data))
+ }
+ expectedMap1 := map[string]string{"key1": maskedString, "key2": maskedString}
+ expectedMap2 := map[string]string{"key3": maskedString, "key4": maskedString}
+ if err := checkMapStringString(expectedMap1)(s.Data[0]); err != nil {
+ return fmt.Errorf("Data[0]: %w", err)
+ }
+ if err := checkMapStringString(expectedMap2)(s.Data[1]); err != nil {
+ return fmt.Errorf("Data[1]: %w", err)
+ }
+ return nil
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ err := anonymizer.Anonymize(tc.object)
+ if err != nil {
+ t.Fatalf("failed to anonymize: %v", err)
+ }
+
+ if err := tc.check(tc.object); err != nil {
+ t.Errorf("check failed: %v", err)
+ }
+ })
+ }
+}
+
+// edge cases tests
+func TestAnonymizeEdgeCases(t *testing.T) {
+ anonymizer := createMockAnonymizer()
+
+ edgeCases := []testCase{
+ // map[string]any with mixed types (mutable and immutable)
+ {
+ name: "map_string_any_mixed_types",
+ object: map[string]any{
+ "string": "test_string",
+ "int": 42,
+ "struct": SimpleStruct{Name: "embedded", Value: "data", Skip: "keep"},
+ "ptr_struct": &SimpleStruct{Name: "pointer", Value: "ptr_data", Skip: "preserve"},
+ "slice": []string{"item1", "item2"},
+ "map": map[string]string{"nested_key": "nested_value"},
+ "empty": "",
+ "nil": nil,
+ },
+ check: func(obj any) error {
+ m := obj.(map[string]any)
+
+ // string should be masked
+ if str, ok := m["string"].(string); !ok || str != maskedString {
+ return fmt.Errorf("string: expected %q, got %v", maskedString, m["string"])
+ }
+
+ // int should remain unchanged
+ if val, ok := m["int"].(int); !ok || val != 42 {
+ return fmt.Errorf("int: expected 42, got %v", m["int"])
+ }
+
+ // struct should be anonymized (note: Go will convert to pointer internally)
+ if s, ok := m["struct"].(SimpleStruct); ok {
+ if s.Name != maskedString || s.Value != maskedString || s.Skip != "keep" {
+ return fmt.Errorf("struct: expected masked fields, got %+v", s)
+ }
+ } else {
+ return fmt.Errorf("struct: expected SimpleStruct, got %T", m["struct"])
+ }
+
+ // ptr_struct should be anonymized
+ if s, ok := m["ptr_struct"].(*SimpleStruct); ok {
+ if s.Name != maskedString || s.Value != maskedString || s.Skip != "preserve" {
+ return fmt.Errorf("ptr_struct: expected masked fields, got %+v", s)
+ }
+ } else {
+ return fmt.Errorf("ptr_struct: expected *SimpleStruct, got %T", m["ptr_struct"])
+ }
+
+ // slice should be anonymized
+ if slice, ok := m["slice"].([]string); ok {
+ expected := []string{maskedString, maskedString}
+ if err := checkSliceStrings(expected)(slice); err != nil {
+ return fmt.Errorf("slice: %w", err)
+ }
+ } else {
+ return fmt.Errorf("slice: expected []string, got %T", m["slice"])
+ }
+
+ // nested map should be anonymized
+ if nestedMap, ok := m["map"].(map[string]string); ok {
+ expected := map[string]string{"nested_key": maskedString}
+ if err := checkMapStringString(expected)(nestedMap); err != nil {
+ return fmt.Errorf("map: %w", err)
+ }
+ } else {
+ return fmt.Errorf("map: expected map[string]string, got %T", m["map"])
+ }
+
+ // empty string should remain empty
+ if str, ok := m["empty"].(string); !ok || str != "" {
+ return fmt.Errorf("empty: expected empty string, got %v", m["empty"])
+ }
+
+ // nil should remain nil
+ if m["nil"] != nil {
+ return fmt.Errorf("nil: expected nil, got %v", m["nil"])
+ }
+
+ return nil
+ },
+ },
+
+ // deeply nested structures
+ {
+ name: "deeply_nested_structures",
+ object: &struct {
+ Level1 *struct {
+ Level2 *struct {
+ Level3 *struct {
+ Level4 string `json:"deep_value"`
+ } `json:"level3"`
+ } `json:"level2"`
+ } `json:"level1"`
+ }{
+ Level1: &struct {
+ Level2 *struct {
+ Level3 *struct {
+ Level4 string `json:"deep_value"`
+ } `json:"level3"`
+ } `json:"level2"`
+ }{
+ Level2: &struct {
+ Level3 *struct {
+ Level4 string `json:"deep_value"`
+ } `json:"level3"`
+ }{
+ Level3: &struct {
+ Level4 string `json:"deep_value"`
+ }{
+ Level4: "deeply_nested_string",
+ },
+ },
+ },
+ },
+ check: func(obj any) error {
+ s := obj.(*struct {
+ Level1 *struct {
+ Level2 *struct {
+ Level3 *struct {
+ Level4 string `json:"deep_value"`
+ } `json:"level3"`
+ } `json:"level2"`
+ } `json:"level1"`
+ })
+
+ if s.Level1 == nil || s.Level1.Level2 == nil || s.Level1.Level2.Level3 == nil {
+ return fmt.Errorf("nested structure is nil")
+ }
+
+ if s.Level1.Level2.Level3.Level4 != maskedString {
+ return fmt.Errorf("deeply nested string: expected %q, got %q", maskedString, s.Level1.Level2.Level3.Level4)
+ }
+
+ return nil
+ },
+ },
+
+ // any containing various types
+ {
+ name: "interface_containing_various_types",
+ object: &struct {
+ Data any `json:"data"`
+ }{
+ Data: map[string]any{
+ "nested_struct": &SimpleStruct{Name: "interface_test", Value: "test_value", Skip: "keep"},
+ "nested_slice": []string{"a", "b", "c"},
+ },
+ },
+ check: func(obj any) error {
+ s := obj.(*struct {
+ Data any `json:"data"`
+ })
+
+ dataMap, ok := s.Data.(map[string]any)
+ if !ok {
+ return fmt.Errorf("Data is not map[string]any: %T", s.Data)
+ }
+
+ // check nested struct
+ if nestedStruct, ok := dataMap["nested_struct"].(*SimpleStruct); ok {
+ if nestedStruct.Name != maskedString || nestedStruct.Value != maskedString || nestedStruct.Skip != "keep" {
+ return fmt.Errorf("nested_struct: expected masked fields, got %+v", nestedStruct)
+ }
+ } else {
+ return fmt.Errorf("nested_struct: expected *SimpleStruct, got %T", dataMap["nested_struct"])
+ }
+
+ // check nested slice
+ if nestedSlice, ok := dataMap["nested_slice"].([]string); ok {
+ expected := []string{maskedString, maskedString, maskedString}
+ if err := checkSliceStrings(expected)(nestedSlice); err != nil {
+ return fmt.Errorf("nested_slice: %w", err)
+ }
+ } else {
+ return fmt.Errorf("nested_slice: expected []string, got %T", dataMap["nested_slice"])
+ }
+
+ return nil
+ },
+ },
+
+ // empty collections
+ {
+ name: "empty_collections",
+ object: &struct {
+ EmptyMap map[string]string `json:"empty_map"`
+ EmptySlice []string `json:"empty_slice"`
+ NilMap map[string]string `json:"nil_map"`
+ NilSlice []string `json:"nil_slice"`
+ }{
+ EmptyMap: make(map[string]string),
+ EmptySlice: make([]string, 0),
+ NilMap: nil,
+ NilSlice: nil,
+ },
+ check: func(obj any) error {
+ s := obj.(*struct {
+ EmptyMap map[string]string `json:"empty_map"`
+ EmptySlice []string `json:"empty_slice"`
+ NilMap map[string]string `json:"nil_map"`
+ NilSlice []string `json:"nil_slice"`
+ })
+
+ if s.EmptyMap == nil || len(s.EmptyMap) != 0 {
+ return fmt.Errorf("EmptyMap should be empty, got %v", s.EmptyMap)
+ }
+ if s.EmptySlice == nil || len(s.EmptySlice) != 0 {
+ return fmt.Errorf("EmptySlice should be empty, got %v", s.EmptySlice)
+ }
+ if s.NilMap != nil {
+ return fmt.Errorf("NilMap should be nil, got %v", s.NilMap)
+ }
+ if s.NilSlice != nil {
+ return fmt.Errorf("NilSlice should be nil, got %v", s.NilSlice)
+ }
+
+ return nil
+ },
+ },
+
+ // circular reference prevention (using pointers)
+ {
+ name: "complex_pointer_structures",
+ object: &struct {
+ Self *struct {
+ Name string `json:"name"`
+ Other *struct {
+ Value string `json:"value"`
+ } `json:"other"`
+ } `json:"self"`
+ }{
+ Self: &struct {
+ Name string `json:"name"`
+ Other *struct {
+ Value string `json:"value"`
+ } `json:"other"`
+ }{
+ Name: "self_reference",
+ Other: &struct {
+ Value string `json:"value"`
+ }{
+ Value: "other_value",
+ },
+ },
+ },
+ check: func(obj any) error {
+ s := obj.(*struct {
+ Self *struct {
+ Name string `json:"name"`
+ Other *struct {
+ Value string `json:"value"`
+ } `json:"other"`
+ } `json:"self"`
+ })
+
+ if s.Self.Name != maskedString {
+ return fmt.Errorf("Self.Name: expected %q, got %q", maskedString, s.Self.Name)
+ }
+ if s.Self.Other.Value != maskedString {
+ return fmt.Errorf("Self.Other.Value: expected %q, got %q", maskedString, s.Self.Other.Value)
+ }
+
+ return nil
+ },
+ },
+ }
+
+ for _, tc := range edgeCases {
+ t.Run(tc.name, func(t *testing.T) {
+ err := anonymizer.Anonymize(tc.object)
+ if err != nil {
+ t.Fatalf("failed to anonymize: %v", err)
+ }
+
+ if err := tc.check(tc.object); err != nil {
+ t.Errorf("check failed: %v", err)
+ }
+ })
+ }
+}
+
+// error cases tests
+func TestAnonymizeErrorCases(t *testing.T) {
+ anonymizer := createMockAnonymizer()
+
+ errorCases := []struct {
+ name string
+ object any
+ expectedErr error
+ }{
+ {
+ name: "immutable_string",
+ object: "immutable string",
+ expectedErr: ErrObjectImmutable,
+ },
+ {
+ name: "immutable_int",
+ object: 42,
+ expectedErr: ErrObjectImmutable,
+ },
+ {
+ name: "immutable_struct_value",
+ object: SimpleStruct{Name: "test", Value: "value", Skip: "keep"},
+ expectedErr: ErrObjectImmutable,
+ },
+ {
+ name: "nil_pointer",
+ object: (*SimpleStruct)(nil),
+ expectedErr: ErrObjectImmutable,
+ },
+ }
+
+ for _, tc := range errorCases {
+ t.Run(tc.name, func(t *testing.T) {
+ err := anonymizer.Anonymize(tc.object)
+ if err != tc.expectedErr {
+ t.Errorf("expected error %v, got %v", tc.expectedErr, err)
+ }
+ })
+ }
+}
+
+// critical edge cases tests for pointer handling and special types
+func TestAnonymizeCriticalEdgeCases(t *testing.T) {
+ anonymizer := createMockAnonymizer()
+
+ t.Run("pointer_handling", func(t *testing.T) {
+ // create test data with various pointer types
+ stringVal := "test_string"
+ intVal := 42
+ skipVal := "skip_value"
+
+ originalStruct := &PointerTestStruct{
+ StringPtr: &stringVal,
+ IntPtr: &intVal,
+ NilStringPtr: nil,
+ NilIntPtr: nil,
+ StructPtr: &SimpleStruct{Name: "struct_name", Value: "struct_value", Skip: "struct_skip"},
+ NilStructPtr: nil,
+ Skip: &skipVal,
+ }
+
+ // store original pointer addresses for identity checks
+ origStringPtr := originalStruct.StringPtr
+ origIntPtr := originalStruct.IntPtr
+ origStructPtr := originalStruct.StructPtr
+ origSkipPtr := originalStruct.Skip
+
+ err := anonymizer.Anonymize(originalStruct)
+ if err != nil {
+ t.Fatalf("failed to anonymize: %v", err)
+ }
+
+ // check that string pointer value is anonymized but pointer identity is preserved
+ if err := checkStringPointer(originalStruct.StringPtr, maskedString, "StringPtr"); err != nil {
+ t.Errorf("StringPtr check failed: %v", err)
+ }
+ if err := checkPointerIdentity(origStringPtr, originalStruct.StringPtr, "StringPtr"); err != nil {
+ t.Errorf("StringPtr identity check failed: %v", err)
+ }
+
+ // check that int pointer remains unchanged (both value and identity)
+ if originalStruct.IntPtr == nil || *originalStruct.IntPtr != 42 {
+ t.Errorf("IntPtr value changed: expected 42, got %v", originalStruct.IntPtr)
+ }
+ if err := checkPointerIdentity(origIntPtr, originalStruct.IntPtr, "IntPtr"); err != nil {
+ t.Errorf("IntPtr identity check failed: %v", err)
+ }
+
+ // check that nil pointers remain nil
+ if err := checkNilPointer(originalStruct.NilStringPtr, "NilStringPtr"); err != nil {
+ t.Errorf("NilStringPtr check failed: %v", err)
+ }
+ if err := checkNilPointer(originalStruct.NilIntPtr, "NilIntPtr"); err != nil {
+ t.Errorf("NilIntPtr check failed: %v", err)
+ }
+ if err := checkNilPointer(originalStruct.NilStructPtr, "NilStructPtr"); err != nil {
+ t.Errorf("NilStructPtr check failed: %v", err)
+ }
+
+ // check that struct pointer is anonymized but identity is preserved
+ if originalStruct.StructPtr.Name != maskedString || originalStruct.StructPtr.Value != maskedString || originalStruct.StructPtr.Skip != "struct_skip" {
+ t.Errorf("StructPtr values incorrect: %+v", originalStruct.StructPtr)
+ }
+ if err := checkPointerIdentity(origStructPtr, originalStruct.StructPtr, "StructPtr"); err != nil {
+ t.Errorf("StructPtr identity check failed: %v", err)
+ }
+
+ // check that skip field pointer remains unchanged (both value and identity)
+ if err := checkStringPointer(originalStruct.Skip, "skip_value", "Skip"); err != nil {
+ t.Errorf("Skip check failed: %v", err)
+ }
+ if err := checkPointerIdentity(origSkipPtr, originalStruct.Skip, "Skip"); err != nil {
+ t.Errorf("Skip identity check failed: %v", err)
+ }
+ })
+
+ t.Run("special_types_preservation", func(t *testing.T) {
+ // create struct with special types that should not be modified
+ originalStruct := &SpecialTypesStruct{
+ String: "test_string",
+ Skip: "skip_value",
+ }
+
+ // set some values in atomic types
+ originalStruct.AtomicInt.Store(123)
+ originalStruct.AtomicBool.Store(true)
+
+ // store original addresses for identity checks
+ origMutex := &originalStruct.Mutex
+ origRWMutex := &originalStruct.RWMutex
+ origAtomicInt := &originalStruct.AtomicInt
+ origAtomicBool := &originalStruct.AtomicBool
+
+ err := anonymizer.Anonymize(originalStruct)
+ if err != nil {
+ t.Fatalf("failed to anonymize: %v", err)
+ }
+
+ // check that string is anonymized
+ if originalStruct.String != maskedString {
+ t.Errorf("String not anonymized: expected %q, got %q", maskedString, originalStruct.String)
+ }
+
+ // check that skip field is not changed
+ if originalStruct.Skip != "skip_value" {
+ t.Errorf("Skip field changed: expected %q, got %q", "skip_value", originalStruct.Skip)
+ }
+
+ // check that special types are not modified (identity preserved)
+ if err := checkPointerIdentity(origMutex, &originalStruct.Mutex, "Mutex"); err != nil {
+ t.Errorf("Mutex identity check failed: %v", err)
+ }
+ if err := checkPointerIdentity(origRWMutex, &originalStruct.RWMutex, "RWMutex"); err != nil {
+ t.Errorf("RWMutex identity check failed: %v", err)
+ }
+ if err := checkPointerIdentity(origAtomicInt, &originalStruct.AtomicInt, "AtomicInt"); err != nil {
+ t.Errorf("AtomicInt identity check failed: %v", err)
+ }
+ if err := checkPointerIdentity(origAtomicBool, &originalStruct.AtomicBool, "AtomicBool"); err != nil {
+ t.Errorf("AtomicBool identity check failed: %v", err)
+ }
+
+ // check that atomic values are preserved
+ if originalStruct.AtomicInt.Load() != 123 {
+ t.Errorf("AtomicInt value changed: expected 123, got %d", originalStruct.AtomicInt.Load())
+ }
+ if !originalStruct.AtomicBool.Load() {
+ t.Errorf("AtomicBool value changed: expected true, got false")
+ }
+ })
+
+ t.Run("deep_pointer_structures", func(t *testing.T) {
+ level2String := "level2_string"
+ level1String := "level1_string"
+
+ originalStruct := &DeepPointerStruct{
+ Level1: &struct {
+ StringPtr *string `json:"string_ptr"`
+ Level2 *struct {
+ StringPtr *string `json:"string_ptr"`
+ NilPtr *string `json:"nil_ptr"`
+ } `json:"level2"`
+ }{
+ StringPtr: &level1String,
+ Level2: &struct {
+ StringPtr *string `json:"string_ptr"`
+ NilPtr *string `json:"nil_ptr"`
+ }{
+ StringPtr: &level2String,
+ NilPtr: nil,
+ },
+ },
+ }
+
+ // store original pointer addresses
+ origLevel1Ptr := originalStruct.Level1
+ origLevel1StringPtr := originalStruct.Level1.StringPtr
+ origLevel2Ptr := originalStruct.Level1.Level2
+ origLevel2StringPtr := originalStruct.Level1.Level2.StringPtr
+
+ err := anonymizer.Anonymize(originalStruct)
+ if err != nil {
+ t.Fatalf("failed to anonymize: %v", err)
+ }
+
+ // check that structure pointers are preserved
+ if err := checkPointerIdentity(origLevel1Ptr, originalStruct.Level1, "Level1"); err != nil {
+ t.Errorf("Level1 identity check failed: %v", err)
+ }
+ if err := checkPointerIdentity(origLevel2Ptr, originalStruct.Level1.Level2, "Level2"); err != nil {
+ t.Errorf("Level2 identity check failed: %v", err)
+ }
+
+ // check that string pointers are preserved but values are anonymized
+ if err := checkPointerIdentity(origLevel1StringPtr, originalStruct.Level1.StringPtr, "Level1.StringPtr"); err != nil {
+ t.Errorf("Level1.StringPtr identity check failed: %v", err)
+ }
+ if err := checkStringPointer(originalStruct.Level1.StringPtr, maskedString, "Level1.StringPtr"); err != nil {
+ t.Errorf("Level1.StringPtr value check failed: %v", err)
+ }
+
+ if err := checkPointerIdentity(origLevel2StringPtr, originalStruct.Level1.Level2.StringPtr, "Level2.StringPtr"); err != nil {
+ t.Errorf("Level2.StringPtr identity check failed: %v", err)
+ }
+ if err := checkStringPointer(originalStruct.Level1.Level2.StringPtr, maskedString, "Level2.StringPtr"); err != nil {
+ t.Errorf("Level2.StringPtr value check failed: %v", err)
+ }
+
+ // check that nil pointer remains nil
+ if err := checkNilPointer(originalStruct.Level1.Level2.NilPtr, "Level2.NilPtr"); err != nil {
+ t.Errorf("Level2.NilPtr check failed: %v", err)
+ }
+ })
+
+ t.Run("map_with_pointers", func(t *testing.T) {
+ stringVal1 := "string1"
+ intVal := 42
+
+ originalMap := map[string]any{
+ "string_ptr": &stringVal1,
+ "nil_string_ptr": (*string)(nil),
+ "int_ptr": &intVal,
+ "nil_int_ptr": (*int)(nil),
+ "struct_ptr": &SimpleStruct{Name: "test", Value: "value", Skip: "skip"},
+ "nil_struct_ptr": (*SimpleStruct)(nil),
+ }
+
+ // store original pointer addresses
+ origStringPtr := originalMap["string_ptr"].(*string)
+ origIntPtr := originalMap["int_ptr"].(*int)
+ origStructPtr := originalMap["struct_ptr"].(*SimpleStruct)
+
+ err := anonymizer.Anonymize(originalMap)
+ if err != nil {
+ t.Fatalf("failed to anonymize: %v", err)
+ }
+
+ // check string pointer
+ if currentPtr, ok := originalMap["string_ptr"].(*string); ok {
+ if err := checkPointerIdentity(origStringPtr, currentPtr, "string_ptr"); err != nil {
+ t.Errorf("string_ptr identity check failed: %v", err)
+ }
+ if err := checkStringPointer(currentPtr, maskedString, "string_ptr"); err != nil {
+ t.Errorf("string_ptr value check failed: %v", err)
+ }
+ } else {
+ t.Errorf("string_ptr type changed")
+ }
+
+ // check int pointer
+ if currentPtr, ok := originalMap["int_ptr"].(*int); ok {
+ if err := checkPointerIdentity(origIntPtr, currentPtr, "int_ptr"); err != nil {
+ t.Errorf("int_ptr identity check failed: %v", err)
+ }
+ if *currentPtr != 42 {
+ t.Errorf("int_ptr value changed: expected 42, got %d", *currentPtr)
+ }
+ } else {
+ t.Errorf("int_ptr type changed")
+ }
+
+ // check struct pointer
+ if currentPtr, ok := originalMap["struct_ptr"].(*SimpleStruct); ok {
+ if err := checkPointerIdentity(origStructPtr, currentPtr, "struct_ptr"); err != nil {
+ t.Errorf("struct_ptr identity check failed: %v", err)
+ }
+ if currentPtr.Name != maskedString || currentPtr.Value != maskedString || currentPtr.Skip != "skip" {
+ t.Errorf("struct_ptr values incorrect: %+v", currentPtr)
+ }
+ } else {
+ t.Errorf("struct_ptr type changed")
+ }
+
+ // check nil pointers
+ if err := checkNilPointer(originalMap["nil_string_ptr"], "nil_string_ptr"); err != nil {
+ t.Errorf("nil_string_ptr check failed: %v", err)
+ }
+ if err := checkNilPointer(originalMap["nil_int_ptr"], "nil_int_ptr"); err != nil {
+ t.Errorf("nil_int_ptr check failed: %v", err)
+ }
+ if err := checkNilPointer(originalMap["nil_struct_ptr"], "nil_struct_ptr"); err != nil {
+ t.Errorf("nil_struct_ptr check failed: %v", err)
+ }
+ })
+
+ t.Run("slice_with_pointers", func(t *testing.T) {
+ stringVal1 := "slice_string1"
+ stringVal2 := "slice_string2"
+ intVal := 99
+
+ originalSlice := []*string{&stringVal1, nil, &stringVal2}
+ originalIntSlice := []*int{&intVal, nil}
+
+ // store original addresses
+ origPtr1 := originalSlice[0]
+ origPtr3 := originalSlice[2]
+ origIntPtr := originalIntSlice[0]
+
+ err := anonymizer.Anonymize(originalSlice)
+ if err != nil {
+ t.Fatalf("failed to anonymize string slice: %v", err)
+ }
+
+ err = anonymizer.Anonymize(originalIntSlice)
+ if err != nil {
+ t.Fatalf("failed to anonymize int slice: %v", err)
+ }
+
+ // check string pointers
+ if err := checkPointerIdentity(origPtr1, originalSlice[0], "slice[0]"); err != nil {
+ t.Errorf("slice[0] identity check failed: %v", err)
+ }
+ if err := checkStringPointer(originalSlice[0], maskedString, "slice[0]"); err != nil {
+ t.Errorf("slice[0] value check failed: %v", err)
+ }
+
+ if err := checkNilPointer(originalSlice[1], "slice[1]"); err != nil {
+ t.Errorf("slice[1] nil check failed: %v", err)
+ }
+
+ if err := checkPointerIdentity(origPtr3, originalSlice[2], "slice[2]"); err != nil {
+ t.Errorf("slice[2] identity check failed: %v", err)
+ }
+ if err := checkStringPointer(originalSlice[2], maskedString, "slice[2]"); err != nil {
+ t.Errorf("slice[2] value check failed: %v", err)
+ }
+
+ // check int pointers (should remain unchanged)
+ if err := checkPointerIdentity(origIntPtr, originalIntSlice[0], "intSlice[0]"); err != nil {
+ t.Errorf("intSlice[0] identity check failed: %v", err)
+ }
+ if originalIntSlice[0] == nil || *originalIntSlice[0] != 99 {
+ t.Errorf("intSlice[0] value changed: expected 99, got %v", originalIntSlice[0])
+ }
+
+ if err := checkNilPointer(originalIntSlice[1], "intSlice[1]"); err != nil {
+ t.Errorf("intSlice[1] nil check failed: %v", err)
+ }
+ })
+
+ t.Run("complex_nested_with_skip_tags", func(t *testing.T) {
+ // test complex structure with skip tags at various levels
+ type NestedWithSkips struct {
+ ProcessData string `json:"process_data"`
+ SkipField string `json:"skip_field" anonymizer:"skip"`
+ Inner *struct {
+ Value string `json:"value"`
+ SkipInner string `json:"skip_inner" anonymizer:"skip"`
+ } `json:"inner"`
+ }
+
+ innerValue := "inner_value"
+ innerSkip := "inner_skip"
+
+ originalStruct := &NestedWithSkips{
+ ProcessData: "process_this",
+ SkipField: "dont_process",
+ Inner: &struct {
+ Value string `json:"value"`
+ SkipInner string `json:"skip_inner" anonymizer:"skip"`
+ }{
+ Value: innerValue,
+ SkipInner: innerSkip,
+ },
+ }
+
+ // store original pointer
+ origInnerPtr := originalStruct.Inner
+
+ err := anonymizer.Anonymize(originalStruct)
+ if err != nil {
+ t.Fatalf("failed to anonymize: %v", err)
+ }
+
+ // check that processed fields are anonymized
+ if originalStruct.ProcessData != maskedString {
+ t.Errorf("ProcessData not anonymized: expected %q, got %q", maskedString, originalStruct.ProcessData)
+ }
+
+ // check that skip fields are not changed
+ if originalStruct.SkipField != "dont_process" {
+ t.Errorf("SkipField changed: expected %q, got %q", "dont_process", originalStruct.SkipField)
+ }
+
+ // check inner structure pointer identity
+ if err := checkPointerIdentity(origInnerPtr, originalStruct.Inner, "Inner"); err != nil {
+ t.Errorf("Inner pointer identity check failed: %v", err)
+ }
+
+ // check inner values
+ if originalStruct.Inner.Value != maskedString {
+ t.Errorf("Inner.Value not anonymized: expected %q, got %q", maskedString, originalStruct.Inner.Value)
+ }
+ if originalStruct.Inner.SkipInner != "inner_skip" {
+ t.Errorf("Inner.SkipInner changed: expected %q, got %q", "inner_skip", originalStruct.Inner.SkipInner)
+ }
+ })
+}
+
+// performance and memory tests
+func TestAnonymizeLargeStructures(t *testing.T) {
+ anonymizer := createMockAnonymizer()
+
+ // create large nested structure
+ largeMap := make(map[string]string)
+ for i := range 1000 {
+ largeMap[fmt.Sprintf("key_%d", i)] = fmt.Sprintf("value_%d", i)
+ }
+
+ largeSlice := make([]string, 1000)
+ for i := range 1000 {
+ largeSlice[i] = fmt.Sprintf("item_%d", i)
+ }
+
+ largeStruct := &struct {
+ LargeMap map[string]string `json:"large_map"`
+ LargeSlice []string `json:"large_slice"`
+ }{
+ LargeMap: largeMap,
+ LargeSlice: largeSlice,
+ }
+
+ err := anonymizer.Anonymize(largeStruct)
+ if err != nil {
+ t.Fatalf("failed to anonymize large structure: %v", err)
+ }
+
+ // verify all strings were masked
+ for _, v := range largeStruct.LargeMap {
+ if v != maskedString {
+ t.Errorf("map value not masked: %s", v)
+ break
+ }
+ }
+
+ for _, v := range largeStruct.LargeSlice {
+ if v != maskedString {
+ t.Errorf("slice value not masked: %s", v)
+ break
+ }
+ }
+}
+
+// additional comprehensive edge cases
+func TestAnonymizeAdditionalEdgeCases(t *testing.T) {
+ anonymizer := createMockAnonymizer()
+
+ t.Run("channels_and_functions", func(t *testing.T) {
+ // test that channels and functions are not modified
+ type ChannelStruct struct {
+ StringChan chan string `json:"string_chan"`
+ IntChan chan int `json:"int_chan"`
+ Func func() string `json:"func"`
+ String string `json:"string"`
+ }
+
+ originalStruct := &ChannelStruct{
+ StringChan: make(chan string, 1),
+ IntChan: make(chan int, 1),
+ Func: func() string { return "test" },
+ String: "test_string",
+ }
+
+ // store original addresses
+ origStringChan := originalStruct.StringChan
+ origIntChan := originalStruct.IntChan
+ origFunc := fmt.Sprintf("%p", originalStruct.Func)
+
+ err := anonymizer.Anonymize(originalStruct)
+ if err != nil {
+ t.Fatalf("failed to anonymize: %v", err)
+ }
+
+ // check that channels and functions are not modified
+ if err := checkPointerIdentity(origStringChan, originalStruct.StringChan, "StringChan"); err != nil {
+ t.Errorf("StringChan identity check failed: %v", err)
+ }
+ if err := checkPointerIdentity(origIntChan, originalStruct.IntChan, "IntChan"); err != nil {
+ t.Errorf("IntChan identity check failed: %v", err)
+ }
+
+ currentFunc := fmt.Sprintf("%p", originalStruct.Func)
+ if origFunc != currentFunc {
+ t.Errorf("Function pointer changed: original %s, current %s", origFunc, currentFunc)
+ }
+
+ // check that string is anonymized
+ if originalStruct.String != maskedString {
+ t.Errorf("String not anonymized: expected %q, got %q", maskedString, originalStruct.String)
+ }
+
+ // verify function still works
+ if originalStruct.Func() != "test" {
+ t.Errorf("Function behavior changed")
+ }
+ })
+
+ t.Run("interface_with_nil_and_pointers", func(t *testing.T) {
+ stringVal := "interface_string"
+ intVal := 42
+
+ originalMap := map[string]any{
+ "nil_interface": nil,
+ "string_interface": "direct_string",
+ "ptr_interface": &stringVal,
+ "int_interface": intVal,
+ "int_ptr_interface": &intVal,
+ }
+
+ // store original pointer
+ origStringPtr := originalMap["ptr_interface"].(*string)
+ origIntPtr := originalMap["int_ptr_interface"].(*int)
+
+ err := anonymizer.Anonymize(originalMap)
+ if err != nil {
+ t.Fatalf("failed to anonymize: %v", err)
+ }
+
+ // check nil interface
+ if originalMap["nil_interface"] != nil {
+ t.Errorf("nil_interface should remain nil, got %v", originalMap["nil_interface"])
+ }
+
+ // check string interface
+ if str, ok := originalMap["string_interface"].(string); !ok || str != maskedString {
+ t.Errorf("string_interface: expected %q, got %v", maskedString, originalMap["string_interface"])
+ }
+
+ // check pointer interface (should preserve pointer identity)
+ if currentPtr, ok := originalMap["ptr_interface"].(*string); ok {
+ if err := checkPointerIdentity(origStringPtr, currentPtr, "ptr_interface"); err != nil {
+ t.Errorf("ptr_interface identity check failed: %v", err)
+ }
+ if err := checkStringPointer(currentPtr, maskedString, "ptr_interface"); err != nil {
+ t.Errorf("ptr_interface value check failed: %v", err)
+ }
+ } else {
+ t.Errorf("ptr_interface type changed")
+ }
+
+ // check int interface (should remain unchanged)
+ if val, ok := originalMap["int_interface"].(int); !ok || val != 42 {
+ t.Errorf("int_interface changed: expected 42, got %v", originalMap["int_interface"])
+ }
+
+ // check int pointer interface (should preserve pointer identity and value)
+ if currentPtr, ok := originalMap["int_ptr_interface"].(*int); ok {
+ if err := checkPointerIdentity(origIntPtr, currentPtr, "int_ptr_interface"); err != nil {
+ t.Errorf("int_ptr_interface identity check failed: %v", err)
+ }
+ if *currentPtr != 42 {
+ t.Errorf("int_ptr_interface value changed: expected 42, got %d", *currentPtr)
+ }
+ } else {
+ t.Errorf("int_ptr_interface type changed")
+ }
+ })
+
+ t.Run("tree_structures_without_cycles", func(t *testing.T) {
+ // test tree-like structures without circular references
+ type Node struct {
+ Value string `json:"value"`
+ Skip string `json:"skip" anonymizer:"skip"`
+ Children []*Node `json:"children"`
+ }
+
+ // create tree structure without parent references to avoid cycles
+ child1 := &Node{Value: "child1", Skip: "child1_skip"}
+ child2 := &Node{Value: "child2", Skip: "child2_skip"}
+ root := &Node{
+ Value: "root",
+ Skip: "root_skip",
+ Children: []*Node{child1, child2},
+ }
+
+ // store original pointers
+ origRoot := root
+ origChild1 := child1
+ origChild2 := child2
+
+ err := anonymizer.Anonymize(root)
+ if err != nil {
+ t.Fatalf("failed to anonymize: %v", err)
+ }
+
+ // check that structure pointers are preserved
+ if err := checkPointerIdentity(origRoot, root, "root"); err != nil {
+ t.Errorf("root identity check failed: %v", err)
+ }
+ if err := checkPointerIdentity(origChild1, root.Children[0], "child1"); err != nil {
+ t.Errorf("child1 identity check failed: %v", err)
+ }
+ if err := checkPointerIdentity(origChild2, root.Children[1], "child2"); err != nil {
+ t.Errorf("child2 identity check failed: %v", err)
+ }
+
+ // check that values are anonymized
+ if root.Value != maskedString {
+ t.Errorf("root.Value not anonymized: expected %q, got %q", maskedString, root.Value)
+ }
+ if child1.Value != maskedString {
+ t.Errorf("child1.Value not anonymized: expected %q, got %q", maskedString, child1.Value)
+ }
+ if child2.Value != maskedString {
+ t.Errorf("child2.Value not anonymized: expected %q, got %q", maskedString, child2.Value)
+ }
+
+ // check that skip fields are preserved
+ if root.Skip != "root_skip" {
+ t.Errorf("root.Skip changed: expected %q, got %q", "root_skip", root.Skip)
+ }
+ if child1.Skip != "child1_skip" {
+ t.Errorf("child1.Skip changed: expected %q, got %q", "child1_skip", child1.Skip)
+ }
+ if child2.Skip != "child2_skip" {
+ t.Errorf("child2.Skip changed: expected %q, got %q", "child2_skip", child2.Skip)
+ }
+ })
+
+ t.Run("circular_reference_handling", func(t *testing.T) {
+ // test that circular references are handled correctly
+ type CircularNode struct {
+ Value string `json:"value"`
+ Next *CircularNode `json:"next"`
+ }
+
+ // create circular structure
+ node1 := &CircularNode{Value: "node1"}
+ node2 := &CircularNode{Value: "node2"}
+ node1.Next = node2
+ node2.Next = node1 // circular reference
+
+ // store original pointers
+ origNode1 := node1
+ origNode2 := node2
+
+ err := anonymizer.Anonymize(node1)
+ if err != nil {
+ t.Fatalf("failed to anonymize circular structure: %v", err)
+ }
+
+ // check that pointers are preserved
+ if err := checkPointerIdentity(origNode1, node1, "node1"); err != nil {
+ t.Errorf("node1 identity check failed: %v", err)
+ }
+ if err := checkPointerIdentity(origNode2, node1.Next, "node2"); err != nil {
+ t.Errorf("node2 identity check failed: %v", err)
+ }
+
+ // check that values are anonymized
+ if node1.Value != maskedString {
+ t.Errorf("node1.Value not anonymized: expected %q, got %q", maskedString, node1.Value)
+ }
+ if node2.Value != maskedString {
+ t.Errorf("node2.Value not anonymized: expected %q, got %q", maskedString, node2.Value)
+ }
+
+ // check that circular reference is preserved
+ if node1.Next != node2 || node2.Next != node1 {
+ t.Errorf("circular reference broken")
+ }
+ })
+}
diff --git a/anonymizer/doc.go b/anonymizer/doc.go
new file mode 100644
index 0000000..6b769ec
--- /dev/null
+++ b/anonymizer/doc.go
@@ -0,0 +1,259 @@
+// Package anonymizer provides comprehensive PII and secrets masking for secure data transmission.
+//
+// The anonymizer package implements a high-performance data anonymization engine designed
+// for protecting sensitive information before transmission to AI services or external systems.
+// It features 300+ built-in patterns for automatic recognition and masking of credentials,
+// personal information, network data, and cloud secrets while preserving data structure
+// and analytical value.
+//
+// # Core Interface
+//
+// The main Anonymizer interface provides multiple ways to anonymize data:
+//
+// type Anonymizer interface {
+// Anonymize(any) error // Deep anonymization of Go structures
+// ReplaceString(string) string // String-based pattern replacement
+// ReplaceBytes([]byte) []byte // Binary data anonymization
+// WrapReader(io.Reader) io.Reader // Streaming anonymization
+// }
+//
+// # Quick Start
+//
+// Basic usage involves creating an anonymizer and processing sensitive data:
+//
+// import "github.com/vxcontrol/cloud/anonymizer"
+//
+// // Create anonymizer with all built-in patterns
+// anon, err := anonymizer.NewAnonymizer(nil)
+// if err != nil {
+// return err
+// }
+//
+// // Anonymize complex data structures
+// sensitiveData := map[string]any{
+// "user_email": "admin@company.com",
+// "api_key": "sk-1234567890abcdef",
+// "database_url": "postgres://user:password123@db.internal:5432/app",
+// "server_ip": "192.168.1.100",
+// }
+//
+// if err := anon.Anonymize(&sensitiveData); err != nil {
+// return err
+// }
+// // Result: all sensitive patterns automatically masked
+//
+// # Pattern Recognition
+//
+// The anonymizer uses three comprehensive pattern databases:
+//
+// General Patterns (patterns/db/general.yml):
+// - Network data: IP addresses, domains, URLs, ports
+// - System data: File paths, configuration values, environment variables
+// - Application data: Session tokens, request IDs, temporary files
+//
+// PII Patterns (patterns/db/pii.yml):
+// - Contact information: Email addresses, phone numbers, postal addresses
+// - Identity data: Social Security Numbers, credit card numbers, personal IDs
+// - Biometric data: Fingerprints, facial recognition patterns
+//
+// Secrets Patterns (patterns/db/secrets.yml):
+// - API credentials: Keys, tokens, authentication headers
+// - Database credentials: Connection strings, passwords, certificates
+// - Cloud provider secrets: AWS/Azure/GCP access keys and service tokens
+//
+// Custom patterns can be added during initialization:
+//
+// customPatterns := []patterns.Pattern{
+// {
+// Name: "internal_token",
+// Regex: `internal_[a-zA-Z0-9]{16}`,
+// },
+// }
+// anon, err := anonymizer.NewAnonymizer(customPatterns)
+//
+// # Anonymization Methods
+//
+// ## Structural Anonymization
+//
+// Deep anonymization of Go structures using reflection:
+//
+// type ErrorReport struct {
+// Message string `json:"message"`
+// Context map[string]any `json:"context"`
+// UserEmail string `json:"user_email"`
+// ConfigPath string `json:"config_path" anonymizer:"skip"`
+// }
+//
+// report := ErrorReport{
+// Message: "Failed to connect to admin@company.com",
+// Context: map[string]any{
+// "api_key": "sk-abcd1234efgh5678",
+// "server": "192.168.1.50",
+// },
+// UserEmail: "john.doe@company.com",
+// ConfigPath: "/etc/app/config.yml", // Will be preserved due to "skip" tag
+// }
+//
+// if err := anon.Anonymize(&report); err != nil {
+// return err
+// }
+// // Result: emails and API keys masked, config path preserved
+//
+// ## String Anonymization
+//
+// Direct string processing for simple use cases:
+//
+// original := "Connect to user admin@company.com with key sk-1234567890"
+// anonymized := anon.ReplaceString(original)
+// // Result: "Connect to user ยง**SSH Connection**ยง with key ยง*****api_key*****ยง"
+//
+// ## Streaming Anonymization
+//
+// Memory-efficient processing for large data streams:
+//
+// file, err := os.Open("large-log-file.txt")
+// if err != nil {
+// return err
+// }
+// defer file.Close()
+//
+// // Create anonymizing reader wrapper
+// anonymizedReader := anon.WrapReader(file)
+//
+// // Process anonymized data without loading entire file into memory
+// scanner := bufio.NewScanner(anonymizedReader)
+// for scanner.Scan() {
+// line := scanner.Text()
+// // Each line has been anonymized during reading
+// processAnonymizedLine(line)
+// }
+//
+// Streaming Features:
+// - Fixed memory footprint regardless of input size
+// - 8KB processing chunks with 1KB overlap for pattern boundaries
+// - High-performance throughput optimized for production workloads
+// - No temporary file creation or memory accumulation
+//
+// # Performance Characteristics
+//
+// Benchmark Results:
+// - String Processing: High-throughput pattern matching and replacement
+// - Structural Processing: Efficient reflection-based deep anonymization
+// - Streaming Processing: Memory-efficient large file handling
+// - Pattern Recognition: Optimized regex engine with experimental.Set
+//
+// Memory Usage:
+// - Fixed memory footprint for streaming operations
+// - Minimal overhead for structural anonymization
+// - Pattern cache optimization for repeated operations
+// - No memory leaks in long-running applications
+//
+// # Advanced Features
+//
+// ## Tag-Based Control
+//
+// Use struct tags to control anonymization behavior:
+//
+// type Config struct {
+// DatabaseURL string `json:"database_url"` // Will be anonymized
+// SystemID string `json:"system_id" anonymizer:"skip"` // Will be preserved
+// SecretKey string `json:"secret_key"` // Will be anonymized
+// }
+//
+// ## Pattern Customization
+//
+// Extend built-in patterns with custom recognition rules:
+//
+// customPatterns := []patterns.Pattern{
+// {
+// Name: "custom_api_key",
+// Regex: `cak_[a-zA-Z0-9]{32}`,
+// },
+// {
+// Name: "internal_user_id",
+// Regex: `user_[0-9]{8}`,
+// },
+// }
+//
+// anon, err := anonymizer.NewAnonymizer(customPatterns)
+//
+// ## Cycle Detection
+//
+// Automatic handling of circular references in complex data structures:
+//
+// type Node struct {
+// Value string
+// Next *Node // Circular references handled automatically
+// }
+//
+// // Safe anonymization even with cycles
+// if err := anon.Anonymize(&node); err != nil {
+// return err
+// }
+//
+// # Integration Examples
+//
+// ## Support Service Integration
+//
+// // Anonymize error reports before AI analysis
+// errorDetails := map[string]any{
+// "error_message": "Database connection failed for user admin@prod.com",
+// "connection_string": "postgres://admin:secret@prod-db:5432/app",
+// "client_ip": "10.0.1.50",
+// }
+//
+// if err := anon.Anonymize(&errorDetails); err != nil {
+// return fmt.Errorf("anonymization failed: %w", err)
+// }
+//
+// // Safe to transmit to AI troubleshooting service
+// response, err := supportAPI.AnalyzeError(ctx, errorDetails)
+//
+// ## Log Processing Integration
+//
+// // Anonymize log streams before storage or analysis
+// logFile, err := os.Open("/var/log/application.log")
+// if err != nil {
+// return err
+// }
+// defer logFile.Close()
+//
+// anonymizedLogs := anon.WrapReader(logFile)
+//
+// // Write anonymized logs to secure storage
+// if _, err := io.Copy(secureStorage, anonymizedLogs); err != nil {
+// return err
+// }
+//
+// # Security Considerations
+//
+// Data Protection:
+// - Irreversible anonymization process (original data cannot be recovered)
+// - Structure-preserving masking maintains data utility for analysis
+// - Comprehensive coverage prevents data leakage through overlooked patterns
+// - Tag-based control allows preservation of non-sensitive identifiers
+//
+// Performance Security:
+// - Memory-hard streaming prevents memory-based side-channel attacks
+// - Pattern compilation optimization prevents timing-based inference
+// - Cycle detection prevents infinite recursion-based DoS attacks
+// - Fixed memory footprint prevents memory exhaustion attacks
+//
+// # Thread Safety
+//
+// All anonymizer operations are thread-safe and can be used concurrently:
+//
+// // Single anonymizer instance can be shared across goroutines
+// var anon anonymizer.Anonymizer
+//
+// // Safe concurrent usage
+// go func() {
+// anon.Anonymize(&data1)
+// }()
+// go func() {
+// anon.ReplaceString(text2)
+// }()
+//
+// The underlying regex engine and pattern matching are optimized for concurrent access
+// with minimal contention and high throughput.
+package anonymizer
diff --git a/anonymizer/parser.go b/anonymizer/parser.go
new file mode 100644
index 0000000..9a1e518
--- /dev/null
+++ b/anonymizer/parser.go
@@ -0,0 +1,198 @@
+package anonymizer
+
+import (
+ "fmt"
+ "reflect"
+ "strings"
+ "sync"
+)
+
+const (
+ // defined anonymizer decorator name for struct tag
+ anonymizerTagName = "anonymizer"
+
+ // specifies if the field should be skipped from anonymization
+ skipField = "skip"
+)
+
+// structField represents a single field found in a struct
+type structField struct {
+ // field name
+ Name string
+ // field index path
+ Index []int
+ // field type
+ Type reflect.Type
+ // whether to skip anonymization
+ Skip bool
+}
+
+// structInfo holds information about a struct for anonymization
+type structInfo struct {
+ Fields []structField
+ Type reflect.Type
+}
+
+var (
+ // cache for struct information
+ structCache = make(map[reflect.Type]*structInfo)
+ cacheMutex sync.RWMutex
+)
+
+// ErrObjectImmutable is returned when trying to anonymize an immutable object
+var ErrObjectImmutable = fmt.Errorf("object is immutable and cannot be anonymized")
+
+// getStructInfo returns struct information from cache or parses it
+func getStructInfo(t reflect.Type) (*structInfo, error) {
+ cacheMutex.RLock()
+ info, exists := structCache[t]
+ cacheMutex.RUnlock()
+
+ if exists {
+ return info, nil
+ }
+
+ // parse struct and cache result
+ cacheMutex.Lock()
+ defer cacheMutex.Unlock()
+
+ // double-check after acquiring write lock
+ if info, exists := structCache[t]; exists {
+ return info, nil
+ }
+
+ info, err := parseStruct(t)
+ if err != nil {
+ return nil, err
+ }
+
+ structCache[t] = info
+ return info, nil
+}
+
+// parseStruct parses struct fields and returns StructInfo
+func parseStruct(t reflect.Type) (*structInfo, error) {
+ if t.Kind() != reflect.Struct {
+ return nil, fmt.Errorf("expected struct type, got %s", t.Kind())
+ }
+
+ fields, err := getStructFields(t)
+ if err != nil {
+ return nil, err
+ }
+
+ return &structInfo{
+ Fields: fields,
+ Type: t,
+ }, nil
+}
+
+// getStructFields returns a list of fields for the given struct type
+func getStructFields(t reflect.Type) ([]structField, error) {
+ // anonymous fields to explore at current and next level
+ current := []structField{}
+ next := []structField{{Type: t}}
+
+ // count of queued names for current level and the next
+ var count, nextCount map[reflect.Type]int
+
+ // types already visited at an earlier level
+ visited := map[reflect.Type]bool{}
+
+ // fields found
+ var fields []structField
+
+ for len(next) > 0 {
+ current, next = next, current[:0]
+ count, nextCount = nextCount, map[reflect.Type]int{}
+
+ for _, f := range current {
+ if visited[f.Type] {
+ continue
+ }
+ visited[f.Type] = true
+
+ // scan f.Type for fields to include
+ for i := 0; i < f.Type.NumField(); i++ {
+ sf := f.Type.Field(i)
+ if sf.Anonymous {
+ t := sf.Type
+ if t.Kind() == reflect.Pointer {
+ t = t.Elem()
+ }
+ if !sf.IsExported() && t.Kind() != reflect.Struct {
+ // ignore embedded fields of unexported non-struct types
+ continue
+ }
+ } else if !sf.IsExported() {
+ // ignore unexported non-embedded fields
+ continue
+ }
+
+ // parse anonymizer tag
+ var skip bool
+ tagAnonymizer := sf.Tag.Get(anonymizerTagName)
+ if tagAnonymizer != "" {
+ opts := strings.Split(tagAnonymizer, ",")
+ for _, opt := range opts {
+ if strings.TrimSpace(opt) == skipField {
+ skip = true
+ break
+ }
+ }
+ }
+
+ index := make([]int, len(f.Index)+1)
+ copy(index, f.Index)
+ index[len(f.Index)] = i
+
+ ft := sf.Type
+ for ft.Name() == "" && ft.Kind() == reflect.Pointer {
+ // follow pointer
+ ft = ft.Elem()
+ }
+
+ // record found field and index sequence
+ if !sf.Anonymous || ft.Kind() != reflect.Struct {
+ field := structField{
+ Name: sf.Name,
+ Index: index,
+ Type: ft,
+ Skip: skip,
+ }
+ fields = append(fields, field)
+ if count[f.Type] > 1 {
+ // if there were multiple instances, add a second
+ fields = append(fields, fields[len(fields)-1])
+ }
+ continue
+ }
+
+ // record new anonymous struct to explore in next round
+ nextCount[ft]++
+ if nextCount[ft] == 1 {
+ next = append(next, structField{Name: ft.Name(), Index: index, Type: ft})
+ }
+ }
+ }
+ }
+
+ return fields, nil
+}
+
+// isMutable checks if the given value can be modified
+func isMutable(v reflect.Value) bool {
+ switch v.Kind() {
+ case reflect.Pointer:
+ return !v.IsNil()
+ case reflect.Map, reflect.Slice:
+ return true
+ case reflect.Interface:
+ if !v.IsNil() {
+ return isMutable(v.Elem())
+ }
+ return false
+ default:
+ return false
+ }
+}
diff --git a/anonymizer/patterns/db/general.yml b/anonymizer/patterns/db/general.yml
new file mode 100644
index 0000000..f0a75a7
--- /dev/null
+++ b/anonymizer/patterns/db/general.yml
@@ -0,0 +1,235 @@
+patterns:
+ # === CREDENTIALS & SECRETS ===
+ - name: Generic API Key
+ regex: '(?i)(?:api[_-]?key|apikey)[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_-]{16,}[''"]?)'
+ - name: Generic Secret
+ regex: '(?i)(?:secret|password|passwd|pwd)[=:]\s*(?P[''"]?[^\s''";,]{8,}[''"]?)'
+ - name: Password Pattern
+ regex: '(?i)password[=:]\s*(?P[^\s;,&<>"'']+)'
+ - name: Generic Token
+ regex: '(?i)(?:token|access[_-]?token)[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_.-]{10,}[''"]?)'
+ - name: Generic Bearer Token
+ regex: '(?i)bearer\s+(?P[a-zA-Z0-9+/=_.-]{20,})'
+ - name: Generic Authorization Header
+ regex: '(?i)authorization[:\s]+(?:(bearer|basic|digest)\s+)?(?P[a-zA-Z0-9+/=_.-]{10,})'
+ - name: Bearer Authorization
+ regex: '(?i)authorization[:\s]+bearer\s+(?Psk_[a-zA-Z0-9_]+)'
+ - name: JWT Token
+ regex: '\b(?PeyJ[a-zA-Z0-9+/=_-]+\.eyJ[a-zA-Z0-9+/=_-]+\.[a-zA-Z0-9+/=_-]+)\b'
+ - name: Generic OAuth Token
+ regex: '(?i)oauth[_-]?token[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_.-]{10,}[''"]?)'
+
+ # === IDENTIFIERS ===
+ - name: Session ID Pattern
+ regex: '(?i)(?:session[_-]?id|sess[_-]?id)[=:]\s*(?P[''"]?[a-zA-Z0-9-_]{10,}[''"]?)'
+ - name: Generic Client Secret
+ regex: '(?i)client[_-]?secret[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_.-]{10,}[''"]?)'
+
+ # === NETWORK ADDRESSES (OPTIMIZED) ===
+ - name: IPv4 Address
+ regex: '\b(?P(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\b'
+ - name: IPv6 Address
+ regex: '(?P(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::1|::ffff:[0-9.]+|(?:[0-9a-fA-F]{1,4}:){1,7}:)'
+ - name: Private IP Range
+ regex: '\b(?P(?:10\.(?:[0-9]{1,3}\.){2}[0-9]{1,3}|172\.(?:1[6-9]|2[0-9]|3[01])\.[0-9]{1,3}\.[0-9]{1,3}|192\.168\.[0-9]{1,3}\.[0-9]{1,3}))\b'
+
+ # === DOMAIN & URL (ENHANCED) ===
+ - name: Domain Name Enhanced
+ # regex: '\b(?P(?:(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63})\b'
+ regex: '\b(?P(?:xn--[a-zA-Z0-9-]+|[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]))\.(?:xn--[a-zA-Z0-9-]+|com|org|net|edu|gov|info|biz|io|ai|co|me|tv|app|dev|tech|us|uk|de|fr|jp|cn|ru|in|br|ca|au|xyz|top|site|club|shop|online)\b'
+ - name: HTTP/HTTPS URL
+ regex: '\b(?Phttps?://[^\s<>"{}|\\^`\[\]]+)'
+ - name: WS URL
+ regex: '\b(?Pwss?://[^\s<>"{}|\\^`\[\]]+)'
+ - name: FTP URL
+ regex: '\b(?P(ftp|ftps|sftp)://[^\s<>"{}|\\^`\[\]]+)'
+ - name: SSH URL
+ regex: '\b(?Pssh://[^\s<>"{}|\\^`\[\]]+)'
+ - name: VNC URL
+ regex: '\b(?Pvnc://[^\s<>"{}|\\^`\[\]]+)'
+ - name: RDP URL
+ regex: '\b(?Prdp://[^\s<>"{}|\\^`\[\]]+)'
+ - name: Telnet URL
+ regex: '\b(?Ptelnet://[^\s<>"{}|\\^`\[\]]+)'
+ - name: MAIL URL
+ regex: '\b(?P(smtp|pop3|imap|nntp)://[^\s<>"{}|\\^`\[\]]+)'
+ - name: SMB URL
+ regex: '\b(?Psmb://[^\s<>"{}|\\^`\[\]]+)'
+ - name: NFS URL
+ regex: '\b(?Pnfs://[^\s<>"{}|\\^`\[\]]+)'
+ - name: Generic Webhook URL
+ regex: '(?Phttps?://[^\s]+/webhook[^\s]*)'
+ - name: Generic Callback URL
+ regex: '(?Phttps?://[^\s]+/callback[^\s]*)'
+
+ # === MAC ADDRESSES ===
+ - name: MAC Address Standard
+ regex: '\b(?P(?:[0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2})\b'
+ - name: MAC Address Cisco Format
+ regex: '\b(?P(?:[0-9a-fA-F]{4}\.){2}[0-9a-fA-F]{4})\b'
+
+ # === EMAIL ADDRESSES ===
+ - name: Email Address
+ regex: '\b(?P[a-zA-Z0-9._%+-]+@(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.(?:com|org|net|edu|gov|info|biz|io|ai|co|me|tv|app|dev|tech|us|uk|de|fr|jp|cn|ru|in|br|ca|au|xyz|top|site|club|shop|online))\b'
+
+ # === DATABASE CONNECTIONS ===
+ - name: Database Connection Generic
+ regex: '(?P(?:mysql|postgresql|postgres|pgsql|mongodb|mssql|oracle|redis)://[^\s<>"{}|\\^`\[\]]+)'
+ - name: JDBC URL
+ regex: '(?Pjdbc:[a-z]+://[^\s<>"{}|\\^`\[\]]+)'
+
+ # === CRYPTOGRAPHIC KEYS ===
+ - name: Private Key Begin
+ regex: '(?P-----BEGIN\s+(?:RSA\s+|DSA\s+|EC\s+|OPENSSH\s+)?PRIVATE\s+KEY-----)'
+ - name: Public Key Begin
+ regex: '(?P-----BEGIN\s+(?:RSA\s+|DSA\s+|EC\s+|OPENSSH\s+)?PUBLIC\s+KEY-----)'
+ - name: Certificate Begin
+ regex: '(?P-----BEGIN\s+CERTIFICATE-----)'
+ - name: SSH Key
+ regex: '\b(?Pssh-(?:rsa|dss|ed25519)\s+[A-Za-z0-9+/=]+)'
+
+ # === BASE64 & ENCODED DATA ===
+ - name: Base64 Long
+ regex: '\b(?P[A-Za-z0-9+/]{80,}={0,2})\b'
+ - name: Hex String Long
+ regex: '\b(?P[0-9a-fA-F]{65,})\b'
+
+ # === ENVIRONMENT & CONFIG ===
+ - name: Environment Variable
+ regex: '(?P\$\{[A-Z_][A-Z0-9_]*\}|\$[A-Z_][A-Z0-9_]*)'
+ - name: Sensitive Config Pattern
+ regex: '(?i)(?P(?:password|secret|token|key|auth|credential|pass|pwd)=[^\s;,&<>"'']+)'
+
+ # === CLOUD PROVIDER PATTERNS ===
+ - name: AWS Access Key
+ regex: '\b(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}\b'
+ - name: Generic Cloud Key
+ regex: '(?i)(?:aws|azure|gcp|google)[_-]?(?:(access|api)[_-]?)?key[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_-]{10,}[''"]?)'
+
+ # === SSH CONNECTION STRINGS ===
+ - name: SSH Connection
+ regex: '(?P[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+(?::[0-9]+)?)'
+
+ # === GENERIC SENSITIVE PATTERNS ===
+ - name: Application ID
+ regex: '(?i)(?:app|application)[_-]?id[=:]\s*(?P[''"]?[a-zA-Z0-9-_]{10,50}[''"]?)'
+ - name: Tenant ID
+ regex: '(?i)tenant[_-]?id[=:]\s*(?P[''"]?[a-zA-Z0-9-_]{10,50}[''"]?)'
+ - name: Subscription ID
+ regex: '(?i)subscription[_-]?id[=:]\s*(?P[''"]?[a-zA-Z0-9-_]{10,50}[''"]?)'
+
+ # === COMMAND LINE CREDENTIALS ===
+ - name: Command Line Password Flags
+ regex: '(?:-p|--password|--passphrase|-pw)\s+(?P[^\s]+)'
+ - name: Command Line Password Assignment
+ regex: '(?:-p|--password|--passphrase|-pw)[=:]\s*(?P[''"]?[^\s''"]+[''"]?)'
+ - name: Command Line Token Flags
+ regex: '(?:--token|-k)\s+(?P[a-zA-Z0-9._-]+)'
+ - name: Command Line API Key
+ regex: '--api-key[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_-]+[''"]?)'
+
+ # === ENVIRONMENT VARIABLES ===
+ - name: Database Password Environment
+ regex: '(?i)(?:MYSQL|POSTGRES|MONGO|REDIS|ELASTIC)_(?:ROOT_)?PASSWORD=(?P[^\s]+)'
+ - name: Generic Password Environment
+ regex: '(?i)[A-Z_]*PASSWORD=(?P[^\s]+)'
+ - name: Build Token Environment
+ regex: '(?i)(?:BUILD|BUILDER)_TOKEN[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_-]+[''"]?)'
+ - name: Auth Token Environment
+ regex: '(?i)[A-Z_]*(?:AUTH_?TOKEN|API_?KEY|SECRET_?KEY|API_?SECRET)[=:]\s*(?P[''"]?[^\s''"]+[''"]?)'
+
+ # === DATABASE CONNECTION STRINGS ===
+ - name: MongoDB Connection String
+ regex: '(?Pmongodb(?:\+srv)?://[^:]+:[^@]+@[^/\s]+(?:/[^\s?]+)?(?:\?[^\s]+)?)'
+ - name: Database URL with Credentials
+ regex: '(?P(?:mysql|postgresql|redis|sftp)://[^:]+:[^@]+@[^/\s]+)'
+ - name: HTTP URL with Credentials
+ regex: '(?Phttps?://[^:]+:[^@]+@[^\s/]+)'
+
+ # === HTTP HEADERS ===
+ - name: X-API-KEY Header
+ regex: '(?i)X-API-KEY[=:]\s*(?P[^\s]+)'
+ - name: X-Auth-Token Header
+ regex: '(?i)X-Auth-Token[=:]\s*(?P[^\s]+)'
+ - name: Access-Token Header
+ regex: '(?i)Access-Token[=:]\s*(?P[^\s]+)'
+ - name: Authorization Bearer Header
+ regex: '(?i)Authorization[=:]\s*Bearer\s+(?P[^\s]+)'
+ - name: Consumer-Secret Header
+ regex: '(?i)Consumer-Secret[=:]\s*(?P[^\s]+)'
+
+ # === WEB TOKENS & SESSIONS ===
+ - name: Access Token Parameter
+ regex: '(?i)access_token=(?P[^\s&]+)'
+ - name: Refresh Token Parameter
+ regex: '(?i)refresh_token=(?P[^\s&]+)'
+ - name: Client Secret Parameter
+ regex: '(?i)client_secret=(?P[^\s&]+)'
+ - name: Cookie Value
+ regex: '(?i)cookie[=:]\s*(?P[^;\s]+)'
+ - name: Session ID Cookie
+ regex: '(?i)session[_-]?id=(?P[^\s;,&]+)'
+
+ # === CONFIGURATION FILES ===
+ - name: XML Password Tag
+ regex: '(?P[^<]+)'
+ - name: MySQL Connection PHP
+ regex: 'new\s+mysqli\([^,]+,\s*[^,]+,\s*[''"](?P[^''"]+)[''"]'
+ - name: Mail Password Config
+ regex: '(?i)(smtp|mail|email)_pass=\s*(?P[^"\s\r\n\t]+)'
+ - name: Htpasswd Password
+ regex: 'htpasswd\s+-b\s+[^\s]+\s+[^\s]+\s+(?P[^\s]+)'
+
+ # === SPECIALIZED FORMATS ===
+ - name: Windows Thumbprint
+ regex: 'THUMB:(?P[A-F0-9]{40})'
+ - name: GUID Pair Format
+ regex: '(?P\{[0-9A-Fa-f-]{32,36}\}[=:]\s*\{[0-9A-Fa-f-]{32,36}\})'
+
+ # === CODING IDENTIFIERS ===
+ - name: Repository Password
+ regex: '(?i)-Drepository\.password=(?P[^\s]+)'
+
+ # === SSH & AUTHENTICATION ===
+ - name: SSH Password Parameter
+ regex: '--ssh_password\s+(?P[^\s]+)'
+ - name: Auth Credential Pattern
+ regex: '-auth\s+[^:]+:(?P[^\s]+)'
+ - name: User Credential Pattern
+ regex: '-u\s*[^:]+:(?P[^\s]+)'
+
+ # === ADVANCED TOKEN PATTERNS ===
+ - name: Pipe Token
+ regex: 'pipe=(?P[a-zA-Z0-9_]+)'
+ - name: Echo Password Pattern
+ regex: 'echo\s+[''"](?P[^''"]+)[''"].*\|\s*su\s*-'
+
+ # === SPECIALIZED TOKENS ===
+ - name: Jupyter Notebook Token
+ regex: '--NotebookApp\.token=[''"](?P[^''"]*)[''"]'
+ - name: MariaDB Password
+ regex: '--set\s+mariadbPassword=(?P[^\s]+)'
+ - name: Recovery Password
+ regex: '-recoveryPassword=(?P[^\s]+)'
+ - name: Sensor Password
+ regex: '--sensor-password\s+(?P\S+)'
+
+ # === PRODUCTION CREDENTIALS ===
+ - name: Auth Token Variable
+ regex: '(?i):_authToken=(?P[a-zA-Z0-9]+)'
+ - name: Twine Password
+ regex: '(?i)TWINE_PASSWORD=(?P[^\s]+)'
+ - name: Key File Echo
+ regex: '--key-file\s+<\(echo\s+-n\s+[''"](?P[^''"]+)[''"]'
+ - name: HTTP Password
+ regex: '--http-passwd=(?P[^\s]+)'
+ - name: Embedding Hash
+ regex: '-Embedding\s+(?P[A-Fa-f0-9]{32})'
+ - name: Repository URL with Token
+ regex: '--repository-url\s+(?Phttps?://[^\s]+)'
+
+ # === REAL WORLD PATTERNS ===
+ - name: Token Assignment
+ regex: 'Token\s+(?P[a-zA-Z0-9]+)'
+ - name: API Key Short
+ regex: 'apiKey=(?P[a-zA-Z0-9]{16})'
diff --git a/anonymizer/patterns/db/pii.yml b/anonymizer/patterns/db/pii.yml
new file mode 100644
index 0000000..ebdf684
--- /dev/null
+++ b/anonymizer/patterns/db/pii.yml
@@ -0,0 +1,209 @@
+patterns:
+ - name: Amex Card
+ regex: \b3[47][0-9]{13}\b
+ - name: Argentina National Identity (DNI) Number
+ regex: \b\d{2}\.\d{3}\.\d{3}\b
+ - name: Arista network configuration
+ regex: "via\\ \\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3},\\ \\d{2}:\\d{2}:\\d{2}"
+ - name: BCGlobal
+ regex: \b(6541|6556)[0-9]{12}\b
+ - name: Bank of America Routing Numbers - California
+ regex: \b(?:121|026)00(?:0|9)(?:358|593)\b
+ - name: Box Links
+ regex: "https://app.box.com/[s|l]/\\S+"
+ - name: CVE Number
+ regex: \bCVE-\d{4}-\d{4,7}\b
+ - name: California Drivers License
+ regex: \b[A-Z]{1}\d{7}\b
+ - name: Canada Passport ID
+ regex: \b[\w]{2}[\d]{6}\b
+ - name: Carte Blanche Card
+ regex: \b389[0-9]{11}\b
+ - name: Cisco Router Config
+ regex: "service\\ timestamps\\ [a-z]{3,5}\\ datetime\\ msec|boot-[a-z]{3,5}-marker|interface\\ [A-Za-z0-9]{0,10}[E,e]thernet"
+ - name: Citibank Routing Numbers - California
+ regex: \b32(?:11|22)71(?:18|72)4\b
+ - name: Credit card - 3
+ regex: \b((4\d{3}|5[1-5]\d{2}|2\d{3}|3[47]\d{1,2})[\s\-]?\d{4,6}[\s\-]?\d{4,6}?([\s\-]\d{3,4})?(\d{3})?)\b
+ - name: Croatia Vat ID card number
+ regex: \bHR\d{11}\b
+ - name: Czech Republic Vat ID card number
+ regex: \bCZ\d{8,10}\b
+ - name: DSA Private Key
+ regex: "-----BEGIN DSA PRIVATE KEY-----(?:[a-zA-Z0-9\\+\\=\\/\"']|\\s)+?-----END DSA PRIVATE KEY-----"
+ - name: Diners Club Card
+ regex: \b3(?:0[0-5]|[68][0-9])[0-9]{11}\b
+ - name: Discover Card
+ regex: \b65[4-9][0-9]{13}|64[4-9][0-9]{13}|6011[0-9]{12}|(622(?:12[6-9]|1[3-9][0-9]|[2-8][0-9][0-9]|9[01][0-9]|92[0-5])[0-9]{10})\b
+ - name: Driver's License Number (simplified)
+ regex: \b[A-Z]{2}-\d{6}\b
+ - name: Dropbox Links
+ regex: "https://www.dropbox.com/(?:s|l)/\\S+"
+ - name: EC Private Key
+ regex: "-----BEGIN (?:EC|ECDSA) PRIVATE KEY-----(?:[a-zA-Z0-9\\+\\=\\/\"']|\\s)+?-----END (?:EC|ECDSA) PRIVATE KEY-----"
+ - name: Email Addresses
+ regex: \b[a-z0-9._%\+\-โ|]+@[a-z0-9.\-โ|]+\.[a-z|]{2,6}\b
+ - name: Encrypted DSA Private Key
+ regex: "(?P-----BEGIN DSA PRIVATE KEY-----[\\s\\S]*?-----END DSA PRIVATE KEY-----)"
+ - name: Encrypted EC Private Key
+ regex: "(?P-----BEGIN (?:EC|ECDSA) PRIVATE KEY-----[\\s\\S]*?-----END (?:EC|ECDSA) PRIVATE KEY-----)"
+ - name: Encrypted Private Key
+ regex: "-----BEGIN ENCRYPTED PRIVATE KEY-----(?:.|\\s)+?-----END ENCRYPTED PRIVATE KEY-----"
+ - name: Encrypted PuTTY SSH DSA Key
+ regex: "PuTTY-User-Key-File-2: ssh-dss\\s*Encryption: aes(?:.|\\s?)*?Private-MAC:"
+ - name: Encrypted RSA Private Key
+ regex: "(?P-----BEGIN RSA PRIVATE KEY-----[\\s\\S]*?-----END RSA PRIVATE KEY-----)"
+ - name: France National ID card (CNI)
+ regex: '\b(?P\d{12})\b'
+ - name: France Passport ID
+ regex: \b\d{2}11\d{5}\b
+ - name: GPS Data
+ regex: '([-+]?)([\d]{1,2})(((\.)(\d+)(,)))(\s*)(([-+]?)([\d]{1,3})((\.)(\d+))?)'
+ - name: Germany Driver's License ID
+ regex: (?i)\b[\d\w]\d{2}[\d\w]{6}\d[\d\w]\b
+ - name: Germany ID card number
+ regex: (?i)\bl\d{8}\b
+ - name: Germany Passport ID
+ regex: (?i)\b[cfghjk]\d{3}\w{5}\d\b
+ - name: Google Application Identifier
+ regex: (?i)[0-9]+-\w+\.apps\.googleusercontent\.com
+ - name: HIPAA PHI National Drug Code
+ regex: \b\d{4,5}-\d{3,4}-\d{1,2}\b
+ - name: Huawei config file
+ regex: "sysname\\ HUAWEI|set\\ authentication\\ password\\ simple\\ huawei"
+ - name: IBAN
+ regex: \b[a-zA-Z]{2}[0-9]{2}[a-zA-Z0-9]{4}[0-9]{7}([a-zA-Z0-9]?){0,16}\b
+ - name: Individual Taxpayer Identification Numbers (ITIN)
+ regex: \b9\d{2}(?:[ \-]?)[7,8]\d(?:[ \-]?)\d{4}\b
+ - name: Insta Payment Card
+ regex: \b63[7-9][0-9]{13}\b
+ - name: Ireland Personal Public Service (PPS) Number
+ regex: \b\d{7}\w{1,2}\b
+ - name: JCB Card
+ regex: \b(?:2131|1800|35\d{3})\d{11}\b
+ - name: KeePass 1.x XML Passwords
+ regex: "\\s*?[\\S\\s]*?[\\S\\s]*?<\\/pwentry>\\s*?<\\/pwlist>"
+ - name: Korean Local Card
+ regex: \b9[0-9]{15}\b
+ - name: Large number of US Phone Numbers
+ regex: \b\d{3}-\d{3}-\d{4}|\(\d{3}\)\ ?\d{3}-?\d{4}\b
+ - name: Laser Card
+ regex: \b(6304|6706|6709|6771)[0-9]{12,15}\b
+ - name: Lightweight Directory Access Protocol
+ regex: "(?:dn|cn|dc|sn):\\s*[a-zA-Z0-9=, ]*"
+ - name: Maestro Card
+ regex: \b(5018|5020|5038|6304|6759|6761|6763)[0-9]{8,15}\b
+ - name: MasterCard
+ regex: \b(?:4[0-9]{12}(?:[0-9]{3})?|[25][1-7][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})\b
+ - name: Metasploit Module
+ regex: "require\\ 'msf/core'|class\\ Metasploit|include\\ Msf::Exploit::\\w+::\\w+"
+ - name: MySQL database dump
+ regex: "DROP DATABASE IF EXISTS(?:.|\\n){5,300}CREATE DATABASE(?:.|\\n){5,300}DROP TABLE IF EXISTS(?:.|\\n){5,300}CREATE TABLE"
+ - name: MySQLite database dump
+ regex: "DROP\\ TABLE\\ IF\\ EXISTS\\ \\[[a-zA-Z]*\\];|CREATE\\ TABLE\\ \\[[a-zA-Z]*\\];"
+ - name: Network Proxy Auto-Config
+ regex: "proxy\\.pac|function\\ FindProxyForURL\\(\\w+,\\ \\w+\\)"
+ - name: Nmap Scan Report
+ regex: "Nmap\\ scan\\ report\\ for\\ [a-zA-Z0-9.]+"
+ - name: PGP Header
+ regex: "-{5}(?:BEGIN|END)\\ PGP\\ MESSAGE-{5}"
+ - name: PGP Private Key Block
+ regex: "-----BEGIN PGP PRIVATE KEY BLOCK-----(?:.|\\s)+?-----END PGP PRIVATE KEY BLOCK-----"
+ - name: PKCS7 Encrypted Data
+ regex: "(?:Signer|Recipient)Info(?:s)?\\ ::=\\ \\w+|[D|d]igest(?:Encryption)?Algorithm|EncryptedKey\\ ::= \\w+"
+ - name: Passport Number (simplified) - 3
+ regex: \b[A-Z]\d{7}\b
+ - name: Password etc passwd
+ regex: "[a-zA-Z0-9\\-]+:[x|\\*]:\\d+:\\d+:[a-zA-Z0-9/\\- \"]*:/[a-zA-Z0-9/\\-]*:/[a-zA-Z0-9/\\-]+"
+ - name: Password etc shadow
+ regex: "[a-zA-Z0-9\\-]+:(?:(?:!!?)|(?:\\*LOCK\\*?)|\\*|(?:\\*LCK\\*?)|(?:\\$.*\\$.*\\$.*?)?):\\d*:\\d*:\\d*:\\d*:\\d*:\\d*:"
+ - name: PlainText Private Key
+ regex: "-----BEGIN PRIVATE KEY-----(?:.|\\s)+?-----END PRIVATE KEY-----"
+ - name: Poland National ID (PESEL)
+ regex: \b\d{11}\b
+ - name: PuTTY SSH DSA Key
+ regex: "PuTTY-User-Key-File-2: ssh-dss\\s*Encryption: none(?:.|\\s?)*?Private-MAC:"
+ - name: PuTTY SSH RSA Key
+ regex: "PuTTY-User-Key-File-2: ssh-rsa\\s*Encryption: none(?:.|\\s?)*?Private-MAC:"
+ - name: Public Key Cryptography System (PKCS)
+ regex: "protocol=\"application/x-pkcs[0-9]{0,2}-signature\""
+ - name: Public encrypted key
+ regex: "(?P-----BEGIN PUBLIC KEY-----[\\s\\S]*?-----END PUBLIC KEY-----)"
+ - name: RSA Private Key
+ regex: "-----BEGIN RSA PRIVATE KEY-----(?:[a-zA-Z0-9\\+\\=\\/\"']|\\s)+?-----END RSA PRIVATE KEY-----"
+ - name: SSL Certificate
+ regex: "-----BEGIN CERTIFICATE-----(?:.|\\n)+?\\s-----END CERTIFICATE-----"
+ - name: SWIFT Codes
+ regex: "\\b[A-Za-z]{4}(?:GB|US|DE|RU|CA|JP|CN)[0-9a-zA-Z]{2,5}\\b"
+ - name: Samba Password config file
+ regex: "[0-9a-zA-Z]+:\\d{3,5}:[0-9a-zA-Z- ]*:[0-9a-zA-Z- ]*:\\[[NUDHTW]+[ ]+\\]:[0-9a-zA-Z- ]*:"
+ - name: Social Security Number (SSN) - 3
+ regex: \b\d{3}-\d{2}-\d{4}\b
+ - name: Social Security Number (SSN) - 4
+ regex: (?:\b\d{3}-?\d{2}-?(\d{4})\b)
+ - name: Solo Card
+ regex: \b(6334|6767)[0-9]{12}|(6334|6767)[0-9]{14}|(6334|6767)[0-9]{15}\b
+ - name: Spain Social Security Number (SSN)
+ regex: \b\d{2}\/?\d{8}\/?\d{2}\b
+ - name: Spain Social Security Number (SSN) - 2
+ regex: \b\d{3}[ -.]\d{2}[ -.]\d{4}\b
+ - name: Sweden Passport ID
+ regex: \b\d{8}\b
+ - name: Switch Card
+ regex: \b(4903|4905|4911|4936|6333|6759)[0-9]{12}|(4903|4905|4911|4936|6333|6759)[0-9]{14}|(4903|4905|4911|4936|6333|6759)[0-9]{15}|564182[0-9]{10}|564182[0-9]{12}\b
+ - name: Tax Number
+ regex: \b[0-9]{10}\b
+ - name: UK Drivers License Numbers
+ regex: \b[A-Z]{5}\d{6}[A-Z]{2}\d{1}[A-Z]{2}\b
+ - name: UK Passport Number
+ regex: \b\d{10}GB[RP]\d{7}[UMF]{1}\d{9}\b
+ - name: US Phone Numbers
+ regex: \b((\+|\b)[1l][\-\. ])?\(?\b[\dOlZSB]{3,5}([\-\. ]|\) ?)[\dOlZSB]{3}[\-\. ][\dOlZSB]{4}\b
+ - name: United Kingdom Driver's license ID
+ regex: \b[\w9]{5}\d{6}[\w9]{2}\d{5}\b
+ - name: United Kingdom National Health Service (NHS) number
+ regex: \b\d{3}\s\d{3}\s\d{4}\b
+ - name: United Kingdom Passport ID
+ regex: \b\d{9}\b
+ - name: btc_addresses
+ regex: \b(?:[13][a-km-zA-HJ-NP-Z0-9]{25,34}|bc1[a-z0-9]{39,59})\b
+ - name: credit_cards
+ regex: '\b(?P(?:(?:\d{4}[- ]?){3}\d{4}|\d{15,16}))\b'
+ - name: facebook_secret
+ regex: "(facebook_secret|FACEBOOK_SECRET|facebook_app_secret|FACEBOOK_APP_SECRET)[a-z_ =\\s\"'\\:]{0,5}[^a-zA-Z0-9][a-f0-9]{32}[^a-zA-Z0-9]"
+ - name: git_repos
+ regex: ((git|ssh|http(s)?)|(git@[\w\.]+))(:(\/\/)?)([\w\.@\:/\-~]+)(\.git)(\/)?
+ - name: github_key
+ regex: "(GITHUB_SECRET|GITHUB_KEY|github_secret|github_key|github_token|GITHUB_TOKEN|github_api_key|GITHUB_API_KEY)[a-z_ =\\s\"'\\:]{0,10}[^a-zA-Z0-9][a-zA-Z0-9]{40}[^a-zA-Z0-9]"
+ - name: google_two_factor_backup
+ regex: "(?:BACKUP VERIFICATION CODES|SAVE YOUR BACKUP CODES)[\\s\\S]{0,300}@"
+ - name: heroku_key
+ regex: "(heroku_api_key|HEROKU_API_KEY|heroku_secret|HEROKU_SECRET)[a-z_ =\\s\"'\\:]{0,10}[^a-zA-Z0-9-]\\w{8}(?:-\\w{4}){3}-\\w{12}[^a-zA-Z0-9\\-]"
+ - name: ipv4
+ regex: (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
+ - name: isbn10
+ regex: '\b(?P(?:[\d]-?){9}[\dxX])\b'
+ - name: isbn13
+ regex: \b(?:[\d]-?){12}[\dxX]\b
+ - name: master_cards
+ regex: \b5[1-5]\d{2}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b
+ - name: phones
+ regex: \b(?:\+?\d{1,3}[-.\s*]?)?(?:\(?\d{3}\)?[-.\s*]?)?\d{3}[-.\s*]?\d{4}\b
+ - name: slack_api_key
+ regex: \b(slack_api_key|SLACK_API_KEY|slack_key|SLACK_KEY)[\s=:"']*xox[bpsa]-[\d\w\-]+\b
+ - name: slack_api_token
+ regex: \bxox[pb](?:-[a-zA-Z0-9]+){4,}\b
+ - name: ssh_dss_public
+ regex: "ssh-dss [0-9A-Za-z+/]+[=]{2}"
+ - name: ssh_rsa_public
+ regex: "ssh-rsa AAAA[0-9A-Za-z+/]+[=]{0,3} [^@]+@[^@]+"
+ - name: ssn - 3
+ regex: \b[0-8][0-9]{2}-[0-9]{2}-[0-9]{4}\b
+ - name: ssn_number
+ regex: \b[0-7][0-9]{2}[-\s][0-9]{2}[-\s][0-9]{4}\b
+ - name: ssn_number - 3
+ regex: \b(?:\d{3}-\d{2}-\d{4})\b
+ - name: visa_cards
+ regex: \b4\d{3}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b
+ - name: visa_credit_card
+ regex: \b4[0-9]{15}\b
diff --git a/anonymizer/patterns/db/secrets.yml b/anonymizer/patterns/db/secrets.yml
new file mode 100644
index 0000000..db65c1c
--- /dev/null
+++ b/anonymizer/patterns/db/secrets.yml
@@ -0,0 +1,202 @@
+patterns:
+ # === SPECIALIZED API KEYS & TOKENS ===
+ - name: GitHub Token
+ regex: '(?Pgh[psorun]_[a-zA-Z0-9]{30,})\b'
+ - name: Slack Token
+ regex: '\b(?Pxox[baprs]-[0-9a-zA-Z-]{10,72})\b'
+ - name: Discord Bot Token
+ regex: '\b(?P[A-Za-z0-9_-]{24}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27})\b'
+ - name: Telegram Bot Token
+ regex: '\b(?P[0-9]{8,10}:[a-zA-Z0-9_-]{35})\b'
+ - name: SendGrid API Key
+ regex: '\b(?PSG\.[a-zA-Z0-9_-]{20,24}\.[a-zA-Z0-9_-]{39,50})\b'
+ - name: Stripe API Key
+ regex: '\b(?P(?:sk|pk|rk)_(?:live|test)_[0-9a-zA-Z]{24,})\b'
+ - name: Square API Key
+ regex: '\b(?Psq0[a-z]{3}-[0-9A-Za-z\-_]{20,50})\b'
+ - name: Twilio SID/Token
+ regex: '\b(?PAC[0-9a-f]{32}|SK[0-9a-fA-F]{32})\b'
+ - name: Mailgun API Key
+ regex: '(?Pkey-[0-9a-zA-Z]{32,})\b'
+ - name: MailChimp API Key
+ regex: '(?P[0-9a-f]{32}-us[0-9]{1,2})\b'
+ - name: PayPal Braintree Token
+ regex: '(?Paccess_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32,})\b'
+ - name: Artifactory API Token
+ regex: '\b(?PAKC[a-zA-Z0-9]{10,})\b'
+ - name: Artifactory Password
+ regex: '\b(?PAP[\dABCDEF][a-zA-Z0-9]{8,})\b'
+ - name: New Relic API Key
+ regex: '(?PNR(?:AA|RA|II|IQ|SP)-[a-f0-9A-Z]{2,})\b'
+ - name: Firebase Database URL
+ regex: '(?P[a-z0-9.-]+\.firebase(?:io|app)\.com)\b'
+ - name: Postman API Key
+ regex: '(?PPMAK-[a-zA-Z0-9]{50,})\b'
+ - name: Notion Token
+ regex: '\b(?Psecret_[A-Za-z0-9]{43})\b'
+ - name: Linear API Key
+ regex: '\b(?Plin_api_[0-9A-Za-z]{40})\b'
+ - name: Doppler Token
+ regex: '(?Pdp\.pt\.[a-zA-Z0-9]{40,})\b'
+ - name: PyPI Upload Token
+ regex: '(?Ppypi-AgEIcHlwaS5vcmc[A-Za-z0-9-_]{50,1000})'
+ - name: RubyGems API Key
+ regex: '(?Prubygems_[a-zA-Z0-9]{48,})\b'
+ - name: GitLab Token
+ regex: '(?Pglpat-[a-zA-Z0-9\-=_]{20,})\b'
+ - name: Dynatrace Token
+ regex: '(?Pdt0[a-zA-Z]{1}[0-9]{2}\.[A-Z0-9]{24,}\.[A-Z0-9]{64,})\b'
+ - name: FCM Server Key
+ regex: '(?PAAAA[a-zA-Z0-9_-]{7}:[a-zA-Z0-9_-]{140,})\b'
+ - name: Bitly Secret Key
+ regex: '(?PR_[0-9a-f]{32,})\b'
+ - name: Shopify Access Token
+ regex: '(?Pshp(?:at|ca|pa|ss)_[a-fA-F0-9]{32,})\b'
+ - name: Microsoft Teams Webhook
+ regex: '(?Phttps://outlook\.office\.com/webhook/[A-Za-z0-9\-@]+/IncomingWebhook/[A-Za-z0-9\-]+/[A-Za-z0-9\-]+)'
+ - name: Discord Webhook
+ regex: '(?Phttps://discord(?:app)?\.com/api/webhooks/[0-9]{17,18}/[A-Za-z0-9\-_]{64,68})'
+ - name: Zapier Webhook
+ regex: '(?Phttps://hooks\.zapier\.com/hooks/catch/[A-Za-z0-9]+/[A-Za-z0-9]+/)'
+ - name: Zoho Webhook
+ regex: '(?Phttps://creator\.zoho\.com/api/[A-Za-z0-9/\-_\.]+\?authtoken=[A-Za-z0-9]+)'
+
+ # === CLOUD PROVIDER SPECIFIC ===
+ - name: AWS Secret Access Key
+ regex: '(?i)(?:aws[_-]?)?secret[_-]?(?:access[_-]?)?key[=:]\s*(?P[A-Za-z0-9/+=]{40})\b'
+ - name: Azure Client Secret
+ regex: '(?i)(?:azure[_-]?)?client[_-]?secret[=:]\s*(?P[a-zA-Z0-9~._-]{34,40})\b'
+ - name: Google OAuth Access Token
+ regex: '\b(?Pya29\.[0-9A-Za-z\-_]+)\b'
+ - name: Google API Key
+ regex: '(?PAIza[0-9a-zA-Z-_]{30,})\b'
+ - name: Cloudinary URL
+ regex: '(?Pcloudinary://[0-9]+:[A-Za-z0-9\-_\.]+@[A-Za-z0-9\-_\.]+)'
+ - name: DigitalOcean Token
+ regex: '(?Pdop_v1_[a-f0-9]{60,})\b'
+ - name: Heroku API Key
+ regex: '(?i)heroku[_-]?(?:api[_-]?)?key[=:]\s*(?P[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})\b'
+
+ # === CONTAINER & CI/CD TOKENS ===
+ - name: Docker Hub Token
+ regex: '\b(?Pdckr_pat_[a-zA-Z0-9_-]{36,})\b'
+ - name: CircleCI Token
+ regex: '(?i)circle[_-]?(?:ci[_-]?)?token[=:]\s*(?P[a-fA-F0-9]{40})\b'
+ - name: Travis CI Token
+ regex: '(?i)travis[_-]?(?:ci[_-]?)?token[=:]\s*(?P[a-zA-Z0-9_]{20,})\b'
+ - name: Jenkins API Token
+ regex: '(?i)jenkins[_-]?(?:api[_-]?)?token[=:]\s*(?P[a-f0-9]{32})\b'
+
+ # === INFRASTRUCTURE SECRETS ===
+ - name: Kubernetes Service Account Token
+ regex: '\b(?PeyJhbGciOiJSUzI1NiIs[A-Za-z0-9+/=_-]+)\b'
+ - name: Ansible Vault Key
+ regex: '(?P\$ANSIBLE_VAULT;[0-9.]+;[A-Z0-9]+\$[a-f0-9]+)\b'
+ - name: Terraform Cloud Token
+ regex: '(?P[A-Za-z0-9]{10,}\.atlasv1\.[A-Za-z0-9]{60,})\b'
+ - name: HashiCorp Vault Token
+ regex: '\b(?Phvs\.[A-Za-z0-9_-]{90,})\b'
+
+ # === DATABASE & CACHE TOKENS ===
+ - name: Redis Auth Token
+ regex: '\b(?Predis://:[a-zA-Z0-9+/=_-]+@[^\s]+)\b'
+ - name: MongoDB Connection with Auth
+ regex: '(?Pmongodb(?:\+srv)?://[^:]+:[^@]+@[^/\s]+(?:/[^\s?]+)?(?:\?[^\s]+)?)'
+ - name: Elasticsearch API Key
+ regex: '(?i)elastic[_-]?(?:search[_-]?)?(?:api[_-]?)?key[=:\s]*"?(?P[A-Za-z0-9+/]{50,}={0,2})"?\b'
+
+ # === API TOKENS BY PATTERN ===
+ - name: Short API Key Pattern
+ regex: '(?i)(?:api[_-]?)?(?:key|token)[=:]\s*(?P[A-Z0-9]{32})\b'
+ - name: Long API Key Pattern
+ regex: '(?i)(?:api[_-]?)?(?:key|token)[=:]\s*(?P[A-Za-z0-9+/=_-]{64,128})\b'
+ - name: Hex Token Pattern
+ regex: '(?i)(?:token|secret|key)[=:]\s*(?P[a-f0-9]{40,64})\b'
+ - name: Base64 Token Pattern
+ regex: '(?i)(?:token|bearer|auth)[=:]\s*(?P[A-Za-z0-9+/]{32,}={0,2})\b'
+
+ # === CERTIFICATES & KEYS ===
+ - name: SSH Private Key Content
+ regex: '(?P-----BEGIN (?:RSA |DSA |EC |OPENSSH )?PRIVATE KEY-----[A-Za-z0-9+/=\s]+-----END (?:RSA |DSA |EC |OPENSSH )?PRIVATE KEY-----)'
+ - name: Certificate Content
+ regex: '(?P-----BEGIN CERTIFICATE-----[A-Za-z0-9+/=\s]+-----END CERTIFICATE-----)'
+ - name: PGP Private Key
+ regex: '(?s)(?P-----BEGIN PGP PRIVATE KEY BLOCK-----.*?-----END PGP PRIVATE KEY BLOCK-----)'
+
+ # === SECURITY TOKENS ===
+ - name: CSRF Token
+ regex: '(?i)csrf[_-]?token[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_-]{20,}[''"]?)\b'
+ - name: XSRF Token
+ regex: '(?i)xsrf[_-]?token[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_-]{20,}[''"]?)\b'
+ - name: Anti-Forgery Token
+ regex: '(?i)__RequestVerificationToken[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_-]{20,}[''"]?)\b'
+
+ # === WEBHOOKS & CALLBACKS ===
+ - name: Generic Webhook Secret
+ regex: '(?i)webhook[_-]?secret[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_-]{20,}[''"]?)\b'
+ - name: Callback Token
+ regex: '(?i)callback[_-]?token[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_-]{20,}[''"]?)\b'
+
+ # === ENCRYPTION KEYS ===
+ - name: AES Key Pattern
+ regex: '(?i)aes[_-]?(?:key|encrypt|cipher)[=:]\s*(?P[A-Fa-f0-9]{32,64})\b'
+ - name: Encryption Salt
+ regex: '(?i)salt[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_-]{16,}[''"]?)\b'
+ - name: IV/Nonce Pattern
+ regex: '(?i)(?:iv|nonce)[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_-]{16,}[''"]?)\b'
+
+ # === SIGNING KEYS ===
+ - name: Signing Secret
+ regex: '(?i)signing[_-]?secret[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_-]{32,}[''"]?)\b'
+ - name: HMAC Secret
+ regex: '(?i)hmac[_-]?secret[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_-]{32,}[''"]?)\b'
+
+ # === OAUTH & SSO TOKENS ===
+ - name: OIDC Client Secret
+ regex: '(?i)oidc[_-]?client[_-]?secret[=:]\s*(?P[''"]?[a-zA-Z0-9+/=_-]{32,}[''"]?)\b'
+ - name: SAML Certificate
+ regex: '(?i)saml[_-]?cert[=:]\s*(?P[''"]?[A-Za-z0-9+/=\s]+[''"]?)\b'
+
+ # === BACKUP & RECOVERY CODES ===
+ - name: Recovery Code Pattern
+ regex: '\b(?P[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4})\b'
+ - name: Backup Code Pattern
+ regex: '(?i)backup[_-]?code[=:]\s*(?P[0-9]{8,12})\b'
+
+ # === SPECIAL FORMATS ===
+ - name: License Key Format
+ regex: '\b(?P[A-Z0-9]{4,5}-[A-Z0-9]{4,5}-[A-Z0-9]{4,5}-[A-Z0-9]{4,5}(?:-[A-Z0-9]{4,5})?)\b'
+ - name: Windows Product Key
+ regex: '\b(?P[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5})\b'
+ - name: Certificate Thumbprint
+ regex: '(?i)thumbprint[=:]\s*(?P[A-F0-9]{40})\b'
+ - name: Database Password in DSN
+ regex: '(?P(?:mysql|postgresql|mssql|oracle)://[^:]+:([^@]+)@[^/\s]+)\b'
+
+ # === MONITORING & ANALYTICS ===
+ - name: DataDog API Key
+ regex: '(?i)datadog[_-]?(?:api[_-]?)?key[=:]\s*(?P[a-zA-Z0-9]{32})\b'
+ - name: New Relic License Key
+ regex: '(?i)(?:newrelic|new[_-]?relic)[_-]?(?:license[_-]?)?key[=:]\s*(?P[a-f0-9]{40})\b'
+ - name: Sentry DSN
+ regex: '(?Phttps://[a-f0-9A-F]{32,}:[a-f0-9A-F]{32,}@[^/\s]+/[0-9]+)'
+
+ # === COMMUNICATION TOKENS ===
+ - name: PubNub Keys
+ regex: '\b(?P(?:pub|sub)-c-[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})\b'
+ - name: Pusher Key
+ regex: '(?i)pusher[_-]?(?:key|token)[=:]\s*(?P[a-z0-9]{20})\b'
+
+ # === FINANCIAL & PAYMENT ===
+ - name: Plaid Secret
+ regex: '(?i)plaid[_-]?(?:secret|key)[=:]\s*(?P[a-z0-9]{30})\b'
+ - name: Razorpay Key
+ regex: '(?Przp_\w{2,6}_\w{10,})\b'
+
+ # === GENERIC HIGH-ENTROPY STRINGS ===
+ - name: High Entropy Base64
+ regex: '(?i)(secret|token|key|auth|credential|pass)[_-]?[=:]\s*(?P[A-Za-z0-9+/]{48,}={0,2})\b'
+ - name: High Entropy Hex
+ regex: '(?i)(secret|token|key|auth|credential|pass)[_-]?[=:]\s*(?P[a-f0-9]{48,})\b'
+ - name: Mixed Case High Entropy
+ regex: '(?i)(secret|token|key|auth|credential|pass)[_-]?[=:]\s*(?P[A-Za-z0-9]{32,})\b'
diff --git a/anonymizer/patterns/patterns.go b/anonymizer/patterns/patterns.go
new file mode 100644
index 0000000..2c08cbf
--- /dev/null
+++ b/anonymizer/patterns/patterns.go
@@ -0,0 +1,82 @@
+package patterns
+
+import (
+ _ "embed"
+
+ "gopkg.in/yaml.v3"
+)
+
+//go:embed db/general.yml
+var GeneralPatterns []byte
+
+//go:embed db/pii.yml
+var PiiPatterns []byte
+
+//go:embed db/secrets.yml
+var SecretsPatterns []byte
+
+type PatternListType uint8
+
+const (
+ PatternListTypeNone PatternListType = 0
+ PatternListTypeGeneral PatternListType = 1
+ PatternListTypePii PatternListType = 2
+ PatternListTypeSecrets PatternListType = 4
+ PatternListTypeAll PatternListType = 7
+)
+
+type Pattern struct {
+ Name string `json:"name" yaml:"name"`
+ Regex string `json:"regex" yaml:"regex"`
+}
+
+type Patterns struct {
+ Patterns []Pattern `json:"patterns" yaml:"patterns"`
+}
+
+func (p *Patterns) Regexes() []string {
+ regexes := make([]string, 0, len(p.Patterns))
+ for _, pattern := range p.Patterns {
+ regexes = append(regexes, pattern.Regex)
+ }
+
+ return regexes
+}
+
+func (p *Patterns) Names() []string {
+ names := make([]string, 0, len(p.Patterns))
+ for _, pattern := range p.Patterns {
+ names = append(names, pattern.Name)
+ }
+ return names
+}
+
+func LoadPatterns(patternListType PatternListType) (*Patterns, error) {
+ var (
+ patterns Patterns
+ resultList Patterns
+ )
+
+ if patternListType&PatternListTypeGeneral != 0 {
+ if err := yaml.Unmarshal(GeneralPatterns, &patterns); err != nil {
+ return nil, err
+ }
+ resultList.Patterns = append(resultList.Patterns, patterns.Patterns...)
+ }
+
+ if patternListType&PatternListTypePii != 0 {
+ if err := yaml.Unmarshal(PiiPatterns, &patterns); err != nil {
+ return nil, err
+ }
+ resultList.Patterns = append(resultList.Patterns, patterns.Patterns...)
+ }
+
+ if patternListType&PatternListTypeSecrets != 0 {
+ if err := yaml.Unmarshal(SecretsPatterns, &patterns); err != nil {
+ return nil, err
+ }
+ resultList.Patterns = append(resultList.Patterns, patterns.Patterns...)
+ }
+
+ return &resultList, nil
+}
diff --git a/anonymizer/patterns/patterns_test.go b/anonymizer/patterns/patterns_test.go
new file mode 100644
index 0000000..e13a126
--- /dev/null
+++ b/anonymizer/patterns/patterns_test.go
@@ -0,0 +1,211 @@
+package patterns
+
+import (
+ "testing"
+
+ "github.com/wasilibs/go-re2"
+ "github.com/wasilibs/go-re2/experimental"
+ "gopkg.in/yaml.v3"
+)
+
+func TestLoadPatternsGeneral(t *testing.T) {
+ patterns, err := LoadPatterns(PatternListTypeGeneral)
+ if err != nil {
+ t.Fatalf("failed to load general patterns: %v", err)
+ }
+
+ if patterns == nil {
+ t.Fatal("patterns should not be nil")
+ }
+
+ if len(patterns.Patterns) == 0 {
+ t.Fatal("general patterns should not be empty")
+ }
+
+ t.Logf("loaded %d general patterns", len(patterns.Patterns))
+}
+
+func TestLoadPatternsPii(t *testing.T) {
+ patterns, err := LoadPatterns(PatternListTypePii)
+ if err != nil {
+ t.Fatalf("failed to load pii patterns: %v", err)
+ }
+
+ if patterns == nil {
+ t.Fatal("patterns should not be nil")
+ }
+
+ if len(patterns.Patterns) == 0 {
+ t.Fatal("pii patterns should not be empty")
+ }
+
+ t.Logf("loaded %d pii patterns", len(patterns.Patterns))
+}
+
+func TestLoadPatternsSecrets(t *testing.T) {
+ patterns, err := LoadPatterns(PatternListTypeSecrets)
+ if err != nil {
+ t.Fatalf("failed to load secrets patterns: %v", err)
+ }
+
+ if patterns == nil {
+ t.Fatal("patterns should not be nil")
+ }
+
+ if len(patterns.Patterns) == 0 {
+ t.Fatal("secrets patterns should not be empty")
+ }
+
+ t.Logf("loaded %d secrets patterns", len(patterns.Patterns))
+}
+
+func TestLoadPatternsAll(t *testing.T) {
+ patterns, err := LoadPatterns(PatternListTypeAll)
+ if err != nil {
+ t.Fatalf("failed to load all patterns: %v", err)
+ }
+
+ if patterns == nil {
+ t.Fatal("patterns should not be nil")
+ }
+
+ if len(patterns.Patterns) == 0 {
+ t.Fatal("all patterns should not be empty")
+ }
+
+ t.Logf("loaded %d total patterns", len(patterns.Patterns))
+}
+
+func TestPatternsMethods(t *testing.T) {
+ patterns, err := LoadPatterns(PatternListTypeGeneral)
+ if err != nil {
+ t.Fatalf("failed to load patterns: %v", err)
+ }
+
+ namesList := patterns.Names()
+ regexList := patterns.Regexes()
+
+ if len(namesList) != len(patterns.Patterns) {
+ t.Errorf("expected %d names, got %d", len(patterns.Patterns), len(namesList))
+ }
+
+ if len(regexList) != len(patterns.Patterns) {
+ t.Errorf("expected %d regexes, got %d", len(patterns.Patterns), len(regexList))
+ }
+
+ for i, name := range namesList {
+ if name == "" {
+ t.Errorf("name at index %d is empty", i)
+ }
+ }
+
+ for i, regex := range regexList {
+ if regex == "" {
+ t.Errorf("regex at index %d is empty", i)
+ }
+ }
+}
+
+func TestPatternsStructure(t *testing.T) {
+ patterns, err := LoadPatterns(PatternListTypeGeneral)
+ if err != nil {
+ t.Fatalf("failed to load patterns: %v", err)
+ }
+
+ for i, pattern := range patterns.Patterns {
+ if pattern.Name == "" {
+ t.Errorf("pattern at index %d has empty name", i)
+ }
+ if pattern.Regex == "" {
+ t.Errorf("pattern at index %d has empty regex", i)
+ }
+ }
+}
+
+func TestPatternsDuplicate(t *testing.T) {
+ patterns, err := LoadPatterns(PatternListTypeAll)
+ if err != nil {
+ t.Fatalf("failed to load patterns: %v", err)
+ }
+
+ patternMap := make(map[string]struct{})
+ for _, pattern := range patterns.Patterns {
+ if _, ok := patternMap[pattern.Name]; ok {
+ t.Errorf("duplicate pattern %s", pattern.Name)
+ }
+ patternMap[pattern.Name] = struct{}{}
+ }
+}
+
+func TestRawPatternsStructure(t *testing.T) {
+ patternsRawData := [][]byte{
+ GeneralPatterns,
+ PiiPatterns,
+ SecretsPatterns,
+ }
+
+ for _, patternData := range patternsRawData {
+ var patterns map[string][]map[string]string
+ if err := yaml.Unmarshal(patternData, &patterns); err != nil {
+ t.Fatalf("failed to unmarshal patterns: %v", err)
+ }
+ if len(patterns) != 1 {
+ t.Errorf("patterns has %d fields, expected 1", len(patterns))
+ }
+ if len(patterns["patterns"]) == 0 {
+ t.Errorf("patterns has no patterns")
+ }
+ for _, pattern := range patterns["patterns"] {
+ if len(pattern) != 2 {
+ t.Errorf("pattern has %d fields, expected 2", len(pattern))
+ }
+ if pattern["name"] == "" {
+ t.Errorf("pattern has empty name")
+ }
+ if pattern["regex"] == "" {
+ t.Errorf("pattern has empty regex")
+ }
+ }
+ }
+}
+
+func TestPatternCompilation(t *testing.T) {
+ patternTypes := []struct {
+ name string
+ ptype PatternListType
+ }{
+ {"general", PatternListTypeGeneral},
+ {"pii", PatternListTypePii},
+ {"secrets", PatternListTypeSecrets},
+ }
+
+ for _, pt := range patternTypes {
+ t.Run(pt.name, func(t *testing.T) {
+ patterns, err := LoadPatterns(pt.ptype)
+ if err != nil {
+ t.Fatalf("failed to load %s patterns: %v", pt.name, err)
+ }
+
+ namesList := patterns.Names()
+ regexList := patterns.Regexes()
+
+ for i, regex := range regexList {
+ if regex == "" {
+ t.Errorf("empty regex at index %d", i)
+ continue
+ }
+
+ if _, err := re2.Compile(regex); err != nil {
+ t.Errorf("failed to compile regex at index %d with name '%s': %s - error: %v",
+ i, namesList[i], regex, err)
+ }
+ }
+
+ if _, err := experimental.CompileSet(regexList); err != nil {
+ t.Errorf("failed to compile regex set: %v", err)
+ }
+
+ t.Logf("successfully compiled %d %s regexes", len(regexList), pt.name)
+ })
+ }
+}
diff --git a/anonymizer/replacer.go b/anonymizer/replacer.go
new file mode 100644
index 0000000..728c0af
--- /dev/null
+++ b/anonymizer/replacer.go
@@ -0,0 +1,154 @@
+package anonymizer
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+ "sync"
+
+ "github.com/wasilibs/go-re2"
+ "github.com/wasilibs/go-re2/experimental"
+)
+
+type Replacer interface {
+ ReplaceString(string) string
+ ReplaceBytes([]byte) []byte
+ WrapReader(io.Reader) io.Reader
+}
+
+type replacer struct {
+ patterns []Pattern
+ regexes []*re2.Regexp
+ set *experimental.Set
+ mx *sync.Mutex
+}
+
+type Pattern struct {
+ Name string
+ Regex string
+}
+
+func NewReplacer(patterns []string, names []string) (Replacer, error) {
+ if len(patterns) != len(names) {
+ return nil, fmt.Errorf("patterns and names must have the same length")
+ }
+
+ pts := make([]Pattern, 0, len(patterns))
+ for i, pt := range patterns {
+ pts = append(pts, Pattern{Regex: pt, Name: names[i]})
+ }
+
+ // it reduces the size of the state machine and speeds up its compilation
+ sort.Slice(pts, func(i, j int) bool {
+ return pts[i].Regex < pts[j].Regex
+ })
+
+ patterns = patterns[:0]
+ for _, pt := range pts {
+ patterns = append(patterns, pt.Regex)
+ }
+
+ set, err := experimental.CompileSet(patterns)
+ if err != nil {
+ return nil, fmt.Errorf("failed to compile regex set: %w", err)
+ }
+
+ regexes := make([]*re2.Regexp, 0, len(patterns))
+ for _, pattern := range patterns {
+ regex, err := re2.Compile(pattern)
+ if err != nil {
+ return nil, fmt.Errorf("failed to compile regex '%s': %w", pattern, err)
+ }
+ regexes = append(regexes, regex)
+ }
+
+ return &replacer{
+ patterns: pts,
+ regexes: regexes,
+ set: set,
+ mx: &sync.Mutex{},
+ }, nil
+}
+
+func (r *replacer) ReplaceString(s string) string {
+ if len(r.regexes) == 0 {
+ return s
+ }
+
+ // mutex here is to improve the performance because concurrent calls slower then single thread
+ r.mx.Lock()
+ defer r.mx.Unlock()
+
+ matches := r.set.FindAllString(s, len(r.regexes))
+ sort.Ints(matches)
+
+ for _, match := range matches {
+ regex := r.regexes[match]
+ s = regex.ReplaceAllStringFunc(s, func(se string) string {
+ ms := regex.FindStringSubmatch(se)
+ if len(ms) < 2 {
+ return r.getReplacePattern(r.patterns[match].Name, se)
+ }
+
+ replaceIndex := regex.SubexpIndex("replace")
+ if replaceIndex == -1 {
+ replaceIndex = 1
+ }
+
+ replace := r.getReplacePattern(r.patterns[match].Name, ms[replaceIndex])
+ return strings.ReplaceAll(se, ms[replaceIndex], replace)
+ })
+ }
+
+ return s
+}
+
+func (r *replacer) ReplaceBytes(b []byte) []byte {
+ if len(r.regexes) == 0 {
+ return b
+ }
+
+ // mutex here is to improve the performance because concurrent calls slower then single thread
+ r.mx.Lock()
+ defer r.mx.Unlock()
+
+ matches := r.set.FindAll(b, len(r.regexes))
+ sort.Ints(matches)
+
+ for _, match := range matches {
+ regex := r.regexes[match]
+ b = regex.ReplaceAllFunc(b, func(se []byte) []byte {
+ ms := regex.FindSubmatch(se)
+ if len(ms) < 2 {
+ return []byte(r.getReplacePattern(r.patterns[match].Name, string(se)))
+ }
+
+ replaceIndex := regex.SubexpIndex("replace")
+ if replaceIndex == -1 {
+ replaceIndex = 1
+ }
+
+ replace := r.getReplacePattern(r.patterns[match].Name, string(ms[replaceIndex]))
+ return bytes.ReplaceAll(se, ms[replaceIndex], []byte(replace))
+ })
+ }
+
+ return b
+}
+
+func (r *replacer) WrapReader(reader io.Reader) io.Reader {
+ return newWrapper(reader, r)
+}
+
+func (r *replacer) getReplacePattern(name, match string) string {
+ if len(match) < len(name)+6 { // 4 is the length of the "ยง**ยง", 2 extra for "*" left and right
+ return fmt.Sprintf("ยง*%s*ยง", name)
+ }
+
+ paddingLength := min(10, (len(match) - len(name) - 2)) // 2 is the length of the "ยง" left and right
+ paddingLeft := strings.Repeat("*", paddingLength/2)
+ paddingRight := strings.Repeat("*", paddingLength-paddingLength/2)
+ return fmt.Sprintf("ยง%s%s%sยง", paddingLeft, name, paddingRight)
+}
diff --git a/anonymizer/replacer_bench_test.go b/anonymizer/replacer_bench_test.go
new file mode 100644
index 0000000..681eec5
--- /dev/null
+++ b/anonymizer/replacer_bench_test.go
@@ -0,0 +1,298 @@
+package anonymizer
+
+import (
+ "fmt"
+ "math/rand"
+ "strings"
+ "testing"
+
+ "github.com/vxcontrol/cloud/anonymizer/patterns"
+ "github.com/vxcontrol/cloud/anonymizer/testdata"
+)
+
+func BenchmarkNewReplacer_1000Patterns(b *testing.B) {
+ patterns, names := testdata.GenerateRegexPatterns(12345, 1000)
+
+ b.ResetTimer()
+ for b.Loop() {
+ _, err := NewReplacer(patterns, names)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func BenchmarkNewReplacer_5000Patterns(b *testing.B) {
+ patterns, names := testdata.GenerateRegexPatterns(12345, 5000)
+
+ b.ResetTimer()
+ for b.Loop() {
+ _, err := NewReplacer(patterns, names)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func BenchmarkNewReplacer_10000Patterns(b *testing.B) {
+ patterns, names := testdata.GenerateRegexPatterns(12345, 10000)
+
+ b.ResetTimer()
+ for b.Loop() {
+ _, err := NewReplacer(patterns, names)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func benchmarkReplace(b *testing.B, patternCount int, stringCount int) {
+ // limit memory to avoid swapping
+ limitMemory()
+
+ patterns, names := testdata.GenerateRegexPatterns(12345, patternCount)
+ testStrings := testdata.GenerateTestStrings(54321, stringCount, 50, 200)
+
+ replacer, err := NewReplacer(patterns, names)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ totalBytes := 0
+ for _, s := range testStrings {
+ totalBytes += len(s)
+ }
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ for _, testString := range testStrings {
+ _ = replacer.ReplaceString(testString)
+ }
+ }
+
+ stringsPerSec := float64(b.N*stringCount) / b.Elapsed().Seconds()
+ bytesPerSec := float64(b.N*totalBytes) / b.Elapsed().Seconds()
+
+ b.ReportMetric(stringsPerSec, "strings/sec")
+ b.ReportMetric(bytesPerSec, "bytes/sec")
+}
+
+func benchmarkParallelReplace(b *testing.B, numGoroutines, patternCount int, stringCount int) {
+ // limit memory to avoid swapping
+ limitMemory()
+
+ patterns, names := testdata.GenerateRegexPatterns(12345, patternCount)
+ testStrings := testdata.GenerateTestStrings(54321, stringCount, 50, 200)
+
+ replacer, err := NewReplacer(patterns, names)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ // calculate average bytes per string for accurate metrics
+ totalBytes := 0
+ for _, s := range testStrings {
+ totalBytes += len(s)
+ }
+
+ b.ResetTimer()
+ b.ReportAllocs()
+ b.SetParallelism(numGoroutines)
+
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ for _, testString := range testStrings {
+ _ = replacer.ReplaceString(testString)
+ }
+ }
+ })
+
+ stringsPerSec := float64(b.N*stringCount) / b.Elapsed().Seconds()
+ bytesPerSec := float64(b.N*totalBytes) / b.Elapsed().Seconds()
+
+ b.ReportMetric(stringsPerSec, "strings/sec")
+ b.ReportMetric(bytesPerSec, "bytes/sec")
+}
+
+func BenchmarkReplace_1000Patterns(b *testing.B) {
+ benchmarkReplace(b, 1000, 10000)
+}
+
+func BenchmarkReplace_5000Patterns(b *testing.B) {
+ benchmarkReplace(b, 5000, 10000)
+}
+
+func BenchmarkReplace_10000Patterns(b *testing.B) {
+ benchmarkReplace(b, 10000, 10000)
+}
+
+func BenchmarkParallel4_Replace_1000Patterns(b *testing.B) {
+ benchmarkParallelReplace(b, 4, 1000, 10000)
+}
+
+func BenchmarkParallel4_Replace_5000Patterns(b *testing.B) {
+ benchmarkParallelReplace(b, 4, 5000, 10000)
+}
+
+func BenchmarkParallel4_Replace_10000Patterns(b *testing.B) {
+ benchmarkParallelReplace(b, 4, 10000, 10000)
+}
+
+func BenchmarkReplace_WorstCase(b *testing.B) {
+ // limit memory to avoid swapping
+ limitMemory()
+
+ // create patterns that will match frequently
+ patterns := []string{
+ `[a-zA-Z0-9]+`,
+ `\w+`,
+ `[0-9]+`,
+ }
+
+ // generate strings that will have many matches
+ rng := rand.New(rand.NewSource(99999))
+ testStrings := make([]string, 1000)
+ totalBytes := 0
+ for i := range len(testStrings) {
+ var sb strings.Builder
+ length := rng.Intn(151) + 50 // 50-200 chars
+ for range length {
+ if rng.Float32() < 0.3 {
+ sb.WriteByte(' ')
+ } else {
+ sb.WriteByte(byte('a' + rng.Intn(26)))
+ }
+ }
+ testStrings[i] = sb.String()
+ totalBytes += len(testStrings[i])
+ }
+
+ replacer, err := NewReplacer(patterns, make([]string, len(patterns)))
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ for _, testString := range testStrings {
+ _ = replacer.ReplaceString(testString)
+ }
+ }
+
+ stringsPerSec := float64(b.N*len(testStrings)) / b.Elapsed().Seconds()
+ bytesPerSec := float64(b.N*totalBytes) / b.Elapsed().Seconds()
+
+ b.ReportMetric(stringsPerSec, "strings/sec")
+ b.ReportMetric(bytesPerSec, "bytes/sec")
+}
+
+func BenchmarkReplace_BestCase(b *testing.B) {
+ // limit memory to avoid swapping
+ limitMemory()
+
+ // patterns that won't match
+ patterns := []string{
+ `xyz123notfound`,
+ `veryrarepattern999`,
+ `impossiblematch`,
+ }
+
+ testStrings := testdata.GenerateTestStrings(77777, 10000, 50, 200)
+
+ replacer, err := NewReplacer(patterns, make([]string, len(patterns)))
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ totalBytes := 0
+ for _, s := range testStrings {
+ totalBytes += len(s)
+ }
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ for _, testString := range testStrings {
+ _ = replacer.ReplaceString(testString)
+ }
+ }
+
+ stringsPerSec := float64(b.N*len(testStrings)) / b.Elapsed().Seconds()
+ bytesPerSec := float64(b.N*totalBytes) / b.Elapsed().Seconds()
+
+ b.ReportMetric(stringsPerSec, "strings/sec")
+ b.ReportMetric(bytesPerSec, "bytes/sec")
+}
+
+func BenchmarkReplace_Production(b *testing.B) {
+ // limit memory to avoid swapping
+ limitMemory()
+
+ patterns, err := patterns.LoadPatterns(patterns.PatternListTypeAll)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ replacer, err := NewReplacer(patterns.Regexes(), patterns.Names())
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ insensitiveDataset, err := testdata.LoadInsensitiveData()
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ datasets, err := testdata.LoadAllTestData()
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ rng := rand.New(rand.NewSource(1234567))
+ getWrappedDataset := func() (string, string) {
+ prefixIdx := rng.Intn(len(insensitiveDataset.Entries))
+ suffixIdx := rng.Intn(len(insensitiveDataset.Entries))
+ prefix := insensitiveDataset.Entries[prefixIdx].Examples
+ suffix := insensitiveDataset.Entries[suffixIdx].Examples
+ prefixLines := strings.Split(prefix, "\n")
+ suffixLines := strings.Split(suffix, "\n")
+ prefixLineIdx := rng.Intn(len(prefixLines))
+ suffixLineIdx := rng.Intn(len(suffixLines))
+ prefix = prefixLines[prefixLineIdx]
+ suffix = suffixLines[suffixLineIdx]
+ return prefix, suffix
+ }
+
+ testStrings := make([]string, 0, 10000)
+ for _, dataset := range datasets {
+ for _, entry := range dataset.Entries {
+ prefix, suffix := getWrappedDataset()
+ for line := range strings.Lines(entry.Examples) {
+ testStrings = append(testStrings, fmt.Sprintf("%s %s %s", prefix, line, suffix))
+ }
+ }
+ }
+
+ totalBytes := 0
+ testStringsLength := len(testStrings)
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for i := 0; b.Loop(); i++ {
+ testString := testStrings[i%testStringsLength]
+ _ = replacer.ReplaceString(testString)
+ totalBytes += len(testString)
+ }
+
+ stringsPerSec := float64(b.N) / b.Elapsed().Seconds()
+ bytesPerSec := float64(totalBytes) / b.Elapsed().Seconds()
+
+ b.ReportMetric(stringsPerSec, "strings/sec")
+ b.ReportMetric(bytesPerSec, "bytes/sec")
+}
diff --git a/anonymizer/replacer_test.go b/anonymizer/replacer_test.go
new file mode 100644
index 0000000..35711e6
--- /dev/null
+++ b/anonymizer/replacer_test.go
@@ -0,0 +1,357 @@
+package anonymizer
+
+import (
+ "fmt"
+ "math/rand"
+ "runtime"
+ "runtime/debug"
+ "strings"
+ "testing"
+
+ "github.com/vxcontrol/cloud/anonymizer/patterns"
+ "github.com/vxcontrol/cloud/anonymizer/testdata"
+)
+
+const testMemoryLimit = 2 * 1024 * 1024 * 1024 // 2GB
+
+func limitMemory() {
+ debug.SetMemoryLimit(testMemoryLimit)
+ runtime.GC()
+}
+
+func TestNewReplacer_ValidPatterns(t *testing.T) {
+ patterns := []string{
+ `password=([^\s]+)`,
+ `\b[a-f0-9]{32}\b`,
+ `--token\s+([a-zA-Z0-9]+)\b`,
+ }
+ names := []string{
+ "password",
+ "hash",
+ "token",
+ }
+
+ replacer, err := NewReplacer(patterns, names)
+ if err != nil {
+ t.Fatalf("Expected no error, got: %v", err)
+ }
+ if replacer == nil {
+ t.Fatal("Expected replacer to be created")
+ }
+}
+
+func TestNewReplacer_InvalidPatterns(t *testing.T) {
+ patterns := []string{
+ `[invalid regex`,
+ `(unclosed group`,
+ `*invalid quantifier`,
+ }
+
+ _, err := NewReplacer(patterns, make([]string, len(patterns)))
+ if err == nil {
+ t.Fatal("Expected error for invalid patterns")
+ }
+}
+
+func TestNewReplacer_EmptyPatterns(t *testing.T) {
+ replacer, err := NewReplacer([]string{}, []string{})
+ if err != nil {
+ t.Fatalf("Expected no error for empty patterns, got: %v", err)
+ }
+ if replacer == nil {
+ t.Fatal("Expected replacer to be created")
+ }
+ result := replacer.ReplaceString("test")
+ if result != "test" {
+ t.Errorf("Expected test, got: %s", result)
+ }
+}
+
+func TestReplace_WithCaptureGroups(t *testing.T) {
+ patterns := []string{`password=([^\s]+)`}
+ replacer, err := NewReplacer(patterns, []string{"PASSWORD"})
+ if err != nil {
+ t.Fatalf("Failed to create replacer: %v", err)
+ }
+
+ tests := []struct {
+ input string
+ expected string
+ }{
+ {
+ input: "mysql -u user password=secret123 -h localhost",
+ expected: "mysql -u user password=ยง*PASSWORD*ยง -h localhost",
+ },
+ {
+ input: "mysql -u user password=toolongsecret123 -h localhost",
+ expected: "mysql -u user password=ยง***PASSWORD***ยง -h localhost",
+ },
+ {
+ input: "mysql -u user password=toolongsecret1234 -h localhost",
+ expected: "mysql -u user password=ยง***PASSWORD****ยง -h localhost",
+ },
+ }
+
+ for _, test := range tests {
+ result := replacer.ReplaceString(test.input)
+ if result != test.expected {
+ t.Errorf("Expected: %s, got: %s", test.expected, result)
+ }
+ }
+}
+
+func TestReplace_WithCaptureGroupReplacement(t *testing.T) {
+ patterns := []string{`test([0-9]+)`}
+ replacer, err := NewReplacer(patterns, []string{"TEST"})
+ if err != nil {
+ t.Fatalf("Failed to create replacer: %v", err)
+ }
+
+ input := "prefix test123 suffix"
+ result := replacer.ReplaceString(input)
+ expected := "prefix testยง*TEST*ยง suffix"
+
+ if result != expected {
+ t.Errorf("Expected: %s, got: %s", expected, result)
+ }
+}
+
+func TestReplace_WithoutCaptureGroups(t *testing.T) {
+ patterns := []string{`test\d+`}
+ replacer, err := NewReplacer(patterns, []string{"TEST"})
+ if err != nil {
+ t.Fatalf("Failed to create replacer: %v", err)
+ }
+
+ input := "prefix test123 suffix"
+ result := replacer.ReplaceString(input)
+ expected := "prefix ยง*TEST*ยง suffix"
+
+ if result != expected {
+ t.Errorf("Expected: %s, got: %s", expected, result)
+ }
+}
+
+func TestReplace_WithNamedGroups(t *testing.T) {
+ patterns := []string{`password=(?P[^\s]+)`}
+ replacer, err := NewReplacer(patterns, []string{"PASSWORD"})
+ if err != nil {
+ t.Fatalf("Failed to create replacer: %v", err)
+ }
+
+ input := "config password=mysecret123456 end"
+ result := replacer.ReplaceString(input)
+ expected := "config password=ยง**PASSWORD**ยง end"
+
+ if result != expected {
+ t.Errorf("Expected: %s, got: %s", expected, result)
+ }
+}
+
+func TestReplace_MultipleMatches(t *testing.T) {
+ patterns := []string{
+ `password=([^\s]+)`,
+ `--token\s+([a-zA-Z0-9]+)\b`,
+ }
+ replacer, err := NewReplacer(patterns, []string{"PASSWORD", "TOKEN"})
+ if err != nil {
+ t.Fatalf("Failed to create replacer: %v", err)
+ }
+
+ input := "password=secret123 --token mytoken4567"
+ result := replacer.ReplaceString(input)
+
+ // check that both secrets are masked
+ if !strings.Contains(result, "ยง*PASSWORD*ยง") {
+ t.Errorf("Expected password to be masked, got: %s", result)
+ }
+ if !strings.Contains(result, "ยง**TOKEN**ยง") {
+ t.Errorf("Expected token to be masked, got: %s", result)
+ }
+ if !strings.Contains(result, "password=") || !strings.Contains(result, "--token ") {
+ t.Errorf("Expected structure to remain, got: %s", result)
+ }
+}
+
+func TestReplace_NoMatches(t *testing.T) {
+ patterns := []string{`password=([^\s]+)`}
+ replacer, err := NewReplacer(patterns, []string{"PASSWORD"})
+ if err != nil {
+ t.Fatalf("Failed to create replacer: %v", err)
+ }
+
+ input := "this string has no sensitive data"
+ result := replacer.ReplaceString(input)
+
+ if result != input {
+ t.Errorf("Expected unchanged string, got: %s", result)
+ }
+}
+
+func TestReplace_EmptyString(t *testing.T) {
+ patterns := []string{`password=([^\s]+)`}
+ replacer, err := NewReplacer(patterns, []string{"PASSWORD"})
+ if err != nil {
+ t.Fatalf("Failed to create replacer: %v", err)
+ }
+
+ result := replacer.ReplaceString("")
+ if result != "" {
+ t.Errorf("Expected empty string, got: %s", result)
+ }
+}
+
+func TestReplace_WithInsensitiveDatasets(t *testing.T) {
+ insensitiveDataset, err := testdata.LoadInsensitiveData()
+ if err != nil {
+ t.Fatalf("failed to load insensitive data: %v", err)
+ }
+
+ allPatterns, err := patterns.LoadPatterns(patterns.PatternListTypeAll)
+ if err != nil {
+ t.Fatalf("failed to load all patterns: %v", err)
+ }
+
+ replacer, err := NewReplacer(allPatterns.Regexes(), allPatterns.Names())
+ if err != nil {
+ t.Fatalf("failed to create replacer: %v", err)
+ }
+
+ // we'll use a simple test to check if any masking occurs
+ for _, entry := range insensitiveDataset.Entries {
+ t.Run(entry.Name, func(t *testing.T) {
+ originalText := entry.Examples
+ replacedText := replacer.ReplaceString(originalText)
+
+ if originalText != replacedText {
+ t.Logf("original text:\n%s", originalText)
+ t.Logf("replaced text:\n%s", replacedText)
+ t.Errorf("unexpected replacement for %s", entry.Name)
+ }
+ })
+ }
+}
+
+func TestReplace_WithTestDatasets(t *testing.T) {
+ datasets, err := testdata.LoadAllTestData()
+ if err != nil {
+ t.Fatalf("failed to load test data: %v", err)
+ }
+
+ allLoadedPatterns, err := patterns.LoadPatterns(patterns.PatternListTypeAll)
+ if err != nil {
+ t.Fatalf("failed to load all patterns: %v", err)
+ }
+
+ // combine patterns
+ allPatterns := allLoadedPatterns.Patterns
+ patternMap := make(map[string]string)
+ for _, p := range allPatterns {
+ patternMap[p.Name] = p.Regex
+ }
+
+ // test each dataset
+ for _, dataset := range datasets {
+ t.Run(dataset.Category, func(t *testing.T) {
+ for _, entry := range dataset.Entries {
+ t.Run(entry.Name, func(t *testing.T) {
+ regex, exists := patternMap[entry.Name]
+ if !exists {
+ t.Skipf("pattern %s not found in loaded patterns", entry.Name)
+ return
+ }
+
+ // create replacer with single pattern
+ replacer, err := NewReplacer([]string{regex}, []string{entry.Name})
+ if err != nil {
+ // skip regexes that can't be compiled (lookbehind/lookahead)
+ if strings.Contains(err.Error(), "invalid perl operator") {
+ t.Skipf("skipping regex with unsupported features: %s", entry.Name)
+ return
+ }
+ t.Fatalf("failed to create replacer for %s: %v", entry.Name, err)
+ }
+
+ result := replacer.ReplaceString(entry.Examples)
+
+ for line := range strings.Lines(result) {
+ // check that masking occurred (should contain ยง*NAME*ยง)
+ foundLeftPadding := strings.Contains(line, "ยง*")
+ foundRightPadding := strings.Contains(line, "*ยง")
+ foundRegexName := strings.Contains(line, entry.Name)
+ if !foundLeftPadding || !foundRightPadding || !foundRegexName {
+ // some patterns may not match due to strict conditions, log but don't fail
+ t.Errorf("no masking occurred for '%s' in line: %s", entry.Name, line)
+ }
+ }
+ })
+ }
+ })
+ }
+}
+
+func TestReplace_WithMixedDatasets(t *testing.T) {
+ datasets, err := testdata.LoadAllTestData()
+ if err != nil {
+ t.Fatalf("failed to load test data: %v", err)
+ }
+
+ insensitiveDataset, err := testdata.LoadInsensitiveData()
+ if err != nil {
+ t.Fatalf("failed to load insensitive data: %v", err)
+ }
+
+ allLoadedPatterns, err := patterns.LoadPatterns(patterns.PatternListTypeAll)
+ if err != nil {
+ t.Fatalf("failed to load all patterns: %v", err)
+ }
+
+ replacer, err := NewReplacer(allLoadedPatterns.Regexes(), allLoadedPatterns.Names())
+ if err != nil {
+ t.Fatalf("failed to create replacer: %v", err)
+ }
+
+ rng := rand.New(rand.NewSource(1234567))
+ getWrappedDataset := func() (string, string) {
+ prefixIdx := rng.Intn(len(insensitiveDataset.Entries))
+ suffixIdx := rng.Intn(len(insensitiveDataset.Entries))
+ prefix := insensitiveDataset.Entries[prefixIdx].Examples
+ suffix := insensitiveDataset.Entries[suffixIdx].Examples
+ prefixLines := strings.Split(prefix, "\n")
+ suffixLines := strings.Split(suffix, "\n")
+ prefixLineIdx := rng.Intn(len(prefixLines))
+ suffixLineIdx := rng.Intn(len(suffixLines))
+ prefix = prefixLines[prefixLineIdx]
+ suffix = suffixLines[suffixLineIdx]
+ return prefix, suffix
+ }
+
+ // test each dataset
+ for _, dataset := range datasets {
+ t.Run(dataset.Category, func(t *testing.T) {
+ for _, entry := range dataset.Entries {
+ t.Run(entry.Name, func(t *testing.T) {
+ prefix, suffix := getWrappedDataset()
+ sample := fmt.Sprintf("%s %s %s", prefix, entry.Examples, suffix)
+
+ // test masking on examples
+ result := replacer.ReplaceString(sample)
+
+ if !strings.Contains(result, prefix) || !strings.Contains(result, suffix) {
+ t.Errorf("insensitive dataset was masked: %s", result)
+ }
+
+ if strings.Contains(result, entry.Examples) {
+ t.Errorf("sensitive dataset was not masked: %s", entry.Examples)
+ }
+
+ for line := range strings.Lines(entry.Examples) {
+ if strings.Contains(result, line) {
+ t.Errorf("sensitive dataset (line: %s) was not masked", line)
+ }
+ }
+ })
+ }
+ })
+ }
+}
diff --git a/anonymizer/testdata/dataset/cloud.txt b/anonymizer/testdata/dataset/cloud.txt
new file mode 100644
index 0000000..07cdc91
--- /dev/null
+++ b/anonymizer/testdata/dataset/cloud.txt
@@ -0,0 +1,37 @@
+===AWS Access Key===
+AKIAIOSFODNN7EXAMPLE
+AKIAI44QH8DHBEXAMPLE
+
+===Generic Cloud Key===
+aws_access_key=AKIAIOSFODNN7EXAMPLE
+gcp_api_key="gcp_service_key_123"
+azure_key=azure_access_key_value
+
+===AWS Secret Access Key===
+aws_secret_access_key=abcdef1234567890abcdef1234567890abcdef12
+AWS_SECRET_KEY=1234567890abcdef1234567890abcdef12345678
+secret_key=fedcba0987654321fedcba0987654321fedcba09
+
+===Azure Client Secret===
+azure_client_secret=1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s
+AZURE_CLIENT_SECRET=abcdef~1234567890.abcdef_1234567890-abc
+client_secret=xyz789~fedcba0987.654321_abcdef-xyz
+
+===Google OAuth Access Token===
+ya29.a0ARrdaM-abcdef1234567890abcdef1234567890abcdef1234567890
+ya29.GlwoBgN_abcdef1234567890abcdef1234567890abcdef1234567890ab
+
+===Google API Key===
+AIzaSyAbcdef1234567890abcdef1234567890a
+AIzaSyD1234567890abcdef1234567890abcdef1
+
+===Cloudinary URL===
+cloudinary://123456789012345:abcdef1234567890abcdef12@my-cloud-name
+cloudinary://987654321098765:fedcba0987654321fedcba09@test-cloud
+
+===DigitalOcean Token===
+dop_v1_abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab
+
+===Heroku API Key===
+heroku_api_key=12345678-1234-5678-9012-123456789ABC
+HEROKU_API_KEY=ABCDEF12-3456-7890-ABCD-EF1234567890
diff --git a/anonymizer/testdata/dataset/config.txt b/anonymizer/testdata/dataset/config.txt
new file mode 100644
index 0000000..a158ba6
--- /dev/null
+++ b/anonymizer/testdata/dataset/config.txt
@@ -0,0 +1,129 @@
+===XML Password Tag===
+xml_password_123
+database_secret
+
+===MySQL Connection PHP===
+new mysqli("localhost", "user", "dbpassword123", "database")
+new mysqli("host", "admin", "secretpass", "mydb")
+
+===Mail Password Config===
+smtp_pass=email_password
+mail_pass=mail_secret_pass
+email_pass=smtp_credentials
+
+===Auth Token Variable===
+:_authToken=abcdef1234567890
+
+===Pipe Token===
+pipe=pipe_token_123
+
+===Token Assignment===
+Token abcdef1234567890
+
+===API Key Short===
+apiKey=abcdef1234567890
+
+===Docker Hub Token===
+dckr_pat_abcdef1234567890abcdef1234567890abcdef123456
+dckr_pat_xyz789_abcdef1234567890abcdef1234567890-abc
+
+===CircleCI Token===
+circle_token=abcdef1234567890abcdef1234567890abcdef12
+CIRCLE_CI_TOKEN=1234567890abcdef1234567890abcdef12345678
+
+===Travis CI Token===
+travis_token=abcdef1234567890abcdef12
+TRAVIS_CI_TOKEN=xyz789fedcba0987654321ab
+
+===Jenkins API Token===
+jenkins_token=abcdef1234567890abcdef1234567890
+JENKINS_API_TOKEN=fedcba0987654321fedcba0987654321
+
+===CSRF Token===
+csrf_token=abcdef1234567890abcdef1234567890abcdef12
+CSRF-TOKEN=fedcba0987654321fedcba0987654321fedcba09
+
+===XSRF Token===
+xsrf_token=xyz123456789012345678901234567890xyz123
+X-XSRF-TOKEN=abc987654321098765432109876543210abc98
+
+===Anti-Forgery Token===
+__RequestVerificationToken=def456789012345678901234567890def456789
+
+===Generic Webhook Secret===
+webhook_secret=secret123456789012345678901234567890
+WEBHOOK-SECRET=webhook_abcdef1234567890abcdef1234567890
+
+===Callback Token===
+callback_token=callback_xyz123456789012345678901234567890
+CALLBACK-TOKEN=token_fedcba0987654321fedcba0987654321
+
+===AES Key Pattern===
+aes_key=ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890AB
+aes_encrypt=FEDCBA0987654321FEDCBA0987654321FEDCBA0987654321
+
+===Encryption Salt===
+salt=abcdef1234567890abcdef1234567890
+SALT="xyz123456789012345678901234567890xyz123"
+
+===IV/Nonce Pattern===
+iv=fedcba0987654321fedcba09
+nonce="abc123456789012345678901234567890abc123"
+
+===Signing Secret===
+signing_secret=sign_abcdef1234567890abcdef1234567890abcdef12
+SIGNING-SECRET=hmac_fedcba0987654321fedcba0987654321fedcba09
+
+===HMAC Secret===
+hmac_secret=hmac_xyz123456789012345678901234567890xyz123
+HMAC-SECRET=secret_abc987654321098765432109876543210abc
+
+===OIDC Client Secret===
+oidc_client_secret=oidc_abcdef1234567890abcdef1234567890abcdef12
+OIDC-CLIENT-SECRET=client_fedcba0987654321fedcba0987654321
+--oidc-client-secret=abcdef1234567890abcdef1234567890abcdef
+
+===SAML Certificate===
+saml_cert="MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF"
+SAML-CERT=LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t
+
+===Recovery Code Pattern===
+ABCD-EFGH-IJKL
+XYZT-1234-5678
+
+===Backup Code Pattern===
+backup_code=123456789012
+BACKUP-CODE=987654321098
+
+===License Key Format===
+ABCDE-12345-FGHIJ-67890-KLMNO
+XYZTW-98765-VUTSR-43210
+
+===Windows Product Key===
+ABCDE-12345-FGHIJ-67890-KLMNO
+VWXYZ-54321-QRSTU-09876-PQRST
+
+===Certificate Thumbprint===
+thumbprint=ABCDEF1234567890ABCDEF1234567890ABCDEF12
+THUMBPRINT=FEDCBA0987654321FEDCBA0987654321FEDCBA09
+
+===Database Password in DSN===
+mysql://user:secretpass@localhost:3306/database
+postgresql://admin:dbpassword@db.example.com:5432/mydb
+
+===Short API Key Pattern===
+api_key=ABCDEF1234567890ABCDEF1234567890
+API_TOKEN=FEDCBA0987654321FEDCBA0987654321
+
+===Long API Key Pattern===
+api_key=abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab
+token=fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321
+
+===Hex Token Pattern===
+token=abcdef1234567890abcdef1234567890abcdef12
+secret=fedcba0987654321fedcba0987654321fedcba09876543
+
+===Base64 Token Pattern===
+token=abcdef1234567890abcdef1234567890abcdef1234567890ab==
+bearer=fedcba0987654321fedcba0987654321fedcba0987654321==
+auth=xyz123456789012345678901234567890123456789012345=
diff --git a/anonymizer/testdata/dataset/credentials.txt b/anonymizer/testdata/dataset/credentials.txt
new file mode 100644
index 0000000..6c5def7
--- /dev/null
+++ b/anonymizer/testdata/dataset/credentials.txt
@@ -0,0 +1,229 @@
+===Generic API Key===
+api_key=sk_test_1234567890abcdef
+API_KEY="live_a1b2c3d4e5f6g7h8i9j0"
+apikey:production_key_xyz123
+
+===Generic Secret===
+secret=my_super_secret_password
+SECRET="database_connection_secret"
+password:admin_password_123
+
+===Password Pattern===
+password=my_secure_password
+PASSWORD=production_db_pass
+
+===Generic Token===
+token="bearer_token_abc123def456"
+access_token=ya29.a0ARrdaM-xyz123
+ACCESS_TOKEN: live_token_value
+
+===Generic Bearer Token===
+bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
+Bearer sk_live_abcdef1234567890
+
+===JWT Token===
+eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
+eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
+
+===Command Line Password Flags===
+mysql -p secretpassword123
+--password my_secure_pass
+-pw strongpassword
+
+===Generic Authorization Header===
+authorization: Bearer sk_live_abc123
+Authorization: Basic dXNlcjpwYXNz
+
+===Bearer Authorization===
+authorization: bearer sk_test_abc123def456
+
+===Generic OAuth Token===
+oauth_token="oauth_access_token_123"
+oauth-token: live_token_value
+
+===Session ID Pattern===
+session_id=abc123def456789
+SESS_ID="session_token_value"
+
+===Generic Client Secret===
+client_secret="oauth_client_secret"
+client-secret: app_secret_123
+
+===Application ID===
+app_id="application_id_123"
+application-id: app_12345678901234567890
+
+===Tenant ID===
+tenant_id="tenant_12345678901234567890"
+tenant-id: tenant_abcdef1234567890
+
+===Subscription ID===
+subscription_id="sub_12345678901234567890"
+subscription-id: sub_abcdef1234567890
+
+===Command Line Password Assignment===
+-p="secret_password"
+--password: my_secure_pass
+
+===Command Line Token Flags===
+--token abc123def456
+-k API_TOKEN_VALUE
+
+===Command Line API Key===
+--api-key="sk_test_12345abcdef"
+--api-key: live_key_value
+
+===Database Password Environment===
+MYSQL_ROOT_PASSWORD=supersecret
+POSTGRES_PASSWORD=dbpass123
+MONGO_PASSWORD=mongo_secret
+REDIS_PASSWORD=redis_pass
+ELASTIC_PASSWORD=elastic_secret
+
+===Generic Password Environment===
+DATABASE_PASSWORD=db_secret
+APP_PASSWORD=app_secret
+SERVICE_PASSWORD=service_pass
+
+===Build Token Environment===
+BUILD_TOKEN="build_abc123"
+BUILDER_TOKEN: builder_secret
+
+===Auth Token Environment===
+AUTH_TOKEN=auth_secret_123
+API_SECRET_KEY=secret_key_value
+SECRET_KEY=app_secret_key
+
+===MongoDB Connection String===
+mongodb+srv://user:password123@cluster.mongodb.net/database
+mongodb://user:pass@localhost:27017/database
+mongodb+srv://user:password@cluster.mongodb.net/database
+
+===Database URL with Credentials===
+mysql://admin:secretpass@localhost:3306/mydb
+postgresql://user:dbpass@host:5432/database
+redis://user:pass@cache.example.com:6379
+
+===HTTP URL with Credentials===
+https://user:token@api.github.com/repo
+
+===X-API-KEY Header===
+X-API-KEY: sk_live_abcdef123456
+
+===X-Auth-Token Header===
+X-Auth-Token: auth_token_value
+
+===Access-Token Header===
+Access-Token: access_123
+
+===Authorization Bearer Header===
+Authorization: Bearer token123
+
+===Consumer-Secret Header===
+Consumer-Secret: consumer_secret_123
+
+===Access Token Parameter===
+access_token=ya29.abc123def456
+
+===Refresh Token Parameter===
+refresh_token=1/refresh_token_value
+
+===Client Secret Parameter===
+client_secret=client_secret_123
+
+===Cookie Value===
+cookie: session_id=sess_abc123
+
+===Session ID Cookie===
+session_id=sess_abc123def456
+
+===Repository URL with Token===
+--repository-url https://token@github.com/user/repo
+
+===SendGrid API Key===
+SG.abcdefghijklmnopqrstuv.1234567890abcdef1234567890abcdef1234567890ab
+SG.xyz123_-abcdef123456.fedcba0987654321fedcba0987654321fedcba09876
+
+===Stripe API Key===
+sk_live_abcdef1234567890abcdef12
+pk_live_1234567890abcdef1234567890
+rk_live_fedcba0987654321fedcba09
+sk_test_abcdef1234567890abcdef12
+pk_test_1234567890abcdef1234567890
+rk_test_fedcba0987654321fedcba09
+
+===Square API Key===
+sq0atp-1234567890abcdef123456
+sq0csp-abcdef1234567890abcdef1234567890abcdef123456
+sq0idp-fedcba0987654321fedcba
+
+===Twilio SID/Token===
+AC1234567890abcdef1234567890abcdef
+SK1234567890ABCDEF1234567890ABCDEF
+
+===Mailgun API Key===
+key-abcdef1234567890abcdef1234567890ab
+key-fedcba0987654321fedcba0987654321
+
+===MailChimp API Key===
+abcdef1234567890abcdef1234567890ab-us12
+fedcba0987654321fedcba0987654321-us20
+
+===PayPal Braintree Token===
+access_token$production$abcdef1234567890$fedcba0987654321fedcba0987654321ab
+
+===Artifactory API Token===
+AKCp2WY7G1234567890abcdef
+AKC1234567890abcdefghijklmnopqrstuvwxyz
+
+===Artifactory Password===
+AP1Aabcdef1234567890
+APBF1234567890abcdef123456
+
+===New Relic API Key===
+NRAA-abcdef1234567890abcdef1234567
+NRRA-fedcba0987654321fedcba0987654321fedcba09876543
+NRII-1234567890abcdef1234567890abcdef
+NRIQ-abcdef1234567890abcdef1234567890ab
+NRSP-abcdef1234567890abcdef123456789012
+
+===Firebase Database URL===
+my-project-12345.firebaseio.com
+test-app.firebaseapp.com
+
+===Postman API Key===
+PMAK-abcdef1234567890abcdef1234567890abcdef1234567890abcdef123
+
+===Notion Token===
+secret_abcdef1234567890abcdef1234567890abcdef12345
+
+===Linear API Key===
+lin_api_abcdef1234567890abcdef1234567890abcdef12
+
+===Doppler Token===
+dp.pt.abcdef1234567890abcdef1234567890abcdef1234
+
+===PyPI Upload Token===
+pypi-AgEIcHlwaS5vcmcmdabcdef1234567890abcdef1234567890abcdef1234567890
+
+===RubyGems API Key===
+rubygems_abcdef1234567890abcdef1234567890abcdef1234567890ab
+
+===GitLab Token===
+glpat-abcdef1234567890abcd
+glpat-xyz123_fedcba0987654321
+
+===Dynatrace Token===
+dt0c01.ABCDEF1234567890ABCDEF12.ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890AB
+
+===FCM Server Key===
+AAAA1234567:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab
+
+===Bitly Secret Key===
+R_abcdef1234567890abcdef1234567890ab
+
+===Shopify Access Token===
+shpat_abcdef1234567890abcdef1234567890ab
+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
+shppa_1234567890abcdef1234567890abcdef
+shpss_abcdef1234567890abcdef1234567890ab
diff --git a/anonymizer/testdata/dataset/crypto.txt b/anonymizer/testdata/dataset/crypto.txt
new file mode 100644
index 0000000..ef7e476
--- /dev/null
+++ b/anonymizer/testdata/dataset/crypto.txt
@@ -0,0 +1,220 @@
+===Private Key Begin===
+-----BEGIN PRIVATE KEY-----
+-----BEGIN RSA PRIVATE KEY-----
+-----BEGIN DSA PRIVATE KEY-----
+-----BEGIN EC PRIVATE KEY-----
+
+===Public Key Begin===
+-----BEGIN PUBLIC KEY-----
+-----BEGIN RSA PUBLIC KEY-----
+
+===Certificate Begin===
+-----BEGIN CERTIFICATE-----
+
+===SSH Key===
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7vbqajDw
+ssh-dss AAAAB3NzaC1kc3MAAACBAP1/U4EddRIpUt9KnC7s5Of2EbdSPO9EAMMeP4C2USZpRV1AIlH7WT2NWPq/xfW6MPbLm1Vs14E7gB00b/JmYLdrmVClpJ+f6AR7ECLCT7up1/63xhv4O1fnxqimFQ8E+4P208UewwI1VBNaFpEy9nXzrith1yrv8iIDGZ3RSAHHAAAAFQCjYCojVXqWH4x8TxS
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
+
+===Base64 Long===
+SGVsbG8gV29ybGQhIFRoaXMgaXMgYSBsb25nIGJhc2U2NCBlbmNvZGVkIHN0cmluZyB0aGF0IGNvbnRhaW5zIG1vcmUgdGhhbiA0MCBjaGFyYWN0ZXJzLg==
+
+===Hex String Long===
+a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef1234567890abcdef
+
+===Windows Thumbprint===
+THUMB:1234567890ABCDEF1234567890ABCDEF12345678
+
+===GUID Pair Format===
+{12345678-1234-1234-1234-123456789012}:{abcdefab-abcd-abcd-abcd-abcdefabdfec}
+
+===Environment Variable===
+${API_KEY}
+$DATABASE_URL
+${SECRET_TOKEN}
+
+===Sensitive Config Pattern===
+password=my_secret_password
+secret=api_secret_key
+token=access_token_value
+
+===DSA Private Key===
+-----BEGIN DSA PRIVATE KEY-----
+MIIBuwIBAAKBgQC7vbqajDw+zFWmfiQ9L4KzQ0pg6S7dgVdT8P5Ulv1CMSjFcMnF
+-----END DSA PRIVATE KEY-----
+
+===EC Private Key===
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIIrYSSNdsgQY9iFAM6YDBfvQqMgIa4KlNEJdLqKE1LhqoAoGCCqGSM49
+-----END EC PRIVATE KEY-----
+
+===Encrypted DSA Private Key===
+-----BEGIN DSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+-----END DSA PRIVATE KEY-----
+
+===Encrypted EC Private Key===
+-----BEGIN EC PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+-----END EC PRIVATE KEY-----
+
+===Encrypted Private Key===
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQI
+-----END ENCRYPTED PRIVATE KEY-----
+
+===Encrypted PuTTY SSH DSA Key===
+PuTTY-User-Key-File-2: ssh-dss
+Encryption: aes256-cbc
+Private-MAC: 12345678901234567890123456789012
+
+===Encrypted RSA Private Key===
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+-----END RSA PRIVATE KEY-----
+
+===PGP Header===
+-----BEGIN PGP MESSAGE-----
+-----END PGP MESSAGE-----
+
+===PGP Private Key Block===
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v2.0.14
+-----END PGP PRIVATE KEY BLOCK-----
+
+===PKCS7 Encrypted Data===
+SignerInfo ::= SEQUENCE
+DigestAlgorithm
+EncryptedKey ::= OCTET
+
+===PlainText Private Key===
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7vbqajDw
+-----END PRIVATE KEY-----
+
+===PuTTY SSH DSA Key===
+PuTTY-User-Key-File-2: ssh-dss
+Encryption: none
+Private-MAC: 12345678901234567890123456789012
+
+===PuTTY SSH RSA Key===
+PuTTY-User-Key-File-2: ssh-rsa
+Encryption: none
+Private-MAC: 12345678901234567890123456789012
+
+===Public Key Cryptography System (PKCS)===
+protocol="application/x-pkcs7-signature"
+
+===Public encrypted key===
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7vbqajDw
+-----END PUBLIC KEY-----
+
+===RSA Private Key===
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA7vbqajDw+zFWmfiQ9L4KzQ0pg6S7dgVdT8P5Ulv1CMSj
+-----END RSA PRIVATE KEY-----
+
+===SSL Certificate===
+-----BEGIN CERTIFICATE-----
+MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF
+-----END CERTIFICATE-----
+
+===ssh_dss_public===
+ssh-dss AAAAB3NzaC1kc3MAAACBAP1/U4EddRIpUt9KnC7s5Of2EbdSPO9EAMMeP4C2USZpRV1AIlH7WT2NWPq/xfW6MPbLm1Vs14E7gB00b/JmYLdrmVClpJ+f6AR7ECLCT7up1/63xhv4O1fnxqimFQ8E+4P208UewwI1VBNaFpEy9nXzrith1yrv8iIDGZ3RSAHHAAAAFQCjYCojVXqWH4x8TxS==
+
+===ssh_rsa_public===
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7vbqajDw user@example.com
+
+===ipv4===
+192.168.1.1
+10.0.0.50
+
+===Box Links===
+https://app.box.com/s/abc123def456
+https://app.box.com/l/xyz789
+
+===Dropbox Links===
+https://www.dropbox.com/s/abc123/file.txt
+https://www.dropbox.com/l/xyz789
+
+===git_repos===
+git@github.com:user/repository.git
+https://github.com/user/repo.git
+
+===Cisco Router Config===
+service timestamps log datetime msec
+interface GigabitEthernet0/1
+
+===Huawei config file===
+sysname HUAWEI
+set authentication password simple huawei
+
+===KeePass 1.x XML Passwords===
+
+
+secret123
+
+
+
+===Lightweight Directory Access Protocol===
+dn: cn=John Doe,ou=people,dc=example,dc=com
+
+===Metasploit Module===
+require 'msf/core'
+class Metasploit
+
+===MySQL database dump===
+DROP DATABASE IF EXISTS test;
+CREATE DATABASE test;
+DROP TABLE IF EXISTS users;
+CREATE TABLE users
+
+===MySQLite database dump===
+DROP TABLE IF EXISTS [users];
+CREATE TABLE [users];
+
+===Network Proxy Auto-Config===
+proxy.pac
+function FindProxyForURL(url, host)
+
+===Nmap Scan Report===
+Nmap scan report for example.com
+
+===Password etc passwd===
+user:x:1000:1000:User:/home/user:/bin/bash
+
+===Password etc shadow===
+user:$6$salt$hash:18000:0:99999:7:::
+
+===Samba Password config file===
+admin:1000:E52CAC67419A9A224A3B108F3FA6CB6D:8846F7EAEE8FB117AD06BDD830B7586C:[U ]:LCT-507C0000:
+guest:1003:NO PASSWORDXXXXXXXXXXXXXXXXXXXXX:NO PASSWORDXXXXXXXXXXXXXXXXXXXXX:[NU ]:LCT-00000000:
+
+===Kubernetes Service Account Token===
+eyJhbGciOiJSUzI1NiIsImtpZCI6InNhLXRva2VuLWtleS0xMjMifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0IiwianRpIjoiYWJjZGVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTAifQ.abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12
+
+===Ansible Vault Key===
+$ANSIBLE_VAULT;1.1;AES256$abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12
+
+===Terraform Cloud Token===
+abcdef1234567890ab.atlasv1.fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba09
+
+===HashiCorp Vault Token===
+hvs.abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab
+
+===SSH Private Key Content===
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7vbqajDw
+-----END PRIVATE KEY-----
+
+===Certificate Content===
+-----BEGIN CERTIFICATE-----
+MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF
+-----END CERTIFICATE-----
+
+===PGP Private Key===
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v2.0.14
+lQdGBF8a1234567890abcdef1234567890abcdef1234567890abcdef1234567890
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/anonymizer/testdata/dataset/financial.txt b/anonymizer/testdata/dataset/financial.txt
new file mode 100644
index 0000000..0511ac1
--- /dev/null
+++ b/anonymizer/testdata/dataset/financial.txt
@@ -0,0 +1,128 @@
+===Amex Card===
+371449635398431
+378282246310005
+
+===IBAN===
+GB82WEST12345698765432
+DE89370400440532013000
+
+===SWIFT Codes===
+CHASUS33XXX
+DEUTDE33XXX
+
+===BCGlobal===
+6541123456789012
+
+===Carte Blanche Card===
+38912345678901
+
+===Diners Club Card===
+30012345678901
+
+===Discover Card===
+6011123456789012
+
+===Insta Payment Card===
+6371234567890123
+
+===JCB Card===
+213112345678901
+
+===Korean Local Card===
+9123456789012345
+
+===Laser Card===
+630412345678901234
+
+===Maestro Card===
+501812345678901234
+
+===Solo Card===
+633412345678901234
+
+===Switch Card===
+490312345678901234567
+
+===Credit card - 3===
+4123 4567 8901 2345
+
+===credit_cards===
+4111-1111-1111-1111
+5555 5555 5555 4444
+
+===visa_cards===
+4123-4567-8901-2345
+
+===visa_credit_card===
+4111111111111111
+
+===master_cards===
+5555-5555-5555-4444
+
+===btc_addresses===
+1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2
+
+===Bank of America Routing Numbers - California===
+121000358
+026009593
+
+===Citibank Routing Numbers - California===
+322271724
+
+===facebook_secret===
+facebook_secret=abcdef1234567890abcdef1234567890;
+
+===github_key===
+github_token=1234567890abcdef1234567890abcdef12345678;
+
+===google_two_factor_backup===
+BACKUP VERIFICATION CODES
+Save these codes: user@example.com
+
+===heroku_key===
+heroku_api_key=12345678-1234-1234-1234-123456789012;
+
+===slack_api_key===
+slack_api_key=xoxb-4567890123456-4567890123456-DEFhijKLmnopQRSvwxYZABcd;
+
+===slack_api_token===
+xoxb-123456789012-123456789012-abcdef1234567890abcdef12-abc123
+
+===DataDog API Key===
+datadog_api_key=abcdef1234567890abcdef1234567890
+DATADOG-KEY=fedcba0987654321fedcba0987654321
+
+===New Relic License Key===
+newrelic_license_key=abcdef1234567890abcdef1234567890abcdef12
+NEW-RELIC-KEY=fedcba0987654321fedcba0987654321fedcba09
+
+===Sentry DSN===
+https://abcdef1234567890abcdef1234567890ab:fedcba0987654321fedcba0987654321@sentry.io/123456
+
+===PubNub Keys===
+pub-c-abcdef12-3456-7890-abcd-ef1234567890
+sub-c-fedcba09-8765-4321-fedc-ba0987654321
+
+===Pusher Key===
+pusher_key=abcdef1234567890abcd
+PUSHER-TOKEN=fedcba0987654321fedc
+
+===Plaid Secret===
+plaid_secret=abcdef1234567890abcdef12345678
+PLAID-KEY=fedcba0987654321fedcba09876543
+
+===Razorpay Key===
+rzp_test_abcdef1234567890
+rzp_live_fedcba0987654321098765
+
+===High Entropy Base64===
+secret=abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890==
+token=fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321==
+
+===High Entropy Hex===
+key=abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab
+secret=fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321
+
+===Mixed Case High Entropy===
+auth=abcdef1234567890ABCDEF1234567890abcdef1234567890ABCDEF1234567890
+credential=FEDCBA0987654321fedcba0987654321FEDCBA0987654321fedcba0987654321
diff --git a/anonymizer/testdata/dataset/insensitive.txt b/anonymizer/testdata/dataset/insensitive.txt
new file mode 100644
index 0000000..0663be4
--- /dev/null
+++ b/anonymizer/testdata/dataset/insensitive.txt
@@ -0,0 +1,790 @@
+===Command Line Arguments===
+--password-file /path/to/file
+--token-length 32
+--api-key-format base64
+--secret-rotation enabled
+--auth-method oauth2
+--key-size 2048
+mysql --password-prompt
+--bearer-token-validation
+--jwt-algorithm HS256
+--csrf-protection enabled
+--oauth-redirect-uri /callback
+--ssl-cert-path /etc/ssl/certs
+--private-key-file /etc/ssl/private
+--public-key-file /etc/ssl/public
+
+===Environment Variables===
+PASSWORD_MIN_LENGTH=8
+TOKEN_EXPIRY_HOURS=24
+API_KEY_ROTATION=daily
+SECRET_MANAGER_MODE=enabled
+AUTH_PROVIDER: google
+KEY_DERIVATION_TYPE=pbkdf2
+JWT_ISSUER=localhost
+CSRF_COOKIE_NAME=_csrf
+OAUTH_SCOPES=read,write
+SSL_VERIFY=true
+PRIVATE_KEY_BITS=4096
+PUBLIC_KEY_FORMAT=pem
+DATABASE_HOST=localhost
+REDIS_PORT=6379
+
+===Configuration Keys===
+password_field: "required"
+token_type: "bearer"
+api_key_version: "v1"
+secret_encryption: "enabled"
+auth: "enabled"
+key: "rsa-2048"
+bearer: "jwt"
+jwt: "hs256"
+csrf: "sameSite"
+oauth: "pkce"
+ssl: "tls1.3"
+private: "encrypted"
+public: "x509"
+database: "postgres"
+redis: "cluster"
+
+===File Paths===
+/etc/password
+/var/lib/tokens
+/opt/api-keys
+/home/user/.secrets
+/tmp/auth_service.log
+/etc/ssl/private
+/etc/ssl/public
+/var/log/bearer_service.log
+/etc/jwt_settings.conf
+/etc/csrf_config.conf
+/etc/oauth_providers.conf
+/var/lib/ssl
+/etc/private
+/etc/public
+/var/database
+/var/redis
+
+===URL Parameters===
+?password_reset=true
+?token_type=bearer
+?api_key_version=v2
+?secret_encrypted=yes
+?auth_required=false
+?key_algorithm=rsa
+?bearer_format=jwt
+?jwt_exp=3600
+?csrf_enabled=true
+?oauth_flow=code
+?ssl_required=true
+?private_data=false
+?public_api=true
+?database_ssl=required
+?redis_auth_required=none
+
+===Log Messages===
+password authentication successful
+token validation passed
+api key rotation scheduled
+secret decryption completed
+auth provider configured
+key generation finished
+bearer token issued
+jwt signature verified
+csrf token generated
+oauth flow initiated
+ssl handshake completed
+private key loaded
+public key exported
+database connection established
+redis cluster ready
+
+===Documentation Examples===
+The password field is required
+Use token for authentication
+API key must be valid
+Secret should be encrypted
+Auth is enabled by default
+Key size is 2048 bits
+Bearer tokens expire in 1 hour
+JWT uses HS256 algorithm
+CSRF protection is mandatory
+OAuth uses PKCE flow
+SSL certificates auto-renew
+Private keys are encrypted
+Public keys are in PEM format
+Database uses connection pooling
+Redis supports clustering
+
+===Code Comments===
+// password validation logic
+// token refresh mechanism
+// api key management
+// secret encryption service
+// auth middleware
+// key derivation function
+// bearer token parser
+// jwt token validator
+// csrf protection filter
+// oauth callback handler
+// ssl context setup
+// private key generator
+// public key exporter
+// database connection pool
+// redis client factory
+
+===Variable Names===
+passwordField
+tokenHeader
+apiKeyManager
+secretService
+authProvider
+keyGenerator
+bearerParser
+jwtValidator
+csrfFilter
+oauthHandler
+sslContext
+privateKeyGen
+publicKeyExp
+databasePool
+redisClient
+
+===Function Names===
+validatePassword()
+parseToken()
+rotateApiKey()
+encryptSecret()
+authenticateUser()
+generateKey()
+parseBearerToken()
+validateJWT()
+generateCSRF()
+handleOAuth()
+setupSSL()
+loadPrivateKey()
+exportPublicKey()
+connectDatabase()
+initRedis()
+
+===Class Names===
+PasswordValidator
+TokenManager
+ApiKeyRotator
+SecretEncryption
+AuthProvider
+KeyGenerator
+BearerTokenParser
+JWTValidator
+CSRFProtection
+OAuthHandler
+SSLContext
+PrivateKeyLoader
+PublicKeyExporter
+DatabaseConnection
+RedisClient
+
+===Error Messages===
+password is too short
+token has expired
+api key is invalid
+secret decryption failed
+auth provider unreachable
+key generation error
+bearer token malformed
+jwt signature invalid
+csrf token missing
+oauth state mismatch
+ssl handshake failed
+private key corrupted
+public key invalid
+database connection timeout
+redis cluster unavailable
+
+===Test Data===
+test-password-123
+mock-token-value
+fake-api-key-xyz
+dummy-secret-abc
+test-auth-provider
+sample-key-data
+example-bearer-token
+test-jwt-payload
+mock-csrf-value
+fake-oauth-state
+test-ssl-cert
+sample-private-key
+example-public-key
+test-database-name
+mock-redis-key
+
+===False Positive Examples===
+SECRET_MANAGER_ENABLED=true
+KEY_DERIVATION_ROUNDS=10000
+TRAVIS_CI_BUILD_NUMBER=01234567890123456789012345678901
+CIRCLE_CI_BUILD_ID=abcdef1234567890abcdef1234567890abcdef12
+JENKINS_BUILD_NUMBER=fedcba0987654321fedcba0987654321
+API_KEY_ROTATION_DAYS=30
+TOKEN_EXPIRY_SECONDS=3600
+HMAC_ALGORITHM=sha256
+AES_KEY_SIZE=256
+RSA_KEY_BITS=2048
+ELASTIC_SEARCH_TIMEOUT=30
+DATADOG_AGENT_VERSION=7.40.1
+NEWRELIC_APP_NAME=myapp
+PUSHER_CLUSTER=us2
+PLAID_ENVIRONMENT=sandbox
+
+===Build IDs and Numbers===
+BUILD_ID=abcdef1234567890abcdef1234567890abcdef12
+BUILD_NUMBER=fedcba0987654321fedcba0987654321
+DEPLOYMENT_ID=deploy-xyz789
+RELEASE_TAG=v1.2.3-abcdef1234567890
+VERSION_HASH=v-abc123def456
+COMMIT_SHORT=abcdef12
+BRANCH_NAME=feature/auth-improvements
+PIPELINE_ID=pipe-xyz789
+
+===Configuration Values===
+TIMEOUT_SECONDS=30
+RETRY_COUNT=3
+MAX_CONNECTIONS=100
+BUFFER_SIZE=8192
+CACHE_TTL=3600
+SESSION_DURATION=86400
+RATE_LIMIT=1000
+BATCH_SIZE=500
+CHUNK_SIZE=1024
+POOL_SIZE=10
+
+===File Extensions and Formats===
+file_key.txt
+certificate_pem.txt
+private_key.txt
+public_key.txt
+config_json.txt
+secrets_yaml.txt
+credentials_conf.txt
+auth_properties.txt
+token_data.txt
+api_xml.txt
+
+===Network and Infrastructure===
+SERVER_PORT=8080
+DATABASE_PORT=5432
+REDIS_PORT=6379
+ELASTICSEARCH_PORT=9200
+KAFKA_PORT=9092
+ZOOKEEPER_PORT=2181
+CONSUL_PORT=8500
+VAULT_PORT=8200
+PROMETHEUS_PORT=9090
+GRAFANA_PORT=3000
+
+===Algorithm and Encryption Names===
+ENCRYPTION_ALGORITHM=AES-256-GCM
+HASH_FUNCTION=SHA-256
+SIGNING_ALGORITHM=RS256
+KEY_DERIVATION_FUNCTION=PBKDF2
+CIPHER_MODE=CBC
+PADDING_SCHEME=PKCS1
+RANDOM_GENERATOR=SecureRandom
+DIGEST_ALGORITHM=SHA-512
+MAC_ALGORITHM=HMAC-SHA256
+COMPRESSION_TYPE=GZIP
+
+===Service Names and Identifiers===
+SERVICE_NAME=auth-service
+COMPONENT_ID=user-management
+MODULE_NAME=encryption-utils
+LIBRARY_VERSION=v2.1.0
+FRAMEWORK_NAME=spring-boot
+PROVIDER_TYPE=oauth2
+CLIENT_TYPE=web-application
+GRANT_TYPE=authorization_code
+RESPONSE_TYPE=code
+SCOPE_TYPE=read-write
+
+===UUID-like but Invalid===
+INSTANCE_ID=inst-12345
+SESSION_TYPE=sess
+REQUEST_ID=req-aaa
+TRACE_ID=trace-000
+SPAN_ID=span-555
+
+===Email-like but Invalid===
+service-account
+admin-user
+system-process
+daemon-service
+process-worker
+worker-thread
+manager-component
+scheduler-service
+monitor-agent
+logger-service
+
+===URL-like but Safe===
+/api/v1/health
+/metrics/prometheus
+/admin/dashboard
+/auth/callback
+/oauth/authorize
+/jwt/validate
+/csrf/generate
+/hmac/verify
+/aes/encrypt
+/rsa/decrypt
+
+===JSON-like Configuration===
+{"timeout": 30}
+{"retries": 3}
+{"enabled": true}
+{"algorithm": "sha256"}
+{"keySize": 2048}
+{"maxAge": 3600}
+{"secure": true}
+{"httpOnly": false}
+{"sameSite": "strict"}
+{"compression": "gzip"}
+
+===Command Line Flags===
+--timeout 30
+--retries 3
+--algorithm sha256
+--key-size 2048
+--max-age 3600
+--secure true
+--http-only false
+--same-site strict
+--compression gzip
+--verbose true
+
+===Database Table Names===
+users_table
+passwords_hash
+tokens_cache
+sessions_store
+api_keys_registry
+secrets_vault
+auth_providers
+oauth_clients
+jwt_blacklist
+csrf_tokens
+
+===Log Levels and Messages===
+INFO: password validation started
+DEBUG: token parsing completed
+WARN: api key rotation needed
+ERROR: secret decryption failed
+TRACE: auth provider contacted
+FATAL: key generation error
+AUDIT: bearer token issued
+SECURITY: jwt signature verified
+ACCESS: csrf token validated
+SYSTEM: oauth flow completed
+
+===Docker and Kubernetes===
+FROM alpine:3.18
+ENV NODE_ENV=production
+LABEL version=1.2.3
+EXPOSE 8080
+WORKDIR /app
+USER 1000:1000
+VOLUME /data
+ENTRYPOINT ["./app"]
+CMD ["--port", "8080"]
+HEALTHCHECK --interval=30s CMD curl -f /health
+
+===Kubernetes YAML===
+apiVersion: v1
+kind: Secret
+metadata:
+ name: my-secret
+ namespace: default
+spec:
+ type: Opaque
+data:
+ username: YWRtaW4=
+ password_hash: MWYyZDFlMmU2N2Rm
+
+===SQL Queries===
+SELECT * FROM users WHERE id = 12345;
+INSERT INTO tokens (value) VALUES ('abcdef1234567890abcdef1234567890abcdef12');
+UPDATE secrets SET encrypted = 'fedcba0987654321fedcba0987654321' WHERE id = 1;
+DELETE FROM sessions WHERE session_id = 'sess_abc123def456789012345678901234567890';
+CREATE TABLE api_keys (id INT, key_hash VARCHAR(64));
+
+===Git Commands===
+git commit -m "Add password validation"
+git push origin feature/token-refresh
+git checkout -b hotfix/api-key-rotation
+git tag v1.2.3-auth-improvements
+git log --grep="secret"
+git diff HEAD~1 -- config/secrets.yaml
+git blame auth/password.go
+git show abcdef1234567890abcdef1234567890abcdef12
+
+===Terraform Configuration===
+variable "password_length" {
+ type = number
+ default = 16
+}
+resource "aws_secretsmanager_secret" "example" {
+ name = "my-secret"
+}
+data "aws_secretsmanager_secret_version" "example" {
+ secret_id = aws_secretsmanager_secret.example.id
+}
+output "secret_arn" {
+ value = aws_secretsmanager_secret.example.arn
+}
+
+===Ansible Playbook===
+- name: Install password manager
+ apt:
+ name: pass
+ state: present
+- name: Configure API key rotation
+ cron:
+ name: "rotate keys"
+ minute: "0"
+ hour: "2"
+ job: "/usr/bin/rotate-keys.sh"
+- name: Set JWT secret
+ lineinfile:
+ path: /etc/app.conf
+ line: "jwt_secret_file=/etc/ssl/jwt.key"
+
+===Nginx Configuration===
+server {
+ listen 443 ssl;
+ server_name api-server;
+ ssl_certificate /etc/ssl/certs/api.crt;
+ ssl_certificate_key /etc/ssl/private/api.key;
+ ssl_session_timeout 5m;
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384;
+ location /api {
+ proxy_pass backend-service;
+ proxy_set_header Auth-Header $http_authorization;
+ }
+}
+
+===Apache Configuration===
+
+ ServerName secure-server
+ SSLEngine on
+ SSLCertificateFile /etc/ssl/certs/server.crt
+ SSLCertificateKeyFile /etc/ssl/private/server.key
+ SSLProtocol all -SSLv2 -SSLv3
+ SSLCipherSuite HIGH:!aNULL:!MD5
+ Header always set Strict-Transport-Security "max-age=3600"
+
+
+===Programming Language Examples===
+const API_KEY_LENGTH = 32;
+const TOKEN_EXPIRY = 3600;
+const SECRET_MIN_LENGTH = 16;
+const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/;
+const JWT_ALGORITHM = 'HS256';
+const HMAC_DIGEST = 'sha256';
+const AES_MODE = 'CBC';
+const RSA_PADDING = 'OAEP';
+function validatePassword(input) { return true; }
+class TokenManager { }
+interface SecretProvider { }
+
+===Monitoring and Metrics===
+cpu_usage_percent=75.5
+memory_usage_bytes=1024
+disk_space_available=2048
+network_bytes_sent=4096
+network_bytes_received=8192
+request_count_total=100000
+error_rate_percent=0.01
+response_time_ms=250
+uptime_seconds=86400
+active_connections=100
+
+===Cloud Provider Resource Names===
+arn:aws:s3:bucket-name
+arn:aws:iam::account:role/MyRole
+arn:aws:lambda:us-east-1:123456:function/MyFunction
+projects/my-project/secrets/my-secret/versions/1
+/subscriptions/sub-123/resourceGroups/myRG
+organizations/org-123/folders/folder-456/projects/proj-789
+
+===Certificate and Key File Names===
+server.crt
+client.key
+ca-bundle.pem
+intermediate.crt
+root-ca.key
+wildcard.crt
+self-signed.key
+dhparam.pem
+truststore.jks
+keystore.p12
+
+===Backup and Archive Names===
+backup-2023-12-01.tar.gz
+secrets-backup-abcdef1234567890.zip
+database-dump-fedcba0987654321.sql
+config-snapshot-2023-12-01.json
+keys-archive-987654321098765432109876543210.tar
+logs-2023-12-01-abcdef1234567890abcdef1234567890.gz
+
+===Process and Service Names===
+auth-service-v1.2.3
+token-validator-worker
+password-hasher-daemon
+secret-rotator-cron
+api-gateway-proxy
+oauth-provider-service
+jwt-issuer-component
+csrf-protection-middleware
+session-manager-redis
+encryption-service-aes
+
+===Fake Sensitive-Looking Data===
+FAKE_PASSWORD_FIELD=not-a-real-password
+MOCK_TOKEN_TYPE=test-token-value
+DUMMY_SECRET_NAME=example-secret-data
+TEST_API_KEY_FORMAT=fake-api-key-123
+SAMPLE_BEARER_TYPE=bearer-token-example
+EXAMPLE_JWT_FORMAT=jwt.payload.signature
+DEMO_CSRF_METHOD=csrf-token-demo
+PLACEHOLDER_OAUTH_TYPE=oauth-token-placeholder
+TEMPLATE_HMAC_ALGO=hmac-signature-template
+PROTOTYPE_AES_MODE=aes-key-prototype
+
+===XML and HTML Examples===
+
+
+ localhost
+ 5432
+ mydb
+
+
+ oauth2
+ 30
+
+
+
+ Auth Page
+
+
+
+
+
+===YAML Configuration Examples===
+database:
+ host: localhost
+ port: 5432
+ name: myapp
+ ssl: require
+auth:
+ provider: oauth2
+ timeout: 30s
+ secret_file: /etc/auth/secret
+logging:
+ level: info
+ format: json
+ output: /var/log/app.log
+
+===INI Configuration Examples===
+[database]
+host=localhost
+port=5432
+name=myapp
+ssl=require
+
+[auth]
+provider=oauth2
+timeout=30
+secret_file=/etc/auth/secret
+
+[logging]
+level=info
+format=json
+output=/var/log/app.log
+
+===TOML Configuration Examples===
+[database]
+host = "localhost"
+port = 5432
+name = "myapp"
+ssl = "require"
+
+[auth]
+provider = "oauth2"
+timeout = "30s"
+secret_file = "/etc/auth/secret"
+
+[logging]
+level = "info"
+format = "json"
+output = "/var/log/app.log"
+
+===Properties Files===
+database.host=localhost
+database.port=5432
+database.name=myapp
+database.ssl=require
+auth.provider=oauth2
+auth.timeout=30
+auth.secret.file=/etc/auth/secret
+logging.level=info
+logging.format=json
+logging.output=/var/log/app.log
+
+===Makefile Examples===
+DOCKER_IMAGE=myapp:latest
+DOCKER_TAG=v1.2.3
+BUILD_ARGS=--no-cache
+TEST_COMMAND=go test ./...
+DEPLOY_ENV=production
+SECRET_FILE=/etc/secrets/app.key
+CONFIG_FILE=/etc/config/app.yaml
+LOG_LEVEL=info
+PORT=8080
+TIMEOUT=30s
+
+===Systemd Service Files===
+[Unit]
+Description=My Application
+After=network.target
+
+[Service]
+Type=simple
+User=myapp
+Group=myapp
+WorkingDirectory=/opt/myapp
+ExecStart=/opt/myapp/bin/app --config /etc/myapp/config.yaml
+Restart=always
+RestartSec=5
+Environment=NODE_ENV=production
+Environment=LOG_LEVEL=info
+Environment=PORT=8080
+
+[Install]
+WantedBy=multi-user.target
+
+===Crontab Examples===
+0 2 * * * /usr/bin/backup-database.sh
+30 1 * * 0 /usr/bin/rotate-logs.sh
+0 0 1 * * /usr/bin/cleanup-temp.sh
+*/5 * * * * /usr/bin/health-check.sh
+0 6 * * 1 /usr/bin/update-certificates.sh
+
+===Package Manager Files===
+name: myapp
+version: 1.2.3
+description: My application
+author: developer
+license: MIT
+dependencies:
+ express: ^4.18.0
+ jsonwebtoken: ^9.0.0
+ bcrypt: ^5.1.0
+ dotenv: ^16.0.0
+devDependencies:
+ jest: ^29.0.0
+ eslint: ^8.0.0
+
+===License Headers===
+Copyright (c) 2023 My Company
+Licensed under the MIT License
+Permission is hereby granted, free of charge
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY
+SPDX-License-Identifier: MIT
+Apache License Version 2.0
+GNU General Public License v3.0
+
+===API Documentation Examples===
+GET /api/v1/users/{id}
+POST /api/v1/auth/login
+PUT /api/v1/users/{id}/password
+DELETE /api/v1/sessions/{session_id}
+PATCH /api/v1/tokens/{token_id}
+OPTIONS /api/v1/oauth/authorize
+HEAD /api/v1/health
+TRACE /api/v1/debug
+
+===HTTP Status Codes===
+200 OK
+201 Created
+400 Bad Request
+401 Unauthorized
+403 Forbidden
+404 Not Found
+429 Too Many Requests
+500 Internal Server Error
+502 Bad Gateway
+503 Service Unavailable
+
+===Regex Patterns in Code===
+/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
+/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/
+/^\d{3}-\d{2}-\d{4}$/
+/^[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}([A-Z0-9]?){0,16}$/
+/^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/
+/^0x[a-fA-F0-9]{40}$/
+/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
+
+===Test Framework Examples===
+describe('Password validation', () => {
+ it('should validate strong passwords', () => {
+ expect(validatePassword('StrongPass123!')).toBe(true);
+ });
+ it('should reject weak passwords', () => {
+ expect(validatePassword('weak')).toBe(false);
+ });
+});
+
+@Test
+public void testTokenValidation() {
+ String token = "test-token-12345";
+ assertTrue(tokenValidator.isValid(token));
+}
+
+func TestAPIKeyRotation(t *testing.T) {
+ key := "test-api-key-67890"
+ assert.True(t, isValidAPIKey(key))
+}
+
+===OpenAPI Specification===
+openapi: 3.0.0
+info:
+ title: My API
+ version: 1.2.3
+paths:
+ /auth/login:
+ post:
+ summary: User login
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ username:
+ type: string
+ password_field:
+ type: string
+ format: password
+ responses:
+ '200':
+ description: Login successful
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ token:
+ type: string
+ format: jwt
diff --git a/anonymizer/testdata/dataset/network.txt b/anonymizer/testdata/dataset/network.txt
new file mode 100644
index 0000000..3ff9114
--- /dev/null
+++ b/anonymizer/testdata/dataset/network.txt
@@ -0,0 +1,91 @@
+===IPv4 Address===
+192.168.1.1
+10.0.0.50
+172.16.0.100
+203.0.113.1
+
+===IPv6 Address===
+2001:0db8:85a3:0000:0000:8a2e:0370:7334
+2001:db8:85a3::8a2e:370:7334
+::1
+::ffff:192.0.2.1
+
+===Private IP Range===
+10.0.0.1
+172.16.0.1
+192.168.1.100
+
+===Domain Name Enhanced===
+google.com
+api.example.com
+subdomain.example.org
+example.co.uk
+xn--nxasmq6b.xn--j6w193g
+
+===HTTP/HTTPS URL===
+https://api.example.com/v1/users
+http://localhost:8080/api
+https://github.com/user/repo
+https://example.com/path?query=value
+
+===WS URL===
+wss://api.example.com/websocket
+ws://localhost:3000/socket
+
+===FTP URL===
+ftp://files.example.com/uploads
+sftp://secure.example.com/data
+ftps://encrypted.example.com/files
+
+===SSH URL===
+ssh://user@server.example.com:22
+
+===VNC URL===
+vnc://192.168.1.100:5900
+
+===RDP URL===
+rdp://server.company.com:3389
+
+===Telnet URL===
+telnet://router.local:23
+
+===MAIL URL===
+smtp://mail.example.com:587
+pop3://mail.example.com:110
+imap://mail.example.com:993
+
+===SMB URL===
+smb://fileserver.local/share
+
+===NFS URL===
+nfs://storage.local/exports
+
+===MAC Address Standard===
+00:1B:44:11:3A:B7
+AA:BB:CC:DD:EE:FF
+
+===MAC Address Cisco Format===
+001b.4411.3ab7
+aabb.ccdd.eeff
+
+===Generic Webhook URL===
+https://api.example.com/webhook/abc123
+https://hooks.slack.com/webhook/xyz456
+
+===Generic Callback URL===
+https://oauth.example.com/callback
+https://app.example.com/callback/auth
+
+===Microsoft Teams Webhook===
+https://outlook.office.com/webhook/12345678-1234-1234-1234-123456789012@abcdef12-3456-7890-abcd-ef1234567890/IncomingWebhook/abcdef1234567890abcdef1234567890ab/12345678-1234-1234-1234-123456789012
+
+===Discord Webhook===
+https://discord.com/api/webhooks/123456789012345678/abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12
+https://discordapp.com/api/webhooks/987654321098765432/fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321
+
+===Zapier Webhook===
+https://hooks.zapier.com/hooks/catch/123456/abcdef/
+https://hooks.zapier.com/hooks/catch/789012/xyz789/
+
+===Zoho Webhook===
+https://creator.zoho.com/api/v2/my-app/report/All_Records?authtoken=abcdef1234567890abcdef12
diff --git a/anonymizer/testdata/dataset/paths.txt b/anonymizer/testdata/dataset/paths.txt
new file mode 100644
index 0000000..b80f3bf
--- /dev/null
+++ b/anonymizer/testdata/dataset/paths.txt
@@ -0,0 +1,29 @@
+===SSH Connection===
+user@server.example.com
+admin@192.168.1.100:22
+deploy@production.company.com:2222
+
+===Database Connection Generic===
+mysql://user:password@localhost:3306/database
+postgresql://admin:secret@db.example.com:5432/mydb
+mongodb://user:pass@mongo.example.com:27017/app
+redis://user:pass@cache.example.com:6379
+mysql://admin:secret@db.example.com:3306/mydb
+postgres://admin:secret@db.example.com:5432/mydb
+redis://admin:secret@cache.example.com:6379
+
+===JDBC URL===
+jdbc:mysql://localhost:3306/database
+jdbc:postgresql://db.example.com:5432/mydb
+
+===Redis Auth Token===
+redis://:abcdef1234567890abcdef1234567890@redis.example.com:6379
+redis://:fedcba0987654321fedcba0987654321@cache.local:6379
+
+===MongoDB Connection with Auth===
+mongodb://user:password123@mongo.example.com:27017/database
+mongodb+srv://admin:secretpass@cluster.mongodb.net/myapp?retryWrites=true&w=majority
+
+===Elasticsearch API Key===
+elastic_api_key=abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef==
+ELASTICSEARCH_API_KEY=fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321==
diff --git a/anonymizer/testdata/dataset/personal.txt b/anonymizer/testdata/dataset/personal.txt
new file mode 100644
index 0000000..3739ece
--- /dev/null
+++ b/anonymizer/testdata/dataset/personal.txt
@@ -0,0 +1,159 @@
+===Email Address===
+user@example.com
+admin@company.org
+test.email+tag@domain.co.uk
+
+===Email Addresses===
+support@example.com
+info@company.org
+contact@domain.net
+
+===US Phone Numbers===
+(555) 123-4567
+555-123-4567
++1-555-123-4567
+
+===Large number of US Phone Numbers===
+555-123-4567
+(555) 123-4567
+
+===Argentina National Identity (DNI) Number===
+12.345.678
+98.765.432
+
+===Canada Passport ID===
+AB123456
+XY987654
+
+===Germany Passport ID===
+C123ABC567
+F456DEF890
+G789GHI123
+K234QR6678
+C567ST9901
+F890UV2234
+
+===France Passport ID===
+121112345
+
+===UK Passport Number===
+1234567890GBR1234567U123456789
+9876543210GBP7654321M987654321
+
+===California Drivers License===
+A1234567
+B9876543
+
+===UK Drivers License Numbers===
+MORGA657054SM9IJ
+SMITH123456AB1CD
+
+===GPS Data===
++40.7128,-74.0060
+-33.8688,151.2093
+
+===Tax Number===
+1234567890
+9876543210
+
+===Arista network configuration===
+via 192.168.1.1, 10:30:45
+
+===CVE Number===
+CVE-2021-1234
+CVE-2023-12345
+
+===France National ID card (CNI)===
+123456789012
+
+===Germany Driver's License ID===
+55667788990
+99001122334
+
+===Germany ID card number===
+L12345678
+L44556677
+
+===HIPAA PHI National Drug Code===
+12345-123-12
+1234-1234-1
+
+===Individual Taxpayer Identification Numbers (ITIN)===
+912-70-1234
+912 78 1234
+
+===Ireland Personal Public Service (PPS) Number===
+1234567A
+1234567AB
+
+===Poland National ID (PESEL)===
+12345678901
+
+===Spain Social Security Number (SSN)===
+12/12345678/12
+12/12345678/12
+
+===Spain Social Security Number (SSN) - 2===
+123 45 6789
+123-45-6789
+
+===Sweden Passport ID===
+12345678
+
+===United Kingdom Driver's license ID===
+MORGA123456AB12345
+
+===United Kingdom National Health Service (NHS) number===
+123 456 7890
+
+===United Kingdom Passport ID===
+123456789
+
+===Passport Number (simplified) - 3===
+A1234567
+
+===isbn10===
+1234567890
+123-4-56789-0
+
+===isbn13===
+1234567890123
+1-2-3-4-5-6-7-8-9-0-1-2-3
+
+===phones===
++1 (555) 123-4567
+555-123-4567
+
+===ssn - 3===
+123-45-6789
+
+===ssn_number===
+123-45-6789
+
+===ssn_number - 3===
+123-45-6789
+
+===Social Security Number (SSN) - 3===
+123-45-6789
+
+===Social Security Number (SSN) - 4===
+123-45-6789
+
+===Croatia Vat ID card number===
+HR12345678901
+
+===Czech Republic Vat ID card number===
+CZ12345678
+CZ1234567890
+
+===Driver's License Number (simplified)===
+AB-123456
+XY-987654
+
+===Google Application Identifier===
+123456789012-abcdef1234567890.apps.googleusercontent.com
+987654321098-xyz123456789abcd.apps.googleusercontent.com
+
+===MasterCard===
+5555555555554444
+5105105105105100
diff --git a/anonymizer/testdata/dataset/services.txt b/anonymizer/testdata/dataset/services.txt
new file mode 100644
index 0000000..43e1f18
--- /dev/null
+++ b/anonymizer/testdata/dataset/services.txt
@@ -0,0 +1,61 @@
+===HTTP Password===
+--http-passwd=http_auth_password
+
+===Htpasswd Password===
+htpasswd -b /etc/passwd username secretpassword
+
+===Jupyter Notebook Token===
+--NotebookApp.token="abcdef1234567890abcdef1234567890ab"
+
+===MariaDB Password===
+--set mariadbPassword=db_secret_pass
+
+===Recovery Password===
+-recoveryPassword=recovery_secret_123
+
+===Sensor Password===
+--sensor-password sensor_pass_123
+
+===Twine Password===
+TWINE_PASSWORD=pypi_upload_password
+
+===Key File Echo===
+--key-file <(echo -n "secret_key_content")
+
+===Repository Password===
+-Drepository.password=maven_repo_pass
+
+===SSH Password Parameter===
+--ssh_password ssh_secret_pass
+
+===Auth Credential Pattern===
+-auth user:password123
+
+===User Credential Pattern===
+-u admin:secret_pass
+
+===Embedding Hash===
+-Embedding abcdef1234567890abcdef1234567890ab
+
+===Echo Password Pattern===
+echo "secret_password" | su -
+
+===GitHub Token===
+ghp_1234567890abcdef1234567890abcdef123456
+gho_abcdef1234567890abcdef1234567890abcd
+ghu_fedcba0987654321fedcba0987654321fedcba0987654321abcdef
+ghs_1a2b3c4d5e6f7890abcdef1234567890abcdef
+ghr_abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12
+
+===Slack Token===
+xoxb-123456789012-123456789012-abcdefghijklmnopqrstuvwx
+xoxp-1234567890123-1234567890123-1234567890123-abcdef1234567890abcdef1234567890ab
+xoxa-2-123456789012-123456789012-abcdefghijklmnopqrstuvwxyz123456
+
+===Discord Bot Token===
+NzkyNzE1NDk0NzI1MjI4NTU0.X-hvzA.Ovy4MCQywSkoMRRclStW4xAYK7I
+ODc4MzgxNzQyMjEyMzQ1NjY3.YR5tQw.K7BhHNt1dfMcEx1vGOItKQYyNqE
+
+===Telegram Bot Token===
+1234567890:AAEhBOwweL4XGCvbdQMRcEtjv0Bk_8jLQg0
+987654321:AAF5OZVpEuFjmEO_f3g1BQNkNk-fqFZEQg0
diff --git a/anonymizer/testdata/testdata.go b/anonymizer/testdata/testdata.go
new file mode 100644
index 0000000..15a1c6c
--- /dev/null
+++ b/anonymizer/testdata/testdata.go
@@ -0,0 +1,234 @@
+package testdata
+
+import (
+ _ "embed"
+ "math/rand"
+ "regexp"
+ "strings"
+)
+
+//go:embed dataset/credentials.txt
+var credentialsData []byte
+
+//go:embed dataset/financial.txt
+var financialData []byte
+
+//go:embed dataset/network.txt
+var networkData []byte
+
+//go:embed dataset/personal.txt
+var personalData []byte
+
+//go:embed dataset/crypto.txt
+var cryptoData []byte
+
+//go:embed dataset/cloud.txt
+var cloudData []byte
+
+//go:embed dataset/services.txt
+var servicesData []byte
+
+//go:embed dataset/paths.txt
+var pathsData []byte
+
+//go:embed dataset/config.txt
+var configData []byte
+
+//go:embed dataset/insensitive.txt
+var insensitiveData []byte
+
+type TestDataEntry struct {
+ Name string
+ Examples string // multiline string with examples
+}
+
+type TestDataset struct {
+ Category string
+ Raw string // entire file content
+ Entries []TestDataEntry
+}
+
+// parseTestDataFile parses test data in format: ===name=== followed by examples
+func parseTestDataFile(data []byte, category string) (*TestDataset, error) {
+ content := string(data)
+ lines := strings.Split(content, "\n")
+
+ dataset := &TestDataset{
+ Category: category,
+ Raw: content,
+ }
+ var currentEntry *TestDataEntry
+ var currentExamples []string
+
+ namePattern := regexp.MustCompile(`^===(.+)===\s*$`)
+
+ for _, line := range lines {
+ line = strings.TrimSpace(line)
+
+ if matches := namePattern.FindStringSubmatch(line); matches != nil {
+ if currentEntry != nil {
+ currentEntry.Examples = strings.Join(currentExamples, "\n")
+ dataset.Entries = append(dataset.Entries, *currentEntry)
+ }
+ currentEntry = &TestDataEntry{
+ Name: matches[1],
+ }
+ currentExamples = make([]string, 0)
+ } else if line != "" && currentEntry != nil {
+ currentExamples = append(currentExamples, line)
+ }
+ }
+
+ if currentEntry != nil {
+ currentEntry.Examples = strings.Join(currentExamples, "\n")
+ dataset.Entries = append(dataset.Entries, *currentEntry)
+ }
+
+ return dataset, nil
+}
+
+func LoadCredentialsData() (*TestDataset, error) {
+ return parseTestDataFile(credentialsData, "credentials")
+}
+
+func LoadFinancialData() (*TestDataset, error) {
+ return parseTestDataFile(financialData, "financial")
+}
+
+func LoadNetworkData() (*TestDataset, error) {
+ return parseTestDataFile(networkData, "network")
+}
+
+func LoadPersonalData() (*TestDataset, error) {
+ return parseTestDataFile(personalData, "personal")
+}
+
+func LoadCryptoData() (*TestDataset, error) {
+ return parseTestDataFile(cryptoData, "crypto")
+}
+
+func LoadCloudData() (*TestDataset, error) {
+ return parseTestDataFile(cloudData, "cloud")
+}
+
+func LoadServicesData() (*TestDataset, error) {
+ return parseTestDataFile(servicesData, "services")
+}
+
+func LoadPathsData() (*TestDataset, error) {
+ return parseTestDataFile(pathsData, "paths")
+}
+
+func LoadConfigData() (*TestDataset, error) {
+ return parseTestDataFile(configData, "config")
+}
+
+func LoadInsensitiveData() (*TestDataset, error) {
+ return parseTestDataFile(insensitiveData, "insensitive")
+}
+
+func LoadAllTestData() ([]*TestDataset, error) {
+ loaders := []func() (*TestDataset, error){
+ LoadCredentialsData,
+ LoadFinancialData,
+ LoadNetworkData,
+ LoadPersonalData,
+ LoadCryptoData,
+ LoadCloudData,
+ LoadServicesData,
+ LoadPathsData,
+ LoadConfigData,
+ }
+
+ datasets := make([]*TestDataset, 0, len(loaders))
+ for _, loader := range loaders {
+ dataset, err := loader()
+ if err != nil {
+ return nil, err
+ }
+ datasets = append(datasets, dataset)
+ }
+
+ return datasets, nil
+}
+
+func GenerateRandomString(rng *rand.Rand, minLen, maxLen int) string {
+ length := rng.Intn(maxLen-minLen+1) + minLen
+ charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 !@#$%^&*()_+-=[]{}|;':\",./<>?"
+
+ var sb strings.Builder
+ sb.Grow(length)
+ for i := 0; i < length; i++ {
+ sb.WriteByte(charset[rng.Intn(len(charset))])
+ }
+ return sb.String()
+}
+
+func GenerateTestStrings(seed int64, count, minLen, maxLen int) []string {
+ rng := rand.New(rand.NewSource(seed))
+ strings := make([]string, count)
+ for i := 0; i < count; i++ {
+ strings[i] = GenerateRandomString(rng, minLen, maxLen)
+ }
+ return strings
+}
+
+func GenerateRegexPatterns(seed int64, count int) ([]string, []string) {
+ rng := rand.New(rand.NewSource(seed))
+
+ patterns := []string{
+ `\b[a-f0-9]{32}\b`,
+ `\b[A-F0-9]{40}\b`,
+ `\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b`,
+ `password=([^\s]+)`,
+ `-p\s+([^\s]+)`,
+ `--token\s+([a-zA-Z0-9]+)\b`,
+ `api[_-]?key[=:]\s*([a-zA-Z0-9]+)`,
+ `secret[=:]\s*([^\s]+)`,
+ `auth[=:]\s*([^\s]+)`,
+ `token[=:]\s*([a-zA-Z0-9]+)`,
+ }
+ names := []string{
+ "md5hash",
+ "sha1hash",
+ "jwt",
+ "password",
+ "cmd_passwd",
+ "token",
+ "api_key",
+ "secret",
+ "auth",
+ "token",
+ }
+
+ basePatterns := make([]string, count)
+ basePatternNames := make([]string, count)
+ for i := 0; i < count; i++ {
+ if i < len(patterns) {
+ basePatterns[i] = patterns[i]
+ basePatternNames[i] = names[i]
+ } else {
+ // Generate variations
+ idx := rng.Intn(len(patterns))
+ basePatterns[i] = patterns[idx]
+ basePatternNames[i] = names[idx]
+ }
+ }
+
+ return basePatterns, basePatternNames
+}
+
+func CreateTestData() map[string]string {
+ return map[string]string{
+ "password_command": "mysql -u user -p secretpass123 -h localhost",
+ "api_key": "api_key=abc123def456 config.json",
+ "token_flag": "--token mytoken123 --verbose",
+ "email": "user@example.com sent notification",
+ "hash_md5": "file hash: a1b2c3d4e5f6789012345678901234567890abcd",
+ "hash_sha1": "commit: 1234567890abcdef1234567890abcdef12345678",
+ "multiple_secrets": "password=secret123 --token=abc456 api_key=xyz789",
+ "no_match": "this string has no sensitive data",
+ "empty": "",
+ "special_chars": "test!@#$%^&*()_+-=[]{}|;':\",./<>?",
+ }
+}
diff --git a/anonymizer/testdata/testdata_test.go b/anonymizer/testdata/testdata_test.go
new file mode 100644
index 0000000..42ee18a
--- /dev/null
+++ b/anonymizer/testdata/testdata_test.go
@@ -0,0 +1,196 @@
+package testdata
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/vxcontrol/cloud/anonymizer/patterns"
+)
+
+func TestLoadAllTestData(t *testing.T) {
+ datasets, err := LoadAllTestData()
+ if err != nil {
+ t.Fatalf("failed to load test data: %v", err)
+ }
+
+ if len(datasets) == 0 {
+ t.Fatal("no datasets loaded")
+ }
+
+ expectedCategories := []string{
+ "credentials", "financial", "network", "personal",
+ "crypto", "cloud", "services", "paths", "config",
+ }
+
+ if len(datasets) != len(expectedCategories) {
+ t.Errorf("expected %d categories, got %d", len(expectedCategories), len(datasets))
+ }
+
+ totalEntries := 0
+ totalExamples := 0
+
+ for _, dataset := range datasets {
+ if dataset.Category == "" {
+ t.Error("dataset category should not be empty")
+ }
+
+ if len(dataset.Entries) == 0 {
+ t.Errorf("dataset %s should have entries", dataset.Category)
+ }
+
+ for _, entry := range dataset.Entries {
+ if entry.Name == "" {
+ t.Errorf("entry name should not be empty in category %s", dataset.Category)
+ }
+ if entry.Examples == "" {
+ t.Errorf("entry %s should have examples", entry.Name)
+ }
+ exampleLines := strings.Split(entry.Examples, "\n")
+ totalExamples += len(exampleLines)
+ }
+
+ totalEntries += len(dataset.Entries)
+ }
+
+ t.Logf("loaded %d datasets with %d entries and %d examples",
+ len(datasets), totalEntries, totalExamples)
+}
+
+func TestIndividualDatasetLoaders(t *testing.T) {
+ loaders := map[string]func() (*TestDataset, error){
+ "credentials": LoadCredentialsData,
+ "financial": LoadFinancialData,
+ "network": LoadNetworkData,
+ "personal": LoadPersonalData,
+ "crypto": LoadCryptoData,
+ "cloud": LoadCloudData,
+ "services": LoadServicesData,
+ "paths": LoadPathsData,
+ "config": LoadConfigData,
+ "insensitive": LoadInsensitiveData,
+ }
+
+ for name, loader := range loaders {
+ t.Run(name, func(t *testing.T) {
+ dataset, err := loader()
+ if err != nil {
+ t.Fatalf("failed to load %s data: %v", name, err)
+ }
+
+ if dataset.Category != name {
+ t.Errorf("expected category %s, got %s", name, dataset.Category)
+ }
+
+ if len(dataset.Entries) == 0 {
+ t.Errorf("%s dataset should have entries", name)
+ }
+ })
+ }
+}
+
+func TestDatasetDuplicates(t *testing.T) {
+ datasets, err := LoadAllTestData()
+ if err != nil {
+ t.Fatalf("failed to load test data: %v", err)
+ }
+
+ testPatterns := make(map[string]struct{})
+ for _, dataset := range datasets {
+ for _, entry := range dataset.Entries {
+ if _, ok := testPatterns[entry.Name]; ok {
+ t.Errorf("duplicate pattern %s in %s", entry.Name, dataset.Category)
+ }
+ testPatterns[entry.Name] = struct{}{}
+ }
+ }
+}
+
+func TestPatternCoverage(t *testing.T) {
+ datasets, err := LoadAllTestData()
+ if err != nil {
+ t.Fatalf("failed to load test data: %v", err)
+ }
+
+ testPatterns := make(map[string]struct{})
+ for _, dataset := range datasets {
+ for _, entry := range dataset.Entries {
+ testPatterns[entry.Name] = struct{}{}
+ }
+ }
+
+ allPatterns, err := patterns.LoadPatterns(patterns.PatternListTypeAll)
+ if err != nil {
+ t.Fatalf("failed to load all patterns: %v", err)
+ }
+ allPatternNames := make(map[string]struct{})
+ for _, pattern := range allPatterns.Patterns {
+ allPatternNames[pattern.Name] = struct{}{}
+ }
+
+ var uncoveredPatterns []string
+
+ for _, pattern := range allPatterns.Patterns {
+ if _, ok := testPatterns[pattern.Name]; !ok {
+ uncoveredPatterns = append(uncoveredPatterns, "general: "+pattern.Name)
+ }
+ }
+
+ if len(uncoveredPatterns) > 0 {
+ t.Errorf("uncovered patterns found (%d):", len(uncoveredPatterns))
+ for _, pattern := range uncoveredPatterns {
+ t.Logf(" - %s", pattern)
+ }
+ t.Error("please add test data for uncovered patterns")
+ } else {
+ t.Log("100% required patterns covered")
+ }
+
+ totalPatterns := len(allPatterns.Patterns)
+ coveredPatterns := len(testPatterns)
+ coveragePercent := float64(coveredPatterns) / float64(totalPatterns) * 100
+
+ t.Logf("pattern coverage: %d/%d (%.1f%%)",
+ coveredPatterns, totalPatterns, coveragePercent)
+
+ if coveragePercent != 100.0 {
+ t.Errorf("pattern coverage too low: %.1f%% (expected 100%%)", coveragePercent)
+ }
+
+ for _, dataset := range datasets {
+ for _, entry := range dataset.Entries {
+ if _, ok := allPatternNames[entry.Name]; !ok {
+ t.Errorf("pattern '%s' in %s is not covered", entry.Name, dataset.Category)
+ }
+ }
+ }
+}
+
+func TestDataFormat(t *testing.T) {
+ datasets, err := LoadAllTestData()
+ if err != nil {
+ t.Fatalf("failed to load test data: %v", err)
+ }
+
+ for _, dataset := range datasets {
+ // check that raw content exists
+ if dataset.Raw == "" {
+ t.Errorf("dataset %s should have raw content", dataset.Category)
+ }
+
+ for _, entry := range dataset.Entries {
+ if entry.Examples == "" {
+ t.Errorf("entry %s in %s has no examples", entry.Name, dataset.Category)
+ continue
+ }
+
+ // check that examples are not empty when split by lines
+ exampleLines := strings.Split(entry.Examples, "\n")
+ for i, example := range exampleLines {
+ if strings.TrimSpace(example) == "" {
+ t.Errorf("empty example at line %d for %s in %s",
+ i, entry.Name, dataset.Category)
+ }
+ }
+ }
+ }
+}
diff --git a/anonymizer/wrapper.go b/anonymizer/wrapper.go
new file mode 100644
index 0000000..75021ff
--- /dev/null
+++ b/anonymizer/wrapper.go
@@ -0,0 +1,68 @@
+package anonymizer
+
+import (
+ "errors"
+ "io"
+)
+
+const (
+ chunkSize = 8 * 1024 // 8KB - chunk size for reading from source
+ overlapSize = 1024 // 1KB - overlap size for pattern matching across chunk boundaries
+)
+
+type wrapper struct {
+ buffer []byte // buffer with processed data ready for output
+ replacer Replacer // replacer for masking sensitive data
+ reader io.Reader // source data reader
+}
+
+func newWrapper(reader io.Reader, replacer Replacer) io.Reader {
+ return &wrapper{
+ buffer: make([]byte, 0, chunkSize),
+ replacer: replacer,
+ reader: reader,
+ }
+}
+
+func (w *wrapper) Read(p []byte) (n int, err error) {
+ // if buffer has enough data for output + preserving overlap area
+ if len(w.buffer) >= len(p)+overlapSize {
+ n = copy(p, w.buffer)
+
+ // shift buffer left, keeping last overlapSize bytes for next read
+ // this ensures pattern matching across chunk boundaries
+ copy(w.buffer, w.buffer[n:])
+ w.buffer = w.buffer[:len(w.buffer)-n]
+
+ return n, nil
+ }
+
+ // read additional data from source
+ chunk := make([]byte, max(len(p)+overlapSize-len(w.buffer), chunkSize))
+ for len(w.buffer) < len(p)+overlapSize {
+ n, err = w.reader.Read(chunk)
+ if n != 0 {
+ // append new data to existing buffer
+ w.buffer = append(w.buffer, chunk[:n]...)
+ // apply replacer to entire buffer - necessary for pattern matching
+ // at the junction of old data (from overlap) and new data
+ w.buffer = w.replacer.ReplaceBytes(w.buffer)
+ }
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ return 0, err
+ }
+ }
+ if len(w.buffer) == 0 {
+ return 0, io.EOF
+ }
+
+ // return data to user, keeping overlap area in buffer
+ n = copy(p, w.buffer)
+ copy(w.buffer, w.buffer[n:])
+ w.buffer = w.buffer[:len(w.buffer)-n]
+
+ return n, nil
+}
diff --git a/anonymizer/wrapper_bench_test.go b/anonymizer/wrapper_bench_test.go
new file mode 100644
index 0000000..56143e3
--- /dev/null
+++ b/anonymizer/wrapper_bench_test.go
@@ -0,0 +1,275 @@
+package anonymizer
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "math/rand"
+ "strings"
+ "testing"
+
+ "github.com/vxcontrol/cloud/anonymizer/patterns"
+ "github.com/vxcontrol/cloud/anonymizer/testdata"
+)
+
+func BenchmarkWrapper_WithRealReplacer(b *testing.B) {
+ datasets, err := testdata.LoadAllTestData()
+ if err != nil {
+ b.Fatalf("failed to load test data: %v", err)
+ }
+
+ insensitiveDataset, err := testdata.LoadInsensitiveData()
+ if err != nil {
+ b.Fatalf("failed to load insensitive data: %v", err)
+ }
+
+ allLoadedPatterns, err := patterns.LoadPatterns(patterns.PatternListTypeAll)
+ if err != nil {
+ b.Fatalf("failed to load all patterns: %v", err)
+ }
+
+ replacer, err := NewReplacer(allLoadedPatterns.Regexes(), allLoadedPatterns.Names())
+ if err != nil {
+ b.Fatalf("failed to create replacer: %v", err)
+ }
+
+ rng := rand.New(rand.NewSource(1234567))
+ getWrappedDataset := func() (string, string) {
+ prefixIdx := rng.Intn(len(insensitiveDataset.Entries))
+ suffixIdx := rng.Intn(len(insensitiveDataset.Entries))
+ prefix := insensitiveDataset.Entries[prefixIdx].Examples
+ suffix := insensitiveDataset.Entries[suffixIdx].Examples
+ prefixLines := strings.Split(prefix, "\n")
+ suffixLines := strings.Split(suffix, "\n")
+ prefixLineIdx := rng.Intn(len(prefixLines))
+ suffixLineIdx := rng.Intn(len(suffixLines))
+ prefix = prefixLines[prefixLineIdx]
+ suffix = suffixLines[suffixLineIdx]
+ return prefix, suffix
+ }
+
+ testDataBuf := bytes.NewBuffer(nil)
+ for testDataBuf.Len() < 100*1024 { // at least 100KB
+ for _, dataset := range datasets {
+ for _, entry := range dataset.Entries {
+ prefix, suffix := getWrappedDataset()
+ testDataBuf.WriteString(prefix)
+ testDataBuf.WriteString(entry.Examples)
+ testDataBuf.WriteString(suffix)
+ testDataBuf.WriteString(" ")
+ }
+ testDataBuf.WriteString("\n")
+ }
+ }
+
+ testData := testDataBuf.Bytes()
+ chunkSizes := []int{1024, 4 * 1024, 8 * 1024, 16 * 1024, 32 * 1024, 64 * 1024}
+
+ for _, size := range chunkSizes {
+ b.Run(fmt.Sprintf("single_thread_reader_%dKB", size/1024), func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ wrapper := replacer.WrapReader(bytes.NewReader(testData))
+ buf := make([]byte, size)
+ for {
+ _, err := wrapper.Read(buf)
+ if err == io.EOF {
+ break
+ }
+ }
+ }
+
+ b.ReportMetric(float64(size), "bytes/op")
+ b.ReportMetric(float64(size*b.N)/float64(b.Elapsed().Seconds()), "bytes/sec")
+ })
+ }
+
+ b.Run("single_thread_reader", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ wrapper := replacer.WrapReader(bytes.NewReader(testData))
+ io.Copy(io.Discard, wrapper)
+ }
+
+ b.ReportMetric(float64(len(testData)), "bytes/op")
+ b.ReportMetric(float64(len(testData)*b.N)/float64(b.Elapsed().Seconds()), "bytes/sec")
+ })
+
+ b.Run("parallel_reader", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ wrapper := replacer.WrapReader(bytes.NewReader(testData))
+ io.Copy(io.Discard, wrapper)
+ }
+ })
+
+ b.ReportMetric(float64(len(testData)), "bytes/op")
+ b.ReportMetric(float64(len(testData)*b.N)/float64(b.Elapsed().Seconds()), "bytes/sec")
+ })
+
+ b.Run("single_thread_replacer", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ _ = replacer.ReplaceBytes(testData)
+ }
+
+ b.ReportMetric(float64(len(testData)), "bytes/op")
+ b.ReportMetric(float64(len(testData)*b.N)/float64(b.Elapsed().Seconds()), "bytes/sec")
+ })
+
+ b.Run("parallel_replacer", func(b *testing.B) {
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ _ = replacer.ReplaceBytes(testData)
+ }
+ })
+
+ b.ReportMetric(float64(len(testData)), "bytes/op")
+ b.ReportMetric(float64(len(testData)*b.N)/float64(b.Elapsed().Seconds()), "bytes/sec")
+ })
+}
+
+func BenchmarkWrapper_MemoryEfficiency(b *testing.B) {
+ // test memory usage with large data and small reads
+ testData := strings.Repeat("memory efficiency test data ", 50000) // ~1.4MB
+ replacer := &mockReplacer{}
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+ buf := make([]byte, 1024) // read in small chunks
+ for {
+ _, err := wrapper.Read(buf)
+ if err == io.EOF {
+ break
+ }
+ }
+ }
+
+ b.ReportMetric(float64(len(testData)), "bytes/op")
+ b.ReportMetric(float64(len(testData)*b.N)/float64(b.Elapsed().Seconds()), "bytes/sec")
+}
+
+func BenchmarkWrapper_ReplacementIntensive(b *testing.B) {
+ // benchmark with data that has many patterns to replace
+ testData := strings.Repeat("password=secret123 token=abc456 api_key=def789 ", 1000)
+ patterns := []string{`password=([^\s]+)`, `token=([^\s]+)`, `api_key=([^\s]+)`}
+ names := []string{"PASSWORD", "TOKEN", "API_KEY"}
+
+ replacer, err := NewReplacer(patterns, names)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+ io.Copy(io.Discard, wrapper)
+ }
+
+ b.ReportMetric(float64(len(testData)), "bytes/op")
+ b.ReportMetric(float64(len(testData)*b.N)/float64(b.Elapsed().Seconds()), "bytes/sec")
+}
+
+func BenchmarkWrapper_NoReplacementNeeded(b *testing.B) {
+ // benchmark with data that has no patterns to replace
+ testData := strings.Repeat("clean log data with no sensitive information ", 1000)
+ patterns := []string{`password=([^\s]+)`, `token=([^\s]+)`}
+ names := []string{"PASSWORD", "TOKEN"}
+
+ replacer, err := NewReplacer(patterns, names)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+ io.Copy(io.Discard, wrapper)
+ }
+
+ b.ReportMetric(float64(len(testData)), "bytes/op")
+ b.ReportMetric(float64(len(testData)*b.N)/float64(b.Elapsed().Seconds()), "bytes/sec")
+}
+
+func BenchmarkWrapper_OverlapHeavy(b *testing.B) {
+ // benchmark scenario where patterns frequently span chunk boundaries
+ baseChunk := strings.Repeat("x", chunkSize-10)
+ pattern := "password=secret123"
+ testData := baseChunk + pattern + baseChunk + pattern + baseChunk
+
+ replacer := &simpleReplacer{
+ pattern: pattern,
+ replacement: "password=MASKED",
+ }
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for b.Loop() {
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+ io.Copy(io.Discard, wrapper)
+ }
+
+ b.ReportMetric(float64(len(testData)), "bytes/op")
+ b.ReportMetric(float64(len(testData)*b.N)/float64(b.Elapsed().Seconds()), "bytes/sec")
+}
+
+// helper types for testing
+type errorReader struct {
+ err error
+}
+
+func (e *errorReader) Read(p []byte) (n int, err error) {
+ return 0, e.err
+}
+
+type intermittentReader struct {
+ data []byte
+ chunks []int
+ index int
+ pos int
+}
+
+func (r *intermittentReader) Read(p []byte) (n int, err error) {
+ if r.index >= len(r.chunks) {
+ return 0, io.EOF
+ }
+
+ chunkSize := r.chunks[r.index]
+ r.index++
+
+ if chunkSize == 0 {
+ return 0, nil // empty read
+ }
+
+ if r.pos >= len(r.data) {
+ return 0, io.EOF
+ }
+
+ available := len(r.data) - r.pos
+ toRead := min(min(chunkSize, len(p)), available)
+
+ copy(p, r.data[r.pos:r.pos+toRead])
+ r.pos += toRead
+
+ return toRead, nil
+}
diff --git a/anonymizer/wrapper_test.go b/anonymizer/wrapper_test.go
new file mode 100644
index 0000000..748abc8
--- /dev/null
+++ b/anonymizer/wrapper_test.go
@@ -0,0 +1,801 @@
+package anonymizer
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/vxcontrol/cloud/anonymizer/patterns"
+ "github.com/vxcontrol/cloud/anonymizer/testdata"
+)
+
+type mockReplacer struct{}
+
+func (m *mockReplacer) ReplaceString(s string) string {
+ return s
+}
+
+func (m *mockReplacer) ReplaceBytes(b []byte) []byte {
+ return b
+}
+
+func (m *mockReplacer) WrapReader(r io.Reader) io.Reader {
+ return r
+}
+
+type simpleReplacer struct {
+ pattern string
+ replacement string
+}
+
+func (sr *simpleReplacer) ReplaceString(s string) string {
+ return strings.ReplaceAll(s, sr.pattern, sr.replacement)
+}
+
+func (sr *simpleReplacer) ReplaceBytes(b []byte) []byte {
+ return bytes.ReplaceAll(b, []byte(sr.pattern), []byte(sr.replacement))
+}
+
+func (sr *simpleReplacer) WrapReader(r io.Reader) io.Reader {
+ return newWrapper(r, sr)
+}
+
+type trackingReplacer struct {
+ pattern string
+ replacement string
+ onCall func() // callback for tracking invocations
+}
+
+func (tr *trackingReplacer) ReplaceString(s string) string {
+ if tr.onCall != nil {
+ tr.onCall()
+ }
+ return strings.ReplaceAll(s, tr.pattern, tr.replacement)
+}
+
+func (tr *trackingReplacer) ReplaceBytes(b []byte) []byte {
+ if tr.onCall != nil {
+ tr.onCall()
+ }
+ return bytes.ReplaceAll(b, []byte(tr.pattern), []byte(tr.replacement))
+}
+
+func (tr *trackingReplacer) WrapReader(r io.Reader) io.Reader {
+ return newWrapper(r, tr)
+}
+
+// test with mock replacer - basic functionality
+func TestWrapper_BasicRead(t *testing.T) {
+ replacer := &mockReplacer{}
+ wrapper := newWrapper(strings.NewReader("test"), replacer)
+ buf := make([]byte, 10)
+
+ n, err := wrapper.Read(buf)
+ if err != nil {
+ t.Fatalf("expected no error, got: %v", err)
+ }
+
+ if n != 4 {
+ t.Fatalf("expected 4 bytes, got: %d", n)
+ }
+ if string(buf[:n]) != "test" {
+ t.Fatalf("expected test, got: %s", string(buf[:n]))
+ }
+
+ n, err = wrapper.Read(buf)
+ if n != 0 {
+ t.Fatalf("expected 0 bytes, got: %d", n)
+ }
+ if err != io.EOF {
+ t.Fatalf("expected EOF, got: %v", err)
+ }
+}
+
+func TestWrapper_EmptyReader(t *testing.T) {
+ replacer := &mockReplacer{}
+ wrapper := newWrapper(strings.NewReader(""), replacer)
+ buf := make([]byte, 10)
+
+ n, err := wrapper.Read(buf)
+ if err != io.EOF {
+ t.Fatalf("expected EOF, got: %v", err)
+ }
+ if n != 0 {
+ t.Fatalf("expected 0 bytes, got: %d", n)
+ }
+}
+
+func TestWrapper_SmallBufferReads(t *testing.T) {
+ testData := "this is a longer test string that should be read in small chunks"
+ replacer := &mockReplacer{}
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+
+ var result strings.Builder
+ buf := make([]byte, 5) // small buffer to force multiple reads
+
+ for {
+ n, err := wrapper.Read(buf)
+ if n > 0 {
+ result.Write(buf[:n])
+ }
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ }
+
+ if result.String() != testData {
+ t.Errorf("expected: %s, got: %s", testData, result.String())
+ }
+}
+
+func TestWrapper_SingleByteReads(t *testing.T) {
+ testData := "single byte reads test"
+ replacer := &mockReplacer{}
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+
+ var result []byte
+ buf := make([]byte, 1)
+
+ for {
+ n, err := wrapper.Read(buf)
+ if n > 0 {
+ result = append(result, buf[0])
+ }
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ }
+
+ if string(result) != testData {
+ t.Errorf("expected: %s, got: %s", testData, string(result))
+ }
+}
+
+func TestWrapper_LargeBufferRead(t *testing.T) {
+ testData := "test data"
+ replacer := &mockReplacer{}
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+
+ buf := make([]byte, 1000) // buffer much larger than data
+ n, err := wrapper.Read(buf)
+
+ if err != nil && err != io.EOF {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ if n != len(testData) {
+ t.Errorf("expected %d bytes, got %d", len(testData), n)
+ }
+
+ if string(buf[:n]) != testData {
+ t.Errorf("expected: %s, got: %s", testData, string(buf[:n]))
+ }
+}
+
+func TestWrapper_MultipleChunkSizes(t *testing.T) {
+ sizes := []int{1, chunkSize / 4, chunkSize / 2, chunkSize, chunkSize * 2, chunkSize * 4}
+
+ for _, size := range sizes {
+ t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
+ // generate test data
+ testData := strings.Repeat("x", size)
+ replacer := &mockReplacer{}
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+
+ // read all data
+ result, err := io.ReadAll(wrapper)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ if string(result) != testData {
+ t.Errorf("data size %d: expected length %d, got %d", size, len(testData), len(result))
+ }
+ })
+ }
+}
+
+func TestWrapper_ChunkBoundaryReplacement(t *testing.T) {
+ // test pattern replacement across chunk boundaries
+ testData := strings.Repeat("a", chunkSize-5) + "secret123" + strings.Repeat("b", chunkSize)
+ replacer := &simpleReplacer{pattern: "secret123", replacement: "MASKED"}
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+
+ result, err := io.ReadAll(wrapper)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ resultStr := string(result)
+ if !strings.Contains(resultStr, "MASKED") {
+ t.Error("pattern should be replaced even across chunk boundaries")
+ }
+ if strings.Contains(resultStr, "secret123") {
+ t.Error("original pattern should not remain after replacement")
+ }
+}
+
+func TestWrapper_OverlapBoundaryHandling(t *testing.T) {
+ // create data where sensitive pattern spans chunk boundary
+ prefix := strings.Repeat("x", chunkSize-3)
+ secret := "password=secret123"
+ suffix := strings.Repeat("y", chunkSize)
+ testData := prefix + secret + suffix
+
+ replacer := &simpleReplacer{pattern: "password=secret123", replacement: "password=MASKED"}
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+
+ result, err := io.ReadAll(wrapper)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ resultStr := string(result)
+ if !strings.Contains(resultStr, "password=MASKED") {
+ t.Error("pattern spanning chunk boundary should be replaced")
+ }
+ if strings.Contains(resultStr, "password=secret123") {
+ t.Error("original pattern should be fully replaced")
+ }
+}
+
+func TestWrapper_WithRealReplacer(t *testing.T) {
+ patterns := []string{`password=([^\s]+)`}
+ names := []string{"PASSWORD"}
+
+ realReplacer, err := NewReplacer(patterns, names)
+ if err != nil {
+ t.Fatalf("failed to create real replacer: %v", err)
+ }
+
+ testData := "connecting with password=secret123 to database"
+ wrapper := newWrapper(strings.NewReader(testData), realReplacer)
+
+ result, err := io.ReadAll(wrapper)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ resultStr := string(result)
+ if !strings.Contains(resultStr, "ยง") {
+ t.Error("real replacer should mask sensitive data with ยง markers")
+ }
+ if strings.Contains(resultStr, "secret123") {
+ t.Error("sensitive data should be masked")
+ }
+}
+
+func TestWrapper_MultiplePatternReplacement(t *testing.T) {
+ patterns := []string{
+ `password=([^\s]+)`,
+ `--token\s+([a-zA-Z0-9]+)\b`,
+ }
+ names := []string{"PASSWORD", "TOKEN"}
+
+ realReplacer, err := NewReplacer(patterns, names)
+ if err != nil {
+ t.Fatalf("failed to create replacer: %v", err)
+ }
+
+ testData := "start password=secret123 middle --token mytoken4567 end"
+ wrapper := newWrapper(strings.NewReader(testData), realReplacer)
+
+ result, err := io.ReadAll(wrapper)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ resultStr := string(result)
+ if !strings.Contains(resultStr, "PASSWORD") {
+ t.Error("password should be masked")
+ }
+ if !strings.Contains(resultStr, "TOKEN") {
+ t.Error("token should be masked")
+ }
+ if strings.Contains(resultStr, "secret123") || strings.Contains(resultStr, "mytoken4567") {
+ t.Error("sensitive values should be masked")
+ }
+}
+
+func TestWrapper_ErrorHandling(t *testing.T) {
+ // test with reader that returns error
+ errorReader := &errorReader{err: fmt.Errorf("read error")}
+ replacer := &mockReplacer{}
+ wrapper := newWrapper(errorReader, replacer)
+
+ buf := make([]byte, 10)
+ _, err := wrapper.Read(buf)
+
+ if err == nil {
+ t.Error("expected error from underlying reader")
+ }
+ if !strings.Contains(err.Error(), "read error") {
+ t.Errorf("expected error message to contain 'read error', got: %v", err)
+ }
+}
+
+func TestWrapper_ZeroLengthRead(t *testing.T) {
+ testData := "test data"
+ replacer := &mockReplacer{}
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+
+ // read with zero-length buffer
+ buf := make([]byte, 0)
+ n, err := wrapper.Read(buf)
+
+ if n != 0 {
+ t.Errorf("expected 0 bytes read, got %d", n)
+ }
+ if err != nil && err != io.EOF {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
+
+func TestWrapper_VariousReadPatterns(t *testing.T) {
+ testData := strings.Repeat("test pattern data ", 100) // create data larger than chunk size
+ replacer := &mockReplacer{}
+
+ readPatterns := []struct {
+ name string
+ bufSize int
+ }{
+ {"tiny reads", 1},
+ {"small reads", 7},
+ {"medium reads", 64},
+ {"large reads", 512},
+ {"very large reads", 2048},
+ {"chunk size reads", chunkSize},
+ {"larger than chunk", chunkSize + 100},
+ }
+
+ for _, pattern := range readPatterns {
+ t.Run(pattern.name, func(t *testing.T) {
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+
+ var result strings.Builder
+ buf := make([]byte, pattern.bufSize)
+
+ for {
+ n, err := wrapper.Read(buf)
+ if n > 0 {
+ result.Write(buf[:n])
+ }
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ }
+
+ if result.String() != testData {
+ t.Error("result should match original data regardless of read pattern")
+ }
+ })
+ }
+}
+
+func TestWrapper_ProductionPatterns(t *testing.T) {
+ // test with production-like patterns and data
+ allPatterns, err := patterns.LoadPatterns(patterns.PatternListTypeAll)
+ if err != nil {
+ t.Skipf("failed to load production patterns: %v", err)
+ }
+
+ replacer, err := NewReplacer(allPatterns.Regexes(), allPatterns.Names())
+ if err != nil {
+ t.Skipf("failed to create replacer with production patterns: %v", err)
+ }
+
+ // load test datasets
+ datasets, err := testdata.LoadAllTestData()
+ if err != nil {
+ t.Skipf("failed to load test data: %v", err)
+ }
+
+ for _, dataset := range datasets {
+ for _, entry := range dataset.Entries {
+ if len(entry.Examples) > 1000 { // limit test data size for performance
+ continue
+ }
+
+ t.Run(fmt.Sprintf("%s_%s", dataset.Category, entry.Name), func(t *testing.T) {
+ wrapper := newWrapper(strings.NewReader(entry.Examples), replacer)
+
+ result, err := io.ReadAll(wrapper)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ // basic sanity check - masked data should not contain original examples
+ resultStr := string(result)
+ lines := strings.Split(entry.Examples, "\n")
+ for _, line := range lines {
+ line = strings.TrimSpace(line)
+ if len(line) > 0 && strings.Contains(resultStr, line) {
+ // some patterns might not match, so this is informational
+ t.Logf("warning: line '%s' was not masked in %s.%s", line, dataset.Category, entry.Name)
+ }
+ }
+ })
+ }
+ }
+}
+
+// test with configurable chunk and overlap sizes
+func TestWrapper_ConfigurableChunkSizes(t *testing.T) {
+ // note: in real implementation, chunkSize and overlapSize should be configurable parameters
+ // this test shows what should be tested if they were configurable
+
+ testSizes := []struct {
+ name string
+ testChunkSize int
+ testOverlapSize int
+ }{
+ {"small_chunk_small_overlap", 512, 128},
+ {"medium_chunk_small_overlap", 2048, 256},
+ {"large_chunk_medium_overlap", 8192, 1024},
+ {"very_large_chunk", 16384, 2048},
+ }
+
+ for _, ts := range testSizes {
+ t.Run(ts.name, func(t *testing.T) {
+ // create test data larger than chunk size
+ testData := strings.Repeat("test data chunk ", ts.testChunkSize/10)
+ replacer := &mockReplacer{}
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+
+ result, err := io.ReadAll(wrapper)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ if string(result) != testData {
+ t.Error("result should match original data regardless of chunk size")
+ }
+ })
+ }
+}
+
+func TestWrapper_OverlapEffectiveness(t *testing.T) {
+ // test that overlap actually helps with boundary patterns
+ // create a pattern that would be split across default chunk boundary
+ prefix := strings.Repeat("a", chunkSize-7) // position pattern near boundary
+ pattern := "password=secret123"
+ suffix := strings.Repeat("b", 100)
+ testData := prefix + pattern + suffix
+
+ // test with simple string replacer
+ replacer := &simpleReplacer{
+ pattern: pattern,
+ replacement: "password=MASKED",
+ }
+
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+ result, err := io.ReadAll(wrapper)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ resultStr := string(result)
+ if strings.Contains(resultStr, "password=secret123") {
+ t.Error("pattern crossing chunk boundary should be replaced due to overlap")
+ }
+ if !strings.Contains(resultStr, "password=MASKED") {
+ t.Error("replacement should be present")
+ }
+}
+
+func TestWrapper_OverlapMechanismDetails(t *testing.T) {
+ // detailed test of overlap mechanism - verify that pattern on chunk boundary
+ // is correctly processed thanks to overlap area
+
+ // create data so that pattern is exactly on chunkSize boundary
+ partialPattern := "passwo" // part of pattern
+ restPattern := "rd=secret123" // rest of pattern
+
+ // first chunk ends on partialPattern
+ prefix := strings.Repeat("x", chunkSize-len(partialPattern))
+ testData := prefix + partialPattern + restPattern + "suffix"
+
+ replacer := &simpleReplacer{
+ pattern: "password=secret123",
+ replacement: "password=MASKED",
+ }
+
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+ result, err := io.ReadAll(wrapper)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ resultStr := string(result)
+ if strings.Contains(resultStr, "password=secret123") {
+ t.Error("split pattern should be replaced thanks to overlap mechanism")
+ }
+ if !strings.Contains(resultStr, "password=MASKED") {
+ t.Error("replacement should be present")
+ }
+ if !strings.Contains(resultStr, "suffix") {
+ t.Error("data after pattern should be preserved")
+ }
+}
+
+func TestWrapper_ReplacerApplicationTiming(t *testing.T) {
+ // test checks that replacer is applied at the right time -
+ // after adding new data to buffer, but before returning data to user
+
+ callCount := 0
+ trackingReplacer := &trackingReplacer{
+ pattern: "secret",
+ replacement: "MASKED",
+ onCall: func() { callCount++ },
+ }
+
+ // data that will require several reads from source
+ testData := strings.Repeat("some data with secret inside ", 1000)
+
+ wrapper := newWrapper(strings.NewReader(testData), trackingReplacer)
+
+ // read data in small chunks
+ buf := make([]byte, 100)
+ totalRead := 0
+
+ for {
+ n, err := wrapper.Read(buf)
+ totalRead += n
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ }
+
+ // replacer should be called multiple times (on each new data addition)
+ if callCount == 0 {
+ t.Error("replacer should be called when processing data")
+ }
+
+ // total size should match original data
+ if totalRead != len(testData) {
+ t.Errorf("expected to read %d bytes, got %d", len(testData), totalRead)
+ }
+}
+
+func TestWrapper_ReplacerIdempotence(t *testing.T) {
+ // test that replacer behaves correctly when applied multiple times to the same data
+ // this is important because in wrapper algorithm replacer is applied to entire buffer
+ // each time when new data is added (for correct overlap area processing)
+ testData := "password=secret123"
+
+ replacer := &simpleReplacer{
+ pattern: "password=secret123",
+ replacement: "password=MASKED_ONCE",
+ }
+
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+ result, err := io.ReadAll(wrapper)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ resultStr := string(result)
+
+ // check that replacement works correctly even with repeated replacer application
+ maskCount := strings.Count(resultStr, "MASKED_ONCE")
+ if maskCount != 1 {
+ t.Errorf("replacement should occur exactly once, got %d occurrences in: %s", maskCount, resultStr)
+ }
+
+ // additional idempotence check - apply replacer once more
+ doubleProcessed := replacer.ReplaceBytes([]byte(resultStr))
+ if string(doubleProcessed) != resultStr {
+ t.Error("replacer should be idempotent - repeated application should not change result")
+ }
+}
+
+func TestWrapper_BufferManagement(t *testing.T) {
+ // test various read patterns to verify buffer management
+ testData := strings.Repeat("buffer management test ", 1000)
+ replacer := &mockReplacer{}
+
+ readPatterns := []struct {
+ name string
+ bufferSizes []int
+ }{
+ {"random_small", []int{1, 3, 7, 2, 5, 8, 1}},
+ {"increasing", []int{1, 2, 4, 8, 16, 32, 64}},
+ {"decreasing", []int{64, 32, 16, 8, 4, 2, 1}},
+ {"mixed_large_small", []int{1024, 1, 2048, 3, 512, 7}},
+ {"around_chunk_size", []int{chunkSize - 1, chunkSize, chunkSize + 1}},
+ {"overlap_related", []int{overlapSize - 1, overlapSize, overlapSize + 1}},
+ }
+
+ for _, pattern := range readPatterns {
+ t.Run(pattern.name, func(t *testing.T) {
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+
+ var result strings.Builder
+ bufSizeIdx := 0
+
+ for {
+ bufSize := pattern.bufferSizes[bufSizeIdx%len(pattern.bufferSizes)]
+ buf := make([]byte, bufSize)
+
+ n, err := wrapper.Read(buf)
+ if n > 0 {
+ result.Write(buf[:n])
+ }
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ bufSizeIdx++
+ }
+
+ if result.String() != testData {
+ t.Error("complex read patterns should not affect data integrity")
+ }
+ })
+ }
+}
+
+func TestWrapper_LargePatternDetection(t *testing.T) {
+ // test detection of patterns larger than overlap size
+ largePattern := strings.Repeat("x", overlapSize+100)
+ testData := "prefix " + largePattern + " suffix"
+
+ replacer := &simpleReplacer{
+ pattern: largePattern,
+ replacement: "LARGE_PATTERN_MASKED",
+ }
+
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+ result, err := io.ReadAll(wrapper)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ resultStr := string(result)
+
+ // the current implementation should handle this, but it's worth testing
+ if !strings.Contains(resultStr, "LARGE_PATTERN_MASKED") {
+ t.Log("note: large pattern (larger than overlap) was not replaced")
+ t.Log("this might be expected behavior depending on implementation")
+ }
+}
+
+func TestWrapper_ConcurrentSafeUsage(t *testing.T) {
+ // test that multiple wrappers can be used safely (no shared state issues)
+ testData := "concurrent test data with password=secret123"
+
+ const numGoroutines = 10
+ results := make(chan string, numGoroutines)
+ errors := make(chan error, numGoroutines)
+
+ replacer := &simpleReplacer{
+ pattern: "password=secret123",
+ replacement: "password=MASKED",
+ }
+
+ for i := range numGoroutines {
+ go func(id int) {
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+ result, err := io.ReadAll(wrapper)
+ if err != nil {
+ errors <- fmt.Errorf("goroutine %d: %w", id, err)
+ return
+ }
+ results <- string(result)
+ }(i)
+ }
+
+ // collect results
+ for range numGoroutines {
+ select {
+ case result := <-results:
+ if !strings.Contains(result, "password=MASKED") {
+ t.Error("concurrent usage should still perform replacements")
+ }
+ if strings.Contains(result, "password=secret123") {
+ t.Error("concurrent usage should mask sensitive data")
+ }
+ case err := <-errors:
+ t.Fatalf("concurrent usage error: %v", err)
+ case <-time.After(5 * time.Second):
+ t.Fatal("timeout waiting for concurrent operations")
+ }
+ }
+}
+
+func TestWrapper_StreamingWithBackpressure(t *testing.T) {
+ // simulate slow reader to test buffer management under backpressure
+ testData := strings.Repeat("streaming test data ", 5000) // ~100KB
+ replacer := &mockReplacer{}
+ wrapper := newWrapper(strings.NewReader(testData), replacer)
+
+ var result strings.Builder
+ buf := make([]byte, 64) // small buffer to create backpressure
+
+ for {
+ n, err := wrapper.Read(buf)
+ if n > 0 {
+ result.Write(buf[:n])
+ // simulate processing delay
+ time.Sleep(1 * time.Microsecond)
+ }
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ }
+
+ if result.String() != testData {
+ t.Error("streaming with backpressure should preserve data integrity")
+ }
+}
+
+func TestWrapper_EmptyChunkHandling(t *testing.T) {
+ // test behavior when underlying reader returns empty reads
+ reader := &intermittentReader{
+ data: []byte("test data with empty reads"),
+ chunks: []int{4, 0, 5, 0, 0, 4, 0, 13}, // mix of data and empty reads
+ index: 0,
+ }
+
+ replacer := &mockReplacer{}
+ wrapper := newWrapper(reader, replacer)
+
+ result, err := io.ReadAll(wrapper)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ expected := "test data with empty reads"
+ if string(result) != expected {
+ t.Errorf("expected: %s, got: %s", expected, string(result))
+ }
+}
+
+func TestWrapper_MemoryUsagePattern(t *testing.T) {
+ // verify that memory usage doesn't grow unbounded
+ largeTestData := strings.Repeat("memory test ", 100000) // ~1.2MB
+ replacer := &mockReplacer{}
+ wrapper := newWrapper(strings.NewReader(largeTestData), replacer)
+
+ // read in small chunks to verify buffer doesn't grow excessively
+ buf := make([]byte, 256)
+ totalRead := 0
+
+ for {
+ n, err := wrapper.Read(buf)
+ if n > 0 {
+ totalRead += n
+ }
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ }
+
+ if totalRead != len(largeTestData) {
+ t.Errorf("expected to read %d bytes, got %d", len(largeTestData), totalRead)
+ }
+}
diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..645b071
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,185 @@
+// Package cloud provides enterprise-grade Go SDK for secure integration with VXControl Cloud Platform.
+//
+// The VXControl Cloud Platform offers a comprehensive suite of cybersecurity services
+// accessible through secure, PoW-protected APIs. This package enables developers to integrate
+// their security tools and applications with advanced cybersecurity services including
+// update management, package distribution, AI-powered troubleshooting, and comprehensive
+// data anonymization.
+//
+// # Architecture
+//
+// The cloud package consists of several core components:
+//
+// - sdk: Main SDK package for API integration with 24 strongly-typed function patterns
+// - models: Type-safe data models with built-in validation for all API interactions
+// - anonymizer: Comprehensive PII/secrets masking engine with 300+ pattern recognition
+// - system: Cross-platform utilities for stable installation ID generation
+// - examples: Production-ready integration examples and usage patterns
+//
+// # Quick Start
+//
+// Basic usage involves configuring API endpoints and building the SDK:
+//
+// import (
+// "github.com/vxcontrol/cloud/sdk"
+// "github.com/vxcontrol/cloud/models"
+// "github.com/vxcontrol/cloud/system"
+// )
+//
+// // Define client structure with API functions
+// type Client struct {
+// UpdatesCheck sdk.CallReqBytesRespBytes
+// ReportError sdk.CallReqBytesRespBytes
+// }
+//
+// // Configure endpoints
+// configs := []sdk.CallConfig{
+// {
+// Calls: []any{&client.UpdatesCheck},
+// Host: "update.pentagi.com",
+// Name: "updates_check",
+// Path: "/api/v1/updates/check",
+// Method: sdk.CallMethodPOST,
+// },
+// }
+//
+// // Initialize SDK
+// err := sdk.Build(configs,
+// sdk.WithClient("MySecTool", "1.0.0"),
+// sdk.WithInstallationID(system.GetInstallationID()),
+// sdk.WithLicenseKey("your-license-key"),
+// )
+//
+// # Core Features
+//
+// Security-First Design:
+// - Memory-hard proof-of-work protection against abuse and DDoS attacks
+// - Ed25519 cryptographic signatures for data integrity verification
+// - AES-GCM end-to-end encryption with forward secrecy
+// - Stable machine identification for installation tracking
+// - Mandatory PII/secrets anonymization for AI troubleshooting
+//
+// Type Safety:
+// - 24 strongly-typed function patterns covering all request/response scenarios
+// - Comprehensive Go models with built-in validation (IValid interface)
+// - Automatic query parameter generation (IQuery interface)
+// - Database integration support with SQL driver interfaces
+//
+// Performance Optimized:
+// - HTTP/2 support with automatic protocol negotiation
+// - Connection pooling and streaming architecture
+// - Memory-efficient processing for large data transfers
+// - Dynamic difficulty scaling based on server load
+//
+// # Available Services
+//
+// Production Services:
+// - Update Service: Component update checking with changelogs
+// - Package Service: Secure package downloads with signature validation
+// - Support Service: Automated error reporting with AI assistance
+// - AI Investigation: Interactive troubleshooting with comprehensive anonymization
+//
+// Development Services:
+// - Threat Intelligence: IOC/IOA database access and threat analysis
+// - Vulnerability Assessment: CVE database integration and security scanning
+// - Knowledge Base: Cybersecurity knowledge search and best practices
+// - Computational Resources: Cloud-based intensive task processing
+//
+// # Data Anonymization
+//
+// The anonymizer package provides comprehensive protection for sensitive data:
+//
+// import "github.com/vxcontrol/cloud/anonymizer"
+//
+// // Initialize anonymizer
+// anon, err := anonymizer.NewAnonymizer(nil)
+// if err != nil {
+// return err
+// }
+//
+// // Anonymize sensitive data structures
+// sensitiveData := map[string]any{
+// "user_email": "admin@company.com",
+// "api_key": "sk-1234567890abcdef",
+// "database_url": "postgres://user:pass@host:5432/db",
+// }
+//
+// if err := anon.Anonymize(&sensitiveData); err != nil {
+// return err
+// }
+// // Result: emails, credentials, URLs automatically masked
+//
+// Pattern Recognition:
+// - 300+ built-in patterns across General, PII, and Secrets categories
+// - Automatic detection of credentials, API keys, database URLs, IP addresses
+// - Structure-preserving anonymization maintains analytical value for AI
+// - Reflection-based processing for complex Go structures and nested data
+//
+// # Package Validation
+//
+// Cryptographic signature validation ensures package integrity:
+//
+// import "github.com/vxcontrol/cloud/models"
+//
+// // Validate package signatures
+// signature := models.SignatureValue("base64-signature")
+// if err := signature.ValidateFile("package.tar.gz"); err != nil {
+// return fmt.Errorf("package validation failed: %w", err)
+// }
+//
+// // Stream-based validation for large files
+// reader := signature.ValidateWrapReader(file)
+// // Process data while validating...
+// if err := reader.Valid(); err != nil {
+// return fmt.Errorf("signature validation failed: %w", err)
+// }
+//
+// Signature Features:
+// - Ed25519 cryptographic signatures with SHA-512 hashing
+// - Streaming validation for large files without memory accumulation
+// - Built-in base64 encoding/decoding with validation
+// - Database integration support for signature storage
+//
+// # System Utilities
+//
+// Cross-platform machine identification for stable installation tracking:
+//
+// import "github.com/vxcontrol/cloud/system"
+//
+// // Generate stable installation ID
+// installationID := system.GetInstallationID()
+// // Returns same UUID for same machine across application restarts
+//
+// // Use in SDK configuration
+// err := sdk.Build(configs,
+// sdk.WithClient("MyApp", "1.0.0"),
+// sdk.WithInstallationID(installationID),
+// )
+//
+// Platform Support:
+// - Linux: /var/lib/dbus/machine-id + SMBIOS data (when available)
+// - macOS: IOPlatformUUID from hardware registry via ioreg
+// - Windows: Registry MachineGuid + system product information via WMI
+//
+// Features:
+// - Deterministic: Same machine always generates same UUID
+// - Cross-Platform: Works on Linux, macOS, Windows
+// - Fallback Logic: Uses hostname when machine ID unavailable
+// - UUID Format: RFC4122 compliant UUID v3 (MD5-based)
+// - Performance: ~17ms generation time (acceptable for SDK initialization)
+//
+// # Examples
+//
+// Complete integration examples are available in the examples/ directory:
+//
+// - examples/check-update: Update service integration with component management
+// - examples/download-installer: Package downloads with streaming signature validation
+// - examples/report-errors: Support workflow with automated data anonymization
+//
+// # License
+//
+// This software is provided under a proprietary license. Commercial licenses are
+// available for enterprise use. Contact info@vxcontrol.com for pricing and terms.
+//
+// Copyright (c) 2025 VXControl LLC. All rights reserved.
+package cloud
diff --git a/examples/check-update/main.go b/examples/check-update/main.go
new file mode 100644
index 0000000..11198ed
--- /dev/null
+++ b/examples/check-update/main.go
@@ -0,0 +1,112 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "flag"
+ "time"
+
+ "github.com/sirupsen/logrus"
+ "github.com/vxcontrol/cloud/models"
+ "github.com/vxcontrol/cloud/sdk"
+ "github.com/vxcontrol/cloud/system"
+)
+
+const (
+ DefaultMaxRetries = 3
+ DefaultTimeout = 30 * time.Second
+)
+
+type Client struct {
+ UpdatesCheck sdk.CallReqBytesRespBytes
+}
+
+func NewClient(serverHost, licenseKey string) (*Client, error) {
+ var client Client
+
+ configs := []sdk.CallConfig{
+ {
+ Calls: []any{&client.UpdatesCheck},
+ Host: serverHost,
+ Name: "updates_check",
+ Path: "/api/v1/updates/check",
+ Method: sdk.CallMethodPOST,
+ },
+ }
+
+ var buildOptions []sdk.Option
+ buildOptions = append(buildOptions,
+ sdk.WithTransport(sdk.DefaultTransport()),
+ sdk.WithInstallationID(system.GetInstallationID()),
+ sdk.WithClient("Check-Update-Example", "1.0.0"),
+ sdk.WithLogger(sdk.WrapLogrus(logrus.StandardLogger())),
+ sdk.WithPowTimeout(DefaultTimeout),
+ sdk.WithMaxRetries(DefaultMaxRetries),
+ )
+ if licenseKey != "" {
+ buildOptions = append(buildOptions, sdk.WithLicenseKey(licenseKey))
+ }
+
+ if err := sdk.Build(configs, buildOptions...); err != nil {
+ return nil, err
+ }
+
+ return &client, nil
+}
+
+func (c *Client) CheckUpdates(
+ ctx context.Context, req models.CheckUpdatesRequest,
+) (models.CheckUpdatesResponse, error) {
+ var resp models.CheckUpdatesResponse
+
+ data, err := json.Marshal(req)
+ if err != nil {
+ return resp, err
+ }
+
+ body, err := c.UpdatesCheck(ctx, data)
+ if err != nil {
+ return resp, err
+ }
+
+ if err := json.Unmarshal(body, &resp); err != nil {
+ return resp, err
+ }
+
+ return resp, nil
+}
+
+func main() {
+ var (
+ serverHost = flag.String("host", "update.pentagi.com", "Cloud server host")
+ licenseKey = flag.String("license", "", "License key (optional)")
+ )
+ flag.Parse()
+
+ logrus.SetLevel(logrus.DebugLevel)
+
+ logrus.Printf("starting check update client")
+ logrus.Printf("cloud server: %s", *serverHost)
+ if *licenseKey != "" {
+ logrus.Printf("license key: %s", *licenseKey)
+ }
+
+ client, err := NewClient(*serverHost, *licenseKey)
+ if err != nil {
+ logrus.Fatalf("failed to create client: %v", err)
+ }
+
+ resp, err := client.CheckUpdates(context.Background(), models.CheckUpdatesRequest{
+ InstallerVersion: "0.0.0",
+ })
+ if err != nil {
+ logrus.Fatalf("failed to check updates: %v", err)
+ }
+
+ body, err := json.MarshalIndent(resp, "", " ")
+ if err != nil {
+ logrus.Fatalf("failed to marshal response: %v", err)
+ }
+
+ logrus.Println(string(body))
+}
diff --git a/examples/download-installer/main.go b/examples/download-installer/main.go
new file mode 100644
index 0000000..a56ff46
--- /dev/null
+++ b/examples/download-installer/main.go
@@ -0,0 +1,168 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "os"
+ "runtime"
+ "time"
+
+ "github.com/sirupsen/logrus"
+ "github.com/vxcontrol/cloud/models"
+ "github.com/vxcontrol/cloud/sdk"
+ "github.com/vxcontrol/cloud/system"
+)
+
+const (
+ DefaultMaxRetries = 3
+ DefaultTimeout = 60 * time.Second
+)
+
+type Client struct {
+ packageInfo sdk.CallReqQueryRespBytes
+ packageDownload sdk.CallReqQueryRespWriter
+}
+
+func (c *Client) DownloadLatestInstaller(ctx context.Context) (string, error) {
+ packageReq := models.PackageInfoRequest{
+ Component: models.ComponentTypeInstaller,
+ Version: "latest",
+ OS: models.OSType(runtime.GOOS),
+ Arch: models.ArchType(runtime.GOARCH),
+ }
+ body, err := c.packageInfo(ctx, packageReq.Query())
+ if err != nil {
+ return "", fmt.Errorf("failed to get package info: %w", err)
+ }
+
+ var packageInfo models.PackageInfoResponse
+ if err := json.Unmarshal(body, &packageInfo); err != nil {
+ return "", fmt.Errorf("failed to unmarshal package info: %w", err)
+ }
+
+ tmpFile, err := os.CreateTemp("", "pentagi-installer")
+ if err != nil {
+ return "", fmt.Errorf("failed to create temp file: %w", err)
+ }
+ defer os.Remove(tmpFile.Name()) // TODO: testing purposes
+ defer tmpFile.Close()
+
+ protectedWriter := packageInfo.Signature.ValidateWrapWriter(tmpFile)
+ downloadReq := models.DownloadPackageRequest{
+ Component: models.ComponentTypeInstaller,
+ Version: packageInfo.Version,
+ OS: packageInfo.OS,
+ Arch: packageInfo.Arch,
+ }
+ if err := c.packageDownload(ctx, downloadReq.Query(), protectedWriter); err != nil {
+ return "", fmt.Errorf("failed to download package: %w", err)
+ }
+
+ if err := protectedWriter.Valid(); err != nil {
+ return "", fmt.Errorf("failed to validate signature: %w", err)
+ }
+
+ return tmpFile.Name(), nil
+}
+
+func (c *Client) GetInstallerInfo(ctx context.Context, version string) (models.PackageInfoResponse, error) {
+ packageReq := models.PackageInfoRequest{
+ Component: models.ComponentTypeInstaller,
+ Version: version,
+ OS: models.OSType(runtime.GOOS),
+ Arch: models.ArchType(runtime.GOARCH),
+ }
+ body, err := c.packageInfo(ctx, packageReq.Query())
+ if err != nil {
+ return models.PackageInfoResponse{}, fmt.Errorf("failed to get package info: %w", err)
+ }
+
+ var packageInfo models.PackageInfoResponse
+ if err := json.Unmarshal(body, &packageInfo); err != nil {
+ return models.PackageInfoResponse{}, fmt.Errorf("failed to unmarshal package info: %w", err)
+ }
+
+ return packageInfo, nil
+}
+
+func NewClient(serverHost, licenseKey string) (*Client, error) {
+ var client Client
+
+ configs := []sdk.CallConfig{
+ {
+ Calls: []any{&client.packageInfo},
+ Host: serverHost,
+ Name: "package_info",
+ Path: "/api/v1/packages/info",
+ Method: sdk.CallMethodGET,
+ },
+ {
+ Calls: []any{&client.packageDownload},
+ Host: serverHost,
+ Name: "package_download",
+ Path: "/api/v1/packages/download",
+ Method: sdk.CallMethodGET,
+ },
+ }
+
+ var buildOptions []sdk.Option
+ buildOptions = append(buildOptions,
+ sdk.WithTransport(sdk.DefaultTransport()),
+ sdk.WithInstallationID(system.GetInstallationID()),
+ sdk.WithClient("Download-Installer-Example", "1.0.0"),
+ sdk.WithLogger(sdk.WrapLogrus(logrus.StandardLogger())),
+ sdk.WithPowTimeout(DefaultTimeout),
+ sdk.WithMaxRetries(DefaultMaxRetries),
+ )
+ if licenseKey != "" {
+ buildOptions = append(buildOptions, sdk.WithLicenseKey(licenseKey))
+ }
+
+ if err := sdk.Build(configs, buildOptions...); err != nil {
+ return nil, err
+ }
+
+ return &client, nil
+}
+
+func main() {
+ var (
+ serverHost = flag.String("host", "update.pentagi.com", "Cloud server host")
+ licenseKey = flag.String("license", "", "License key (optional)")
+ )
+ flag.Parse()
+
+ logrus.SetLevel(logrus.DebugLevel)
+
+ logrus.Printf("starting check update client")
+ logrus.Printf("cloud server: %s", *serverHost)
+ if *licenseKey != "" {
+ logrus.Printf("license key: %s", *licenseKey)
+ }
+
+ client, err := NewClient(*serverHost, *licenseKey)
+ if err != nil {
+ logrus.Fatalf("failed to create client: %v", err)
+ }
+
+ resp, err := client.GetInstallerInfo(context.Background(), "latest")
+ if err != nil {
+ logrus.Fatalf("failed to check updates: %v", err)
+ }
+
+ body, err := json.MarshalIndent(resp, "", " ")
+ if err != nil {
+ logrus.Fatalf("failed to marshal response: %v", err)
+ }
+
+ logrus.Println(string(body))
+
+ installerPath, err := client.DownloadLatestInstaller(context.Background())
+ if err != nil {
+ logrus.Fatalf("failed to download installer: %v", err)
+ }
+
+ logrus.WithField("installer_path", installerPath).Println("installer downloaded")
+}
diff --git a/examples/report-errors/main.go b/examples/report-errors/main.go
new file mode 100644
index 0000000..5fd68e4
--- /dev/null
+++ b/examples/report-errors/main.go
@@ -0,0 +1,408 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io"
+ "runtime"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/sirupsen/logrus"
+ "github.com/vxcontrol/cloud/anonymizer"
+ "github.com/vxcontrol/cloud/models"
+ "github.com/vxcontrol/cloud/sdk"
+ "github.com/vxcontrol/cloud/system"
+)
+
+const (
+ DefaultMaxRetries = 3
+ DefaultTimeout = 60 * time.Second
+)
+
+type Client struct {
+ errorReport sdk.CallReqBytesRespBytes
+ issueCreate sdk.CallReqBytesRespBytes
+ issueInvestigate sdk.CallReqBytesRespBytes
+ issueInvestigateWithSteam sdk.CallReqBytesRespReader
+ anonymizer anonymizer.Anonymizer
+}
+
+// ReportError sends an automated error report to support
+func (c *Client) ReportError(
+ ctx context.Context, component models.ComponentType, errorDetails map[string]any,
+) error {
+ // anonymize sensitive data before transmission
+ if err := c.anonymizer.Anonymize(&errorDetails); err != nil {
+ return fmt.Errorf("failed to anonymize error details, sending original data: %w", err)
+ }
+
+ errorReq := models.SupportErrorRequest{
+ Component: component,
+ Version: "0.0.0",
+ OS: models.OSType(runtime.GOOS),
+ Arch: models.ArchType(runtime.GOARCH),
+ ErrorDetails: errorDetails,
+ }
+
+ data, err := json.Marshal(errorReq)
+ if err != nil {
+ return fmt.Errorf("failed to marshal error request: %w", err)
+ }
+
+ responseData, err := c.errorReport(ctx, data)
+ if err != nil {
+ return fmt.Errorf("failed to report error: %w", err)
+ }
+
+ var response models.SupportErrorResponse
+ if err := json.Unmarshal(responseData, &response); err != nil {
+ return fmt.Errorf("failed to unmarshal error response: %w", err)
+ }
+
+ return nil
+}
+
+// CreateSupportIssue creates a support issue with AI assistance
+func (c *Client) CreateSupportIssue(
+ ctx context.Context, component models.ComponentType, errorDetails any, logs []models.SupportLogs,
+) (uuid.UUID, error) {
+ // anonymize error details before transmission
+ if err := c.anonymizer.Anonymize(&errorDetails); err != nil {
+ return uuid.Nil, fmt.Errorf("failed to anonymize error details, sending original data: %w", err)
+ }
+
+ // anonymize logs before transmission
+ if err := c.anonymizer.Anonymize(&logs); err != nil {
+ return uuid.Nil, fmt.Errorf("failed to anonymize logs, sending original data: %w", err)
+ }
+
+ issueReq := models.SupportIssueRequest{
+ Component: component,
+ Version: "0.0.0",
+ OS: models.OSType(runtime.GOOS),
+ Arch: models.ArchType(runtime.GOARCH),
+ Logs: logs,
+ ErrorDetails: errorDetails,
+ }
+
+ data, err := json.Marshal(issueReq)
+ if err != nil {
+ return uuid.Nil, fmt.Errorf("failed to marshal issue request: %w", err)
+ }
+
+ responseData, err := c.issueCreate(ctx, data)
+ if err != nil {
+ return uuid.Nil, fmt.Errorf("failed to create issue: %w", err)
+ }
+
+ var response models.SupportIssueResponse
+ if err := json.Unmarshal(responseData, &response); err != nil {
+ return uuid.Nil, fmt.Errorf("failed to unmarshal issue response: %w", err)
+ }
+
+ return response.IssueID, nil
+}
+
+// InvestigateIssue starts AI-powered investigation of a support issue
+func (c *Client) InvestigateIssue(
+ ctx context.Context, issueID uuid.UUID, userInput string,
+) (string, error) {
+ investigationReq := models.SupportInvestigationRequest{
+ IssueID: issueID,
+ UseSteam: false,
+ UserInput: userInput,
+ }
+
+ data, err := json.Marshal(investigationReq)
+ if err != nil {
+ return "", fmt.Errorf("failed to marshal investigation request: %w", err)
+ }
+
+ responseData, err := c.issueInvestigate(ctx, data)
+ if err != nil {
+ return "", fmt.Errorf("failed to investigate issue: %w", err)
+ }
+
+ var response models.SupportInvestigationResponse
+ if err := json.Unmarshal(responseData, &response); err != nil {
+ return "", fmt.Errorf("failed to unmarshal investigation response: %w", err)
+ }
+
+ return response.Answer, nil
+}
+
+// InvestigateIssueWithSteam starts AI-powered investigation of a support issue with steam response
+func (c *Client) InvestigateIssueWithSteam(
+ ctx context.Context, issueID uuid.UUID, userInput string,
+) (string, error) {
+ investigationReq := models.SupportInvestigationRequest{
+ IssueID: issueID,
+ UseSteam: true,
+ UserInput: userInput,
+ }
+
+ data, err := json.Marshal(investigationReq)
+ if err != nil {
+ return "", fmt.Errorf("failed to marshal investigation request: %w", err)
+ }
+
+ stream, err := c.issueInvestigateWithSteam(ctx, data)
+ if err != nil {
+ return "", fmt.Errorf("failed to investigate issue: %w", err)
+ }
+
+ response, err := io.ReadAll(stream)
+ if err != nil {
+ return "", fmt.Errorf("failed to read investigation response: %w", err)
+ }
+
+ return string(response), nil
+}
+
+func NewClient(serverHost, licenseKey string) (*Client, error) {
+ // initialize anonymizer for automatic PII/secrets protection
+ // automatically masks credentials, emails, IPs, database URLs, API keys, etc.
+ anon, err := anonymizer.NewAnonymizer(nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create anonymizer: %w", err)
+ }
+
+ client := Client{
+ anonymizer: anon,
+ }
+
+ configs := []sdk.CallConfig{
+ {
+ Calls: []any{&client.errorReport},
+ Host: serverHost,
+ Name: "error_report",
+ Path: "/api/v1/errors/report",
+ Method: sdk.CallMethodPOST,
+ },
+ {
+ Calls: []any{&client.issueCreate},
+ Host: serverHost,
+ Name: "issue_create",
+ Path: "/api/v1/issues/create",
+ Method: sdk.CallMethodPOST,
+ },
+ {
+ Calls: []any{&client.issueInvestigate, &client.issueInvestigateWithSteam},
+ Host: serverHost,
+ Name: "issue_investigate",
+ Path: "/api/v1/issues/investigate",
+ Method: sdk.CallMethodPOST,
+ },
+ }
+
+ var buildOptions []sdk.Option
+ buildOptions = append(buildOptions,
+ sdk.WithTransport(sdk.DefaultTransport()),
+ sdk.WithInstallationID(system.GetInstallationID()),
+ sdk.WithClient("Report-Errors-Example", "1.0.0"),
+ sdk.WithLogger(sdk.WrapLogrus(logrus.StandardLogger())),
+ sdk.WithPowTimeout(DefaultTimeout),
+ sdk.WithMaxRetries(DefaultMaxRetries),
+ )
+ if licenseKey != "" {
+ buildOptions = append(buildOptions, sdk.WithLicenseKey(licenseKey))
+ }
+
+ if err := sdk.Build(configs, buildOptions...); err != nil {
+ return nil, err
+ }
+
+ return &client, nil
+}
+
+func getErrorDetails() map[string]any {
+ return map[string]any{
+ "error_type": "connection_timeout",
+ "message": "Failed to connect to target host admin@company.com with API key sk-1234567890abcdef",
+ "target_host": "192.168.1.100",
+ "port": 443,
+ "timeout_ms": 5000,
+ "retry_count": 3,
+ "stack_trace": "goroutine panic at scanner.go:142",
+ "database_url": "postgres://user:password123@db.internal:5432/app",
+ "api_endpoint": "https://api.service.com/v1/data?token=abc123xyz789",
+ "user_email": "john.doe@company.com",
+ "config_path": "/etc/myapp/config.yml",
+ }
+}
+
+func getComponentsLogs() []models.SupportLogs {
+ return []models.SupportLogs{
+ {
+ Component: models.ComponentTypePentagi,
+ Logs: []string{
+ "ERROR: Scanner pattern match timeout after 30s for user john.doe@company.com",
+ "WARN: Memory usage exceeded 80% threshold, DB connection postgres://admin:secret@db:5432/prod",
+ "INFO: Switching to backup scanning method with API key sk-abcd1234efgh5678",
+ "ERROR: Backup method failed - insufficient permissions for file /home/user/.ssh/id_rsa",
+ "DEBUG: Processing request from IP 10.0.1.50 with session token sess_9876543210",
+ "TRACE: Credit card validation failed for 4532-1234-5678-9012",
+ },
+ },
+ }
+}
+
+func getIssueDetails() map[string]any {
+ return map[string]any{
+ "problem_description": "Scanner fails to detect specific vulnerability patterns on server admin@prod.company.com",
+ "affected_patterns": []string{"CVE-2023-1234", "CVE-2023-5678"},
+ "environment": "production",
+ "severity": "high",
+ "server_config": "aws_access_key_id=AKIAIOSFODNN7EXAMPLE aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
+ "affected_users": []string{"admin@company.com", "support@company.com", "security@company.com"},
+ "database_info": "mysql://root:supersecret@prod-db.internal:3306/vulnerabilities",
+ "internal_token": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.example.token",
+ }
+}
+
+func main() {
+ var (
+ serverHost = flag.String("host", "support.pentagi.com", "Cloud server host")
+ licenseKey = flag.String("license", "", "License key (optional)")
+ example = flag.String("example", "all", "Example to run: error, issue, investigate, steam, all")
+ )
+ flag.Parse()
+
+ logrus.SetLevel(logrus.DebugLevel)
+
+ logrus.Printf("starting support client")
+ logrus.Printf("server: %s", *serverHost)
+ if *licenseKey != "" {
+ logrus.Printf("license: configured")
+ }
+
+ client, err := NewClient(*serverHost, *licenseKey)
+ if err != nil {
+ logrus.Fatalf("failed to create client: %v", err)
+ }
+
+ ctx := context.Background()
+
+ switch *example {
+ case "error":
+ runErrorReporting(ctx, client)
+ case "issue":
+ runIssueCreation(ctx, client)
+ case "investigate":
+ runInvestigation(ctx, client)
+ case "steam":
+ runSteamInvestigation(ctx, client)
+ case "all":
+ runAllExamples(ctx, client)
+ default:
+ logrus.Fatalf("unknown example: %s. Use: error, issue, investigate, steam, all", *example)
+ }
+
+ logrus.Println("support client demonstration completed")
+}
+
+func runErrorReporting(ctx context.Context, client *Client) {
+ logrus.Println("=== Automated Error Reporting ===")
+
+ if err := client.ReportError(
+ ctx, models.ComponentTypePentagi, getErrorDetails(),
+ ); err != nil {
+ logrus.Errorf("failed to report error: %v", err)
+ return
+ }
+
+ logrus.Println("error reported successfully with data anonymization")
+}
+
+func runIssueCreation(ctx context.Context, client *Client) {
+ logrus.Println("=== Support Issue Creation ===")
+
+ issueID, err := client.CreateSupportIssue(
+ ctx, models.ComponentTypePentagi, getIssueDetails(), getComponentsLogs(),
+ )
+ if err != nil {
+ logrus.Errorf("failed to create support issue: %v", err)
+ return
+ }
+
+ logrus.WithField("issue_id", issueID).Println("support issue created with data anonymization")
+}
+
+func runInvestigation(ctx context.Context, client *Client) {
+ logrus.Println("=== AI Investigation ===")
+
+ // first create an issue
+ issueID, err := client.CreateSupportIssue(
+ ctx, models.ComponentTypePentagi, getIssueDetails(), getComponentsLogs(),
+ )
+ if err != nil {
+ logrus.Errorf("failed to create support issue: %v", err)
+ return
+ }
+
+ logrus.WithField("issue_id", issueID).Println("created issue")
+
+ // then investigate
+ userInput := "The scanner works fine with other CVE patterns but consistently fails on these two specific ones. Could this be related to pattern complexity or memory constraints?"
+
+ answer, err := client.InvestigateIssue(ctx, issueID, userInput)
+ if err != nil {
+ logrus.Errorf("failed to investigate issue: %v", err)
+ return
+ }
+
+ logrus.WithField("answer", answer).Println("AI investigation result")
+
+ // follow-up investigation
+ followUpInput := "Based on your analysis, what specific configuration changes would you recommend to resolve the memory constraint issue?"
+
+ followUpAnswer, err := client.InvestigateIssue(ctx, issueID, followUpInput)
+ if err != nil {
+ logrus.Errorf("failed to investigate follow-up: %v", err)
+ return
+ }
+
+ logrus.WithFields(logrus.Fields{
+ "issue_id": issueID,
+ "answer": followUpAnswer,
+ }).Println("follow-up recommendations")
+}
+
+func runSteamInvestigation(ctx context.Context, client *Client) {
+ logrus.Println("=== AI Steam Investigation ===")
+
+ // first create an issue
+ issueID, err := client.CreateSupportIssue(
+ ctx, models.ComponentTypePentagi, getIssueDetails(), getComponentsLogs(),
+ )
+ if err != nil {
+ logrus.Errorf("failed to create support issue: %v", err)
+ return
+ }
+
+ logrus.WithField("issue_id", issueID).Println("created issue")
+
+ // steam investigation
+ userInput := "Please provide a detailed analysis of the scanner performance issues and step-by-step troubleshooting guide."
+
+ steamAnswer, err := client.InvestigateIssueWithSteam(ctx, issueID, userInput)
+ if err != nil {
+ logrus.Errorf("failed to steam investigate issue: %v", err)
+ return
+ }
+
+ logrus.WithFields(logrus.Fields{
+ "issue_id": issueID,
+ "answer": steamAnswer,
+ }).Println("steam investigation result")
+}
+
+func runAllExamples(ctx context.Context, client *Client) {
+ runErrorReporting(ctx, client)
+ runIssueCreation(ctx, client)
+ runInvestigation(ctx, client)
+ runSteamInvestigation(ctx, client)
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..74f5b30
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,35 @@
+module github.com/vxcontrol/cloud
+
+go 1.24.0
+
+require (
+ github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
+ github.com/go-ole/go-ole v1.3.0
+ github.com/go-playground/validator/v10 v10.26.0
+ github.com/google/uuid v1.6.0
+ github.com/sirupsen/logrus v1.9.3
+ github.com/wasilibs/go-re2 v1.10.0
+ golang.org/x/crypto v0.40.0
+ golang.org/x/net v0.42.0
+ golang.org/x/sys v0.34.0
+ gopkg.in/yaml.v3 v3.0.1
+)
+
+// test dependencies
+require github.com/stretchr/testify v1.11.0
+
+require (
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/gabriel-vasile/mimetype v1.4.9 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/google/go-cmp v0.7.0 // indirect
+ github.com/kr/pretty v0.3.1 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/rogpeppe/go-internal v1.14.1 // indirect
+ github.com/tetratelabs/wazero v1.9.0 // indirect
+ github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect
+ golang.org/x/text v0.27.0 // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..f07f096
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,67 @@
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
+github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
+github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
+github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
+github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
+github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
+github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
+github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
+github.com/wasilibs/go-re2 v1.10.0 h1:vQZEBYZOCA9jdBMmrO4+CvqyCj0x4OomXTJ4a5/urQ0=
+github.com/wasilibs/go-re2 v1.10.0/go.mod h1:k+5XqO2bCJS+QpGOnqugyfwC04nw0jaglmjrrkG8U6o=
+github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4=
+github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY=
+golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
+golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
+golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
+golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
+golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
+golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/models/init.go b/models/init.go
new file mode 100644
index 0000000..d0aa0f5
--- /dev/null
+++ b/models/init.go
@@ -0,0 +1,133 @@
+package models
+
+import (
+ "crypto/ed25519"
+ "reflect"
+ "regexp"
+
+ "github.com/go-playground/validator/v10"
+)
+
+const (
+ semverRegexString = `^(([0-9]+\.[0-9]+(\.[0-9]+)?)|latest)$`
+ semverexRegexString = `^(((v)?[0-9]+\.[0-9]+(\.[0-9]+)?(\.[0-9]+)?(-[a-zA-Z0-9]+)?)|latest)$`
+)
+
+var (
+ validate *validator.Validate
+)
+
+func GetValidator() *validator.Validate {
+ return validate
+}
+
+// IValid is interface to control all models from user code
+type IValid interface {
+ Valid() error
+}
+
+// IQuery is interface to control all models from user code
+type IQuery interface {
+ Query() map[string]string
+}
+
+func templateValidatorString(regexpString string) validator.Func {
+ regexpValue := regexp.MustCompile(regexpString)
+ return func(fl validator.FieldLevel) bool {
+ field := fl.Field()
+ matchString := func(str string) bool {
+ if str == "" && fl.Param() == "omitempty" {
+ return true
+ }
+ return regexpValue.MatchString(str)
+ }
+
+ switch field.Kind() {
+ case reflect.String:
+ return matchString(fl.Field().String())
+ case reflect.Slice, reflect.Array:
+ for i := 0; i < field.Len(); i++ {
+ if !matchString(field.Index(i).String()) {
+ return false
+ }
+ }
+ return true
+ case reflect.Map:
+ for _, k := range field.MapKeys() {
+ if !matchString(field.MapIndex(k).String()) {
+ return false
+ }
+ }
+ return true
+ default:
+ return false
+ }
+ }
+}
+
+func deepValidator() validator.Func {
+ return func(fl validator.FieldLevel) bool {
+ if iv, ok := fl.Field().Interface().(IValid); ok {
+ if err := iv.Valid(); err != nil {
+ return false
+ }
+ }
+
+ return true
+ }
+}
+
+func getSignaturePublicKey() ed25519.PublicKey {
+ rawPublic := [40]byte{
+ 0x37, 0xEC, 0xF3, 0xAB, 0xDF, 0x9C, 0xCB, 0x9E,
+ 0xE0, 0x39, 0xA0, 0x99, 0xAB, 0x51, 0x3F, 0x16,
+ 0x40, 0x8A, 0x8E, 0x18, 0x90, 0x77, 0xD0, 0x05,
+ 0xED, 0x4C, 0x37, 0x5D, 0x28, 0x01, 0xA8, 0x7E,
+ 0x58, 0x65, 0x84, 0xA8, 0x64, 0x40, 0xD0, 0x60,
+ }
+
+ result := [40]byte{rawPublic[0]>>4 | rawPublic[0]<<4}
+ for i := 1; i < 40; i++ {
+ result[i] = result[i-1] ^ rawPublic[i-1] ^ rawPublic[i]
+ }
+
+ return ed25519.PublicKey(result[result[37]:result[39]])
+}
+
+func init() {
+ validate = validator.New()
+ _ = validate.RegisterValidation("semver", templateValidatorString(semverRegexString))
+ _ = validate.RegisterValidation("semverex", templateValidatorString(semverexRegexString))
+ _ = validate.RegisterValidation("valid", deepValidator())
+
+ _, _ = reflect.ValueOf(ComponentType("")).Interface().(IValid)
+ _, _ = reflect.ValueOf(ComponentStatus("")).Interface().(IValid)
+ _, _ = reflect.ValueOf(ProductStack("")).Interface().(IValid)
+ _, _ = reflect.ValueOf(OSType("")).Interface().(IValid)
+ _, _ = reflect.ValueOf(ArchType("")).Interface().(IValid)
+
+ // signature service models validation
+ _, _ = reflect.ValueOf(SignatureValue("")).Interface().(IValid)
+
+ // update service models validation
+ _, _ = reflect.ValueOf(CheckUpdatesRequest{}).Interface().(IValid)
+ _, _ = reflect.ValueOf(ComponentInfo{}).Interface().(IValid)
+ _, _ = reflect.ValueOf(CheckUpdatesResponse{}).Interface().(IValid)
+ _, _ = reflect.ValueOf(UpdateInfo{}).Interface().(IValid)
+
+ // package service models validation
+ _, _ = reflect.ValueOf(PackageInfoRequest{}).Interface().(IValid)
+ _, _ = reflect.ValueOf(PackageInfoRequest{}).Interface().(IQuery)
+ _, _ = reflect.ValueOf(PackageInfoResponse{}).Interface().(IValid)
+ _, _ = reflect.ValueOf(DownloadPackageRequest{}).Interface().(IValid)
+ _, _ = reflect.ValueOf(DownloadPackageRequest{}).Interface().(IQuery)
+
+ // support service models validation
+ _, _ = reflect.ValueOf(SupportErrorRequest{}).Interface().(IValid)
+ _, _ = reflect.ValueOf(SupportErrorResponse{}).Interface().(IValid)
+ _, _ = reflect.ValueOf(SupportIssueRequest{}).Interface().(IValid)
+ _, _ = reflect.ValueOf(SupportIssueResponse{}).Interface().(IValid)
+ _, _ = reflect.ValueOf(SupportLogs{}).Interface().(IValid)
+ _, _ = reflect.ValueOf(SupportInvestigationRequest{}).Interface().(IValid)
+ _, _ = reflect.ValueOf(SupportInvestigationResponse{}).Interface().(IValid)
+}
diff --git a/models/package.go b/models/package.go
new file mode 100644
index 0000000..5aa7552
--- /dev/null
+++ b/models/package.go
@@ -0,0 +1,57 @@
+package models
+
+// PackageInfoRequest represents public API request to get package info
+type PackageInfoRequest struct {
+ Component ComponentType `json:"component" validate:"required,valid"`
+ Version string `json:"version" validate:"required,semver"`
+ OS OSType `json:"os" validate:"required,valid"`
+ Arch ArchType `json:"arch" validate:"required,valid"`
+}
+
+func (p PackageInfoRequest) Valid() error {
+ return validate.Struct(p)
+}
+
+func (p PackageInfoRequest) Query() map[string]string {
+ return map[string]string{
+ "component": p.Component.String(),
+ "version": p.Version,
+ "os": p.OS.String(),
+ "arch": p.Arch.String(),
+ }
+}
+
+// PackageInfoResponse represents response for package info
+type PackageInfoResponse struct {
+ Size int64 `json:"size" validate:"required,min=1"`
+ Hash string `json:"hash" validate:"required,len=64"`
+ Version string `json:"version" validate:"required,semver"`
+ OS OSType `json:"os" validate:"required,valid"`
+ Arch ArchType `json:"arch" validate:"required,valid"`
+ Signature SignatureValue `json:"signature,omitempty" validate:"omitempty,valid"`
+}
+
+func (p PackageInfoResponse) Valid() error {
+ return validate.Struct(p)
+}
+
+// DownloadPackageRequest represents public API request to download package
+type DownloadPackageRequest struct {
+ Component ComponentType `json:"component" validate:"required,valid"`
+ Version string `json:"version" validate:"required,semver"`
+ OS OSType `json:"os" validate:"required,valid"`
+ Arch ArchType `json:"arch" validate:"required,valid"`
+}
+
+func (p DownloadPackageRequest) Valid() error {
+ return validate.Struct(p)
+}
+
+func (p DownloadPackageRequest) Query() map[string]string {
+ return map[string]string{
+ "component": p.Component.String(),
+ "version": p.Version,
+ "os": p.OS.String(),
+ "arch": p.Arch.String(),
+ }
+}
diff --git a/models/signature.go b/models/signature.go
new file mode 100644
index 0000000..bb56521
--- /dev/null
+++ b/models/signature.go
@@ -0,0 +1,163 @@
+package models
+
+import (
+ "crypto/ed25519"
+ "crypto/sha512"
+ "database/sql/driver"
+ "encoding/base64"
+ "fmt"
+ "hash"
+ "io"
+ "os"
+)
+
+type SignatureValue string
+
+func (sv *SignatureValue) FromBytes(b []byte) (SignatureValue, error) {
+ if sv == nil {
+ return "", fmt.Errorf("signature value is nil")
+ }
+
+ if len(b) != ed25519.SignatureSize {
+ return "", fmt.Errorf("invalid signature length")
+ }
+
+ *sv = SignatureValue(base64.StdEncoding.WithPadding(base64.NoPadding).EncodeToString(b))
+
+ return *sv, nil
+}
+
+func (sv SignatureValue) ToBytes() ([]byte, error) {
+ return base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(string(sv))
+}
+
+func (sv SignatureValue) String() string {
+ return string(sv)
+}
+
+func (sv *SignatureValue) Scan(value any) error {
+ if value == nil {
+ *sv = ""
+ return nil
+ }
+
+ if bv, err := driver.String.ConvertValue(value); err == nil {
+ if v, ok := bv.(string); ok {
+ *sv = SignatureValue(v)
+ return nil
+ }
+ }
+
+ return fmt.Errorf("cannot scan %T into SignatureValue", value)
+}
+
+func (sv SignatureValue) Value() (driver.Value, error) {
+ return string(sv), nil
+}
+
+func (sv SignatureValue) Valid() error {
+ signature, err := sv.ToBytes()
+ if err != nil {
+ return fmt.Errorf("failed to decode signature: %w", err)
+ }
+
+ if len(signature) != ed25519.SignatureSize {
+ return fmt.Errorf("invalid signature length")
+ }
+
+ return nil
+}
+
+func (sv SignatureValue) ValidateFile(path string) error {
+ file, err := os.Open(path)
+ if err != nil {
+ return fmt.Errorf("failed to open file: %w", err)
+ }
+ defer file.Close()
+
+ reader := sv.ValidateWrapReader(file)
+
+ if _, err = io.Copy(io.Discard, reader); err != nil {
+ return fmt.Errorf("failed to validate file: %w", err)
+ }
+
+ return reader.Valid()
+}
+
+func (sv SignatureValue) ValidateData(data []byte) error {
+ h := sha512.New()
+ h.Write(data)
+ return sv.ValidateHash(h.Sum(nil))
+}
+
+func (sv SignatureValue) ValidateHash(hash []byte) error {
+ if len(hash) != sha512.Size {
+ return fmt.Errorf("invalid hash length")
+ }
+
+ signature, err := sv.ToBytes()
+ if err != nil {
+ return fmt.Errorf("failed to decode signature: %w", err)
+ }
+
+ valid := ed25519.Verify(getSignaturePublicKey(), hash, signature)
+ if !valid {
+ return fmt.Errorf("failed to verify signature")
+ }
+
+ return nil
+}
+
+func (sv SignatureValue) ValidateWrapReader(r io.Reader) SignatureReader {
+ return &signatureReader{h: sha512.New(), r: r, s: sv}
+}
+
+func (sv SignatureValue) ValidateWrapWriter(w io.Writer) SignatureWriter {
+ return &signatureWriter{h: sha512.New(), w: w, s: sv}
+}
+
+type SignatureWriter interface {
+ io.Writer
+ IValid
+}
+
+type signatureWriter struct {
+ h hash.Hash
+ w io.Writer
+ s SignatureValue
+}
+
+func (w *signatureWriter) Write(p []byte) (n int, err error) {
+ n, err = w.w.Write(p)
+ if n != 0 {
+ _, _ = w.h.Write(p[:n])
+ }
+ return
+}
+
+func (w *signatureWriter) Valid() error {
+ return w.s.ValidateHash(w.h.Sum(nil))
+}
+
+type SignatureReader interface {
+ io.Reader
+ IValid
+}
+
+type signatureReader struct {
+ h hash.Hash
+ r io.Reader
+ s SignatureValue
+}
+
+func (r *signatureReader) Read(p []byte) (n int, err error) {
+ n, err = r.r.Read(p)
+ if n != 0 {
+ _, _ = r.h.Write(p[:n])
+ }
+ return
+}
+
+func (r *signatureReader) Valid() error {
+ return r.s.ValidateHash(r.h.Sum(nil))
+}
diff --git a/models/signature_test.go b/models/signature_test.go
new file mode 100644
index 0000000..235e5e3
--- /dev/null
+++ b/models/signature_test.go
@@ -0,0 +1,300 @@
+package models
+
+import (
+ "bytes"
+ "encoding/base64"
+ "math/rand"
+ "os"
+ "testing"
+)
+
+// test scenario with data and expected signature
+type testScenario struct {
+ name string
+ data []byte
+ signature string
+}
+
+// static test scenarios with pre-generated signatures
+var testScenarios = []testScenario{
+ {
+ name: "empty_data",
+ data: []byte(nil),
+ signature: "kXpU580XomsHUc0W1Epv83kx9958ZEoc82c6v9CWn4VCzUhiv2d65Z6pxyvcDAqcTWVksFNTq+WSuQI4BPgcDQ",
+ },
+ {
+ name: "short_text",
+ data: []byte("hello"),
+ signature: "HBn0P1kMfFysBgg8I7mFws2IPDFMGd7sodrVCheJjmwNIq0Fnw1YVarizd0UgFC50hZ2HAsCAhyqWKkJToaqCw",
+ },
+ {
+ name: "medium_text",
+ data: []byte("test signature validation"),
+ signature: "QPLFdvQOth0fD5p47Dv+DFePquvqp1tvSafsUtM31ylKKoTMrJUc3Xd2wrqVGDU2KhwjuA0FH6CgDdlmZ73PAw",
+ },
+ {
+ name: "json_data",
+ data: []byte(`{"version":"1.0","component":"pentagi","action":"update"}`),
+ signature: "kqTQFYJr2ExaYpjPltYtHBBN/MGIyuqLufOJC2VuWw2Lx0LsLfTrEqCVZZBJQMq80lbUP822AvMgJRGXqddEDQ",
+ },
+ {
+ name: "binary_data",
+ data: []byte("\x00\x01\x02\x03\xff\xfe\xfd\xfc"),
+ signature: "3NsPpsAQWatTJFg2D8jvvHmN3yIVKtCn/fFKiE2+emGKY1NlLh25OFFDBC0K5gx3bLJUOBQfprY7wVugXOw6AA",
+ },
+ {
+ name: "repeated_pattern_256b",
+ data: bytes.Repeat([]byte("A"), 256),
+ signature: "xZEOlq7qcr+ONVMxqCaNLXcJY/R5y7Jr1CVM7nYZmrUNMfQlC+7c/FMfaBNivxZTq4+8WYcDXNeh8RpUbkXeDg",
+ },
+ {
+ name: "repeated_pattern_700b",
+ data: bytes.Repeat([]byte("pattern"), 100),
+ signature: "x8ufqgotq9jzOIi6+9ONOTvRtx02bGnnHkgyz+UFJK2kNzXlaI0RDnN/iEWfVUy6qzxF+K4H0gLw7+LdulmOAA",
+ },
+ {
+ name: "random_64b",
+ data: func() []byte {
+ r := rand.New(rand.NewSource(42))
+ data := make([]byte, 64)
+ r.Read(data)
+ return data
+ }(),
+ signature: "Nar735e4spAROIEzmSBgNC9fyF/hQLg0WMHv4legbksB3ag3nBvGGGgcryJ3u3E/svYQRXaTKKbgZT08a7OdBQ",
+ },
+ {
+ name: "random_10kb",
+ data: func() []byte {
+ data := make([]byte, 10*1024)
+ r := rand.New(rand.NewSource(123))
+ r.Read(data)
+ return data
+ }(),
+ signature: "4jjazjdCwcn6anPK4j6yTmcZBuEByZCD6dAz8XMRER9mCSyYiFWJHxeeCIp5Q18zzbUpXJP5Gy+7AcTH6s+3CQ",
+ },
+ {
+ name: "structured_100kb",
+ data: func() []byte {
+ data := make([]byte, 100*1024)
+ for i := range data {
+ data[i] = byte(i % 256)
+ }
+ return data
+ }(),
+ signature: "/qVFpJ8CTXgF4DZmsIsztDiLg7Wdr2OUolaWnNCBGB7CP8XHq7Mb9t8wo4vOU9tC3iiPKMaDiz1wN04KfyJxCw",
+ },
+ {
+ name: "stress_10mb",
+ data: func() []byte {
+ data := make([]byte, 10*1024*1024)
+ r := rand.New(rand.NewSource(456))
+ for i := 0; i < len(data); i += 4096 {
+ end := min(i+4096, len(data))
+ _, _ = r.Read(data[i:end])
+ }
+ return data
+ }(),
+ signature: "ynchaH0EPK+vwyjUcXN6IONf0RkYn0BdReTqjobmAVkK0eNUObsXYZoaCmw9ImcVtASVgEJ3VPo8TOV7cnCBDw",
+ },
+}
+
+func TestSignatureValue_FromBytes(t *testing.T) {
+ // test with real signature from first scenario
+ realSig, _ := base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(testScenarios[0].signature)
+
+ t.Run("valid_signature", func(t *testing.T) {
+ var sv SignatureValue
+ result, err := sv.FromBytes(realSig)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if result != SignatureValue(testScenarios[0].signature) {
+ t.Error("signature mismatch")
+ }
+ })
+
+ t.Run("invalid_length", func(t *testing.T) {
+ var sv SignatureValue
+ _, err := sv.FromBytes([]byte("short"))
+ if err == nil {
+ t.Error("expected error for invalid length")
+ }
+ })
+
+ t.Run("nil_receiver", func(t *testing.T) {
+ var sv *SignatureValue
+ _, err := sv.FromBytes(realSig)
+ if err == nil {
+ t.Error("expected error for nil receiver")
+ }
+ })
+}
+
+func TestSignatureValue_ValidateData(t *testing.T) {
+ for _, scenario := range testScenarios {
+ t.Run(scenario.name, func(t *testing.T) {
+ signature := SignatureValue(scenario.signature)
+ err := signature.ValidateData(scenario.data)
+ if err != nil {
+ t.Errorf("validation failed for %s: %v", scenario.name, err)
+ }
+ })
+ }
+
+ // test with corrupted signature
+ t.Run("corrupted_signature", func(t *testing.T) {
+ corruptedSig := SignatureValue("invalid_signature_base64")
+ err := corruptedSig.ValidateData(testScenarios[0].data)
+ if err == nil {
+ t.Error("expected validation failure for corrupted signature")
+ }
+ })
+
+ // test with wrong data
+ t.Run("wrong_data", func(t *testing.T) {
+ signature := SignatureValue(testScenarios[1].signature)
+ wrongData := append(testScenarios[1].data, byte('x'))
+ err := signature.ValidateData(wrongData)
+ if err == nil {
+ t.Error("expected validation failure for wrong data")
+ }
+ })
+}
+
+func TestSignatureValue_ValidateFile(t *testing.T) {
+ // create temp file with known data
+ tmpfile, err := os.CreateTemp("", "signature_test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(tmpfile.Name())
+
+ testData := testScenarios[2].data
+ if _, err := tmpfile.Write(testData); err != nil {
+ t.Fatal(err)
+ }
+ tmpfile.Close()
+
+ t.Run("valid_file", func(t *testing.T) {
+ signature := SignatureValue(testScenarios[2].signature)
+ err := signature.ValidateFile(tmpfile.Name())
+ if err != nil {
+ t.Errorf("file validation failed: %v", err)
+ }
+ })
+
+ t.Run("nonexistent_file", func(t *testing.T) {
+ signature := SignatureValue(testScenarios[0].signature)
+ err := signature.ValidateFile("/nonexistent/path")
+ if err == nil {
+ t.Error("expected error for nonexistent file")
+ }
+ })
+}
+
+func TestSignatureValue_WrapReader(t *testing.T) {
+ scenario := testScenarios[3] // json data scenario
+ reader := bytes.NewReader(scenario.data)
+ signature := SignatureValue(scenario.signature)
+
+ wrappedReader := signature.ValidateWrapReader(reader)
+
+ // read all data
+ readData, err := bytes.NewBuffer(nil).ReadFrom(wrappedReader)
+ if err != nil {
+ t.Errorf("read error: %v", err)
+ }
+ if readData != int64(len(scenario.data)) {
+ t.Errorf("read %d bytes, expected %d", readData, len(scenario.data))
+ }
+
+ // validate
+ if err := wrappedReader.Valid(); err != nil {
+ t.Errorf("validation failed: %v", err)
+ }
+}
+
+func TestSignatureValue_WrapWriter(t *testing.T) {
+ scenario := testScenarios[4] // binary data scenario
+ var buffer bytes.Buffer
+ signature := SignatureValue(scenario.signature)
+
+ wrappedWriter := signature.ValidateWrapWriter(&buffer)
+
+ // write data
+ n, err := wrappedWriter.Write(scenario.data)
+ if err != nil {
+ t.Errorf("write error: %v", err)
+ }
+ if n != len(scenario.data) {
+ t.Errorf("wrote %d bytes, expected %d", n, len(scenario.data))
+ }
+
+ // validate
+ if err := wrappedWriter.Valid(); err != nil {
+ t.Errorf("validation failed: %v", err)
+ }
+
+ // verify written data
+ if !bytes.Equal(buffer.Bytes(), scenario.data) {
+ t.Error("written data doesn't match original")
+ }
+}
+
+func TestSignatureValue_EdgeCases(t *testing.T) {
+ t.Run("invalid_base64", func(t *testing.T) {
+ signature := SignatureValue("invalid!@#base64")
+ err := signature.Valid()
+ if err == nil {
+ t.Error("expected error for invalid base64")
+ }
+ })
+
+ t.Run("wrong_length_base64", func(t *testing.T) {
+ shortSig := base64.StdEncoding.WithPadding(base64.NoPadding).EncodeToString(make([]byte, 32))
+ signature := SignatureValue(shortSig)
+ err := signature.Valid()
+ if err == nil {
+ t.Error("expected error for wrong signature length")
+ }
+ })
+
+ t.Run("invalid_hash_length", func(t *testing.T) {
+ signature := SignatureValue(testScenarios[0].signature)
+ err := signature.ValidateHash([]byte("short_hash"))
+ if err == nil {
+ t.Error("expected error for invalid hash length")
+ }
+ })
+}
+
+// benchmarks for performance testing
+func BenchmarkSignatureValue_ValidateData_Small(b *testing.B) {
+ signature := SignatureValue(testScenarios[1].signature) // short text
+ data := testScenarios[1].data
+
+ b.ResetTimer()
+ for b.Loop() {
+ _ = signature.ValidateData(data)
+ }
+}
+
+func BenchmarkSignatureValue_ValidateData_Large(b *testing.B) {
+ signature := SignatureValue(testScenarios[8].signature) // 10KB
+ data := testScenarios[8].data
+
+ b.ResetTimer()
+ for b.Loop() {
+ _ = signature.ValidateData(data)
+ }
+}
+
+func BenchmarkSignatureValue_ValidateData_Huge(b *testing.B) {
+ signature := SignatureValue(testScenarios[10].signature) // 10MB
+ data := testScenarios[10].data
+
+ b.ResetTimer()
+ for b.Loop() {
+ _ = signature.ValidateData(data)
+ }
+}
diff --git a/models/support.go b/models/support.go
new file mode 100644
index 0000000..2778922
--- /dev/null
+++ b/models/support.go
@@ -0,0 +1,78 @@
+package models
+
+import "github.com/google/uuid"
+
+// SupportErrorRequest represents public API request to report an error automatically
+type SupportErrorRequest struct {
+ Component ComponentType `json:"component" validate:"required,valid"`
+ Version string `json:"version" validate:"required,semver"`
+ OS OSType `json:"os" validate:"required,valid"`
+ Arch ArchType `json:"arch" validate:"required,valid"`
+ ErrorDetails any `json:"error_details" validate:"required"`
+}
+
+func (p SupportErrorRequest) Valid() error {
+ return validate.Struct(p)
+}
+
+// SupportErrorResponse represents response for error reporting (empty for now, reserved for future)
+type SupportErrorResponse struct {
+ // Reserved for future expansion
+}
+
+func (ser SupportErrorResponse) Valid() error {
+ return validate.Struct(ser)
+}
+
+// SupportIssueRequest represents public API request to report an issue manually with AI assistance
+type SupportIssueRequest struct {
+ Component ComponentType `json:"component" validate:"required,valid"`
+ Version string `json:"version" validate:"required,semver"`
+ OS OSType `json:"os" validate:"required,valid"`
+ Arch ArchType `json:"arch" validate:"required,valid"`
+ Logs []SupportLogs `json:"logs" validate:"omitempty,valid"`
+ ErrorDetails any `json:"error_details" validate:"required"`
+}
+
+func (sir SupportIssueRequest) Valid() error {
+ return validate.Struct(sir)
+}
+
+// SupportLogs represents logs for a component
+type SupportLogs struct {
+ Component ComponentType `json:"component" validate:"required,valid"`
+ Logs []string `json:"logs" validate:"omitempty,dive,min=1,max=8192"`
+}
+
+func (sl SupportLogs) Valid() error {
+ return validate.Struct(sl)
+}
+
+// SupportIssueResponse represents response for issue reporting with AI assistance
+type SupportIssueResponse struct {
+ IssueID uuid.UUID `json:"issue_id" validate:"required"`
+}
+
+func (sir SupportIssueResponse) Valid() error {
+ return validate.Struct(sir)
+}
+
+// SupportInvestigationRequest represents public API request to investigate an issue with AI assistance
+type SupportInvestigationRequest struct {
+ IssueID uuid.UUID `json:"issue_id" validate:"required"`
+ UseSteam bool `json:"use_steam,omitempty" validate:"omitempty"`
+ UserInput string `json:"user_input" validate:"required,min=1,max=4000"`
+}
+
+func (sir SupportInvestigationRequest) Valid() error {
+ return validate.Struct(sir)
+}
+
+// SupportInvestigationResponse represents response for investigation of issue with AI assistance (without steam response)
+type SupportInvestigationResponse struct {
+ Answer string `json:"answer" validate:"required"`
+}
+
+func (sir SupportInvestigationResponse) Valid() error {
+ return validate.Struct(sir)
+}
diff --git a/models/types.go b/models/types.go
new file mode 100644
index 0000000..99e58eb
--- /dev/null
+++ b/models/types.go
@@ -0,0 +1,220 @@
+package models
+
+import (
+ "database/sql/driver"
+ "fmt"
+)
+
+// ComponentType represents component type enum
+type ComponentType string
+
+const (
+ ComponentTypePentagi ComponentType = "pentagi"
+ ComponentTypeScraper ComponentType = "scraper"
+ ComponentTypeLangfuseWorker ComponentType = "langfuse-worker"
+ ComponentTypeLangfuseWeb ComponentType = "langfuse-web"
+ ComponentTypeGrafana ComponentType = "grafana"
+ ComponentTypeOtelcol ComponentType = "otelcol"
+ ComponentTypeWorker ComponentType = "worker"
+ ComponentTypeInstaller ComponentType = "installer"
+ ComponentTypeEngine ComponentType = "engine"
+)
+
+func (ct ComponentType) String() string {
+ return string(ct)
+}
+
+func (ct *ComponentType) Scan(value any) error {
+ if value == nil {
+ *ct = ""
+ return nil
+ }
+ if bv, err := driver.String.ConvertValue(value); err == nil {
+ if v, ok := bv.(string); ok {
+ *ct = ComponentType(v)
+ return nil
+ }
+ }
+ return fmt.Errorf("cannot scan %T into ComponentType", value)
+}
+
+func (ct ComponentType) Value() (driver.Value, error) {
+ return string(ct), nil
+}
+
+func (ct ComponentType) Valid() error {
+ switch ct {
+ case ComponentTypePentagi, ComponentTypeScraper,
+ ComponentTypeLangfuseWorker, ComponentTypeLangfuseWeb,
+ ComponentTypeGrafana, ComponentTypeOtelcol,
+ ComponentTypeWorker, ComponentTypeInstaller, ComponentTypeEngine:
+ return nil
+ default:
+ return fmt.Errorf("invalid ComponentType: %s", ct)
+ }
+}
+
+// ComponentStatus represents component status enum
+type ComponentStatus string
+
+const (
+ ComponentStatusUnused ComponentStatus = "unused"
+ ComponentStatusConnected ComponentStatus = "connected"
+ ComponentStatusInstalled ComponentStatus = "installed"
+ ComponentStatusRunning ComponentStatus = "running"
+)
+
+func (cs ComponentStatus) String() string {
+ return string(cs)
+}
+
+func (cs *ComponentStatus) Scan(value any) error {
+ if value == nil {
+ *cs = ""
+ return nil
+ }
+ if bv, err := driver.String.ConvertValue(value); err == nil {
+ if v, ok := bv.(string); ok {
+ *cs = ComponentStatus(v)
+ return nil
+ }
+ }
+ return fmt.Errorf("cannot scan %T into ComponentStatus", value)
+}
+
+func (cs ComponentStatus) Value() (driver.Value, error) {
+ return string(cs), nil
+}
+
+func (cs ComponentStatus) Valid() error {
+ switch cs {
+ case ComponentStatusUnused, ComponentStatusConnected,
+ ComponentStatusInstalled, ComponentStatusRunning:
+ return nil
+ default:
+ return fmt.Errorf("invalid ComponentStatus: %s", cs)
+ }
+}
+
+// ProductStack represents product stack enum
+type ProductStack string
+
+const (
+ ProductStackPentagi ProductStack = "pentagi"
+ ProductStackLangfuse ProductStack = "langfuse"
+ ProductStackObservability ProductStack = "observability"
+ ProductStackWorker ProductStack = "worker"
+ ProductStackInstaller ProductStack = "installer"
+ ProductStackEngine ProductStack = "engine"
+)
+
+func (ps ProductStack) String() string {
+ return string(ps)
+}
+
+func (ps *ProductStack) Scan(value any) error {
+ if value == nil {
+ *ps = ""
+ return nil
+ }
+ if bv, err := driver.String.ConvertValue(value); err == nil {
+ if v, ok := bv.(string); ok {
+ *ps = ProductStack(v)
+ return nil
+ }
+ }
+ return fmt.Errorf("cannot scan %T into ProductStack", value)
+}
+
+func (ps ProductStack) Value() (driver.Value, error) {
+ return string(ps), nil
+}
+
+func (ps ProductStack) Valid() error {
+ switch ps {
+ case ProductStackPentagi, ProductStackLangfuse, ProductStackObservability,
+ ProductStackWorker, ProductStackInstaller, ProductStackEngine:
+ return nil
+ default:
+ return fmt.Errorf("invalid ProductStack: %s", ps)
+ }
+}
+
+// OSType represents operating system enum
+type OSType string
+
+const (
+ OSTypeWindows OSType = "windows"
+ OSTypeLinux OSType = "linux"
+ OSTypeDarwin OSType = "darwin"
+)
+
+func (os OSType) String() string {
+ return string(os)
+}
+
+func (os *OSType) Scan(value any) error {
+ if value == nil {
+ *os = ""
+ return nil
+ }
+ if bv, err := driver.String.ConvertValue(value); err == nil {
+ if v, ok := bv.(string); ok {
+ *os = OSType(v)
+ return nil
+ }
+ }
+ return fmt.Errorf("cannot scan %T into OSType", value)
+}
+
+func (os OSType) Value() (driver.Value, error) {
+ return string(os), nil
+}
+
+func (os OSType) Valid() error {
+ switch os {
+ case OSTypeWindows, OSTypeLinux, OSTypeDarwin:
+ return nil
+ default:
+ return fmt.Errorf("invalid OSType: %s", os)
+ }
+}
+
+// ArchType represents CPU architecture enum
+type ArchType string
+
+const (
+ ArchTypeAMD64 ArchType = "amd64"
+ ArchTypeARM64 ArchType = "arm64"
+)
+
+func (arch ArchType) String() string {
+ return string(arch)
+}
+
+func (arch *ArchType) Scan(value any) error {
+ if value == nil {
+ *arch = ""
+ return nil
+ }
+ if bv, err := driver.String.ConvertValue(value); err == nil {
+ if v, ok := bv.(string); ok {
+ *arch = ArchType(v)
+ return nil
+ }
+ }
+ return fmt.Errorf("cannot scan %T into ArchType", value)
+}
+
+func (arch ArchType) Value() (driver.Value, error) {
+ return string(arch), nil
+}
+
+func (arch ArchType) Valid() error {
+ switch arch {
+ case ArchTypeAMD64, ArchTypeARM64:
+ return nil
+ default:
+ return fmt.Errorf("invalid ArchType: %s", arch)
+ }
+}
diff --git a/models/update.go b/models/update.go
new file mode 100644
index 0000000..7b93b57
--- /dev/null
+++ b/models/update.go
@@ -0,0 +1,52 @@
+package models
+
+// CheckUpdatesRequest represents public API request for checking updates
+type CheckUpdatesRequest struct {
+ InstallerVersion string `json:"installer_version" validate:"required,semver"`
+ InstallerOS OSType `json:"installer_os" validate:"required,valid"`
+ InstallerArch ArchType `json:"installer_arch" validate:"required,valid"`
+
+ // Current installed components
+ Components []ComponentInfo `json:"components" validate:"dive,valid"`
+}
+
+func (p CheckUpdatesRequest) Valid() error {
+ return validate.Struct(p)
+}
+
+// ComponentInfo represents information about installed component
+type ComponentInfo struct {
+ Component ComponentType `json:"component" validate:"required,valid"`
+ Status ComponentStatus `json:"status" validate:"required,valid"`
+ Version *string `json:"version,omitempty" validate:"omitempty,semver"`
+ Hash *string `json:"hash,omitempty" validate:"omitempty,min=1,max=255"`
+ Repository *string `json:"repository,omitempty" validate:"omitempty,min=1,max=255"`
+ Tag *string `json:"tag,omitempty" validate:"omitempty,min=1,max=100"`
+}
+
+func (c ComponentInfo) Valid() error {
+ return validate.Struct(c)
+}
+
+// CheckUpdatesResponse represents response for update check
+type CheckUpdatesResponse struct {
+ Updates []UpdateInfo `json:"updates" validate:"dive,valid"`
+}
+
+func (c CheckUpdatesResponse) Valid() error {
+ return validate.Struct(c)
+}
+
+// UpdateInfo represents available update information
+type UpdateInfo struct {
+ Stack ProductStack `json:"stack" validate:"required,valid" swaggertype:"string"`
+ HasUpdate bool `json:"has_update"`
+ CurrentVersion *string `json:"current_version,omitempty" validate:"omitempty,semver"`
+ LatestVersion *string `json:"latest_version,omitempty" validate:"omitempty,semver"`
+ Changelog *string `json:"changelog,omitempty" validate:"omitempty"`
+ ReleaseNotes *string `json:"release_notes,omitempty" validate:"omitempty"`
+}
+
+func (u UpdateInfo) Valid() error {
+ return validate.Struct(u)
+}
diff --git a/sdk/calls.go b/sdk/calls.go
new file mode 100644
index 0000000..19f0cf1
--- /dev/null
+++ b/sdk/calls.go
@@ -0,0 +1,574 @@
+package sdk
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net/url"
+ "time"
+)
+
+const (
+ DefaultClientName = "sdk"
+ DefaultClientVersion = "1.0.0"
+ DefaultPowTimeout = 30 * time.Second
+ DefaultWaitTime = 10 * time.Second
+ DefaultMaxRetries = 3
+)
+
+type (
+ // returns a response body as bytes
+ CallReqRespBytes func(ctx context.Context) ([]byte, error)
+ // returns a reader for response body
+ CallReqRespReader func(ctx context.Context) (io.ReadCloser, error)
+ // writes response body to writer
+ CallReqRespWriter func(ctx context.Context, w io.Writer) error
+ // returns a response body as bytes and gets request query parameters
+ CallReqQueryRespBytes func(ctx context.Context, query map[string]string) ([]byte, error)
+ // returns a reader for response body and gets request query parameters
+ CallReqQueryRespReader func(ctx context.Context, query map[string]string) (io.ReadCloser, error)
+ // writes response body to writer and gets request query parameters
+ CallReqQueryRespWriter func(ctx context.Context, query map[string]string, w io.Writer) error
+ // returns a response body as bytes and gets request position arguments and query parameters
+ CallReqWithArgsRespBytes func(ctx context.Context, args []string) ([]byte, error)
+ // returns a reader for response body and gets request position arguments and query parameters
+ CallReqWithArgsRespReader func(ctx context.Context, args []string) (io.ReadCloser, error)
+ // writes response body to writer and gets request position arguments and query parameters
+ CallReqWithArgsRespWriter func(ctx context.Context, args []string, w io.Writer) error
+ // returns a response body as bytes and gets request position arguments and query parameters
+ CallReqQueryWithArgsRespBytes func(ctx context.Context, args []string, query map[string]string) ([]byte, error)
+ // returns a reader for response body and gets request position arguments and query parameters
+ CallReqQueryWithArgsRespReader func(ctx context.Context, args []string, query map[string]string) (io.ReadCloser, error)
+ // writes response body to writer and gets request position arguments and query parameters
+ CallReqQueryWithArgsRespWriter func(ctx context.Context, args []string, query map[string]string, w io.Writer) error
+
+ // returns a response body as bytes and gets request body as bytes
+ CallReqBytesRespBytes func(ctx context.Context, body []byte) ([]byte, error)
+ // returns a reader for response body and gets request body as bytes
+ CallReqBytesRespReader func(ctx context.Context, body []byte) (io.ReadCloser, error)
+ // writes response body to writer and gets request body as bytes
+ CallReqBytesRespWriter func(ctx context.Context, body []byte, w io.Writer) error
+ // returns a response body as bytes and gets request body as reader and length
+ CallReqReaderRespBytes func(ctx context.Context, r io.Reader, l int64) ([]byte, error)
+ // returns a reader for response body and gets request body as reader and length
+ CallReqReaderRespReader func(ctx context.Context, r io.Reader, l int64) (io.ReadCloser, error)
+ // writes response body to writer and gets request body as reader and length
+ CallReqReaderRespWriter func(ctx context.Context, r io.Reader, l int64, w io.Writer) error
+
+ // returns a response body as bytes and gets request position arguments and request body as bytes
+ CallReqBytesWithArgsRespBytes func(ctx context.Context, args []string, body []byte) ([]byte, error)
+ // returns a reader for response body and gets request position arguments and request body as bytes
+ CallReqBytesWithArgsRespReader func(ctx context.Context, args []string, body []byte) (io.ReadCloser, error)
+ // writes response body to writer and gets request position arguments and request body as bytes
+ CallReqBytesWithArgsRespWriter func(ctx context.Context, args []string, body []byte, w io.Writer) error
+ // returns a response body as bytes and gets position arguments and request body as reader and length
+ CallReqReaderWithArgsRespBytes func(ctx context.Context, args []string, r io.Reader, l int64) ([]byte, error)
+ // returns a reader for response body and gets position arguments and request body as reader and length
+ CallReqReaderWithArgsRespReader func(ctx context.Context, args []string, r io.Reader, l int64) (io.ReadCloser, error)
+ // writes response body to writer and gets position arguments and request body as reader and length
+ CallReqReaderWithArgsRespWriter func(ctx context.Context, args []string, r io.Reader, l int64, w io.Writer) error
+)
+
+type CallType interface {
+ CallReqRespBytes |
+ CallReqRespReader |
+ CallReqRespWriter |
+ CallReqQueryRespBytes |
+ CallReqQueryRespReader |
+ CallReqQueryRespWriter |
+ CallReqWithArgsRespBytes |
+ CallReqWithArgsRespReader |
+ CallReqWithArgsRespWriter |
+ CallReqQueryWithArgsRespBytes |
+ CallReqQueryWithArgsRespReader |
+ CallReqQueryWithArgsRespWriter |
+ CallReqBytesRespBytes |
+ CallReqBytesRespReader |
+ CallReqBytesRespWriter |
+ CallReqReaderRespBytes |
+ CallReqReaderRespReader |
+ CallReqReaderRespWriter |
+ CallReqBytesWithArgsRespBytes |
+ CallReqBytesWithArgsRespReader |
+ CallReqBytesWithArgsRespWriter |
+ CallReqReaderWithArgsRespBytes |
+ CallReqReaderWithArgsRespReader |
+ CallReqReaderWithArgsRespWriter
+}
+
+type CallPointerType interface {
+ *CallReqRespBytes |
+ *CallReqRespReader |
+ *CallReqRespWriter |
+ *CallReqQueryRespBytes |
+ *CallReqQueryRespReader |
+ *CallReqQueryRespWriter |
+ *CallReqWithArgsRespBytes |
+ *CallReqWithArgsRespReader |
+ *CallReqWithArgsRespWriter |
+ *CallReqQueryWithArgsRespBytes |
+ *CallReqQueryWithArgsRespReader |
+ *CallReqQueryWithArgsRespWriter |
+ *CallReqBytesRespBytes |
+ *CallReqBytesRespReader |
+ *CallReqBytesRespWriter |
+ *CallReqReaderRespBytes |
+ *CallReqReaderRespReader |
+ *CallReqReaderRespWriter |
+ *CallReqBytesWithArgsRespBytes |
+ *CallReqBytesWithArgsRespReader |
+ *CallReqBytesWithArgsRespWriter |
+ *CallReqReaderWithArgsRespBytes |
+ *CallReqReaderWithArgsRespReader |
+ *CallReqReaderWithArgsRespWriter
+}
+
+type CallMethod string
+
+const (
+ CallMethodGET CallMethod = "GET"
+ CallMethodPOST CallMethod = "POST"
+ CallMethodPUT CallMethod = "PUT"
+ CallMethodPATCH CallMethod = "PATCH"
+ CallMethodDELETE CallMethod = "DELETE"
+)
+
+type CallConfig struct {
+ Calls []any // slice of pointers to CallType function (each must be CallPointerType)
+ Host string // server host (domain name or IP address)
+ Name string // unique route name on the server side
+ Path string // route path may contain position arguments (e.g. /users/:id)
+ Method CallMethod // HTTP method (supports: GET, POST, PUT, PATCH, DELETE)
+}
+
+func checkCallType(call any) error {
+ if call == nil {
+ return fmt.Errorf("call %T must be a valid pointer to a function", call)
+ }
+
+ switch call.(type) {
+ case *CallReqRespBytes:
+ case *CallReqRespReader:
+ case *CallReqRespWriter:
+ case *CallReqQueryRespBytes:
+ case *CallReqQueryRespReader:
+ case *CallReqQueryRespWriter:
+ case *CallReqWithArgsRespBytes:
+ case *CallReqWithArgsRespReader:
+ case *CallReqWithArgsRespWriter:
+ case *CallReqQueryWithArgsRespBytes:
+ case *CallReqQueryWithArgsRespReader:
+ case *CallReqQueryWithArgsRespWriter:
+ case *CallReqBytesRespBytes:
+ case *CallReqBytesRespReader:
+ case *CallReqBytesRespWriter:
+ case *CallReqReaderRespBytes:
+ case *CallReqReaderRespReader:
+ case *CallReqReaderRespWriter:
+ case *CallReqBytesWithArgsRespBytes:
+ case *CallReqBytesWithArgsRespReader:
+ case *CallReqBytesWithArgsRespWriter:
+ case *CallReqReaderWithArgsRespBytes:
+ case *CallReqReaderWithArgsRespReader:
+ case *CallReqReaderWithArgsRespWriter:
+ default:
+ return fmt.Errorf("invalid call type: %T", call)
+ }
+
+ return nil
+}
+
+func isCallWithArgs(call any) bool {
+ switch call.(type) {
+ case *CallReqWithArgsRespBytes:
+ case *CallReqWithArgsRespReader:
+ case *CallReqWithArgsRespWriter:
+ case *CallReqQueryWithArgsRespBytes:
+ case *CallReqQueryWithArgsRespReader:
+ case *CallReqQueryWithArgsRespWriter:
+ case *CallReqBytesWithArgsRespBytes:
+ case *CallReqBytesWithArgsRespReader:
+ case *CallReqBytesWithArgsRespWriter:
+ case *CallReqReaderWithArgsRespBytes:
+ case *CallReqReaderWithArgsRespReader:
+ case *CallReqReaderWithArgsRespWriter:
+ default:
+ return false
+ }
+
+ return true
+}
+
+func isCallWithBody(call any) bool {
+ switch call.(type) {
+ case *CallReqBytesRespBytes:
+ case *CallReqBytesRespReader:
+ case *CallReqBytesRespWriter:
+ case *CallReqReaderRespBytes:
+ case *CallReqReaderRespReader:
+ case *CallReqReaderRespWriter:
+ case *CallReqBytesWithArgsRespBytes:
+ case *CallReqBytesWithArgsRespReader:
+ case *CallReqBytesWithArgsRespWriter:
+ case *CallReqReaderWithArgsRespBytes:
+ case *CallReqReaderWithArgsRespReader:
+ case *CallReqReaderWithArgsRespWriter:
+ default:
+ return false
+ }
+
+ return true
+}
+
+func isCallWithQuery(call any) bool {
+ switch call.(type) {
+ case *CallReqQueryRespBytes:
+ case *CallReqQueryRespReader:
+ case *CallReqQueryRespWriter:
+ case *CallReqQueryWithArgsRespBytes:
+ case *CallReqQueryWithArgsRespReader:
+ case *CallReqQueryWithArgsRespWriter:
+ default:
+ return false
+ }
+
+ return true
+}
+
+type pathGenerator func(args []string) (string, error)
+
+// call request parameters and response results
+type callContext struct {
+ reqCallURL url.URL // request call target URL
+ reqTicketURL url.URL // request ticket URL
+ reqMethod string // request method
+ reqPathArgs []string // position arguments for path template
+ reqQueryArgs map[string]string // request query parameters
+ reqBodyReader io.Reader // optional input argument if body is provided
+ reqBodyLength int64 // size of the original body in bytes
+ respStatusCode int // fill after invoke
+ respBodyWriter io.Writer // optional input argument
+ respBodyReader io.ReadCloser // fill after invoke
+ restWaitTime time.Duration // fill after invoke if error is temporary
+ context.Context // original context
+}
+
+type callFunc struct {
+ sdk sdk
+ cfg CallConfig
+ gen pathGenerator
+}
+
+func (c *callFunc) invokeWithRetries(cctx *callContext) error {
+ path, err := c.gen(cctx.reqPathArgs)
+ if err != nil {
+ return fmt.Errorf("%w: invalid path args: %w", ErrInvalidRequest, err)
+ }
+
+ queryArgs := url.Values{}
+ for key, value := range cctx.reqQueryArgs {
+ queryArgs.Add(key, value)
+ }
+
+ cctx.reqMethod = string(c.cfg.Method)
+ cctx.reqCallURL = url.URL{
+ Scheme: defaultScheme,
+ Host: c.cfg.Host,
+ Path: path,
+ RawQuery: queryArgs.Encode(),
+ }
+ cctx.reqTicketURL = url.URL{
+ Scheme: defaultScheme,
+ Host: c.cfg.Host,
+ Path: defaultTicketPath + c.cfg.Name,
+ }
+
+ maxRetries := max(c.sdk.maxRetries, 1)
+
+ for attempt := range maxRetries {
+ if err = c.invokeRequest(cctx); err == nil {
+ return nil
+ }
+
+ if !isTemporaryError(err) {
+ msg := fmt.Sprintf("error on attempt %d/%d for %s", attempt+1, maxRetries, c.cfg.Name)
+ c.sdk.logger.WithError(err).Error(msg)
+ break
+ }
+
+ if attempt < maxRetries-1 {
+ waitTime := c.calculateWaitTime(err, cctx)
+ msg := fmt.Sprintf("temporary error on attempt %d/%d for %s", attempt+1, maxRetries, c.cfg.Name)
+ c.sdk.logger.WithError(err).Debugf("%s, waiting %v before retry", msg, waitTime)
+
+ select {
+ case <-cctx.Done():
+ return cctx.Err()
+ case <-time.After(waitTime):
+ // continue to retry
+ }
+ } else {
+ return fmt.Errorf("%w after %d attempts: %w", ErrMaxRetriesExceeded, maxRetries, err)
+ }
+ }
+
+ return err
+}
+
+func (c *callFunc) invokeWithBytes(cctx *callContext) ([]byte, error) {
+ if err := c.invokeWithRetries(cctx); err != nil {
+ return nil, err
+ }
+
+ if cctx.respBodyReader == nil {
+ return nil, fmt.Errorf("%w: response reader is not set", ErrClientInternal)
+ }
+
+ defer func() {
+ if err := cctx.respBodyReader.Close(); err != nil {
+ c.sdk.logger.WithError(err).Errorf("failed to close response reader")
+ }
+ }()
+
+ respBody, err := io.ReadAll(cctx.respBodyReader)
+ if err != nil {
+ return nil, fmt.Errorf("%w: failed to read response body: %w", ErrClientInternal, err)
+ }
+
+ return respBody, nil
+}
+
+func (c *callFunc) invokeWithReader(cctx *callContext) (io.ReadCloser, error) {
+ if err := c.invokeWithRetries(cctx); err != nil {
+ return nil, err
+ }
+
+ if cctx.respBodyReader == nil {
+ return nil, fmt.Errorf("%w: response reader is not set", ErrClientInternal)
+ }
+
+ return cctx.respBodyReader, nil
+}
+
+func (c *callFunc) invokeWithWriter(cctx *callContext) error {
+ if cctx.respBodyWriter == nil {
+ return fmt.Errorf("%w: response writer is required", ErrInvalidRequest)
+ }
+
+ if err := c.invokeWithRetries(cctx); err != nil {
+ return err
+ }
+
+ // just double check that response reader is closed
+ if cctx.respBodyReader != nil {
+ if err := cctx.respBodyReader.Close(); err != nil {
+ c.sdk.logger.WithError(err).Errorf("failed to close response reader")
+ }
+ }
+
+ return nil
+}
+
+// calculateWaitTime determines how long to wait before retry based on error type and response
+func (c *callFunc) calculateWaitTime(err error, cctx *callContext) time.Duration {
+ switch {
+ case errors.Is(err, ErrTooManyRequestsRPM) || errors.Is(err, ErrExperimentTimeout):
+ if cctx != nil && cctx.restWaitTime > 0 {
+ return min(cctx.restWaitTime, DefaultWaitTime)
+ }
+ return DefaultWaitTime
+ case errors.Is(err, ErrTooManyRequests):
+ return 5 * time.Second
+ case errors.Is(err, ErrBadGateway) || errors.Is(err, ErrServerInternal):
+ return 3 * time.Second
+ default:
+ return 1 * time.Second
+ }
+}
+
+func (c *callFunc) callReqRespBytes(ctx context.Context) ([]byte, error) {
+ cctx := callContext{Context: ctx}
+ return c.invokeWithBytes(&cctx)
+}
+
+func (c *callFunc) callReqRespReader(ctx context.Context) (io.ReadCloser, error) {
+ cctx := callContext{Context: ctx}
+ return c.invokeWithReader(&cctx)
+}
+
+func (c *callFunc) callReqRespWriter(ctx context.Context, w io.Writer) error {
+ cctx := callContext{Context: ctx, respBodyWriter: w}
+ return c.invokeWithWriter(&cctx)
+}
+
+func (c *callFunc) callReqQueryRespBytes(ctx context.Context, query map[string]string) ([]byte, error) {
+ cctx := callContext{Context: ctx, reqQueryArgs: query}
+ return c.invokeWithBytes(&cctx)
+}
+
+func (c *callFunc) callReqQueryRespReader(ctx context.Context, query map[string]string) (io.ReadCloser, error) {
+ cctx := callContext{Context: ctx, reqQueryArgs: query}
+ return c.invokeWithReader(&cctx)
+}
+
+func (c *callFunc) callReqQueryRespWriter(ctx context.Context, query map[string]string, w io.Writer) error {
+ cctx := callContext{Context: ctx, reqQueryArgs: query, respBodyWriter: w}
+ return c.invokeWithWriter(&cctx)
+}
+
+func (c *callFunc) callReqWithArgsRespBytes(ctx context.Context, args []string) ([]byte, error) {
+ cctx := callContext{Context: ctx, reqPathArgs: args}
+ return c.invokeWithBytes(&cctx)
+}
+
+func (c *callFunc) callReqWithArgsRespReader(ctx context.Context, args []string) (io.ReadCloser, error) {
+ cctx := callContext{Context: ctx, reqPathArgs: args}
+ return c.invokeWithReader(&cctx)
+}
+
+func (c *callFunc) callReqWithArgsRespWriter(ctx context.Context, args []string, w io.Writer) error {
+ cctx := callContext{Context: ctx, reqPathArgs: args, respBodyWriter: w}
+ return c.invokeWithWriter(&cctx)
+}
+
+func (c *callFunc) callReqQueryWithArgsRespBytes(ctx context.Context, args []string, query map[string]string) ([]byte, error) {
+ cctx := callContext{Context: ctx, reqPathArgs: args, reqQueryArgs: query}
+ return c.invokeWithBytes(&cctx)
+}
+
+func (c *callFunc) callReqQueryWithArgsRespReader(ctx context.Context, args []string, query map[string]string) (io.ReadCloser, error) {
+ cctx := callContext{Context: ctx, reqPathArgs: args, reqQueryArgs: query}
+ return c.invokeWithReader(&cctx)
+}
+
+func (c *callFunc) callReqQueryWithArgsRespWriter(ctx context.Context, args []string, query map[string]string, w io.Writer) error {
+ cctx := callContext{Context: ctx, reqPathArgs: args, reqQueryArgs: query, respBodyWriter: w}
+ return c.invokeWithWriter(&cctx)
+}
+
+func (c *callFunc) callReqBytesRespBytes(ctx context.Context, body []byte) ([]byte, error) {
+ cctx := callContext{Context: ctx, reqBodyReader: bytes.NewReader(body), reqBodyLength: int64(len(body))}
+ return c.invokeWithBytes(&cctx)
+}
+
+func (c *callFunc) callReqBytesRespReader(ctx context.Context, body []byte) (io.ReadCloser, error) {
+ cctx := callContext{Context: ctx, reqBodyReader: bytes.NewReader(body), reqBodyLength: int64(len(body))}
+ return c.invokeWithReader(&cctx)
+}
+
+func (c *callFunc) callReqBytesRespWriter(ctx context.Context, body []byte, w io.Writer) error {
+ cctx := callContext{Context: ctx, reqBodyReader: bytes.NewReader(body), reqBodyLength: int64(len(body)), respBodyWriter: w}
+ return c.invokeWithWriter(&cctx)
+}
+
+func (c *callFunc) callReqReaderRespBytes(ctx context.Context, r io.Reader, l int64) ([]byte, error) {
+ cctx := callContext{Context: ctx, reqBodyReader: r, reqBodyLength: l}
+ return c.invokeWithBytes(&cctx)
+}
+
+func (c *callFunc) callReqReaderRespReader(ctx context.Context, r io.Reader, l int64) (io.ReadCloser, error) {
+ cctx := callContext{Context: ctx, reqBodyReader: r, reqBodyLength: l}
+ return c.invokeWithReader(&cctx)
+}
+
+func (c *callFunc) callReqReaderRespWriter(ctx context.Context, r io.Reader, l int64, w io.Writer) error {
+ cctx := callContext{Context: ctx, reqBodyReader: r, reqBodyLength: l, respBodyWriter: w}
+ return c.invokeWithWriter(&cctx)
+}
+
+func (c *callFunc) callReqBytesWithArgsRespBytes(ctx context.Context, args []string, body []byte) ([]byte, error) {
+ cctx := callContext{Context: ctx, reqPathArgs: args, reqBodyReader: bytes.NewReader(body), reqBodyLength: int64(len(body))}
+ return c.invokeWithBytes(&cctx)
+}
+
+func (c *callFunc) callReqBytesWithArgsRespReader(ctx context.Context, args []string, body []byte) (io.ReadCloser, error) {
+ cctx := callContext{Context: ctx, reqPathArgs: args, reqBodyReader: bytes.NewReader(body), reqBodyLength: int64(len(body))}
+ return c.invokeWithReader(&cctx)
+}
+
+func (c *callFunc) callReqBytesWithArgsRespWriter(ctx context.Context, args []string, body []byte, w io.Writer) error {
+ cctx := callContext{Context: ctx, reqPathArgs: args, reqBodyReader: bytes.NewReader(body), reqBodyLength: int64(len(body)), respBodyWriter: w}
+ return c.invokeWithWriter(&cctx)
+}
+
+func (c *callFunc) callReqReaderWithArgsRespBytes(ctx context.Context, args []string, r io.Reader, l int64) ([]byte, error) {
+ cctx := callContext{Context: ctx, reqPathArgs: args, reqBodyReader: r, reqBodyLength: l}
+ return c.invokeWithBytes(&cctx)
+}
+
+func (c *callFunc) callReqReaderWithArgsRespReader(ctx context.Context, args []string, r io.Reader, l int64) (io.ReadCloser, error) {
+ cctx := callContext{Context: ctx, reqPathArgs: args, reqBodyReader: r, reqBodyLength: l}
+ return c.invokeWithReader(&cctx)
+}
+
+func (c *callFunc) callReqReaderWithArgsRespWriter(ctx context.Context, args []string, r io.Reader, l int64, w io.Writer) error {
+ cctx := callContext{Context: ctx, reqPathArgs: args, reqBodyReader: r, reqBodyLength: l, respBodyWriter: w}
+ return c.invokeWithWriter(&cctx)
+}
+
+func fillCallFunc(cfg CallConfig, sdk sdk, gen pathGenerator) error {
+ cfn := callFunc{
+ sdk: sdk,
+ cfg: cfg,
+ gen: gen,
+ }
+
+ for _, call := range cfg.Calls {
+ switch fn := call.(type) {
+ case *CallReqRespBytes:
+ *fn = cfn.callReqRespBytes
+ case *CallReqRespReader:
+ *fn = cfn.callReqRespReader
+ case *CallReqRespWriter:
+ *fn = cfn.callReqRespWriter
+ case *CallReqQueryRespBytes:
+ *fn = cfn.callReqQueryRespBytes
+ case *CallReqQueryRespReader:
+ *fn = cfn.callReqQueryRespReader
+ case *CallReqQueryRespWriter:
+ *fn = cfn.callReqQueryRespWriter
+ case *CallReqWithArgsRespBytes:
+ *fn = cfn.callReqWithArgsRespBytes
+ case *CallReqWithArgsRespReader:
+ *fn = cfn.callReqWithArgsRespReader
+ case *CallReqWithArgsRespWriter:
+ *fn = cfn.callReqWithArgsRespWriter
+ case *CallReqQueryWithArgsRespBytes:
+ *fn = cfn.callReqQueryWithArgsRespBytes
+ case *CallReqQueryWithArgsRespReader:
+ *fn = cfn.callReqQueryWithArgsRespReader
+ case *CallReqQueryWithArgsRespWriter:
+ *fn = cfn.callReqQueryWithArgsRespWriter
+ case *CallReqBytesRespBytes:
+ *fn = cfn.callReqBytesRespBytes
+ case *CallReqBytesRespReader:
+ *fn = cfn.callReqBytesRespReader
+ case *CallReqBytesRespWriter:
+ *fn = cfn.callReqBytesRespWriter
+ case *CallReqReaderRespBytes:
+ *fn = cfn.callReqReaderRespBytes
+ case *CallReqReaderRespReader:
+ *fn = cfn.callReqReaderRespReader
+ case *CallReqReaderRespWriter:
+ *fn = cfn.callReqReaderRespWriter
+ case *CallReqBytesWithArgsRespBytes:
+ *fn = cfn.callReqBytesWithArgsRespBytes
+ case *CallReqBytesWithArgsRespReader:
+ *fn = cfn.callReqBytesWithArgsRespReader
+ case *CallReqBytesWithArgsRespWriter:
+ *fn = cfn.callReqBytesWithArgsRespWriter
+ case *CallReqReaderWithArgsRespBytes:
+ *fn = cfn.callReqReaderWithArgsRespBytes
+ case *CallReqReaderWithArgsRespReader:
+ *fn = cfn.callReqReaderWithArgsRespReader
+ case *CallReqReaderWithArgsRespWriter:
+ *fn = cfn.callReqReaderWithArgsRespWriter
+ default:
+ return fmt.Errorf("invalid call type: %T", fn)
+ }
+ }
+
+ return nil
+}
diff --git a/sdk/calls_test.go b/sdk/calls_test.go
new file mode 100644
index 0000000..370cbdc
--- /dev/null
+++ b/sdk/calls_test.go
@@ -0,0 +1,208 @@
+package sdk
+
+import (
+ "testing"
+)
+
+func TestCallTypeValidation(t *testing.T) {
+ // test all valid call types
+ validTypes := []any{
+ (*CallReqRespBytes)(nil),
+ (*CallReqRespReader)(nil),
+ (*CallReqRespWriter)(nil),
+ (*CallReqQueryRespBytes)(nil),
+ (*CallReqQueryRespReader)(nil),
+ (*CallReqQueryRespWriter)(nil),
+ (*CallReqWithArgsRespBytes)(nil),
+ (*CallReqWithArgsRespReader)(nil),
+ (*CallReqWithArgsRespWriter)(nil),
+ (*CallReqQueryWithArgsRespBytes)(nil),
+ (*CallReqQueryWithArgsRespReader)(nil),
+ (*CallReqQueryWithArgsRespWriter)(nil),
+ (*CallReqBytesRespBytes)(nil),
+ (*CallReqBytesRespReader)(nil),
+ (*CallReqBytesRespWriter)(nil),
+ (*CallReqReaderRespBytes)(nil),
+ (*CallReqReaderRespReader)(nil),
+ (*CallReqReaderRespWriter)(nil),
+ (*CallReqBytesWithArgsRespBytes)(nil),
+ (*CallReqBytesWithArgsRespReader)(nil),
+ (*CallReqBytesWithArgsRespWriter)(nil),
+ (*CallReqReaderWithArgsRespBytes)(nil),
+ (*CallReqReaderWithArgsRespReader)(nil),
+ (*CallReqReaderWithArgsRespWriter)(nil),
+ }
+
+ for _, call := range validTypes {
+ if err := checkCallType(call); err != nil {
+ t.Errorf("checkCallType(%T) should be valid, got error: %v", call, err)
+ }
+ }
+
+ // test invalid types
+ invalidTypes := []any{
+ nil,
+ "string",
+ 123,
+ struct{}{},
+ (*string)(nil),
+ }
+
+ for _, call := range invalidTypes {
+ if err := checkCallType(call); err == nil {
+ t.Errorf("checkCallType(%T) should be invalid", call)
+ }
+ }
+}
+
+func TestCallTypeHelpers(t *testing.T) {
+ t.Run("isCallWithArgs", func(t *testing.T) {
+ tests := []struct {
+ call any
+ want bool
+ }{
+ {(*CallReqWithArgsRespBytes)(nil), true},
+ {(*CallReqQueryWithArgsRespReader)(nil), true},
+ {(*CallReqBytesWithArgsRespWriter)(nil), true},
+ {(*CallReqRespBytes)(nil), false},
+ {(*CallReqQueryRespBytes)(nil), false},
+ }
+
+ for _, tt := range tests {
+ got := isCallWithArgs(tt.call)
+ if got != tt.want {
+ t.Errorf("isCallWithArgs(%T) = %v, want %v", tt.call, got, tt.want)
+ }
+ }
+ })
+
+ t.Run("isCallWithBody", func(t *testing.T) {
+ tests := []struct {
+ call any
+ want bool
+ }{
+ {(*CallReqBytesRespBytes)(nil), true},
+ {(*CallReqReaderRespReader)(nil), true},
+ {(*CallReqBytesWithArgsRespWriter)(nil), true},
+ {(*CallReqRespBytes)(nil), false},
+ {(*CallReqQueryRespBytes)(nil), false},
+ }
+
+ for _, tt := range tests {
+ got := isCallWithBody(tt.call)
+ if got != tt.want {
+ t.Errorf("isCallWithBody(%T) = %v, want %v", tt.call, got, tt.want)
+ }
+ }
+ })
+
+ t.Run("isCallWithQuery", func(t *testing.T) {
+ tests := []struct {
+ call any
+ want bool
+ }{
+ {(*CallReqQueryRespBytes)(nil), true},
+ {(*CallReqQueryWithArgsRespReader)(nil), true},
+ {(*CallReqRespBytes)(nil), false},
+ {(*CallReqBytesRespBytes)(nil), false},
+ }
+
+ for _, tt := range tests {
+ got := isCallWithQuery(tt.call)
+ if got != tt.want {
+ t.Errorf("isCallWithQuery(%T) = %v, want %v", tt.call, got, tt.want)
+ }
+ }
+ })
+}
+
+func TestFillCallFunc(t *testing.T) {
+ sdkPtr := defaultSDK()
+ pathGen := func(args []string) (string, error) {
+ return "/test", nil
+ }
+
+ t.Run("single_call", func(t *testing.T) {
+ var testCall CallReqRespBytes
+ cfg := CallConfig{
+ Calls: []any{&testCall},
+ Host: "api.example.com",
+ Name: "test",
+ Path: "/test",
+ Method: CallMethodGET,
+ }
+
+ err := fillCallFunc(cfg, *sdkPtr, pathGen)
+ if err != nil {
+ t.Errorf("fillCallFunc() error = %v", err)
+ return
+ }
+
+ if testCall == nil {
+ t.Error("CallReqRespBytes was not set")
+ }
+ })
+
+ t.Run("multiple_calls", func(t *testing.T) {
+ var bytesCall CallReqRespBytes
+ var readerCall CallReqRespReader
+ var writerCall CallReqRespWriter
+
+ cfg := CallConfig{
+ Calls: []any{&bytesCall, &readerCall, &writerCall},
+ Host: "api.example.com",
+ Name: "test",
+ Path: "/test",
+ Method: CallMethodGET,
+ }
+
+ err := fillCallFunc(cfg, *sdkPtr, pathGen)
+ if err != nil {
+ t.Errorf("fillCallFunc() error = %v", err)
+ return
+ }
+
+ if bytesCall == nil || readerCall == nil || writerCall == nil {
+ t.Error("not all call functions were set")
+ }
+ })
+
+ t.Run("invalid_call_type", func(t *testing.T) {
+ cfg := CallConfig{
+ Calls: []any{"invalid"},
+ Host: "api.example.com",
+ Name: "test",
+ Path: "/test",
+ Method: CallMethodGET,
+ }
+
+ err := fillCallFunc(cfg, *sdkPtr, pathGen)
+ if err == nil {
+ t.Error("expected error for invalid call type")
+ }
+ })
+
+ t.Run("mixed_call_types", func(t *testing.T) {
+ var argsCall CallReqWithArgsRespBytes
+ var bodyCall CallReqBytesRespReader
+ var queryCall CallReqQueryRespWriter
+
+ cfg := CallConfig{
+ Calls: []any{&argsCall, &bodyCall, &queryCall},
+ Host: "api.example.com",
+ Name: "test",
+ Path: "/test",
+ Method: CallMethodPOST,
+ }
+
+ err := fillCallFunc(cfg, *sdkPtr, pathGen)
+ if err != nil {
+ t.Errorf("fillCallFunc() error = %v", err)
+ return
+ }
+
+ if argsCall == nil || bodyCall == nil || queryCall == nil {
+ t.Error("not all mixed call functions were set")
+ }
+ })
+}
diff --git a/sdk/cypher.go b/sdk/cypher.go
new file mode 100644
index 0000000..5d1d235
--- /dev/null
+++ b/sdk/cypher.go
@@ -0,0 +1,360 @@
+package sdk
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "encoding/binary"
+ "fmt"
+ "io"
+)
+
+const (
+ defaultChunkSize = 16 * 1024 // 16KB
+ maxChunkSize = 1024 * 1024 // 1MB
+)
+
+// EncryptBytes encrypts data using GCM streaming method
+func EncryptBytes(data []byte, key [16]byte, iv [16]byte) ([]byte, error) {
+ src := io.NopCloser(bytes.NewReader(data))
+
+ // use streaming encryption
+ encryptedReader, err := EncryptStream(src, key, iv)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create encrypted stream: %w", err)
+ }
+
+ // read all encrypted data
+ var result bytes.Buffer
+ _, err = io.Copy(&result, encryptedReader)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read encrypted data: %w", err)
+ }
+
+ err = encryptedReader.Close()
+ if err != nil {
+ return nil, fmt.Errorf("failed to close encrypted stream: %w", err)
+ }
+
+ return result.Bytes(), nil
+}
+
+// DecryptBytes decrypts data using GCM streaming method
+func DecryptBytes(encryptedData []byte, key [16]byte, iv [16]byte) ([]byte, error) {
+ src := io.NopCloser(bytes.NewReader(encryptedData))
+
+ // use streaming decryption
+ decryptedReader, err := DecryptStream(src, key, iv)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create decrypted stream: %w", err)
+ }
+
+ // read all decrypted data
+ var result bytes.Buffer
+ _, err = io.Copy(&result, decryptedReader)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read decrypted data: %w", err)
+ }
+
+ err = decryptedReader.Close()
+ if err != nil {
+ return nil, fmt.Errorf("failed to close decrypted stream: %w", err)
+ }
+
+ return result.Bytes(), nil
+}
+
+// EncryptProxy encrypts data from src and writes to dst in blocking manner
+func EncryptProxy(src io.Reader, dst io.Writer, key [16]byte, iv [16]byte) error {
+ encryptedReader, err := EncryptStream(src, key, iv)
+ if err != nil {
+ return fmt.Errorf("failed to create encrypted stream: %w", err)
+ }
+ defer encryptedReader.Close()
+
+ _, err = io.Copy(dst, encryptedReader)
+ if err != nil {
+ return fmt.Errorf("failed to copy encrypted data: %w", err)
+ }
+
+ return nil
+}
+
+// DecryptProxy decrypts data from src and writes to dst in blocking manner
+func DecryptProxy(src io.Reader, dst io.Writer, key [16]byte, iv [16]byte) error {
+ decryptedReader, err := DecryptStream(src, key, iv)
+ if err != nil {
+ return fmt.Errorf("failed to create decrypted stream: %w", err)
+ }
+ defer decryptedReader.Close()
+
+ _, err = io.Copy(dst, decryptedReader)
+ if err != nil {
+ return fmt.Errorf("failed to copy decrypted data: %w", err)
+ }
+
+ return nil
+}
+
+// streamEncryptor implements true streaming encryption using AES-GCM
+type streamEncryptor struct {
+ src io.Reader
+ gcm cipher.AEAD
+ iv [16]byte
+ buffer []byte
+ index int
+ finished bool
+ chunkSize int
+}
+
+// EncryptStream creates a true streaming encryptor using AES-GCM
+func EncryptStream(src io.Reader, key [16]byte, iv [16]byte) (io.ReadCloser, error) {
+ block, err := aes.NewCipher(key[:])
+ if err != nil {
+ return nil, fmt.Errorf("failed to create AES cipher: %w", err)
+ }
+
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create GCM: %w", err)
+ }
+
+ return &streamEncryptor{
+ src: src,
+ gcm: gcm,
+ iv: iv,
+ buffer: make([]byte, 0),
+ index: 0,
+ finished: false,
+ chunkSize: defaultChunkSize,
+ }, nil
+}
+
+func (se *streamEncryptor) Read(p []byte) (n int, err error) {
+ if se.finished && se.index >= len(se.buffer) {
+ return 0, io.EOF
+ }
+
+ // serve from buffer if available
+ if se.index < len(se.buffer) {
+ n = copy(p, se.buffer[se.index:])
+ se.index += n
+ return n, nil
+ }
+
+ // need more data - read and encrypt chunk
+ if !se.finished {
+ if err := se.encryptNextChunk(); err != nil {
+ if err == io.EOF {
+ se.finished = true
+ // try to serve remaining data from buffer
+ if se.index < len(se.buffer) {
+ n = copy(p, se.buffer[se.index:])
+ se.index += n
+ if se.index >= len(se.buffer) {
+ return n, io.EOF
+ }
+ return n, nil
+ }
+ return 0, io.EOF
+ }
+ return 0, err
+ }
+
+ // serve from newly filled buffer
+ if se.index < len(se.buffer) {
+ n = copy(p, se.buffer[se.index:])
+ se.index += n
+ return n, nil
+ }
+ }
+
+ return 0, io.EOF
+}
+
+func (se *streamEncryptor) encryptNextChunk() error {
+ // read chunk from source
+ chunk := make([]byte, se.chunkSize)
+ srcN, srcErr := se.src.Read(chunk)
+
+ if srcN == 0 && srcErr == io.EOF {
+ return io.EOF
+ }
+
+ if srcErr != nil && srcErr != io.EOF {
+ return fmt.Errorf("source read error: %w", srcErr)
+ }
+
+ // encrypt the chunk with random nonce
+ nonce := make([]byte, se.gcm.NonceSize())
+ if _, err := rand.Read(nonce); err != nil {
+ return fmt.Errorf("failed to generate nonce: %w", err)
+ }
+
+ // encrypt with GCM
+ ciphertext := se.gcm.Seal(nil, nonce, chunk[:srcN], nil)
+
+ // XOR nonce with IV before transmission
+ maskedNonce := make([]byte, len(nonce))
+ copy(maskedNonce, nonce)
+ xorWithIV(maskedNonce, se.iv)
+
+ // create chunk: 4-byte length + masked_nonce + ciphertext
+ chunkLen := uint32(len(maskedNonce) + len(ciphertext))
+ chunkData := make([]byte, 4+len(maskedNonce)+len(ciphertext))
+ binary.BigEndian.PutUint32(chunkData[0:4], chunkLen)
+ copy(chunkData[4:4+len(maskedNonce)], maskedNonce)
+ copy(chunkData[4+len(maskedNonce):], ciphertext)
+
+ se.buffer = append(se.buffer, chunkData...)
+
+ if srcErr == io.EOF {
+ return io.EOF
+ }
+
+ return nil
+}
+
+func (se *streamEncryptor) Close() error {
+ if closer, ok := any(se.src).(io.Closer); ok && closer != nil {
+ return closer.Close()
+ }
+ return nil
+}
+
+// streamDecryptor implements true streaming decryption using AES-GCM
+type streamDecryptor struct {
+ src io.Reader
+ gcm cipher.AEAD
+ iv [16]byte
+ readBuffer []byte // buffer for reading encrypted chunks
+ plainBuffer []byte // buffer for decrypted data
+ index int
+ finished bool
+}
+
+// DecryptStream creates a true streaming decryptor using AES-GCM
+func DecryptStream(src io.Reader, key [16]byte, iv [16]byte) (io.ReadCloser, error) {
+ block, err := aes.NewCipher(key[:])
+ if err != nil {
+ return nil, fmt.Errorf("failed to create AES cipher: %w", err)
+ }
+
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create GCM: %w", err)
+ }
+
+ return &streamDecryptor{
+ src: src,
+ gcm: gcm,
+ iv: iv,
+ readBuffer: make([]byte, 0),
+ plainBuffer: make([]byte, 0),
+ index: 0,
+ finished: false,
+ }, nil
+}
+
+func (sd *streamDecryptor) Read(p []byte) (n int, err error) {
+ if sd.finished && sd.index >= len(sd.plainBuffer) {
+ return 0, io.EOF
+ }
+
+ // serve from plaintext buffer if available
+ if sd.index < len(sd.plainBuffer) {
+ n = copy(p, sd.plainBuffer[sd.index:])
+ sd.index += n
+ return n, nil
+ }
+
+ // need more data - read and decrypt next chunk
+ if !sd.finished {
+ if err := sd.decryptNextChunk(); err != nil {
+ if err == io.EOF {
+ sd.finished = true
+ if sd.index < len(sd.plainBuffer) {
+ n = copy(p, sd.plainBuffer[sd.index:])
+ sd.index += n
+ if sd.index >= len(sd.plainBuffer) {
+ return n, io.EOF
+ }
+ return n, nil
+ }
+ return 0, io.EOF
+ }
+ return 0, err
+ }
+
+ // serve from newly decrypted buffer
+ if sd.index < len(sd.plainBuffer) {
+ n = copy(p, sd.plainBuffer[sd.index:])
+ sd.index += n
+ return n, nil
+ }
+ }
+
+ return 0, io.EOF
+}
+
+func (sd *streamDecryptor) decryptNextChunk() error {
+ // read chunk length (4 bytes)
+ lengthBuf := make([]byte, 4)
+ _, err := io.ReadFull(sd.src, lengthBuf)
+ if err == io.EOF {
+ return io.EOF
+ }
+ if err != nil {
+ return fmt.Errorf("failed to read chunk length: %w", err)
+ }
+
+ chunkLen := binary.BigEndian.Uint32(lengthBuf)
+ if chunkLen == 0 || chunkLen > maxChunkSize {
+ return fmt.Errorf("invalid chunk length: %d", chunkLen)
+ }
+
+ // read chunk data (nonce + ciphertext)
+ chunkData := make([]byte, chunkLen)
+ _, err = io.ReadFull(sd.src, chunkData)
+ if err != nil {
+ return fmt.Errorf("failed to read chunk data: %w", err)
+ }
+
+ // extract masked nonce and ciphertext
+ nonceSize := sd.gcm.NonceSize()
+ if len(chunkData) < nonceSize {
+ return fmt.Errorf("chunk too short for nonce")
+ }
+
+ maskedNonce := chunkData[:nonceSize]
+ ciphertext := chunkData[nonceSize:]
+
+ // restore original nonce by XOR with IV
+ nonce := make([]byte, len(maskedNonce))
+ copy(nonce, maskedNonce)
+ xorWithIV(nonce, sd.iv)
+
+ // decrypt with GCM using restored nonce
+ plaintext, err := sd.gcm.Open(nil, nonce, ciphertext, nil)
+ if err != nil {
+ return fmt.Errorf("GCM decryption failed: %w", err)
+ }
+
+ sd.plainBuffer = append(sd.plainBuffer, plaintext...)
+
+ return nil
+}
+
+func (sd *streamDecryptor) Close() error {
+ if closer, ok := any(sd.src).(io.Closer); ok && closer != nil {
+ return closer.Close()
+ }
+ return nil
+}
+
+func xorWithIV(nonce []byte, iv [16]byte) {
+ for i := 0; i < len(nonce) && i < 16; i++ {
+ nonce[i] ^= iv[i]
+ }
+}
diff --git a/sdk/cypher_test.go b/sdk/cypher_test.go
new file mode 100644
index 0000000..d958fbd
--- /dev/null
+++ b/sdk/cypher_test.go
@@ -0,0 +1,1183 @@
+package sdk
+
+import (
+ "bytes"
+ "crypto/rand"
+ "encoding/binary"
+ "fmt"
+ "io"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestEncryptDecryptBytes(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+
+ tests := []struct {
+ name string
+ data []byte
+ }{
+ {
+ name: "empty data",
+ data: []byte{},
+ },
+ {
+ name: "small data",
+ data: []byte("hello"),
+ },
+ {
+ name: "exact block size",
+ data: make([]byte, 16),
+ },
+ {
+ name: "large data",
+ data: make([]byte, 1024),
+ },
+ {
+ name: "odd size data",
+ data: make([]byte, 33),
+ },
+ {
+ name: "json payload",
+ data: []byte(`{"message": "test", "timestamp": 1234567890, "data": [1,2,3,4,5]}`),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // fill test data with pattern for large arrays
+ if len(tt.data) > 10 {
+ for i := range tt.data {
+ tt.data[i] = byte(i % 256)
+ }
+ }
+
+ // encrypt
+ encrypted, err := EncryptBytes(tt.data, key, iv)
+ require.NoError(t, err)
+
+ if len(tt.data) > 0 {
+ assert.True(t, len(encrypted) > len(tt.data), "encrypted data should be larger for non-empty data")
+ } else {
+ // empty data produces no chunks, so no encrypted output
+ assert.Equal(t, 0, len(encrypted), "empty data should produce no encrypted output")
+ }
+
+ // decrypt
+ decrypted, err := DecryptBytes(encrypted, key, iv)
+ require.NoError(t, err)
+ assert.Equal(t, len(tt.data), len(decrypted), "plaintext length should match original")
+ if len(tt.data) == 0 {
+ assert.Empty(t, decrypted, "decrypted empty data should be empty")
+ } else {
+ assert.Equal(t, tt.data, decrypted, "decrypted data should match original")
+ }
+ })
+ }
+}
+
+func TestEncryptBytesErrors(t *testing.T) {
+ validKey := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ validIV := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+ zeroKey := [16]byte{} // all zeros - should still work with AES
+
+ tests := []struct {
+ name string
+ data []byte
+ key [16]byte
+ iv [16]byte
+ wantError bool
+ }{
+ {
+ name: "valid encryption",
+ data: []byte("test"),
+ key: validKey,
+ iv: validIV,
+ wantError: false,
+ },
+ {
+ name: "zero key should work",
+ data: []byte("test"),
+ key: zeroKey,
+ iv: validIV,
+ wantError: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := EncryptBytes(tt.data, tt.key, tt.iv)
+ if tt.wantError {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
+func TestDecryptBytesErrors(t *testing.T) {
+ validKey := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ validIV := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+ wrongKey := [16]byte{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}
+
+ // create valid encrypted data for testing
+ testData := []byte("test data for decryption")
+ validEncrypted, err := EncryptBytes(testData, validKey, validIV)
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ data []byte
+ key [16]byte
+ iv [16]byte
+ wantError string
+ }{
+ {
+ name: "empty data",
+ data: []byte{},
+ key: validKey,
+ iv: validIV,
+ wantError: "", // empty data should work (produces empty result)
+ },
+ {
+ name: "invalid chunk data",
+ data: []byte{1, 2, 3, 4, 5},
+ key: validKey,
+ iv: validIV,
+ wantError: "invalid chunk length",
+ },
+ {
+ name: "wrong key",
+ data: validEncrypted,
+ key: wrongKey,
+ iv: validIV,
+ wantError: "GCM decryption failed",
+ },
+ {
+ name: "valid decryption",
+ data: validEncrypted,
+ key: validKey,
+ iv: validIV,
+ wantError: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := DecryptBytes(tt.data, tt.key, tt.iv)
+ if tt.wantError != "" {
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), tt.wantError)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
+// Test compatibility between original and new streaming methods
+func TestStreamCompatibility(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+
+ tests := []struct {
+ name string
+ data string
+ readSize int
+ }{
+ {
+ name: "empty stream",
+ data: "",
+ readSize: 1024,
+ },
+ {
+ name: "small stream",
+ data: "hello world",
+ readSize: 1024,
+ },
+ {
+ name: "small stream read by bytes",
+ data: "hello world",
+ readSize: 1,
+ },
+ {
+ name: "medium stream",
+ data: strings.Repeat("test data ", 100),
+ readSize: 64,
+ },
+ {
+ name: "large stream",
+ data: strings.Repeat("large test data with pattern ", 1000),
+ readSize: 256,
+ },
+ {
+ name: "block-aligned data",
+ data: strings.Repeat("x", 16*10), // exactly 10 blocks
+ readSize: 32,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ encrypted1, err := EncryptBytes([]byte(tt.data), key, iv)
+ require.NoError(t, err)
+
+ decrypted1, err := DecryptStream(io.NopCloser(bytes.NewReader(encrypted1)), key, iv)
+ require.NoError(t, err)
+
+ var result1 bytes.Buffer
+ _, err = io.Copy(&result1, decrypted1)
+ require.NoError(t, err)
+ require.NoError(t, decrypted1.Close())
+
+ // test new stream methods (AES-GCM)
+ source2 := io.NopCloser(strings.NewReader(tt.data))
+ encryptedReader2, err := EncryptStream(source2, key, iv)
+ require.NoError(t, err)
+
+ var encrypted2 bytes.Buffer
+ buf := make([]byte, tt.readSize)
+ for {
+ n, err := encryptedReader2.Read(buf)
+ if n > 0 {
+ encrypted2.Write(buf[:n])
+ }
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+ }
+ require.NoError(t, encryptedReader2.Close())
+
+ decrypted2, err := DecryptBytes(encrypted2.Bytes(), key, iv)
+ require.NoError(t, err)
+
+ var result2 bytes.Buffer
+ result2.Write(decrypted2)
+
+ // both methods should produce the same plaintext
+ assert.Equal(t, tt.data, result1.String(), "encrypt bytes method should match input")
+ assert.Equal(t, tt.data, result2.String(), "encrypt stream2 method should match input")
+ })
+ }
+}
+
+func TestStream2ErrorHandling(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+
+ t.Run("encrypt stream2 with read error", func(t *testing.T) {
+ errorReader := &errorReadCloser{err: fmt.Errorf("read error")}
+
+ encryptedReader, err := EncryptStream(errorReader, key, iv)
+ require.NoError(t, err)
+
+ // try to read - should get error
+ buf := make([]byte, 64)
+ _, err = encryptedReader.Read(buf)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "read error")
+ })
+
+ t.Run("decrypt stream2 with corrupted chunk length", func(t *testing.T) {
+ // create invalid data with corrupted length
+ corruptedData := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x02, 0x03} // invalid length + some data
+ source := io.NopCloser(bytes.NewReader(corruptedData))
+
+ decryptedReader, err := DecryptStream(source, key, iv)
+ require.NoError(t, err)
+
+ // try to read - should get error
+ buf := make([]byte, 64)
+ _, err = decryptedReader.Read(buf)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "invalid chunk length")
+ })
+
+ t.Run("decrypt stream2 with truncated chunk", func(t *testing.T) {
+ // create data with valid length but insufficient data
+ truncatedData := []byte{0x00, 0x00, 0x00, 0x20} // claims 32 bytes but no data follows
+ source := io.NopCloser(bytes.NewReader(truncatedData))
+
+ decryptedReader, err := DecryptStream(source, key, iv)
+ require.NoError(t, err)
+
+ // try to read - should get error
+ buf := make([]byte, 64)
+ _, err = decryptedReader.Read(buf)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "failed to read chunk data")
+ })
+
+ t.Run("decrypt stream2 with invalid GCM data", func(t *testing.T) {
+ // create data with valid length but invalid GCM content
+ invalidGCMData := make([]byte, 4+12+16) // length + nonce + invalid ciphertext
+ binary.BigEndian.PutUint32(invalidGCMData[0:4], 12+16) // nonce + ciphertext length
+ rand.Read(invalidGCMData[4:]) // random invalid data
+
+ source := io.NopCloser(bytes.NewReader(invalidGCMData))
+
+ decryptedReader, err := DecryptStream(source, key, iv)
+ require.NoError(t, err)
+
+ // try to read - should get GCM error
+ buf := make([]byte, 64)
+ _, err = decryptedReader.Read(buf)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "GCM decryption failed")
+ })
+}
+
+func TestStream2LargeDataStreaming(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+
+ // test various data sizes
+ sizes := []int{0, 1, 15, 16, 17, 1023, 1024, 1025, 4095, 4096, 4097, 16383, 16384, 16385}
+
+ for _, size := range sizes {
+ t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
+ // generate test data with pattern
+ testData := make([]byte, size)
+ for i := range testData {
+ testData[i] = byte(i % 256)
+ }
+
+ // encrypt with stream2
+ source := io.NopCloser(bytes.NewReader(testData))
+ encryptedReader, err := EncryptStream(source, key, iv)
+ require.NoError(t, err)
+
+ // read encrypted data with various buffer sizes
+ var encryptedBuf bytes.Buffer
+ bufSize := 1 + (size % 100) // variable buffer size from 1 to 100
+ if bufSize < 1 {
+ bufSize = 1
+ }
+ buf := make([]byte, bufSize)
+
+ for {
+ n, err := encryptedReader.Read(buf)
+ if n > 0 {
+ encryptedBuf.Write(buf[:n])
+ }
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+ }
+ require.NoError(t, encryptedReader.Close())
+
+ // decrypt with stream2
+ decryptedReader, err := DecryptStream(io.NopCloser(&encryptedBuf), key, iv)
+ require.NoError(t, err)
+
+ // read decrypted data with different buffer size
+ var decryptedBuf bytes.Buffer
+ bufSize = 1 + ((size + 50) % 200) // different variable buffer size
+ if bufSize < 1 {
+ bufSize = 1
+ }
+ buf = make([]byte, bufSize)
+
+ for {
+ n, err := decryptedReader.Read(buf)
+ if n > 0 {
+ decryptedBuf.Write(buf[:n])
+ }
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+ }
+ require.NoError(t, decryptedReader.Close())
+
+ // verify result
+ assert.Equal(t, size, decryptedBuf.Len(), "decrypted size should match original")
+ if size == 0 {
+ assert.Equal(t, 0, decryptedBuf.Len(), "empty data should produce empty result")
+ } else {
+ assert.Equal(t, testData, decryptedBuf.Bytes(), "decrypted content should match original")
+ }
+ })
+ }
+}
+
+func TestStream2ReadPatterns(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+
+ // create test data with pattern
+ testData := make([]byte, 5000) // 5KB
+ for i := range testData {
+ testData[i] = byte(i % 256)
+ }
+
+ // encrypt the data
+ source := io.NopCloser(bytes.NewReader(testData))
+ encryptedReader, err := EncryptStream(source, key, iv)
+ require.NoError(t, err)
+
+ var encryptedBuf bytes.Buffer
+ _, err = io.Copy(&encryptedBuf, encryptedReader)
+ require.NoError(t, err)
+ require.NoError(t, encryptedReader.Close())
+
+ readPatterns := []struct {
+ name string
+ readSize int
+ }{
+ {"single byte reads", 1},
+ {"tiny reads", 3},
+ {"small reads", 7},
+ {"medium reads", 64},
+ {"large reads", 512},
+ {"very large reads", 2048},
+ {"exact chunk reads", 1024}, // matches internal chunk size
+ }
+
+ for _, pattern := range readPatterns {
+ t.Run(pattern.name, func(t *testing.T) {
+ // decrypt with specific read pattern
+ decryptedReader, err := DecryptStream(io.NopCloser(bytes.NewReader(encryptedBuf.Bytes())), key, iv)
+ require.NoError(t, err)
+
+ var decryptedBuf bytes.Buffer
+ buf := make([]byte, pattern.readSize)
+ for {
+ n, err := decryptedReader.Read(buf)
+ if n > 0 {
+ decryptedBuf.Write(buf[:n])
+ }
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+ }
+
+ require.NoError(t, decryptedReader.Close())
+ assert.Equal(t, testData, decryptedBuf.Bytes(), "decrypted data should match original regardless of read pattern")
+ })
+ }
+}
+
+func TestConcurrentStreamOperations(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+
+ testData := []byte("concurrent test data for stream operations")
+
+ // test multiple concurrent encryptions with Stream2
+ const numGoroutines = 20
+ results := make(chan []byte, numGoroutines)
+ errors := make(chan error, numGoroutines)
+
+ for i := 0; i < numGoroutines; i++ {
+ go func() {
+ source := io.NopCloser(bytes.NewReader(testData))
+ encryptedReader, err := EncryptStream(source, key, iv)
+ if err != nil {
+ errors <- err
+ return
+ }
+
+ var encrypted bytes.Buffer
+ _, err = io.Copy(&encrypted, encryptedReader)
+ if err != nil {
+ errors <- err
+ return
+ }
+ encryptedReader.Close()
+
+ results <- encrypted.Bytes()
+ }()
+ }
+
+ // collect and verify results
+ for i := 0; i < numGoroutines; i++ {
+ select {
+ case encrypted := <-results:
+ // decrypt and verify
+ decrypted, err := DecryptStream(io.NopCloser(bytes.NewReader(encrypted)), key, iv)
+ require.NoError(t, err)
+
+ var result bytes.Buffer
+ _, err = io.Copy(&result, decrypted)
+ require.NoError(t, err)
+ require.NoError(t, decrypted.Close())
+
+ assert.Equal(t, testData, result.Bytes())
+ case err := <-errors:
+ t.Fatalf("concurrent encryption failed: %v", err)
+ case <-time.After(10 * time.Second):
+ t.Fatal("timeout waiting for concurrent operations")
+ }
+ }
+}
+
+func TestMemoryEfficiency(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+
+ // create very large test data (1MB)
+ const dataSize = 1024 * 1024
+ testData := make([]byte, dataSize)
+ for i := range testData {
+ testData[i] = byte(i % 256)
+ }
+
+ t.Run("large data streaming without memory explosion", func(t *testing.T) {
+ // encrypt with small read buffer to test true streaming
+ source := io.NopCloser(bytes.NewReader(testData))
+ encryptedReader, err := EncryptStream(source, key, iv)
+ require.NoError(t, err)
+
+ var encryptedBuf bytes.Buffer
+ buf := make([]byte, 1024) // small buffer - should not load entire 1MB into memory
+ for {
+ n, err := encryptedReader.Read(buf)
+ if n > 0 {
+ encryptedBuf.Write(buf[:n])
+ }
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+ }
+ require.NoError(t, encryptedReader.Close())
+
+ // decrypt with small read buffer
+ decryptedReader, err := DecryptStream(io.NopCloser(&encryptedBuf), key, iv)
+ require.NoError(t, err)
+
+ var decryptedBuf bytes.Buffer
+ buf = make([]byte, 512) // different small buffer size
+ for {
+ n, err := decryptedReader.Read(buf)
+ if n > 0 {
+ decryptedBuf.Write(buf[:n])
+ }
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+ }
+ require.NoError(t, decryptedReader.Close())
+
+ // verify result
+ assert.Equal(t, dataSize, decryptedBuf.Len(), "decrypted size should match original")
+ assert.Equal(t, testData, decryptedBuf.Bytes(), "decrypted content should match original")
+ })
+}
+
+func TestProxyFunctions(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+
+ tests := []struct {
+ name string
+ data string
+ }{
+ {
+ name: "empty data",
+ data: "",
+ },
+ {
+ name: "small data",
+ data: "hello proxy world",
+ },
+ {
+ name: "medium data",
+ data: strings.Repeat("proxy test data ", 100), // ~1.6KB
+ },
+ {
+ name: "large data",
+ data: strings.Repeat("large proxy data chunk ", 500), // ~11.5KB
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ testData := []byte(tt.data)
+
+ // test EncryptProxy + DecryptProxy round trip
+ var encryptedBuf bytes.Buffer
+ src := bytes.NewReader(testData)
+
+ // encrypt using proxy
+ err := EncryptProxy(src, &encryptedBuf, key, iv)
+ require.NoError(t, err)
+
+ // verify encrypted data is not empty for non-empty input
+ if len(testData) > 0 {
+ assert.True(t, encryptedBuf.Len() > 0, "encrypted data should not be empty for non-empty input")
+ }
+
+ // decrypt using proxy
+ var decryptedBuf bytes.Buffer
+ err = DecryptProxy(&encryptedBuf, &decryptedBuf, key, iv)
+ require.NoError(t, err)
+
+ // verify round trip
+ assert.Equal(t, testData, decryptedBuf.Bytes(), "proxy round trip should preserve data")
+ })
+ }
+}
+
+func TestProxyErrorHandling(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+
+ t.Run("encrypt proxy with read error", func(t *testing.T) {
+ errorReader := &errorReadCloser{err: fmt.Errorf("read error")}
+ var buf bytes.Buffer
+
+ err := EncryptProxy(errorReader, &buf, key, iv)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "read error")
+ })
+
+ t.Run("decrypt proxy with write error", func(t *testing.T) {
+ // create valid encrypted data
+ testData := []byte("test data")
+ var encryptedBuf bytes.Buffer
+ src := bytes.NewReader(testData)
+ err := EncryptProxy(src, &encryptedBuf, key, iv)
+ require.NoError(t, err)
+
+ // try to decrypt to error writer
+ errorWriter := &errorWriter{err: fmt.Errorf("write error")}
+ err = DecryptProxy(&encryptedBuf, errorWriter, key, iv)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "write error")
+ })
+
+ t.Run("decrypt proxy with corrupted data", func(t *testing.T) {
+ corruptedData := bytes.NewReader([]byte("corrupted"))
+ var buf bytes.Buffer
+
+ err := DecryptProxy(corruptedData, &buf, key, iv)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "invalid chunk length")
+ })
+}
+
+func TestProxyVsStreamConsistency(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+
+ testData := []byte("consistency test between proxy and stream methods")
+
+ // encrypt using proxy
+ var proxyEncrypted bytes.Buffer
+ src1 := bytes.NewReader(testData)
+ err := EncryptProxy(src1, &proxyEncrypted, key, iv)
+ require.NoError(t, err)
+
+ // encrypt using stream
+ src2 := io.NopCloser(bytes.NewReader(testData))
+ streamEncrypted, err := EncryptStream(src2, key, iv)
+ require.NoError(t, err)
+
+ var streamEncryptedBuf bytes.Buffer
+ _, err = io.Copy(&streamEncryptedBuf, streamEncrypted)
+ require.NoError(t, err)
+ streamEncrypted.Close()
+
+ // encrypted results will be different due to random nonces, but should have same structure
+ assert.Equal(t, streamEncryptedBuf.Len(), proxyEncrypted.Len(), "proxy and stream encryption should produce same length")
+
+ // decrypt both using proxy
+ var proxyDecrypted1, proxyDecrypted2 bytes.Buffer
+
+ err = DecryptProxy(&proxyEncrypted, &proxyDecrypted1, key, iv)
+ require.NoError(t, err)
+
+ err = DecryptProxy(&streamEncryptedBuf, &proxyDecrypted2, key, iv)
+ require.NoError(t, err)
+
+ // both should produce original data
+ assert.Equal(t, testData, proxyDecrypted1.Bytes(), "proxy decryption should work")
+ assert.Equal(t, testData, proxyDecrypted2.Bytes(), "proxy should decrypt stream-encrypted data")
+}
+
+// errorWriter is a helper for testing write errors
+type errorWriter struct {
+ err error
+}
+
+func (e *errorWriter) Write(p []byte) (n int, err error) {
+ return 0, e.err
+}
+
+func TestEncryptBytesChunking(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+
+ tests := []struct {
+ name string
+ dataSize int
+ expectedChunks int
+ }{
+ {
+ name: "empty data",
+ dataSize: 0,
+ expectedChunks: 0, // empty data produces no chunks
+ },
+ {
+ name: "small data - one chunk",
+ dataSize: 500,
+ expectedChunks: 1,
+ },
+ {
+ name: "exact chunk size",
+ dataSize: defaultChunkSize,
+ expectedChunks: 1,
+ },
+ {
+ name: "slightly over one chunk",
+ dataSize: defaultChunkSize + 1,
+ expectedChunks: 2,
+ },
+ {
+ name: "two chunks",
+ dataSize: 2*defaultChunkSize - 1,
+ expectedChunks: 2,
+ },
+ {
+ name: "three chunks",
+ dataSize: 2*defaultChunkSize + defaultChunkSize/2,
+ expectedChunks: 3,
+ },
+ {
+ name: "large data - multiple chunks",
+ dataSize: 1000 * defaultChunkSize,
+ expectedChunks: 1000,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // generate test data
+ testData := make([]byte, tt.dataSize)
+ for i := range testData {
+ testData[i] = byte(i % 256)
+ }
+
+ // encrypt using EncryptBytes
+ encrypted, err := EncryptBytes(testData, key, iv)
+ require.NoError(t, err)
+
+ // analyze encrypted data to count chunks
+ chunkCount := 0
+ offset := 0
+
+ for offset < len(encrypted) {
+ // ensure we have at least 4 bytes for length
+ if offset+4 > len(encrypted) {
+ break
+ }
+
+ // read chunk length
+ chunkLen := binary.BigEndian.Uint32(encrypted[offset : offset+4])
+
+ // validate chunk length
+ if chunkLen == 0 || chunkLen > 1024*1024 {
+ t.Fatalf("invalid chunk length at offset %d: %d", offset, chunkLen)
+ }
+
+ // ensure we have the complete chunk
+ totalChunkSize := 4 + int(chunkLen) // length prefix + chunk data
+ if offset+totalChunkSize > len(encrypted) {
+ t.Fatalf("incomplete chunk at offset %d: need %d bytes, have %d", offset, totalChunkSize, len(encrypted)-offset)
+ }
+
+ chunkCount++
+ offset += totalChunkSize
+ }
+
+ // verify chunk count matches expectation
+ assert.Equal(t, tt.expectedChunks, chunkCount, "chunk count should match expected")
+
+ // verify decryption works correctly
+ decrypted, err := DecryptBytes(encrypted, key, iv)
+ require.NoError(t, err)
+
+ if tt.dataSize == 0 {
+ assert.Empty(t, decrypted, "empty data should decrypt to empty")
+ } else {
+ assert.Equal(t, testData, decrypted, "decrypted data should match original")
+ }
+ })
+ }
+}
+
+func TestNonceXORMasking(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00}
+
+ testData := []byte("test data for nonce XOR verification")
+
+ // encrypt with stream2
+ source := io.NopCloser(bytes.NewReader(testData))
+ encryptedReader, err := EncryptStream(source, key, iv)
+ require.NoError(t, err)
+
+ // read encrypted data and analyze nonce masking
+ var encryptedBuf bytes.Buffer
+ _, err = io.Copy(&encryptedBuf, encryptedReader)
+ require.NoError(t, err)
+ require.NoError(t, encryptedReader.Close())
+
+ encryptedData := encryptedBuf.Bytes()
+
+ // verify that encrypted data contains masked nonces
+ // extract first chunk to verify nonce is XORed
+ if len(encryptedData) >= 16 { // 4-byte length + 12-byte GCM nonce
+ chunkLen := binary.BigEndian.Uint32(encryptedData[0:4])
+ require.True(t, chunkLen >= 12, "chunk should contain at least nonce")
+
+ if len(encryptedData) >= int(4+chunkLen) {
+ maskedNonce := encryptedData[4:16] // first 12 bytes of chunk data
+
+ // verify nonce is actually masked (XORed with IV)
+ // we can't predict exact nonce value, but we can verify it's not all zeros
+ allZeros := true
+ for _, b := range maskedNonce {
+ if b != 0 {
+ allZeros = false
+ break
+ }
+ }
+ assert.False(t, allZeros, "masked nonce should not be all zeros (extremely unlikely)")
+ }
+ }
+
+ // decrypt and verify correctness
+ decryptedReader, err := DecryptStream(io.NopCloser(&encryptedBuf), key, iv)
+ require.NoError(t, err)
+
+ var decryptedBuf bytes.Buffer
+ _, err = io.Copy(&decryptedBuf, decryptedReader)
+ require.NoError(t, err)
+ require.NoError(t, decryptedReader.Close())
+
+ // verify decryption worked correctly despite nonce masking
+ assert.Equal(t, testData, decryptedBuf.Bytes(), "decryption should work correctly with XOR masked nonces")
+}
+
+func TestXORWithIVFunction(t *testing.T) {
+ iv := [16]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00}
+
+ tests := []struct {
+ name string
+ nonce []byte
+ expected []byte
+ }{
+ {
+ name: "12-byte nonce (GCM standard)",
+ nonce: []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC},
+ expected: []byte{0xBB, 0x99, 0xFF, 0x99, 0xBB, 0x99, 0x66, 0xAA, 0xAA, 0xEE, 0xEE, 0xAA}, // XOR result
+ },
+ {
+ name: "16-byte nonce",
+ nonce: []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
+ expected: []byte{0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0x99, 0x88, 0x77, 0x66, 0xFF}, // XOR result
+ },
+ {
+ name: "8-byte nonce",
+ nonce: []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77},
+ expected: []byte{0xAA, 0xAA, 0xEE, 0xEE, 0xAA, 0xAA, 0x77, 0x55}, // XOR result
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // test XOR operation
+ nonceCopy := make([]byte, len(tt.nonce))
+ copy(nonceCopy, tt.nonce)
+
+ xorWithIV(nonceCopy, iv)
+ assert.Equal(t, tt.expected, nonceCopy, "XOR result should match expected")
+
+ // test that double XOR restores original
+ xorWithIV(nonceCopy, iv)
+ assert.Equal(t, tt.nonce, nonceCopy, "double XOR should restore original nonce")
+ })
+ }
+}
+
+func TestNonceNotVisibleInTransmission(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
+
+ // create test data that would produce predictable nonce pattern if not masked
+ testData := []byte("predictable test data for nonce visibility test")
+
+ // encrypt multiple times to get multiple chunks with different nonces
+ var allEncryptedData [][]byte
+ for i := 0; i < 5; i++ {
+ source := io.NopCloser(bytes.NewReader(testData))
+ encryptedReader, err := EncryptStream(source, key, iv)
+ require.NoError(t, err)
+
+ var encryptedBuf bytes.Buffer
+ _, err = io.Copy(&encryptedBuf, encryptedReader)
+ require.NoError(t, err)
+ require.NoError(t, encryptedReader.Close())
+
+ allEncryptedData = append(allEncryptedData, encryptedBuf.Bytes())
+ }
+
+ // analyze nonce areas in encrypted data
+ for i, encrypted := range allEncryptedData {
+ if len(encrypted) >= 16 { // 4-byte length + 12-byte masked nonce
+ chunkLen := binary.BigEndian.Uint32(encrypted[0:4])
+ require.True(t, chunkLen >= 12, "chunk should contain at least nonce")
+
+ if len(encrypted) >= int(4+chunkLen) {
+ maskedNonce := encrypted[4:16] // first 12 bytes should be masked nonce
+
+ // verify that masked nonces are different between encryptions
+ // (this proves nonces are random and properly masked)
+ for j, otherEncrypted := range allEncryptedData {
+ if i != j && len(otherEncrypted) >= 16 {
+ otherMaskedNonce := otherEncrypted[4:16]
+ assert.NotEqual(t, maskedNonce, otherMaskedNonce,
+ "masked nonces should be different between encryptions %d and %d", i, j)
+ }
+ }
+
+ // verify that nonce is properly masked (not original random bytes)
+ // by checking it's not the IV XORed with all zeros (which would be the IV itself)
+ assert.NotEqual(t, iv[:12], maskedNonce, "masked nonce should not equal IV (would indicate zero nonce)")
+ }
+ }
+ }
+
+ // verify all encryptions decrypt correctly
+ for i, encrypted := range allEncryptedData {
+ decryptedReader, err := DecryptStream(io.NopCloser(bytes.NewReader(encrypted)), key, iv)
+ require.NoError(t, err, "decryption %d should succeed", i)
+
+ var decryptedBuf bytes.Buffer
+ _, err = io.Copy(&decryptedBuf, decryptedReader)
+ require.NoError(t, err, "reading decrypted data %d should succeed", i)
+ require.NoError(t, decryptedReader.Close())
+
+ assert.Equal(t, testData, decryptedBuf.Bytes(), "decrypted data %d should match original", i)
+ }
+}
+
+func TestEdgeCasesAndBoundaries(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+
+ t.Run("zero-length read buffers", func(t *testing.T) {
+ testData := []byte("test data")
+
+ source := io.NopCloser(bytes.NewReader(testData))
+ encryptedReader, err := EncryptStream(source, key, iv)
+ require.NoError(t, err)
+
+ // read with zero-length buffer
+ buf := make([]byte, 0)
+ n, err := encryptedReader.Read(buf)
+ assert.Equal(t, 0, n)
+ assert.NoError(t, err)
+
+ require.NoError(t, encryptedReader.Close())
+ })
+
+ t.Run("multiple small reads across chunk boundaries", func(t *testing.T) {
+ // create data larger than chunk size to test boundary crossing
+ testData := make([]byte, 2500) // > 2 chunks
+ for i := range testData {
+ testData[i] = byte(i % 256)
+ }
+
+ source := io.NopCloser(bytes.NewReader(testData))
+ encryptedReader, err := EncryptStream(source, key, iv)
+ require.NoError(t, err)
+
+ var encryptedBuf bytes.Buffer
+ _, err = io.Copy(&encryptedBuf, encryptedReader)
+ require.NoError(t, err)
+ require.NoError(t, encryptedReader.Close())
+
+ // decrypt with very small reads (smaller than chunk size)
+ decryptedReader, err := DecryptStream(io.NopCloser(&encryptedBuf), key, iv)
+ require.NoError(t, err)
+
+ var result bytes.Buffer
+ buf := make([]byte, 7) // small odd-sized buffer
+ for {
+ n, err := decryptedReader.Read(buf)
+ if n > 0 {
+ result.Write(buf[:n])
+ }
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+ }
+
+ require.NoError(t, decryptedReader.Close())
+ assert.Equal(t, testData, result.Bytes())
+ })
+
+ t.Run("immediate EOF from source", func(t *testing.T) {
+ // empty source that immediately returns EOF
+ source := io.NopCloser(strings.NewReader(""))
+
+ encryptedReader, err := EncryptStream(source, key, iv)
+ require.NoError(t, err)
+
+ // should handle empty stream gracefully
+ buf := make([]byte, 64)
+ n, err := encryptedReader.Read(buf)
+
+ // might return 0 bytes with EOF or some encrypted empty data
+ if err == io.EOF {
+ assert.Equal(t, 0, n)
+ } else {
+ assert.NoError(t, err)
+ }
+
+ require.NoError(t, encryptedReader.Close())
+ })
+}
+
+func TestRandomDataConsistency(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+
+ // test with truly random data of various sizes
+ sizes := []int{0, 1, 16, 17, 64, 65, 256, 257, 1024, 1025, 4096, 4097}
+
+ for _, size := range sizes {
+ t.Run(fmt.Sprintf("random_size_%d", size), func(t *testing.T) {
+ // generate random data
+ testData := make([]byte, size)
+ if size > 0 {
+ _, err := rand.Read(testData)
+ require.NoError(t, err)
+ }
+
+ // test both bytes and stream methods
+ // bytes method
+ encryptedBytes, err := EncryptBytes(testData, key, iv)
+ require.NoError(t, err)
+
+ decryptedBytes, err := DecryptBytes(encryptedBytes, key, iv)
+ require.NoError(t, err)
+ assert.Equal(t, testData, decryptedBytes, "bytes method should work correctly")
+
+ // stream2 method
+ source := io.NopCloser(bytes.NewReader(testData))
+ encryptedReader, err := EncryptStream(source, key, iv)
+ require.NoError(t, err)
+
+ var encryptedStream bytes.Buffer
+ _, err = io.Copy(&encryptedStream, encryptedReader)
+ require.NoError(t, err)
+ require.NoError(t, encryptedReader.Close())
+
+ decryptedReader, err := DecryptStream(io.NopCloser(&encryptedStream), key, iv)
+ require.NoError(t, err)
+
+ var decryptedStream bytes.Buffer
+ _, err = io.Copy(&decryptedStream, decryptedReader)
+ require.NoError(t, err)
+ require.NoError(t, decryptedReader.Close())
+
+ if len(testData) == 0 {
+ assert.Equal(t, 0, decryptedStream.Len(), "stream2 method should handle empty data")
+ } else {
+ assert.Equal(t, testData, decryptedStream.Bytes(), "stream2 method should work correctly")
+ }
+ })
+ }
+}
+
+// Performance and stress tests adapted from encryptor_test.go
+func TestHighVolumeStreaming(t *testing.T) {
+ key := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+
+ // test sizes: 16KB, 1MB, 32MB
+ sizes := []int{16 * 1024, 1024 * 1024, 32 * 1024 * 1024}
+
+ for _, size := range sizes {
+ t.Run(fmt.Sprintf("volume_%dB", size), func(t *testing.T) {
+ // generate large test data
+ testData := make([]byte, size)
+ for i := range testData {
+ testData[i] = byte(i % 256)
+ }
+
+ // encrypt with stream2
+ source := io.NopCloser(bytes.NewReader(testData))
+ encryptedReader, err := EncryptStream(source, key, iv)
+ require.NoError(t, err)
+
+ // read encrypted data efficiently
+ var encryptedBuf bytes.Buffer
+ buf := make([]byte, 8192) // 8KB buffer for efficiency
+ for {
+ n, err := encryptedReader.Read(buf)
+ if n > 0 {
+ encryptedBuf.Write(buf[:n])
+ }
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+ }
+ require.NoError(t, encryptedReader.Close())
+
+ // decrypt with stream2
+ decryptedReader, err := DecryptStream(io.NopCloser(&encryptedBuf), key, iv)
+ require.NoError(t, err)
+
+ // verify by reading in chunks
+ buf = make([]byte, 8192)
+ totalRead := 0
+ for {
+ n, err := decryptedReader.Read(buf)
+ if n > 0 {
+ // verify chunk matches original data
+ assert.Equal(t, testData[totalRead:totalRead+n], buf[:n],
+ "chunk at offset %d should match original", totalRead)
+ totalRead += n
+ }
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+ }
+
+ require.NoError(t, decryptedReader.Close())
+ assert.Equal(t, size, totalRead, "total read should match original size")
+ })
+ }
+}
+
+// errorReadCloser is a helper for testing error conditions
+type errorReadCloser struct {
+ err error
+}
+
+func (e *errorReadCloser) Read(p []byte) (n int, err error) {
+ return 0, e.err
+}
+
+func (e *errorReadCloser) Close() error {
+ return nil
+}
diff --git a/sdk/doc.go b/sdk/doc.go
new file mode 100644
index 0000000..7df2cae
--- /dev/null
+++ b/sdk/doc.go
@@ -0,0 +1,594 @@
+// Package sdk provides enterprise-grade Go SDK for secure integration with VXControl Cloud Platform APIs.
+//
+// The SDK implements a comprehensive security framework featuring memory-hard proof-of-work
+// protection, end-to-end AES-GCM encryption, Ed25519 signatures, and automatic retry logic.
+// It offers 24 strongly-typed function patterns covering all request/response scenarios
+// with built-in connection pooling, HTTP/2 support, and streaming architecture for
+// high-performance integration with cloud services.
+//
+// # Architecture Overview
+//
+// The SDK consists of several core components working together:
+//
+// - Call Generation: 24 strongly-typed function patterns for all API scenarios
+// - Transport Layer: HTTP/2 optimized transport with connection pooling
+// - Security Engine: PoW challenges, AES-GCM encryption, Ed25519 signatures
+// - License System: Cryptographic license validation with tier enforcement
+// - Logging Framework: Structured logging with configurable levels and formats
+//
+// # Quick Start
+//
+// Basic SDK usage involves defining client structure, configuring endpoints, and building:
+//
+// import "github.com/vxcontrol/cloud/sdk"
+//
+// // Define client with typed API functions
+// type Client struct {
+// CheckUpdates sdk.CallReqBytesRespBytes // JSON request/response
+// DownloadFile sdk.CallReqQueryRespWriter // Query params, stream response
+// UploadData sdk.CallReqReaderRespBytes // Stream request, JSON response
+// }
+//
+// // Configure endpoints
+// configs := []sdk.CallConfig{
+// {
+// Calls: []any{&client.CheckUpdates},
+// Host: "api.example.com",
+// Name: "check_updates",
+// Path: "/api/v1/updates/check",
+// Method: sdk.CallMethodPOST,
+// },
+// {
+// Calls: []any{&client.DownloadFile},
+// Host: "files.example.com",
+// Name: "download_file",
+// Path: "/files/:id",
+// Method: sdk.CallMethodGET,
+// },
+// }
+//
+// // Initialize SDK with options
+// err := sdk.Build(configs,
+// sdk.WithClient("MyApp", "1.0.0"),
+// sdk.WithLicenseKey("XXXX-XXXX-XXXX-XXXX"),
+// sdk.WithPowTimeout(30*time.Second),
+// sdk.WithMaxRetries(3),
+// )
+// if err != nil {
+// return err
+// }
+//
+// // Use generated functions
+// updateData := []byte(`{"version": "1.0.0"}`)
+// response, err := client.CheckUpdates(context.Background(), updateData)
+//
+// # Call Function Types
+//
+// The SDK provides 24 strongly-typed function patterns covering all request/response scenarios:
+//
+// ## Basic Patterns (No Parameters)
+// - CallReqRespBytes: Simple GET endpoints returning JSON/binary data
+// - CallReqRespReader: File downloads or large data streams
+// - CallReqRespWriter: Direct output streaming to custom writers
+//
+// ## Query Parameter Patterns
+// - CallReqQueryRespBytes: Filtered queries (?limit=10&offset=20)
+// - CallReqQueryRespReader: Query-based file downloads
+// - CallReqQueryRespWriter: Query-based streaming responses
+//
+// ## Path Argument Patterns
+// - CallReqWithArgsRespBytes: RESTful resource access (/users/:id)
+// - CallReqWithArgsRespReader: Resource-specific file downloads
+// - CallReqWithArgsRespWriter: Resource-specific streaming
+//
+// ## Combined Patterns
+// - CallReqQueryWithArgsRespBytes: Path args + query params
+// - CallReqQueryWithArgsRespReader: Complex resource queries with streaming
+// - CallReqQueryWithArgsRespWriter: Advanced query scenarios
+//
+// ## Request Body Patterns
+// - CallReqBytesRespBytes: JSON API calls (POST/PUT with JSON payload)
+// - CallReqBytesRespReader: JSON request with streaming response
+// - CallReqBytesRespWriter: JSON request with writer output
+// - CallReqReaderRespBytes: File uploads with JSON response
+// - CallReqReaderRespReader: Stream-to-stream processing
+// - CallReqReaderRespWriter: Upload streaming with output streaming
+//
+// ## Advanced Combined Patterns
+// - CallReqBytesWithArgsRespBytes: RESTful updates with JSON payloads
+// - CallReqBytesWithArgsRespReader: Resource updates with streaming responses
+// - CallReqBytesWithArgsRespWriter: Resource updates with output streaming
+// - CallReqReaderWithArgsRespBytes: File uploads to specific resources
+// - CallReqReaderWithArgsRespReader: Stream processing with resource targeting
+// - CallReqReaderWithArgsRespWriter: Complex stream processing scenarios
+//
+// Example usage patterns:
+//
+// // Simple JSON API
+// type API struct {
+// GetUser sdk.CallReqWithArgsRespBytes // GET /users/:id
+// UpdateUser sdk.CallReqBytesWithArgsRespBytes // PUT /users/:id + JSON body
+// ListUsers sdk.CallReqQueryRespBytes // GET /users?limit=10
+// UploadFile sdk.CallReqReaderRespBytes // POST /upload + file stream
+// }
+//
+// # Security Framework
+//
+// ## Proof-of-Work Protection
+//
+// All API calls require solving memory-hard proof-of-work challenges:
+//
+// // Automatic PoW solving process:
+// // 1. Request challenge ticket from server
+// // 2. Solve memory-hard puzzle (12-1024KB, variable AES iterations)
+// // 3. Include PoW signature with every API request
+// // 4. Server validates signature before processing
+//
+// PoW Algorithm Features:
+// - Memory-hard algorithm designed for GPU and FPGA resistance
+// - Dynamic difficulty scaling based on server load and license tier
+// - Variable parameters prevent hardware optimization (millions of combinations)
+// - Configurable timeout support for different hardware capabilities
+//
+// ## Cryptographic Protection
+//
+// Multi-layer encryption and signature system:
+//
+// // Session Key Generation
+// sessionKey := [16]byte{} // AES-128 for request/response encryption
+// sessionIV := [16]byte{} // Unique IV per request
+//
+// // NaCL Key Exchange
+// clientPublic, clientPrivate := box.GenerateKey(rand.Reader)
+// sharedKey := box.Precompute(serverPublic, clientPrivate)
+//
+// // Request Encryption
+// encryptedBody := EncryptStream(requestBody, sessionKey, sessionIV)
+//
+// // Response Decryption
+// decryptedResponse := DecryptStream(responseBody, sessionKey, sessionIV)
+//
+// Cryptographic Features:
+// - AES-GCM streaming encryption with 1KB configurable chunks
+// - NaCL (Curve25519) key exchange for session key protection
+// - Ed25519 signatures for data integrity verification
+// - Forward secrecy through daily server key rotation
+//
+// ## License System
+//
+// Enterprise license validation with cryptographic verification:
+//
+// // License introspection
+// info, err := sdk.IntrospectLicenseKey("XXXX-XXXX-XXXX-XXXX")
+// if err != nil {
+// return err
+// }
+//
+// // License properties
+// switch info.Type {
+// case sdk.LicenseExpireable:
+// fmt.Printf("License expires: %v", info.ExpiredAt)
+// case sdk.LicensePerpetual:
+// fmt.Println("Perpetual license")
+// }
+//
+// // Feature flags
+// for i, flag := range info.Flags {
+// fmt.Printf("Feature %d: %v", i, flag)
+// }
+//
+// License Features:
+// - Base32 encoding with validation checksums
+// - Cryptographic verification prevents tampering
+// - Expiration time validation with day-aligned precision
+// - Feature flags for tier-based access control
+// - PBKDF2-based fingerprinting for license correlation
+//
+// # Streaming Architecture
+//
+// ## Encryption Streaming
+//
+// Memory-efficient encryption for large data transfers:
+//
+// // Encrypt large files without memory accumulation
+// file, err := os.Open("large-file.dat")
+// if err != nil {
+// return err
+// }
+// defer file.Close()
+//
+// // Create streaming encryptor
+// encryptedStream, err := sdk.EncryptStream(file, sessionKey, sessionIV)
+// if err != nil {
+// return err
+// }
+// defer encryptedStream.Close()
+//
+// // Stream encrypted data to destination
+// _, err = io.Copy(destination, encryptedStream)
+//
+// ## Decryption Streaming
+//
+// Streaming decryption with authentication validation:
+//
+// // Decrypt response stream directly to file
+// outputFile, err := os.Create("decrypted-output.dat")
+// if err != nil {
+// return err
+// }
+// defer outputFile.Close()
+//
+// // Use DecryptProxy for direct streaming
+// err = sdk.DecryptProxy(encryptedResponse, outputFile, sessionKey, sessionIV)
+// if err != nil {
+// return fmt.Errorf("decryption failed: %w", err)
+// }
+//
+// Streaming Features:
+// - Configurable chunk sizes (default 16KB, max 1MB)
+// - Per-chunk authentication prevents tampering
+// - Random nonces ensure GCM security properties
+// - No memory accumulation regardless of data size
+//
+// # Transport Layer
+//
+// ## HTTP/2 Optimization
+//
+// Production-optimized transport configuration:
+//
+// transport := sdk.DefaultTransport()
+//
+// // Customize for specific requirements
+// transport.MaxConnsPerHost = 500
+// transport.ResponseHeaderTimeout = 5 * time.Minute
+//
+// err := sdk.Build(configs, sdk.WithTransport(transport))
+//
+// Transport Features:
+// - HTTP/2 with automatic protocol negotiation
+// - Connection pooling with configurable limits
+// - TLS 1.2+ with certificate validation
+// - Proxy support with environment variable detection
+//
+// ## Request Processing
+//
+// Complete request lifecycle with automatic security:
+//
+// // SDK automatically handles:
+// // 1. PoW ticket acquisition
+// // 2. Challenge solving with configurable timeout
+// // 3. Request encryption and signing
+// // 4. Response decryption and validation
+// // 5. Error handling and retry logic
+//
+// Processing Features:
+// - Automatic retry logic for temporary errors
+// - Exponential backoff with server-provided timing
+// - Context cancellation support throughout
+// - Comprehensive error classification and handling
+//
+// # Error Handling
+//
+// ## Error Classification
+//
+// Structured error handling with automatic retry logic:
+//
+// data, err := api.SomeCall(ctx, requestData)
+// if err != nil {
+// switch {
+// case errors.Is(err, sdk.ErrTooManyRequestsRPM):
+// // Temporary: Will be retried automatically with server timing
+// log.Info("Rate limited, retrying with backoff")
+//
+// case errors.Is(err, sdk.ErrForbidden):
+// // Fatal: Check license validity or authentication
+// log.Error("Access denied - verify license key")
+//
+// case errors.Is(err, sdk.ErrExperimentTimeout):
+// // Temporary: Increase PoW timeout for slower systems
+// log.Warn("PoW timeout - consider increasing timeout")
+//
+// default:
+// log.Error("Unexpected error:", err)
+// }
+// }
+//
+// ## Error Types
+//
+// Temporary Errors (automatically retried):
+// - ErrBadGateway: Server maintenance or overload
+// - ErrServerInternal: Temporary server issues
+// - ErrTooManyRequests: Standard rate limiting
+// - ErrTooManyRequestsRPM: Rate limiting with server-provided backoff
+// - ErrExperimentTimeout: PoW solving timeout (increase timeout or retry)
+//
+// Fatal Errors (no retry):
+// - ErrBadRequest: Invalid request format or parameters
+// - ErrForbidden: Invalid license or insufficient permissions
+// - ErrNotFound: Unknown endpoint or resource
+// - ErrTooManyRequestsRPH/RPD: Long-term rate limits exceeded
+// - ErrInvalidSignature: Cryptographic validation failure
+// - ErrReplayAttack: Security violation detected
+//
+// # Logging Integration
+//
+// ## Structured Logging
+//
+// Built-in logging framework with multiple adapters:
+//
+// import "github.com/sirupsen/logrus"
+//
+// // Use default logger
+// logger := sdk.DefaultLogger()
+// logger.SetLevel(sdk.LevelDebug)
+//
+// // Or wrap existing logrus instance
+// logrusLogger := logrus.New()
+// logrusLogger.SetLevel(logrus.InfoLevel)
+// wrappedLogger := sdk.WrapLogrus(logrusLogger)
+//
+// // Configure SDK with logger
+// err := sdk.Build(configs, sdk.WithLogger(wrappedLogger))
+//
+// ## Custom Logger Integration
+//
+// Implement Logger interface for custom logging backends:
+//
+// type CustomLogger struct {
+// // Your logging implementation
+// }
+//
+// func (l *CustomLogger) SetLevel(level sdk.Level) { /* ... */ }
+// func (l *CustomLogger) GetLevel() sdk.Level { /* ... */ }
+// func (l *CustomLogger) WithError(err error) sdk.Entry { /* ... */ }
+// func (l *CustomLogger) WithField(key string, value any) sdk.Entry { /* ... */ }
+// // ... implement all Logger interface methods
+//
+// Logging Features:
+// - Contextual logging with fields and errors
+// - Configurable log levels (Trace, Debug, Info, Warn, Error, Fatal, Panic)
+// - Request tracing with timing and retry information
+// - Cryptographic operation logging for security auditing
+//
+// # Advanced Configuration
+//
+// ## Option Pattern
+//
+// Flexible SDK configuration using functional options:
+//
+// err := sdk.Build(configs,
+// // Required: Client identification
+// sdk.WithClient("MySecurityTool", "2.1.0"),
+//
+// // Optional: License for premium features
+// sdk.WithLicenseKey("XXXX-XXXX-XXXX-XXXX"),
+//
+// // Optional: Performance tuning
+// sdk.WithPowTimeout(60*time.Second), // For slower systems
+// sdk.WithMaxRetries(5), // For unreliable networks
+//
+// // Optional: Custom transport
+// sdk.WithTransport(customTransport),
+//
+// // Optional: Structured logging
+// sdk.WithLogger(customLogger),
+//
+// // Optional: Installation tracking
+// sdk.WithInstallationID(installationUUID),
+// )
+//
+// ## Transport Customization
+//
+// Advanced HTTP transport configuration:
+//
+// transport := sdk.DefaultTransport()
+//
+// // Corporate proxy configuration
+// proxyURL, _ := url.Parse("http://proxy.company.com:8080")
+// transport.Proxy = http.ProxyURL(proxyURL)
+//
+// // Custom TLS configuration
+// transport.TLSClientConfig = &tls.Config{
+// MinVersion: tls.VersionTLS12,
+// // Add custom certificate validation
+// }
+//
+// // Performance tuning
+// transport.MaxConnsPerHost = 500
+// transport.ResponseHeaderTimeout = 5 * time.Minute
+//
+// err := sdk.Build(configs, sdk.WithTransport(transport))
+//
+// # Path Templates
+//
+// ## RESTful Resource Patterns
+//
+// Automatic path argument substitution for RESTful APIs:
+//
+// // Configure endpoint with path arguments
+// {
+// Calls: []any{&client.GetUserPosts},
+// Path: "/users/:userId/posts/:postId", // Path template
+// Method: sdk.CallMethodGET,
+// }
+//
+// // Generated function signature includes args parameter
+// posts, err := client.GetUserPosts(ctx, []string{"123", "456"})
+// // Generates: GET /users/123/posts/456
+//
+// ## Query Parameter Generation
+//
+// Automatic query string generation from Go models:
+//
+// type QueryParams struct {
+// Limit int `url:"limit"`
+// Offset int `url:"offset"`
+// Filter string `url:"filter"`
+// }
+//
+// params := QueryParams{Limit: 10, Offset: 20, Filter: "active"}
+//
+// // Convert to query map for SDK
+// queryMap := map[string]string{
+// "limit": "10",
+// "offset": "20",
+// "filter": "active",
+// }
+//
+// results, err := client.SearchItems(ctx, queryMap)
+// // Generates: GET /search?limit=10&offset=20&filter=active
+//
+// # Performance Optimization
+//
+// ## Connection Management
+//
+// Optimized connection pooling and reuse:
+//
+// // Default settings optimized for production
+// transport := sdk.DefaultTransport()
+// // MaxIdleConns: 50 (total idle connections)
+// // MaxIdleConnsPerHost: 10 (per-host idle connections)
+// // MaxConnsPerHost: 300 (max active connections per host)
+// // IdleConnTimeout: 90s (idle connection lifetime)
+//
+// ## Streaming Performance
+//
+// Memory-efficient processing for large data:
+//
+// // Upload large file without loading into memory
+// file, err := os.Open("large-dataset.json")
+// if err != nil {
+// return err
+// }
+// defer file.Close()
+//
+// stat, _ := file.Stat()
+// response, err := client.ProcessLargeData(ctx, file, stat.Size())
+//
+// // Download large response directly to file
+// outputFile, err := os.Create("processed-result.json")
+// if err != nil {
+// return err
+// }
+// defer outputFile.Close()
+//
+// err = client.GetLargeResult(ctx, queryParams, outputFile)
+//
+// # License Management
+//
+// ## License Validation
+//
+// Comprehensive license verification and introspection:
+//
+// // Validate license before SDK initialization
+// licenseInfo, err := sdk.IntrospectLicenseKey("XXXX-XXXX-XXXX-XXXX")
+// if err != nil {
+// return fmt.Errorf("invalid license: %w", err)
+// }
+//
+// if !licenseInfo.IsValid() {
+// return fmt.Errorf("license is invalid or expired")
+// }
+//
+// // Check license type and permissions
+// switch licenseInfo.Type {
+// case sdk.LicenseExpireable:
+// if licenseInfo.IsExpired() {
+// return fmt.Errorf("license expired on %v", licenseInfo.ExpiredAt)
+// }
+// fmt.Printf("License valid until %v", licenseInfo.ExpiredAt)
+//
+// case sdk.LicensePerpetual:
+// fmt.Println("Perpetual license - no expiration")
+// }
+//
+// // Feature flag checking
+// if licenseInfo.Flags[0] {
+// fmt.Println("Premium feature enabled")
+// }
+//
+// ## License Encoding
+//
+// Base32 encoding with multiple validation layers:
+//
+// // License key format: XXXX-XXXX-XXXX-XXXX
+//
+// # Production Deployment
+//
+// ## Configuration Best Practices
+//
+// Recommended production configuration:
+//
+// // Production-ready SDK setup
+// configs := []sdk.CallConfig{
+// // ... your API endpoints
+// }
+//
+// err := sdk.Build(configs,
+// // Required: Application identification
+// sdk.WithClient("YourApp", appVersion),
+//
+// // Recommended: License for premium features
+// sdk.WithLicenseKey(os.Getenv("VXCONTROL_LICENSE")),
+//
+// // Performance: Adjust for your hardware
+// sdk.WithPowTimeout(45*time.Second),
+// sdk.WithMaxRetries(5),
+//
+// // Monitoring: Production logging
+// sdk.WithLogger(productionLogger),
+//
+// // Identity: Stable installation tracking
+// sdk.WithInstallationID(persistentInstallationID),
+// )
+//
+// ## Monitoring Integration
+//
+// SDK provides comprehensive metrics for monitoring:
+//
+// // Logger captures:
+// // - Request timing and retry information
+// // - PoW solving performance and difficulty
+// // - Cryptographic operation success/failure
+// // - Network errors and recovery attempts
+// // - License validation and tier information
+//
+// ## Error Recovery
+//
+// Robust error handling for production environments:
+//
+// // Automatic recovery scenarios:
+// // - Network timeouts: Automatic retry with exponential backoff
+// // - Rate limiting: Server-provided backoff timing respected
+// // - PoW timeouts: Configurable timeout with retry support
+// // - Server overload: Automatic retry with jittered delays
+// // - License issues: Clear error messages for resolution
+//
+// # Thread Safety
+//
+// All SDK operations are thread-safe and optimized for concurrent usage:
+//
+// // Single SDK instance can handle concurrent requests
+// var client Client
+// sdk.Build(configs, options...)
+//
+// // Safe concurrent API calls
+// go func() {
+// client.CheckUpdates(ctx1, data1)
+// }()
+// go func() {
+// client.ReportError(ctx2, data2)
+// }()
+//
+// Concurrency Features:
+// - Thread-safe connection pooling
+// - Concurrent PoW solving (one per request)
+// - Shared session key generation
+// - Atomic retry counters and statistics
+//
+// Thread Safety Implementation:
+// - Immutable configuration after Build()
+// - Per-request context isolation
+// - Lock-free hot paths for performance
+// - Safe concurrent access to shared resources
+package sdk
diff --git a/sdk/license.go b/sdk/license.go
new file mode 100644
index 0000000..b4c37b5
--- /dev/null
+++ b/sdk/license.go
@@ -0,0 +1,389 @@
+package sdk
+
+import (
+ "crypto/md5"
+ "crypto/pbkdf2"
+ "crypto/sha256"
+ "encoding/binary"
+ "encoding/hex"
+ "errors"
+ "hash/crc32"
+ mrand "math/rand/v2"
+ "strings"
+ "time"
+)
+
+type LicenseType uint8
+
+const (
+ LicenseUnknown LicenseType = iota
+ LicenseExpireable
+ LicensePerpetual
+)
+
+func (t LicenseType) String() string {
+ switch t {
+ case LicenseExpireable:
+ return "expireable"
+ case LicensePerpetual:
+ return "perpetual"
+ default:
+ return "unknown"
+ }
+}
+
+func (t *LicenseType) Scan(value any) error {
+ switch value {
+ case "expireable":
+ *t = LicenseExpireable
+ case "perpetual":
+ *t = LicensePerpetual
+ default:
+ return errors.New("invalid license type")
+ }
+
+ return nil
+}
+
+var ErrInvalidLicence = errors.New("invalid license")
+
+type LicenseInfo struct {
+ Type LicenseType // license type (expireable or perpetual)
+ Flags [7]bool // permission flags
+ ExpiredAt time.Time // license expired at
+ CreatedAt time.Time // license created at
+}
+
+func (l *LicenseInfo) IsValid() bool {
+ return ((l.Type == LicenseExpireable && !l.ExpiredAt.IsZero() && !l.IsExpired()) ||
+ (l.Type == LicensePerpetual && l.ExpiredAt.IsZero())) &&
+ !l.CreatedAt.IsZero() && !l.CreatedAt.After(alignDays(time.Now().UTC()))
+}
+
+func (l *LicenseInfo) IsExpired() bool {
+ return l.Type == LicenseExpireable && l.ExpiredAt.After(alignDays(time.Now().UTC()))
+}
+
+type licenseData struct {
+ data [10]byte // license data
+ seed uint16 // seed 12 bits
+ baseConst uint64
+ seedConst uint64
+ LicenseInfo
+}
+
+const (
+ constBase uint64 = 0xC5618DA3 // 32 bits
+ maxDays uint16 = 0x0FFF // 12 bits
+ mask18 = 0x3FFFF
+ mask12 = 0xFFF
+ mask8 = 0xFF
+ mask4 = 0xF
+ mask2 = 0x3
+)
+
+type constXorShiftType uint8
+
+const (
+ constXorShiftCreatedAt constXorShiftType = 0
+ constXorShiftExpiredAt constXorShiftType = 12
+ constXorShiftFlags constXorShiftType = 24
+)
+
+type constXorMaskType uint64
+
+const (
+ constXorMaskCreatedAt constXorMaskType = mask12
+ constXorMaskExpiredAt constXorMaskType = mask12
+ constXorMaskFlags constXorMaskType = mask8
+)
+
+var (
+ emptyLicenseKey = [10]byte{}
+ emptyLicenseFP = [16]byte{}
+)
+
+var timeSince = time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
+
+var seed = [32]byte{
+ 0x4c, 0x7c, 0x16, 0x80, 0xb4, 0xdf, 0x5a, 0xfe,
+ 0x37, 0x0a, 0xb2, 0x83, 0xbe, 0x91, 0x3f, 0x44,
+ 0x45, 0x49, 0xb1, 0x43, 0xd0, 0xf3, 0xe5, 0xe2,
+ 0x13, 0xdd, 0x4d, 0x4e, 0x2c, 0xb6, 0xb3, 0x6b,
+}
+
+func decodeLicenseKey(key string) [10]byte {
+ key = strings.ReplaceAll(key, "-", "")
+ if len(key) != 16 {
+ return emptyLicenseKey
+ }
+
+ alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZ234679"
+ indices := [16]byte{}
+ for idx, char := range key {
+ index := strings.IndexByte(alphabet, byte(char))
+ if index == -1 {
+ return emptyLicenseKey
+ }
+
+ indices[idx] = byte(index)
+ }
+
+ compact := func(indices [8]byte) []byte {
+ result, value := [8]byte{}, uint64(0)
+ for idx := range 8 {
+ value |= uint64(indices[idx]) << (5 * idx)
+ }
+ binary.BigEndian.PutUint64(result[:], value)
+ for idx := range 4 {
+ result[7-idx], result[idx] = result[idx], result[7-idx]
+ }
+ return result[0:5]
+ }
+ compacted := [10]byte{}
+ copy(compacted[0:5], compact([8]byte(indices[0:8])))
+ copy(compacted[5:10], compact([8]byte(indices[8:16])))
+
+ return [10]byte(compacted)
+}
+
+func computeLicenseKeyFP(key [10]byte) [16]byte {
+ var fp [16]byte
+
+ if key == emptyLicenseKey {
+ return fp
+ }
+
+ salt := md5.Sum(key[:])
+ for idx := byte(0); idx < 128 && salt[0] != idx; idx++ {
+ salt = md5.Sum(salt[:])
+ }
+ for salt[0] > 128 {
+ salt = md5.Sum(salt[:])
+ }
+
+ skey := hex.EncodeToString(salt[:])
+ rkey, err := pbkdf2.Key(sha256.New, skey, salt[0:16], 100_000, 64)
+ if err != nil {
+ return fp
+ }
+
+ copy(fp[0:16], rkey[0:16])
+ xor(fp[0:16], rkey[16:32])
+ xor(fp[0:16], rkey[32:48])
+ xor(fp[0:16], rkey[48:64])
+
+ return fp
+}
+
+func IntrospectLicenseKey(key string) (*LicenseInfo, error) {
+ license := licenseData{}
+
+ if err := license.restore(decodeLicenseKey(key)); err != nil {
+ return nil, err
+ }
+
+ return &license.LicenseInfo, nil
+}
+
+func (l *licenseData) restore(data [10]byte) error {
+ copy(l.data[:], data[:])
+
+ data, l.seed = extractSeed(data)
+ if restored := fillChecksum(data); data != restored {
+ return ErrInvalidLicence
+ }
+
+ lcg := mrand.NewChaCha8(seed)
+ for range l.seed {
+ l.seedConst = lcg.Uint64()
+ }
+
+ id := binary.BigEndian.Uint32(l.data[0:4])
+ flags := binary.BigEndian.Uint16(l.data[4:6])
+ expiredAt := binary.BigEndian.Uint16(l.data[6:8])
+ createdAt := binary.BigEndian.Uint16(l.data[8:10])
+ salt := extractSalt(id, flags)
+
+ if err := l.decodeCreatedAt(salt, createdAt); err != nil {
+ return err
+ }
+
+ for range encodeDays(l.CreatedAt) {
+ l.baseConst = lcg.Uint64()
+ }
+
+ if err := l.decodeExpiredAt(salt, expiredAt); err != nil {
+ return err
+ }
+ if err := l.decodeFlags(flags); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (l *licenseData) decodeCreatedAt(salt, days uint16) error {
+ days &= mask12
+ days ^= l.seed & mask12
+ days ^= uint16(getConstXOR(0, constXorShiftCreatedAt, constXorMaskCreatedAt))
+ days ^= salt
+ l.CreatedAt = decodeDays(days)
+
+ if now := alignDays(time.Now().UTC()); l.CreatedAt.After(now) {
+ return ErrInvalidLicence
+ }
+
+ return nil
+}
+
+func (l *licenseData) decodeExpiredAt(salt, days uint16) error {
+ days &= mask12
+ days ^= uint16(getConstXOR(l.baseConst, constXorShiftExpiredAt, constXorMaskExpiredAt))
+ days ^= uint16(getConstXOR(l.seedConst, constXorShiftExpiredAt, constXorMaskExpiredAt))
+ days ^= salt
+
+ if days != maxDays {
+ if days < encodeDays(l.CreatedAt) {
+ return ErrInvalidLicence
+ }
+ l.ExpiredAt = decodeDays(days)
+ }
+
+ return nil
+}
+
+func (l *licenseData) decodeFlags(flags uint16) error {
+ flags &= mask8
+ flags ^= uint16(getConstXOR(l.baseConst, constXorShiftFlags, constXorMaskFlags))
+ flags ^= uint16(getConstXOR(l.seedConst, constXorShiftFlags, constXorMaskFlags))
+
+ l.parseFlags(uint8(flags))
+
+ if l.Type == LicensePerpetual && !l.ExpiredAt.IsZero() {
+ return ErrInvalidLicence
+ }
+ if l.Type == LicenseExpireable && l.ExpiredAt.IsZero() {
+ return ErrInvalidLicence
+ }
+
+ return nil
+}
+
+func (l *licenseData) parseFlags(flags uint8) {
+ flags &= mask8
+ if flags&(1<<0) == 1 {
+ l.Type = LicenseExpireable
+ } else {
+ l.Type = LicensePerpetual
+ }
+ for idx := range 7 {
+ l.Flags[idx] = flags>>(idx+1)&1 == 1
+ }
+}
+
+func extractSalt(id uint32, flags uint16) uint16 {
+ p0, p1 := id>>12&mask18, uint16(id&mask8)
+ flags &= mask8
+
+ var salt uint16
+ salt = uint16(p1&mask2)<<10 | uint16(p0&mask2)<<8 | flags
+ salt ^= uint16(p0>>2&mask4)<<8 | p1
+ salt ^= uint16(p0 >> 6 & mask12)
+
+ return salt & mask12
+}
+
+func extractSeed(data [10]byte) ([10]byte, uint16) {
+ x0 := uint16(data[4]&mask4) ^ uint16(data[4]>>4&mask4)
+ x1 := uint16(data[4] & mask4)
+ x2 := uint16(data[4] >> 4 & mask4)
+ p0 := uint16(data[8] >> 4 & mask4)
+ p1 := uint16(data[6] >> 4 & mask4)
+ p2 := uint16(data[2] & mask4)
+
+ p2 ^= x2 ^ p1
+ p1 ^= x1 ^ p0
+ p0 ^= x0
+
+ data[8] = byte(p0)<<4 | data[8]&mask4
+ data[6] = byte(p1)<<4 | data[6]&mask4
+ data[2] = byte(p2) | data[2]&(mask4<<4)
+
+ return data, p2<<8 | p1<<4 | p0
+}
+
+func fillChecksum(data [10]byte) [10]byte {
+ license := [80]byte{}
+ copy(license[0:10], data[0:10])
+
+ license[0] &= ^(byte(3) << 6)
+ license[4] = license[0] ^ license[1] ^ license[2]
+
+ crc32Hash := crc32.ChecksumIEEE(license[0:10])
+ binary.BigEndian.PutUint32(license[10:14], crc32Hash)
+
+ license[14] = license[2]&mask4 | license[4]&(mask4<<4)
+ license[15] = license[6]&(mask4<<4) | license[8]>>4&mask4
+
+ md5Hash := md5.Sum(license[0:16])
+ copy(license[16:32], md5Hash[0:16])
+
+ sha256Hash := sha256.Sum256(license[0:32])
+ copy(license[32:64], sha256Hash[0:32])
+
+ reverseData := [16]byte{
+ license[15], license[14], license[13], license[12],
+ license[11], license[10], license[9], license[8],
+ license[7], license[6], license[5], license[4],
+ license[3], license[2], license[1], license[0],
+ }
+
+ md5Hash = md5.Sum(reverseData[0:16])
+ copy(license[64:80], md5Hash[0:16])
+
+ luhnSum := uint16(0)
+ for i := range len(license) {
+ weight := uint16(1)
+ if i%2 == 1 {
+ weight = 3
+ }
+ luhnSum += (uint16(license[i]) * weight)
+ luhnSum &= mask12
+ }
+ license[4] = byte(luhnSum & mask8)
+ license[0] |= byte(luhnSum>>8&mask2) << 6
+
+ return [10]byte(license[0:10])
+}
+
+func getConstXOR(lbase uint64, shift constXorShiftType, mask constXorMaskType) uint16 {
+ return uint16((constBase ^ lbase) >> shift & uint64(mask))
+}
+
+func decodeDays(d uint16) time.Time {
+ return timeSince.AddDate(0, 0, int(d))
+}
+
+func encodeDays(t time.Time) uint16 {
+ return max(uint16(t.Sub(timeSince).Hours()/24), 0)
+}
+
+func alignDays(t time.Time) time.Time {
+ td := encodeDays(t)
+ at := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
+ if encodeDays(at) == td {
+ return at
+ }
+
+ for d := range maxDays {
+ if at = decodeDays(d); encodeDays(at) == td {
+ return at
+ }
+ if at = decodeDays(-d); encodeDays(at) == td {
+ return at
+ }
+ }
+
+ return t
+}
diff --git a/sdk/license_test.go b/sdk/license_test.go
new file mode 100644
index 0000000..aed6527
--- /dev/null
+++ b/sdk/license_test.go
@@ -0,0 +1,534 @@
+package sdk
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestIntrospectLicenseKeyWithTestData(t *testing.T) {
+ testData := loadSDKTestData(t)
+
+ for _, license := range testData.Licenses {
+ t.Run(license.Name, func(t *testing.T) {
+ info, err := IntrospectLicenseKey(license.Key)
+
+ if !license.Valid {
+ invalid := err != nil || info == nil || !info.IsValid()
+ assert.True(t, invalid, "expected failure for invalid license")
+ return
+ }
+
+ assert.NoError(t, err, "valid license should not produce error")
+ assert.NotNil(t, info, "valid license should return info")
+
+ // verify license type
+ if license.ExpiredAt.IsZero() {
+ assert.Equal(t, LicensePerpetual, info.Type, "perpetual license type")
+ } else {
+ assert.Equal(t, LicenseExpireable, info.Type, "expireable license type")
+ }
+
+ // verify dates
+ assert.Equal(t, license.CreatedAt.Truncate(24*time.Hour),
+ info.CreatedAt.Truncate(24*time.Hour), "created at should match")
+
+ if !license.ExpiredAt.IsZero() {
+ // client calculates absolute expiration date from encoded data
+ // this may differ from test data which stores relative dates
+ assert.False(t, info.ExpiredAt.IsZero(), "expireable license should have expired at")
+ assert.True(t, info.ExpiredAt.After(info.CreatedAt), "expired at should be after created at")
+ }
+
+ // verify flags structure
+ for i, expected := range license.Flags {
+ assert.Equal(t, expected, info.Flags[i],
+ "flag %d should match expected value", i)
+ }
+ })
+ }
+}
+
+func TestLicenseKeyDecoding(t *testing.T) {
+ testCases := []struct {
+ name string
+ key string
+ expected bool
+ }{
+ {
+ name: "valid license format",
+ key: "4MTY-NZVR-64QI-U26X",
+ expected: true,
+ },
+ {
+ name: "empty key",
+ key: "AAAA-AAAA-AAAA-AAAA",
+ expected: false,
+ },
+ {
+ name: "invalid format - no dashes",
+ key: "4MTYNZVR64QIU26X", // client strips dashes, so this is actually valid
+ expected: true,
+ },
+ {
+ name: "invalid format - wrong length",
+ key: "4MTY-NZVR-64QI",
+ expected: false,
+ },
+ {
+ name: "invalid characters",
+ key: "4MT0-NZVR-64QI-U26X", // contains '0'
+ expected: false,
+ },
+ {
+ name: "wrong dash positions",
+ key: "4MTY-NZV-R64Q-IU26X", // client strips all dashes, so position doesn't matter
+ expected: true,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ decoded := decodeLicenseKey(tc.key)
+
+ if tc.expected {
+ assert.NotEqual(t, emptyLicenseKey, decoded,
+ "valid key should decode to non-empty data")
+ } else {
+ assert.Equal(t, emptyLicenseKey, decoded,
+ "invalid key should decode to empty data")
+ }
+ })
+ }
+}
+
+func TestLicenseDataRestore(t *testing.T) {
+ testData := loadSDKTestData(t)
+
+ for _, license := range testData.Licenses {
+ if !license.Valid {
+ info, err := IntrospectLicenseKey(license.Key)
+ invalid := err != nil || info == nil || !info.IsValid()
+ assert.True(t, invalid, "expected failure for invalid license")
+ continue
+ }
+
+ t.Run(license.Name, func(t *testing.T) {
+ // decode license key
+ decodedKey := decodeLicenseKey(license.Key)
+ assert.NotEqual(t, emptyLicenseKey, decodedKey, "should decode successfully")
+
+ // restore license data
+ lic := &licenseData{}
+ err := lic.restore(decodedKey)
+ assert.NoError(t, err, "restore should succeed for valid license")
+
+ // validate basic fields
+ assert.True(t, lic.CreatedAt.Year() >= 2025, "created at should be reasonable")
+
+ if license.ExpiredAt.IsZero() {
+ assert.Equal(t, LicensePerpetual, lic.Type, "perpetual license")
+ } else {
+ assert.Equal(t, LicenseExpireable, lic.Type, "expireable license")
+ assert.False(t, lic.ExpiredAt.IsZero(), "expireable license should have expired at")
+ }
+
+ // verify dates
+ assert.Equal(t, license.CreatedAt, lic.CreatedAt, "created at should match")
+ assert.Equal(t, license.ExpiredAt, lic.ExpiredAt, "expired at should match")
+ assert.Equal(t, license.Flags, lic.Flags, "flags should match")
+
+ // verify fingerprint
+ fp, err := base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(license.FP)
+ assert.NoError(t, err, "test data should have valid hex fingerprint")
+ assert.Equal(t, [16]byte(fp), computeLicenseKeyFP(decodedKey), "fingerprint should match")
+ })
+ }
+}
+
+func TestChecksumValidation(t *testing.T) {
+ testData := loadSDKTestData(t)
+
+ // find valid license for corruption testing
+ var validLicense testLicense
+ for _, license := range testData.Licenses {
+ if license.Valid {
+ validLicense = license
+ break
+ }
+ }
+
+ if validLicense.Name == "" {
+ t.Skip("no valid license found in test data")
+ }
+
+ originalData := decodeLicenseKey(validLicense.Key)
+ assert.NotEqual(t, emptyLicenseKey, originalData, "test license should be valid")
+
+ // test single bit corruption
+ t.Run("single_bit_corruption", func(t *testing.T) {
+ detectedErrors := 0
+ totalTests := 0
+
+ bits := len(originalData) * 8
+ for i := range bits {
+ corruptedData := originalData
+ corruptedData[i/8] ^= 1 << (i % 8)
+
+ if corruptedData == originalData {
+ continue // no actual change
+ }
+
+ totalTests++
+ lic := &licenseData{}
+ if err := lic.restore(corruptedData); err != nil {
+ detectedErrors++
+ }
+ }
+
+ detectionRate := float64(detectedErrors) / float64(totalTests) * 100
+ t.Logf("single bit error detection: %.1f%% (%d/%d)",
+ detectionRate, detectedErrors, totalTests)
+
+ assert.Equal(t, 100.0, detectionRate,
+ "should detect all single bit errors")
+ })
+}
+
+func TestFingerprintConsistency(t *testing.T) {
+ testData := loadSDKTestData(t)
+
+ for _, license := range testData.Licenses {
+ t.Run(license.Name, func(t *testing.T) {
+ // decode license key
+ decodedKey := decodeLicenseKey(license.Key)
+ assert.NotEqual(t, emptyLicenseKey, decodedKey, "should decode successfully")
+
+ // compute fingerprint
+ computedFP := computeLicenseKeyFP(decodedKey)
+
+ // decode expected fingerprint from test data
+ expectedFP, err := base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(license.FP)
+ assert.NoError(t, err, "test data should have valid hex fingerprint")
+ assert.Len(t, expectedFP, 16, "fingerprint should be 16 bytes")
+
+ if len(expectedFP) == 16 {
+ assert.Equal(t, [16]byte(expectedFP), computedFP,
+ "computed fingerprint should match test data")
+ }
+ })
+ }
+}
+
+func TestTimeEncoding(t *testing.T) {
+ testCases := []struct {
+ name string
+ time time.Time
+ }{
+ {
+ name: "base_date",
+ time: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
+ },
+ {
+ name: "recent_date",
+ time: time.Date(2025, 9, 16, 0, 0, 0, 0, time.UTC),
+ },
+ {
+ name: "future_date",
+ time: time.Date(2035, 12, 31, 0, 0, 0, 0, time.UTC),
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // test encoding/decoding roundtrip
+ encoded := encodeDays(tc.time)
+ decoded := decodeDays(encoded)
+
+ assert.Equal(t, tc.time.Truncate(24*time.Hour),
+ decoded.Truncate(24*time.Hour),
+ "time encoding should be reversible")
+
+ // test alignment
+ aligned := alignDays(tc.time)
+ assert.Equal(t, 0, aligned.Hour(), "aligned time should have zero hour")
+ assert.Equal(t, 0, aligned.Minute(), "aligned time should have zero minute")
+ assert.Equal(t, 0, aligned.Second(), "aligned time should have zero second")
+ })
+ }
+}
+
+func TestLicenseValidation(t *testing.T) {
+ testData := loadSDKTestData(t)
+
+ for _, license := range testData.Licenses {
+ t.Run(license.Name+"_validation", func(t *testing.T) {
+ decoded := decodeLicenseKey(license.Key)
+
+ if !license.Valid {
+ // for invalid licenses, decoding might succeed but restoration should fail
+ lic := &licenseData{}
+ err := lic.restore(decoded)
+ if err == nil && lic.IsValid() {
+ t.Errorf("expected restoration to fail for invalid license %s", license.Name)
+ }
+ return
+ }
+
+ assert.NotEqual(t, emptyLicenseKey, decoded, "valid license should decode")
+
+ // restoration should succeed for valid licenses
+ lic := &licenseData{}
+ err := lic.restore(decoded)
+ assert.NoError(t, err, "valid license should restore without error")
+
+ // basic sanity checks
+ assert.True(t, lic.CreatedAt.Year() >= 2025, "reasonable creation year")
+
+ if lic.Type == LicenseExpireable {
+ assert.False(t, lic.ExpiredAt.IsZero(), "expireable license should have expiration")
+ }
+ })
+ }
+}
+
+func TestClientSecurityBoundaries(t *testing.T) {
+ // test basic format validation - client should reject invalid formats
+ invalidKeys := []string{
+ "",
+ "SHORT",
+ "TOOLONGKEYWITHINVALIDFORMAT",
+ "4MTY-NZVR-64QI-U26X-EXTRA",
+ "4MT0-NZVR-64QI-U26X", // contains forbidden character '0'
+ "4MT1-NZVR-64QI-U26X", // contains forbidden character '1'
+ "4MT5-NZVR-64QI-U26X", // contains forbidden character '5'
+ "4MT8-NZVR-64QI-U26X", // contains forbidden character '8'
+ }
+
+ for _, key := range invalidKeys {
+ t.Run("invalid_"+key, func(t *testing.T) {
+ _, err := IntrospectLicenseKey(key)
+ assert.Error(t, err, "invalid license format should produce error")
+ })
+ }
+}
+
+func TestClientLicensePerformance(t *testing.T) {
+ testData := loadSDKTestData(t)
+
+ // find first valid license
+ var validLicense testLicense
+ for _, license := range testData.Licenses {
+ if license.Valid {
+ validLicense = license
+ break
+ }
+ }
+
+ if validLicense.Name == "" {
+ t.Skip("no valid license found in test data")
+ }
+
+ const iterations = 1000
+
+ // benchmark license introspection
+ start := time.Now()
+ for range iterations {
+ info, err := IntrospectLicenseKey(validLicense.Key)
+ if err != nil || info == nil || !info.IsValid() {
+ t.Fatal("expected failure for invalid license")
+ }
+ _ = info
+ }
+ duration := time.Since(start)
+
+ avgTime := duration / iterations
+ rate := float64(iterations) / duration.Seconds()
+
+ t.Logf("license introspection performance:")
+ t.Logf(" %d operations in %v", iterations, duration)
+ t.Logf(" average time: %v per operation", avgTime)
+ t.Logf(" rate: %.0f operations/second", rate)
+
+ // client operations should be fast (no complex cryptographic operations)
+ if avgTime > 500*time.Microsecond {
+ t.Errorf("license introspection too slow: %v > 500ฮผs", avgTime)
+ }
+}
+
+func TestLicenseInfoMethods(t *testing.T) {
+ now := time.Now().UTC()
+ yesterday := now.AddDate(0, 0, -1)
+ tomorrow := now.AddDate(0, 0, 1)
+
+ testCases := []struct {
+ name string
+ license LicenseInfo
+ expectedValid bool
+ expectedExpired bool
+ }{
+ {
+ name: "valid_perpetual_license",
+ license: LicenseInfo{
+ Type: LicensePerpetual,
+ Flags: [7]bool{true, false, true, false, false, false, false},
+ ExpiredAt: time.Time{}, // zero time for perpetual
+ CreatedAt: yesterday,
+ },
+ expectedValid: true,
+ expectedExpired: false,
+ },
+ {
+ name: "expireable_with_past_date",
+ license: LicenseInfo{
+ Type: LicenseExpireable,
+ Flags: [7]bool{false, true, false, true, false, false, false},
+ ExpiredAt: yesterday, // past date
+ CreatedAt: now.AddDate(0, 0, -10),
+ },
+ expectedValid: true, // not expired per client logic
+ expectedExpired: false, // .After(now) is false for yesterday
+ },
+ {
+ name: "expireable_expires_tomorrow",
+ license: LicenseInfo{
+ Type: LicenseExpireable,
+ Flags: [7]bool{true, true, true, true, false, false, false},
+ ExpiredAt: tomorrow,
+ CreatedAt: yesterday,
+ },
+ expectedValid: false, // !IsExpired() will be false since tomorrow.After(now) = true
+ expectedExpired: true, // tomorrow.After(now) = true
+ },
+ {
+ name: "invalid_perpetual_with_expiry",
+ license: LicenseInfo{
+ Type: LicensePerpetual,
+ Flags: [7]bool{false, false, false, false, false, false, false},
+ ExpiredAt: tomorrow, // perpetual shouldn't have expiry
+ CreatedAt: yesterday,
+ },
+ expectedValid: false, // fails perpetual validation (!ExpiredAt.IsZero())
+ expectedExpired: false, // IsExpired() returns false for perpetual type
+ },
+ {
+ name: "invalid_future_created_at",
+ license: LicenseInfo{
+ Type: LicenseExpireable,
+ Flags: [7]bool{true, false, false, false, false, false, false},
+ ExpiredAt: now.AddDate(0, 0, 10), // future expiry
+ CreatedAt: tomorrow, // future creation date
+ },
+ expectedValid: false, // fails future CreatedAt check
+ expectedExpired: true, // future ExpiredAt.After(now) = true
+ },
+ {
+ name: "invalid_zero_created_at",
+ license: LicenseInfo{
+ Type: LicenseExpireable,
+ Flags: [7]bool{false, false, false, false, false, false, false},
+ ExpiredAt: tomorrow, // future expiry
+ CreatedAt: time.Time{}, // zero time
+ },
+ expectedValid: false, // fails zero CreatedAt check
+ expectedExpired: true, // tomorrow.After(now) = true
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ assert.Equal(t, tc.expectedValid, tc.license.IsValid(),
+ "IsValid() result should match expected")
+ assert.Equal(t, tc.expectedExpired, tc.license.IsExpired(),
+ "IsExpired() result should match expected")
+ })
+ }
+}
+
+func TestLicenseTypeString(t *testing.T) {
+ testCases := []struct {
+ licenseType LicenseType
+ expected string
+ }{
+ {LicenseUnknown, "unknown"},
+ {LicenseExpireable, "expireable"},
+ {LicensePerpetual, "perpetual"},
+ {LicenseType(99), "unknown"}, // invalid type
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.expected, func(t *testing.T) {
+ assert.Equal(t, tc.expected, tc.licenseType.String())
+ })
+ }
+}
+
+func TestLicenseTypeScan(t *testing.T) {
+ testCases := []struct {
+ name string
+ input any
+ expected LicenseType
+ shouldError bool
+ }{
+ {
+ name: "expireable_string",
+ input: "expireable",
+ expected: LicenseExpireable,
+ shouldError: false,
+ },
+ {
+ name: "perpetual_string",
+ input: "perpetual",
+ expected: LicensePerpetual,
+ shouldError: false,
+ },
+ {
+ name: "invalid_string",
+ input: "invalid",
+ expected: LicenseUnknown,
+ shouldError: true,
+ },
+ {
+ name: "numeric_input",
+ input: 123,
+ expected: LicenseUnknown,
+ shouldError: true,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ var licenseType LicenseType
+ err := licenseType.Scan(tc.input)
+
+ if tc.shouldError {
+ assert.Error(t, err, "should return error for invalid input")
+ } else {
+ assert.NoError(t, err, "should not return error for valid input")
+ assert.Equal(t, tc.expected, licenseType, "scanned type should match expected")
+ }
+ })
+ }
+}
+
+func loadSDKTestData(t *testing.T) *testData {
+ t.Helper()
+
+ dataPath := filepath.Join("testdata", "data.json")
+ data, err := os.ReadFile(dataPath)
+ if err != nil {
+ t.Fatalf("failed to load test data: %v", err)
+ }
+
+ var td testData
+ if err := json.Unmarshal(data, &td); err != nil {
+ t.Fatalf("failed to parse test data: %v", err)
+ }
+
+ return &td
+}
diff --git a/sdk/logger.go b/sdk/logger.go
new file mode 100644
index 0000000..a3cfb25
--- /dev/null
+++ b/sdk/logger.go
@@ -0,0 +1,112 @@
+package sdk
+
+import (
+ "context"
+
+ "github.com/sirupsen/logrus"
+)
+
+type Level string
+
+const (
+ LevelTrace Level = "trace"
+ LevelDebug Level = "debug"
+ LevelInfo Level = "info"
+ LevelWarn Level = "warn"
+ LevelError Level = "error"
+ LevelFatal Level = "fatal"
+ LevelPanic Level = "panic"
+)
+
+type Logger interface {
+ SetLevel(level Level)
+ GetLevel() Level
+ Entry
+}
+
+type Entry interface {
+ WithError(err error) Entry
+ WithField(key string, value any) Entry
+ WithFields(fields map[string]any) Entry
+ WithContext(ctx context.Context) Entry
+ Log
+}
+
+type Log interface {
+ Trace(args ...any)
+ Tracef(format string, args ...any)
+ Debug(args ...any)
+ Debugf(format string, args ...any)
+ Info(args ...any)
+ Infof(format string, args ...any)
+ Warn(args ...any)
+ Warnf(format string, args ...any)
+ Error(args ...any)
+ Errorf(format string, args ...any)
+ Fatal(args ...any)
+ Fatalf(format string, args ...any)
+ Panic(args ...any)
+ Panicf(format string, args ...any)
+}
+
+type logger struct {
+ *logrus.Logger
+}
+
+func (l *logger) SetLevel(level Level) {
+ switch level {
+ case LevelTrace, LevelDebug, LevelInfo, LevelWarn, LevelError, LevelFatal, LevelPanic:
+ var lv logrus.Level
+ if err := lv.UnmarshalText([]byte(level)); err != nil {
+ l.Logger.SetLevel(logrus.InfoLevel)
+ }
+ }
+}
+
+func (l *logger) GetLevel() Level {
+ return Level(l.Logger.GetLevel().String())
+}
+
+func (l *logger) WithError(err error) Entry {
+ return &entry{Entry: l.Logger.WithError(err)}
+}
+
+func (l *logger) WithField(key string, value any) Entry {
+ return &entry{Entry: l.Logger.WithField(key, value)}
+}
+
+func (l *logger) WithFields(fields map[string]any) Entry {
+ return &entry{Entry: l.Logger.WithFields(fields)}
+}
+
+func (l *logger) WithContext(ctx context.Context) Entry {
+ return &entry{Entry: l.Logger.WithContext(ctx)}
+}
+
+type entry struct {
+ *logrus.Entry
+}
+
+func (e *entry) WithError(err error) Entry {
+ return &entry{Entry: e.Entry.WithError(err)}
+}
+
+func (e *entry) WithField(key string, value any) Entry {
+ return &entry{Entry: e.Entry.WithField(key, value)}
+}
+
+func (e *entry) WithFields(fields map[string]any) Entry {
+ return &entry{Entry: e.Entry.WithFields(fields)}
+}
+
+func (e *entry) WithContext(ctx context.Context) Entry {
+ return &entry{Entry: e.Entry.WithContext(ctx)}
+}
+
+func DefaultLogger() Logger {
+ return &logger{Logger: logrus.StandardLogger()}
+}
+
+func WrapLogrus(l *logrus.Logger) Logger {
+ return &logger{Logger: l}
+}
diff --git a/sdk/mock_test.go b/sdk/mock_test.go
new file mode 100644
index 0000000..3eaa8d7
--- /dev/null
+++ b/sdk/mock_test.go
@@ -0,0 +1,484 @@
+package sdk
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "crypto/tls"
+ "encoding/base64"
+ "encoding/binary"
+ "encoding/json"
+ "fmt"
+ "hash/crc32"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+ "golang.org/x/crypto/nacl/box"
+)
+
+const testResponseData = `{"status":"success","message":"test completed"}`
+
+type testData struct {
+ Tickets []testTicket `json:"tickets"`
+ Licenses []testLicense `json:"licenses"`
+ ResponseData []testResponse `json:"response_data"`
+}
+
+type testTicket struct {
+ Name string `json:"name"`
+ Ticket string `json:"ticket"`
+ RequestID string `json:"request_id"`
+ SignatureNonce string `json:"signature_nonce"`
+ ServerKey string `json:"server_key"`
+ InstallationID string `json:"installation_id"`
+ Valid bool `json:"valid"`
+ id [16]byte `json:"-"`
+ key [16]byte `json:"-"`
+ nonce [16]byte `json:"-"`
+}
+
+type testLicense struct {
+ Name string `json:"name"`
+ FP string `json:"fp"`
+ Key string `json:"key"`
+ Flags [7]bool `json:"flags"`
+ ExpiredAt time.Time `json:"expired_at"`
+ CreatedAt time.Time `json:"created_at"`
+ Valid bool `json:"valid"`
+}
+
+type testResponse struct {
+ Name string `json:"name"`
+ Data string `json:"data"`
+}
+
+func loadTestData() *testData {
+ dataPath := filepath.Join("testdata", "data.json")
+ data, err := os.ReadFile(dataPath)
+ if err != nil {
+ panic(fmt.Sprintf("failed to load test data: %v", err))
+ }
+
+ var td testData
+ if err := json.Unmarshal(data, &td); err != nil {
+ panic(fmt.Sprintf("failed to parse test data: %v", err))
+ }
+
+ for idx := range td.Tickets {
+ ticket := td.Tickets[idx]
+
+ idData, err := uuid.Parse(ticket.InstallationID)
+ if err != nil {
+ panic(fmt.Sprintf("failed to parse installation ID: %v", err))
+ }
+ td.Tickets[idx].id = [16]byte(idData)
+
+ keyData, err := base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(ticket.ServerKey)
+ if err != nil {
+ panic(fmt.Sprintf("failed to decode server key: %v", err))
+ }
+ if len(keyData) == 16 {
+ td.Tickets[idx].key = [16]byte(keyData)
+ }
+
+ nonceData, err := base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(ticket.SignatureNonce)
+ if err != nil {
+ panic(fmt.Sprintf("failed to decode signature nonce: %v", err))
+ }
+ if len(nonceData) == 16 {
+ td.Tickets[idx].nonce = [16]byte(nonceData)
+ }
+ }
+
+ return &td
+}
+
+func newTestSDK() sdk {
+ var err error
+ s := defaultSDK()
+
+ s.clientPublicKey, s.clientPrivateKey, err = box.GenerateKey(rand.Reader)
+ if err != nil {
+ panic(fmt.Sprintf("failed to generate client keys: %v", err))
+ }
+
+ s.transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+ s.client = &http.Client{
+ Transport: s.transport,
+ }
+
+ return *s
+}
+
+// mockServer simulates real server middleware chain behavior
+type mockServer struct {
+ keys *mockServerKeys
+ testData *testData
+ ticketsByID map[string]testTicket // RequestID -> ticket data
+ ticketsByName map[string]testTicket // name -> ticket data for /api/v1/ticket/{name}
+ licenses map[[32]byte]bool
+}
+
+type mockServerKeys struct {
+ public *[32]byte
+ private *[32]byte
+}
+
+func generateMockKeys() *mockServerKeys {
+ pub, priv, err := box.GenerateKey(rand.Reader)
+ if err != nil {
+ panic(fmt.Sprintf("failed to generate server keys: %v", err))
+ }
+
+ return &mockServerKeys{
+ public: pub,
+ private: priv,
+ }
+}
+
+func newMockServer() *mockServer {
+ testData := loadTestData()
+ tickets := make(map[string]testTicket)
+ ticketsByName := make(map[string]testTicket)
+
+ for _, ticket := range testData.Tickets {
+ if ticket.Valid && ticket.RequestID != "" {
+ tickets[ticket.RequestID] = ticket
+ }
+ if ticket.Name != "" {
+ ticketsByName[ticket.Name] = ticket
+ }
+ }
+
+ return &mockServer{
+ keys: generateMockKeys(),
+ testData: testData,
+ ticketsByID: tickets,
+ ticketsByName: ticketsByName,
+ licenses: make(map[[32]byte]bool),
+ }
+}
+
+func (ms *mockServer) getPublicKey() *[32]byte {
+ return ms.keys.public
+}
+
+func (ms *mockServer) getPrivateKey() *[32]byte {
+ return ms.keys.private
+}
+
+func (ms *mockServer) createServer() *httptest.Server {
+ return httptest.NewServer(http.HandlerFunc(ms.handler))
+}
+
+func (ms *mockServer) createTLSServer() *httptest.Server {
+ server := httptest.NewTLSServer(http.HandlerFunc(ms.handler))
+ // disable certificate verification for testing
+ server.Client().Transport = &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ }
+ return server
+}
+
+func (ms *mockServer) handler(w http.ResponseWriter, r *http.Request) {
+ if !ms.validateBasicHeaders(w, r) {
+ return
+ }
+
+ if strings.HasPrefix(r.URL.Path, defaultTicketPath) {
+ ms.handleTicketEndpoint(w, r)
+ return
+ }
+
+ if !ms.validatePoWSignature(w, r) {
+ return
+ }
+
+ ms.sendResponse(w, r)
+}
+
+func (ms *mockServer) validateBasicHeaders(w http.ResponseWriter, r *http.Request) bool {
+ licenseKeyHeader := r.Header.Get(headerXLicenseKey)
+ installationID := r.Header.Get(headerXInstallationID)
+ requestID := r.Header.Get(headerXRequestID)
+
+ if installationID == "" || requestID == "" {
+ ms.sendError(w, ErrBadRequest)
+ return false
+ }
+
+ if _, err := uuid.Parse(installationID); err != nil {
+ ms.sendError(w, ErrBadRequest)
+ return false
+ }
+
+ if _, err := uuid.Parse(requestID); err != nil {
+ ms.sendError(w, ErrBadRequest)
+ return false
+ }
+
+ if licenseKeyHeader == "" {
+ return true
+ }
+
+ if !ms.validateLicenseKey(licenseKeyHeader) {
+ ms.sendError(w, ErrBadRequest)
+ return false
+ }
+
+ return true
+}
+
+func (ms *mockServer) validateLicenseKey(licenseKeyHeader string) bool {
+ licenseKey, err := base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(licenseKeyHeader)
+ if err != nil {
+ return false
+ }
+
+ if len(licenseKey) != 32 {
+ return false
+ }
+
+ if _, exists := ms.licenses[[32]byte(licenseKey)]; exists {
+ return false
+ } else {
+ ms.licenses[[32]byte(licenseKey)] = true
+ }
+
+ xor(licenseKey[16:32], licenseKey[0:16])
+
+ return computeLicenseKeyFP(testLicenseKey) == [16]byte(licenseKey[16:32])
+}
+
+func (ms *mockServer) handleTicketEndpoint(w http.ResponseWriter, r *http.Request) {
+ sessionKey, sessionIV, err := ms.validateRequestKey(r)
+ if err != nil {
+ ms.sendError(w, ErrBadRequest)
+ return
+ }
+
+ ticket, exists := ms.ticketsByName[strings.TrimPrefix(r.URL.Path, defaultTicketPath)]
+ if !exists {
+ ms.sendError(w, ErrNotFound)
+ return
+ }
+
+ ms.sendSuccess(w, []byte(ticket.Ticket), sessionKey, sessionIV)
+}
+
+func (ms *mockServer) validateRequestKey(r *http.Request) ([16]byte, [16]byte, error) {
+ requestKeyHeader := r.Header.Get(headerXRequestKey)
+ if requestKeyHeader == "" {
+ return [16]byte{}, [16]byte{}, fmt.Errorf("missing X-Request-Key header")
+ }
+
+ decodedKeyHeader, err := base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(requestKeyHeader)
+ if err != nil || len(decodedKeyHeader) != 120 {
+ return [16]byte{}, [16]byte{}, fmt.Errorf("invalid X-Request-Key header")
+ }
+
+ clientPublicKey := [32]byte(decodedKeyHeader[0:32])
+
+ // restore original client public key
+ installationID := r.Header.Get(headerXInstallationID)
+ instID, _ := uuid.Parse(installationID)
+ instIDBytes := [16]byte(instID)
+ xor(clientPublicKey[0:16], instIDBytes[0:16])
+ xor(clientPublicKey[16:32], instIDBytes[0:16])
+
+ nonce := [24]byte(decodedKeyHeader[32:56])
+ encryptedPayload := decodedKeyHeader[56:120]
+
+ var sharedKey [32]byte
+ box.Precompute(&sharedKey, &clientPublicKey, ms.getPrivateKey())
+
+ sessionData, ok := box.OpenAfterPrecomputation(nil, encryptedPayload, &nonce, &sharedKey)
+ if !ok || len(sessionData) != 48 {
+ return [16]byte{}, [16]byte{}, fmt.Errorf("failed to decrypt session data")
+ }
+
+ return [16]byte(sessionData[0:16]), [16]byte(sessionData[16:32]), nil
+}
+
+func (ms *mockServer) validatePoWSignature(w http.ResponseWriter, r *http.Request) bool {
+ signHeader := r.Header.Get(headerXRequestSign)
+ if signHeader == "" {
+ ms.sendError(w, ErrForbidden)
+ return false
+ }
+
+ requestID := r.Header.Get(headerXRequestID)
+ if requestID == "" {
+ ms.sendError(w, ErrForbidden)
+ return false
+ }
+
+ signData, err := base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(signHeader)
+ if err != nil || len(signData) < 48 {
+ ms.sendError(w, ErrInvalidSignature)
+ return false
+ }
+
+ ticket, exists := ms.ticketsByID[requestID]
+ if !exists {
+ ms.sendError(w, ErrForbidden)
+ return false
+ }
+
+ xor(signData[0:16], ticket.id[0:16])
+ if [16]byte(signData[0:16]) != ticket.nonce {
+ ms.sendError(w, ErrInvalidSignature)
+ return false
+ }
+
+ sc, err := aes.NewCipher(ticket.key[0:16])
+ if err != nil {
+ ms.sendError(w, ErrServerInternal)
+ return false
+ }
+
+ iv := generateIV(ticket.id)
+ scb := cipher.NewCBCDecrypter(sc, iv[0:16])
+ scb.CryptBlocks(signData[16:48], signData[16:48])
+
+ version := signData[27]
+ if version != sdkProtocolVersion {
+ ms.sendError(w, ErrInvalidSignature)
+ return false
+ }
+
+ hash := binary.BigEndian.Uint32(signData[44:48])
+ if hash != crc32.ChecksumIEEE(signData[16:44]) {
+ ms.sendError(w, ErrInvalidSignature)
+ return false
+ }
+
+ timestamp := time.UnixMicro(int64(binary.BigEndian.Uint64(signData[28:36])))
+ if timestamp.After(time.Now().UTC().Add(3 * time.Second)) {
+ ms.sendError(w, ErrInvalidSignature)
+ return false
+ }
+ if timestamp.Before(time.Now().UTC().Add(-3 * time.Second)) {
+ ms.sendError(w, ErrInvalidSignature)
+ return false
+ }
+
+ return true
+}
+
+func (ms *mockServer) sendResponse(w http.ResponseWriter, r *http.Request) {
+ ticket, exists := ms.ticketsByID[r.Header.Get(headerXRequestID)]
+ if !exists {
+ ms.sendError(w, ErrForbidden)
+ return
+ }
+
+ // extract name after /api/v1/call/ and handle different endpoint patterns
+ name := strings.TrimPrefix(r.URL.Path, "/api/v1/call/")
+
+ // handle different endpoint patterns for comprehensive testing
+ var responseData string
+ switch {
+ case name == "test_json" || name == "echo" || strings.HasPrefix(name, "basic") ||
+ strings.HasPrefix(name, "query") || strings.HasPrefix(name, "args") ||
+ strings.HasPrefix(name, "body") || strings.HasPrefix(name, "reader"):
+ // return standard test response for all test endpoints
+ responseData = testResponseData
+ default:
+ // check if it matches any configured response data
+ for _, response := range ms.testData.ResponseData {
+ if response.Name == name {
+ responseData = response.Data
+ break
+ }
+ }
+ if responseData == "" {
+ ms.sendError(w, ErrNotFound)
+ return
+ }
+ }
+
+ // read request body if present (for body validation testing)
+ if r.Body != nil {
+ // we don't need to validate the body content, just consume it
+ // to simulate server behavior except echo endpoint
+ if name != "echo" {
+ defer r.Body.Close()
+ if _, err := io.Copy(io.Discard, r.Body); err != nil {
+ ms.sendError(w, ErrServerInternal)
+ return
+ }
+ } else {
+ bodyReader, err := DecryptStream(r.Body, ticket.key, generateIV(ticket.id))
+ if err != nil {
+ ms.sendError(w, ErrServerInternal)
+ return
+ }
+ defer bodyReader.Close()
+
+ body, err := io.ReadAll(bodyReader)
+ if err != nil {
+ ms.sendError(w, ErrServerInternal)
+ return
+ }
+ responseData = string(body)
+ }
+ }
+
+ ms.sendSuccess(w, []byte(responseData), ticket.key, generateIV(ticket.id))
+}
+
+func (ms *mockServer) sendError(w http.ResponseWriter, err error) {
+ var statusCode int
+ var errorCode string
+
+ switch err {
+ case ErrBadRequest:
+ statusCode = http.StatusBadRequest
+ errorCode = "BadRequest"
+ case ErrForbidden:
+ statusCode = http.StatusForbidden
+ errorCode = "Forbidden"
+ case ErrTooManyRequestsRPM:
+ statusCode = http.StatusTooManyRequests
+ errorCode = "TooManyRequestsRPM"
+ case ErrInvalidSignature:
+ statusCode = http.StatusForbidden
+ errorCode = "Forbidden"
+ case ErrNotFound:
+ statusCode = http.StatusNotFound
+ errorCode = "NotFound"
+ default:
+ statusCode = http.StatusInternalServerError
+ errorCode = "Internal"
+ }
+
+ response := map[string]string{
+ "status": "error",
+ "code": errorCode,
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(statusCode)
+ if err := json.NewEncoder(w).Encode(response); err != nil {
+ ms.sendError(w, ErrServerInternal)
+ return
+ }
+}
+
+func (ms *mockServer) sendSuccess(w http.ResponseWriter, data []byte, key, iv [16]byte) {
+ encryptedData, err := EncryptBytes(data, key, iv)
+ if err != nil {
+ ms.sendError(w, ErrServerInternal)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Write(encryptedData)
+}
diff --git a/sdk/pow.go b/sdk/pow.go
new file mode 100644
index 0000000..6e6dc2d
--- /dev/null
+++ b/sdk/pow.go
@@ -0,0 +1,289 @@
+package sdk
+
+import (
+ "bytes"
+ "context"
+ "crypto/aes"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/binary"
+ "errors"
+ "fmt"
+)
+
+const volumeUnit = 128
+
+type result struct {
+ Key []byte
+ Catalyst []byte
+ Signature []byte
+ Experiments int
+ Recipe formula
+}
+
+type formula struct {
+ CauldronSize uint16
+ Stirrings uint16
+ CatalystChange uint8
+ Samples uint16
+ SampleSize uint8
+ RestTime uint16
+ Capacity uint16
+}
+
+var (
+ ErrInvalidFormula = errors.New("corrupted formula")
+ ErrCauldronBlocked = errors.New("elixir brewing failed")
+ ErrAlchemicalFailure = errors.New("mystical process disrupted")
+ ErrExperimentTimeout = errors.New("transmutation time exceeded")
+ ErrPhilosopherStone = errors.New("philosopher stone not found")
+)
+
+// transmute finds the correct transmutation using unlimited time unless context timeout is set or cancelled manually
+func transmute(ctx context.Context, formula string, essence []byte) (result, error) {
+ manuscript, err := base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(formula)
+ if err != nil {
+ return result{}, errors.Join(ErrInvalidFormula, err)
+ }
+
+ if len(manuscript) < 76 {
+ return result{}, errors.Join(ErrInvalidFormula, fmt.Errorf("incomplete recipe instructions"))
+ }
+
+ recipe := decipherRecipe(manuscript[64:])
+ if err := validateRecipe(recipe); err != nil {
+ return result{}, errors.Join(ErrInvalidFormula, err)
+ }
+
+ base := manuscript[0:16]
+ materials := manuscript[16:48]
+ goal := manuscript[48:64]
+
+ vessel := sha256.New()
+ mixture := append(essence, materials...)
+ transmutation := make([]byte, len(base))
+ blueprint := make([]byte, 0, 32)
+ veins := make([]int, recipe.Samples)
+ blueprintMixed := make([]byte, 16)
+
+ experiment := result{
+ Catalyst: materials[0:16],
+ Signature: materials[16:32],
+ Recipe: recipe,
+ }
+
+ elixir, err := brewElixir(materials[0:16], materials[16:32], recipe.CauldronSize, recipe.CatalystChange)
+ if err != nil {
+ return experiment, errors.Join(ErrCauldronBlocked, err)
+ }
+
+ for attempt := range uint32(1 << 20) {
+ experiment.Experiments++
+ if experiment.Experiments%128 == 0 {
+ select {
+ case <-ctx.Done():
+ return experiment, ErrExperimentTimeout
+ default:
+ }
+ }
+
+ copy(transmutation, base)
+ modifyBase(transmutation, attempt)
+
+ vessel.Write(transmutation)
+ vessel.Write(mixture)
+ blueprint = vessel.Sum(blueprint[:0])
+ vessel.Reset()
+
+ if !mapVeinsInto(blueprint, veins, recipe.Samples, recipe.CauldronSize, recipe.SampleSize) {
+ return experiment, ErrAlchemicalFailure
+ }
+
+ essences, err := extractEssences(elixir, veins, recipe.SampleSize)
+ if err != nil {
+ return experiment, errors.Join(ErrAlchemicalFailure, err)
+ }
+
+ refined, err := distill(essences, recipe.Stirrings)
+ if err != nil {
+ return experiment, errors.Join(ErrAlchemicalFailure, err)
+ }
+
+ fuseInto(blueprintMixed, blueprint[0:16], blueprint[16:32])
+ infuseWith(refined, blueprintMixed)
+
+ if bytes.Equal(refined, goal[0:16]) {
+ experiment.Key = transmutation
+ return experiment, nil
+ }
+ }
+
+ return experiment, ErrPhilosopherStone
+}
+
+func decipherRecipe(data []byte) formula {
+ return formula{
+ CauldronSize: binary.BigEndian.Uint16(data[0:2]),
+ Stirrings: binary.BigEndian.Uint16(data[2:4]),
+ CatalystChange: data[4],
+ Samples: binary.BigEndian.Uint16(data[5:7]),
+ SampleSize: data[7],
+ RestTime: binary.BigEndian.Uint16(data[8:10]),
+ Capacity: binary.BigEndian.Uint16(data[10:12]),
+ }
+}
+
+func validateRecipe(recipe formula) error {
+ if recipe.CauldronSize == 0 {
+ return fmt.Errorf("cauldron cannot be empty")
+ }
+ if recipe.Stirrings == 0 {
+ return fmt.Errorf("mixture requires stirring")
+ }
+ if recipe.CatalystChange == 0 {
+ return fmt.Errorf("catalyst renewal needed")
+ }
+ if recipe.Samples == 0 {
+ return fmt.Errorf("no essence samples available")
+ }
+ if recipe.SampleSize == 0 {
+ return fmt.Errorf("sample portion invalid")
+ }
+ return nil
+}
+
+func brewElixir(catalystKey, catalystData []byte, cauldronSize uint16, changeInterval uint8) ([]byte, error) {
+ totalVials := uint32(cauldronSize) * volumeUnit / 16
+ potion := make([]byte, totalVials*16)
+
+ vessel, err := aes.NewCipher(catalystKey)
+ if err != nil {
+ return nil, err
+ }
+
+ essence := make([]byte, 16)
+ copy(essence, catalystData)
+
+ mixer := sha256.New()
+
+ for vialNum := range totalVials {
+ vessel.Encrypt(essence, essence)
+ mixer.Write(essence)
+ copy(potion[vialNum*16:(vialNum+1)*16], essence)
+
+ if (vialNum+1)%uint32(changeInterval) == 0 {
+ newCatalyst := mixer.Sum(nil)
+ if vessel, err = aes.NewCipher(merge(newCatalyst[0:16], newCatalyst[16:32])); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ return potion, nil
+}
+
+func mapVeinsInto(blueprint []byte, veins []int, samples, cauldronSize uint16, portionSize uint8) bool {
+ blueprintLen := len(blueprint)
+ blueprintQuads := blueprintLen / 4
+ availableDepth := uint32(cauldronSize)*volumeUnit - uint32(portionSize)
+
+ if blueprintLen < 4 {
+ return false
+ }
+
+ for i := range int(samples) {
+ primary, secondary, multiplier := i%blueprintQuads, i/blueprintQuads, (i/blueprintLen)+1
+ position := (primary*4*multiplier + secondary*multiplier) % (blueprintLen - 3)
+ veins[i] = int(binary.BigEndian.Uint32(blueprint[position:]) % availableDepth)
+ }
+
+ return true
+}
+
+func extractEssences(elixir []byte, veins []int, portionSize uint8) ([32]byte, error) {
+ mixer := sha256.New()
+
+ for _, vein := range veins {
+ if vein < 0 || vein+int(portionSize) > len(elixir) {
+ return [32]byte{}, fmt.Errorf("vein position out of bounds: %d", vein)
+ }
+ portion := elixir[vein : vein+int(portionSize)]
+ mixer.Write(portion)
+ }
+
+ return [32]byte(mixer.Sum(nil)), nil
+}
+
+func distill(material [32]byte, stirrings uint16) ([]byte, error) {
+ catalyzer := material[0:16]
+ vessel, err := aes.NewCipher(catalyzer)
+ if err != nil {
+ return nil, fmt.Errorf("catalyst corruption detected: %w", err)
+ }
+
+ solution := make([]byte, 16)
+ copy(solution, material[16:32])
+
+ for cycle := range stirrings {
+ vessel.Encrypt(solution, solution)
+ if cycle%11 == 10 {
+ for dropIdx := range solution {
+ solution[dropIdx] ^= (solution[dropIdx] & 0xF0) >> (dropIdx % 8)
+ }
+ }
+ if cycle%13 == 12 {
+ for dropIdx := range solution {
+ solution[dropIdx] ^= (solution[dropIdx] & 0x0F) << (dropIdx % 8)
+ }
+ }
+ if cycle%17 == 16 {
+ for dropIdx := range solution {
+ solution[dropIdx] ^= 0xFF ^ (1 << (dropIdx % 8))
+ }
+ }
+ }
+
+ return solution, nil
+}
+
+func merge(first, second []byte) []byte {
+ maxVolume := max(len(first), len(second))
+ mixture := make([]byte, maxVolume)
+ fuseInto(mixture, first, second)
+ return mixture
+}
+
+func fuseInto(cauldron, first, second []byte) {
+ for dropIdx := range min(max(len(first), len(second)), len(cauldron)) {
+ var dropA, dropB byte
+ if dropIdx < len(first) {
+ dropA = first[dropIdx]
+ }
+ if dropIdx < len(second) {
+ dropB = second[dropIdx]
+ }
+ cauldron[dropIdx] = dropA ^ dropB
+ }
+}
+
+func infuseWith(baseElixir, additive []byte) {
+ for dropIdx := range min(len(baseElixir), len(additive)) {
+ baseElixir[dropIdx] ^= additive[dropIdx]
+ }
+}
+
+func modifyBase(transmutation []byte, variation uint32) {
+ if variation == 0 {
+ return
+ }
+
+ for essenceIdx := range len(transmutation) * 8 {
+ if (variation>>essenceIdx)&1 == 1 {
+ vialIdx := essenceIdx / 8
+ crystalPos := 7 - (essenceIdx % 8)
+ if vialIdx < len(transmutation) {
+ transmutation[vialIdx] ^= 1 << crystalPos
+ }
+ }
+ }
+}
diff --git a/sdk/sdk.go b/sdk/sdk.go
new file mode 100644
index 0000000..c1427f9
--- /dev/null
+++ b/sdk/sdk.go
@@ -0,0 +1,297 @@
+package sdk
+
+import (
+ "crypto/rand"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+ "golang.org/x/crypto/nacl/box"
+)
+
+var (
+ ErrInvalidRequest = errors.New("invalid request")
+ ErrInvalidConfiguration = errors.New("invalid configuration")
+ ErrClientInternal = errors.New("internal client error")
+
+ // server errors - needs exact match with server response.code
+ ErrBadGateway = errors.New("bad gateway") // temporary - can retry
+ ErrServerInternal = errors.New("internal server error") // temporary - can retry
+ ErrBadRequest = errors.New("bad request") // fatal - don't retry
+ ErrForbidden = errors.New("forbidden") // fatal - don't retry
+ ErrNotFound = errors.New("not found") // fatal - don't retry
+ ErrTooManyRequests = errors.New("rate limit exceeded") // temporary - can retry with default delay
+ ErrTooManyRequestsRPM = errors.New("RPM limit exceeded") // temporary - can retry with RestTime
+ ErrTooManyRequestsRPH = errors.New("RPH limit exceeded") // fatal - too long wait
+ ErrTooManyRequestsRPD = errors.New("RPD limit exceeded") // fatal - too long wait
+ ErrInvalidSignature = errors.New("invalid signature") // fatal - crypto issue
+ ErrReplayAttack = errors.New("replay attack") // fatal - security issue
+
+ // client errors
+ ErrTicketFailed = errors.New("failed to get ticket")
+ ErrPoWFailed = errors.New("proof of work failed")
+ ErrCryptoFailed = errors.New("cryptographic operation failed")
+ ErrRequestFailed = errors.New("request execution failed")
+ ErrMaxRetriesExceeded = errors.New("maximum retry attempts exceeded")
+)
+
+// ServerErrorResponse represents server error response format
+type ServerErrorResponse struct {
+ Status string `json:"status"`
+ Code string `json:"code"`
+}
+
+// isTemporaryError determines if error is temporary and can be retried
+func isTemporaryError(err error) bool {
+ switch err {
+ case ErrBadGateway, ErrServerInternal, ErrTooManyRequests, ErrTooManyRequestsRPM, ErrExperimentTimeout:
+ return true
+ default:
+ return false
+ }
+}
+
+// parseServerError parses server error response and returns appropriate error
+func parseServerError(statusCode int, body []byte) error {
+ if statusCode == 200 {
+ return nil
+ }
+
+ var serverErr ServerErrorResponse
+ if err := json.Unmarshal(body, &serverErr); err != nil {
+ return fmt.Errorf("HTTP %d: %w", statusCode, ErrRequestFailed)
+ }
+
+ switch serverErr.Code {
+ case "BadGateway":
+ return ErrBadGateway
+ case "Internal":
+ return ErrServerInternal
+ case "BadRequest":
+ return ErrBadRequest
+ case "Forbidden":
+ return ErrForbidden
+ case "NotFound":
+ return ErrNotFound
+ case "TooManyRequests":
+ return ErrTooManyRequests
+ case "TooManyRequestsRPM":
+ return ErrTooManyRequestsRPM
+ case "TooManyRequestsRPH":
+ return ErrTooManyRequestsRPH
+ case "TooManyRequestsRPD":
+ return ErrTooManyRequestsRPD
+ default:
+ return fmt.Errorf("%s: %w", serverErr.Code, ErrRequestFailed)
+ }
+}
+
+type Option func(*sdk)
+
+func WithTransport(transport *http.Transport) Option {
+ return func(s *sdk) {
+ if transport != nil {
+ s.transport = transport
+ }
+ }
+}
+
+func WithLogger(logger Logger) Option {
+ return func(s *sdk) {
+ if logger != nil {
+ s.logger = logger
+ }
+ }
+}
+
+func WithClient(name string, version string) Option {
+ return func(s *sdk) {
+ s.clientName = name
+ s.clientVersion = version
+ }
+}
+
+func WithPowTimeout(timeout time.Duration) Option {
+ return func(s *sdk) {
+ s.powTimeout = timeout
+ }
+}
+
+func WithMaxRetries(maxRetries int) Option {
+ return func(s *sdk) {
+ s.maxRetries = maxRetries
+ }
+}
+
+func WithLicenseKey(key string) Option {
+ return func(s *sdk) {
+ info, err := IntrospectLicenseKey(key)
+ if err == nil && info != nil && info.IsValid() {
+ s.licenseKey = decodeLicenseKey(key)
+ s.licenseFP = computeLicenseKeyFP(s.licenseKey)
+ }
+ }
+}
+
+func WithInstallationID(installationID [16]byte) Option {
+ return func(s *sdk) {
+ s.installationID = installationID
+ }
+}
+
+func withServerPublicKey(serverPublicKey *[32]byte) Option {
+ return func(s *sdk) {
+ s.serverPublicKey = serverPublicKey
+ }
+}
+
+type sdk struct {
+ clientName string
+ clientVersion string
+ client *http.Client
+ transport *http.Transport
+ logger Logger
+ powTimeout time.Duration
+ maxRetries int
+ licenseKey [10]byte
+ licenseFP [16]byte
+ installationID [16]byte
+
+ // NaCL keypair for session key encryption
+ clientPublicKey *[32]byte
+ clientPrivateKey *[32]byte
+ serverPublicKey *[32]byte
+}
+
+func defaultSDK() *sdk {
+ installationID := [16]byte(uuid.New())
+
+ return &sdk{
+ clientName: DefaultClientName,
+ clientVersion: DefaultClientVersion,
+ transport: DefaultTransport(),
+ logger: DefaultLogger(),
+ powTimeout: DefaultPowTimeout,
+ maxRetries: DefaultMaxRetries,
+ installationID: installationID,
+ serverPublicKey: getServerPublicKey(),
+ }
+}
+
+func Build(configs []CallConfig, options ...Option) error {
+ sdk := defaultSDK()
+ for _, option := range options {
+ option(sdk)
+ }
+
+ sdk.client = &http.Client{
+ Transport: sdk.transport,
+ }
+
+ var err error
+ if sdk.clientPublicKey, sdk.clientPrivateKey, err = box.GenerateKey(rand.Reader); err != nil {
+ return fmt.Errorf("%w: failed to generate client NaCL keypair: %w", ErrClientInternal, err)
+ }
+ if sdk.clientPublicKey == nil || sdk.clientPrivateKey == nil {
+ return fmt.Errorf("%w: failed to generate client NaCL keypair", ErrClientInternal)
+ }
+
+ for idx, cfg := range configs {
+ if err := sdk.buildCall(cfg); err != nil {
+ format := "failed to build call[%d] '%s': %w: %w"
+ return fmt.Errorf(format, idx, cfg.Name, ErrInvalidConfiguration, err)
+ }
+ }
+
+ return nil
+}
+
+func (s sdk) buildCall(cfg CallConfig) error {
+ if cfg.Host == "" {
+ return fmt.Errorf("host is required")
+ }
+
+ if cfg.Name == "" {
+ return fmt.Errorf("name is required")
+ }
+
+ for _, call := range cfg.Calls {
+ if err := checkCallType(call); err != nil {
+ return err
+ }
+ }
+
+ switch cfg.Method {
+ case CallMethodGET, CallMethodDELETE:
+ if slices.ContainsFunc(cfg.Calls, isCallWithBody) {
+ return fmt.Errorf("call with body is not supported for GET and DELETE methods")
+ }
+ case CallMethodPUT, CallMethodPATCH:
+ if !slices.ContainsFunc(cfg.Calls, isCallWithBody) {
+ return fmt.Errorf("call with body is required for POST, PUT and PATCH methods")
+ }
+ fallthrough
+ case CallMethodPOST:
+ if slices.ContainsFunc(cfg.Calls, isCallWithQuery) {
+ return fmt.Errorf("call with query is not supported for POST, PUT and PATCH methods")
+ }
+ default:
+ return fmt.Errorf("invalid call method: '%s'", cfg.Method)
+ }
+
+ pathGenerator, argsNumber, err := s.parsePath(cfg.Path)
+ if err != nil {
+ return fmt.Errorf("invalid path: '%s': %w", cfg.Path, err)
+ }
+
+ for _, call := range cfg.Calls {
+ if argsNumber > 0 && !isCallWithArgs(call) {
+ return fmt.Errorf("call with position arguments must use variant call type with args")
+ }
+ }
+
+ if err := fillCallFunc(cfg, s, pathGenerator); err != nil {
+ return fmt.Errorf("failed to fill call func: %w", err)
+ }
+
+ return nil
+}
+
+// parsePath parses the path and returns the path template and position arguments number
+func (s sdk) parsePath(p string) (pathGenerator, int, error) {
+ parts := make([]string, 0)
+ names := make([]string, 0)
+ indices := make([]int, 0)
+ for idx, part := range strings.Split(p, "/") {
+ if strings.HasPrefix(part, ":") {
+ indices = append(indices, idx)
+ names = append(names, strings.TrimPrefix(part, ":"))
+ }
+ parts = append(parts, part)
+ }
+
+ return func(args []string) (string, error) {
+ if len(indices) == 0 {
+ return p, nil
+ }
+
+ if len(args) == 0 {
+ return "", fmt.Errorf("no arguments provided: must be %d: %v", len(indices), names)
+ }
+ if len(args) != len(indices) {
+ return "", fmt.Errorf("invalid number of arguments: must be %d: %v", len(indices), names)
+ }
+
+ parts = slices.Clone(parts)
+ for idx, arg := range args {
+ parts[indices[idx]] = arg
+ }
+
+ return strings.Join(parts, "/"), nil
+ }, len(indices), nil
+}
diff --git a/sdk/sdk_test.go b/sdk/sdk_test.go
new file mode 100644
index 0000000..ba34cbe
--- /dev/null
+++ b/sdk/sdk_test.go
@@ -0,0 +1,357 @@
+package sdk
+
+import (
+ "encoding/binary"
+ "fmt"
+ "testing"
+ "time"
+)
+
+var testLicenseKey = [10]byte{5, 214, 101, 133, 220, 47, 241, 139, 53, 124}
+
+func ExampleBuild() {
+ var calls struct {
+ DownloadInstaller CallReqRespWriter
+ SendError CallReqBytesRespBytes
+ }
+ callsConfig := []CallConfig{
+ {
+ Calls: []any{&calls.DownloadInstaller},
+ Host: "localhost",
+ Name: "download-installer",
+ Path: "/api/v1/downloads/installer",
+ Method: CallMethodGET,
+ },
+ {
+ Calls: []any{&calls.SendError},
+ Host: "localhost",
+ Name: "send-error",
+ Path: "/api/v1/support/errors",
+ Method: CallMethodPOST,
+ },
+ }
+ if err := Build(callsConfig); err != nil {
+ panic(fmt.Sprintf("failed to build SDK: %v", err))
+ }
+
+ fmt.Println(calls.DownloadInstaller != nil)
+ fmt.Println(calls.SendError != nil)
+ // Output:
+ // true
+ // true
+}
+
+func TestBuildValidation(t *testing.T) {
+ tests := []struct {
+ config []CallConfig
+ wantErr bool
+ }{
+ {
+ config: []CallConfig{{
+ Calls: []any{new(CallReqRespBytes)},
+ Host: "api.example.com",
+ Name: "test",
+ Path: "/api/v1/test",
+ Method: CallMethodGET,
+ }},
+ wantErr: false,
+ },
+ {
+ config: []CallConfig{{
+ Calls: []any{new(CallReqRespBytes)},
+ Name: "test",
+ Path: "/api/v1/test",
+ Method: CallMethodGET,
+ }},
+ wantErr: true, // missing host
+ },
+ {
+ config: []CallConfig{{
+ Calls: []any{new(CallReqBytesRespBytes)},
+ Host: "api.example.com",
+ Name: "test",
+ Path: "/api/v1/test",
+ Method: CallMethodGET,
+ }},
+ wantErr: true, // body with GET
+ },
+ }
+
+ for _, tt := range tests {
+ err := Build(tt.config)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Build() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ }
+}
+
+func TestServerErrorParsing(t *testing.T) {
+ tests := []struct {
+ statusCode int
+ body []byte
+ wantErr error
+ }{
+ {200, []byte("success"), nil},
+ {502, []byte(`{"code":"BadGateway"}`), ErrBadGateway},
+ {429, []byte(`{"code":"TooManyRequestsRPM"}`), ErrTooManyRequestsRPM},
+ {500, []byte("invalid json"), ErrRequestFailed},
+ }
+
+ for _, tt := range tests {
+ err := parseServerError(tt.statusCode, tt.body)
+ if tt.wantErr == nil {
+ if err != nil {
+ t.Errorf("parseServerError(%d) error = %v, wantErr nil", tt.statusCode, err)
+ }
+ } else {
+ if err == nil {
+ t.Errorf("parseServerError(%d) error = nil, wantErr %v", tt.statusCode, tt.wantErr)
+ }
+ }
+ }
+}
+
+func TestBuildCallValidation(t *testing.T) {
+ s := defaultSDK()
+
+ t.Run("method_body_compatibility", func(t *testing.T) {
+ // GET/DELETE should not have body calls
+ err := s.buildCall(CallConfig{
+ Host: "api.com",
+ Name: "test",
+ Path: "/test",
+ Method: CallMethodGET,
+ Calls: []any{new(CallReqBytesRespBytes)},
+ })
+ if err == nil {
+ t.Error("expected error for GET with body")
+ }
+
+ // POST/PUT/PATCH should not have query calls
+ err = s.buildCall(CallConfig{
+ Host: "api.com",
+ Name: "test",
+ Path: "/test",
+ Method: CallMethodPOST,
+ Calls: []any{new(CallReqQueryRespBytes)},
+ })
+ if err == nil {
+ t.Error("expected error for POST with query")
+ }
+ })
+
+ t.Run("path_args_validation", func(t *testing.T) {
+ // Path with args should use WithArgs call types
+ err := s.buildCall(CallConfig{
+ Host: "api.com",
+ Name: "test",
+ Path: "/users/:id",
+ Method: CallMethodGET,
+ Calls: []any{new(CallReqRespBytes)},
+ })
+ if err == nil {
+ t.Error("expected error for path args without WithArgs call type")
+ }
+
+ // Valid args configuration
+ err = s.buildCall(CallConfig{
+ Host: "api.com",
+ Name: "test",
+ Path: "/users/:id",
+ Method: CallMethodGET,
+ Calls: []any{new(CallReqWithArgsRespBytes)},
+ })
+ if err != nil {
+ t.Errorf("valid args config failed: %v", err)
+ }
+ })
+}
+
+func TestSDKInitialization(t *testing.T) {
+ sdk := defaultSDK()
+
+ if sdk.clientName != DefaultClientName || sdk.clientVersion != DefaultClientVersion {
+ t.Error("default client info incorrect")
+ }
+
+ if sdk.installationID == [16]byte{} {
+ t.Error("installation ID not generated")
+ }
+
+ if sdk.clientPublicKey != nil || sdk.clientPrivateKey != nil {
+ t.Error("NaCL keys should be nil before Build()")
+ }
+
+ // test options
+ WithClient("test", "2.0")(sdk)
+ if sdk.clientName != "test" {
+ t.Errorf("WithClient failed: got %s", sdk.clientName)
+ }
+
+ WithMaxRetries(3)(sdk)
+ if sdk.maxRetries != 3 {
+ t.Errorf("WithMaxRetries failed: got %d", sdk.maxRetries)
+ }
+}
+
+func TestPathTemplates(t *testing.T) {
+ sdk := defaultSDK()
+
+ t.Run("simple_path", func(t *testing.T) {
+ generator, argsCount, err := sdk.parsePath("/api/v1/test")
+ if err != nil {
+ t.Fatalf("parsePath error: %v", err)
+ }
+ if argsCount != 0 {
+ t.Errorf("expected 0 args, got %d", argsCount)
+ }
+
+ result, err := generator(nil)
+ if err != nil {
+ t.Errorf("generator error: %v", err)
+ }
+ if result != "/api/v1/test" {
+ t.Errorf("expected /api/v1/test, got %s", result)
+ }
+ })
+
+ t.Run("single_arg_path", func(t *testing.T) {
+ generator, argsCount, err := sdk.parsePath("/api/v1/users/:id")
+ if err != nil {
+ t.Fatalf("parsePath error: %v", err)
+ }
+ if argsCount != 1 {
+ t.Errorf("expected 1 arg, got %d", argsCount)
+ }
+
+ result, err := generator([]string{"123"})
+ if err != nil {
+ t.Errorf("generator error: %v", err)
+ }
+ if result != "/api/v1/users/123" {
+ t.Errorf("expected /api/v1/users/123, got %s", result)
+ }
+
+ // Test error cases
+ _, err = generator([]string{})
+ if err == nil {
+ t.Error("expected error for missing args")
+ }
+
+ _, err = generator([]string{"123", "456"})
+ if err == nil {
+ t.Error("expected error for too many args")
+ }
+ })
+
+ t.Run("multiple_args_path", func(t *testing.T) {
+ generator, argsCount, err := sdk.parsePath("/api/v1/users/:userId/posts/:postId")
+ if err != nil {
+ t.Fatalf("parsePath error: %v", err)
+ }
+ if argsCount != 2 {
+ t.Errorf("expected 2 args, got %d", argsCount)
+ }
+
+ result, err := generator([]string{"user123", "post456"})
+ if err != nil {
+ t.Errorf("generator error: %v", err)
+ }
+ if result != "/api/v1/users/user123/posts/post456" {
+ t.Errorf("expected /api/v1/users/user123/posts/post456, got %s", result)
+ }
+ })
+
+ t.Run("complex_path", func(t *testing.T) {
+ generator, argsCount, err := sdk.parsePath("/api/:version/users/:id/settings/:key")
+ if err != nil {
+ t.Fatalf("parsePath error: %v", err)
+ }
+ if argsCount != 3 {
+ t.Errorf("expected 3 args, got %d", argsCount)
+ }
+
+ result, err := generator([]string{"v2", "user123", "theme"})
+ if err != nil {
+ t.Errorf("generator error: %v", err)
+ }
+ expected := "/api/v2/users/user123/settings/theme"
+ if result != expected {
+ t.Errorf("expected %s, got %s", expected, result)
+ }
+ })
+}
+
+func TestSDKOptions(t *testing.T) {
+ t.Run("all_options", func(t *testing.T) {
+ sdk := defaultSDK()
+
+ // Test all option functions
+ WithClient("TestApp", "1.2.3")(sdk)
+ if sdk.clientName != "TestApp" || sdk.clientVersion != "1.2.3" {
+ t.Error("WithClient option failed")
+ }
+
+ WithPowTimeout(45 * time.Second)(sdk)
+ if sdk.powTimeout != 45*time.Second {
+ t.Error("WithPowTimeout option failed")
+ }
+
+ WithMaxRetries(5)(sdk)
+ if sdk.maxRetries != 5 {
+ t.Error("WithMaxRetries option failed")
+ }
+
+ WithLicenseKey(encodeLicenseKey(testLicenseKey))(sdk)
+ if sdk.licenseKey != testLicenseKey || sdk.licenseFP != computeLicenseKeyFP(testLicenseKey) {
+ t.Error("WithLicenseKey option failed")
+ }
+
+ testID := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
+ WithInstallationID(testID)(sdk)
+ if sdk.installationID != testID {
+ t.Error("WithInstallationID option failed")
+ }
+
+ transport := DefaultTransport()
+ WithTransport(transport)(sdk)
+ if sdk.transport != transport {
+ t.Error("WithTransport option failed")
+ }
+
+ logger := DefaultLogger()
+ WithLogger(logger)(sdk)
+ if sdk.logger != logger {
+ t.Error("WithLogger option failed")
+ }
+ })
+}
+
+func encodeLicenseKey(key [10]byte) string {
+ alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZ234679"
+ expand := func(bkey [5]byte) [8]byte {
+ result := [8]byte{}
+ for idx := 4; idx >= 0; idx-- {
+ result[4-idx+3] = bkey[idx]
+ }
+ pkey := binary.BigEndian.Uint64(result[:])
+ for idx := range 8 {
+ result[idx] = byte((pkey >> (5 * idx)) & 0x1F)
+ }
+ return result
+ }
+
+ result := [19]byte{}
+ for idx, b := range expand([5]byte(key[0:5])) {
+ result[idx+idx/4] = alphabet[b]
+ }
+ for idx, b := range expand([5]byte(key[5:10])) {
+ result[idx+8+idx/4+2] = alphabet[b]
+ }
+
+ result[4] = '-'
+ result[9] = '-'
+ result[14] = '-'
+
+ return string(result[:])
+}
diff --git a/sdk/testdata/data.json b/sdk/testdata/data.json
new file mode 100644
index 0000000..7b8913e
--- /dev/null
+++ b/sdk/testdata/data.json
@@ -0,0 +1,124 @@
+{
+ "tickets": [
+ {
+ "name": "valid_success",
+ "ticket": "5aG+SXRUmFEg99igmVwtVyomxjNulBoKwtBOKiKmw68/+43USoXL97dZ9sj4UOLivUZ/7yPxFTwBxv59pUzUkABxA3RaAjJAAAoAPA",
+ "request_id": "e0ab0348-1aa9-5457-55e4-79004c7530f7",
+ "signature_nonce": "P/uN1EqFy/e3WfbI+FDi4g",
+ "server_key": "BaG+SXRUmFEg99igmVwtVw",
+ "installation_id": "ca8dc57b-743d-4e5d-9734-372a6ed3f358",
+ "valid": true
+ },
+ {
+ "name": "valid_timeout",
+ "ticket": "okXk0l9FLYdYulcA78gDti3Dr8+8btDZB9HYg8Do1sz9l92tTokrnSsQiT1BlukRQGJfyhhZ3zbEJtQQDD+SvwCRA0tXAixAAAoAPA",
+ "request_id": "3c131185-5715-9b08-8749-3f5462dec462",
+ "signature_nonce": "/ZfdrU6JK50rEIk9QZbpEQ",
+ "server_key": "ix+E0l9FLYdYulcA78gDtg",
+ "installation_id": "11d0be4a-eb7b-4bd1-8098-e7d7a23612ae",
+ "valid": true
+ },
+ {
+ "name": "invalid_corrupted",
+ "ticket": "NZUa7kpTv4U5aHTQAyOVMySL17OMF4llLmzY0rcUMay2FnCqwO0Zm6JLWF6lPEK1XGCFHXarsnPOrMFVg48OMgBmBJF4Ah0gAAoAPA",
+ "request_id": "07c09d5e-b200-4b61-8710-da78d6064e19",
+ "signature_nonce": "",
+ "server_key": "",
+ "installation_id": "57e724dc-d1d9-4a92-ad59-c22b30bedae7",
+ "valid": false
+ }
+ ],
+ "licenses": [
+ {
+ "name": "valid_perpetual",
+ "fp": "zv0FFUSDIJBZb2NTzxIB7g",
+ "key": "FQVL-WCS3-PJ4X-Y2QP",
+ "flags": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false
+ ],
+ "expired_at": "0001-01-01T00:00:00Z",
+ "created_at": "2025-09-16T00:00:00Z",
+ "valid": true
+ },
+ {
+ "name": "valid_expireable",
+ "fp": "JAQgjdgdOW1CI3pm3+wMyA",
+ "key": "NIXH-EMHI-DZS6-2Y6R",
+ "flags": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false
+ ],
+ "expired_at": "2035-01-01T00:00:00Z",
+ "created_at": "2025-09-16T00:00:00Z",
+ "valid": false
+ },
+ {
+ "name": "invalid_expired",
+ "fp": "g4JMek9k8niqC1jTG9I5eg",
+ "key": "NOLR-7WY6-WA67-OHJQ",
+ "flags": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false
+ ],
+ "expired_at": "2025-06-16T00:00:00Z",
+ "created_at": "2025-06-15T00:00:00Z",
+ "valid": true
+ },
+ {
+ "name": "invalid_future_created_at",
+ "fp": "qNPz47dbkG4Sa+WUxXfwxg",
+ "key": "WUB4-9M4H-IW3C-SA94",
+ "flags": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false
+ ],
+ "expired_at": "0001-01-01T00:00:00Z",
+ "created_at": "2035-01-01T00:00:00Z",
+ "valid": false
+ },
+ {
+ "name": "invalid_checksum",
+ "fp": "z5zCRk8ePLXLaPqpLx7GqQ",
+ "key": "IKNG-P26E-9TWK-W7G9",
+ "flags": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false
+ ],
+ "expired_at": "0001-01-01T00:00:00Z",
+ "created_at": "2025-09-16T00:00:00Z",
+ "valid": false
+ }
+ ],
+ "response_data": [
+ {
+ "name": "test_json",
+ "data": "{\"status\":\"success\",\"message\":\"test completed\"}"
+ }
+ ]
+}
diff --git a/sdk/transport.go b/sdk/transport.go
new file mode 100644
index 0000000..2c90646
--- /dev/null
+++ b/sdk/transport.go
@@ -0,0 +1,383 @@
+package sdk
+
+import (
+ "context"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/binary"
+ "fmt"
+ "hash/crc32"
+ "io"
+ "net"
+ "net/http"
+ "time"
+
+ "github.com/google/uuid"
+ "golang.org/x/crypto/nacl/box"
+ "golang.org/x/net/http2"
+)
+
+const (
+ defaultScheme = "https"
+ defaultTicketPath = "/api/v1/ticket/"
+ sdkProtocolVersion = 1
+)
+
+const (
+ headerXInstallationID = "X-Installation-ID"
+ headerXRequestID = "X-Request-ID"
+ headerXRequestKey = "X-Request-Key"
+ headerXRequestSign = "X-Request-Sign"
+ headerXLicenseKey = "X-License-Key"
+)
+
+func DefaultTransport() *http.Transport {
+ // create dialer with optimal settings
+ dialer := &net.Dialer{
+ Timeout: 5 * time.Second, // connection timeout
+ KeepAlive: 30 * time.Second, // keep-alive probe interval
+ }
+
+ // create transport with production-optimized settings for 2-minute requests
+ transport := &http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ DialContext: dialer.DialContext,
+
+ // connection pooling settings
+ MaxIdleConns: 50, // total max idle connections across all hosts
+ MaxIdleConnsPerHost: 10, // max idle connections per host
+ MaxConnsPerHost: 300, // max active connections per host
+ IdleConnTimeout: 90 * time.Second, // how long idle connections stay open
+
+ // timeout settings for backend API processing
+ TLSHandshakeTimeout: 10 * time.Second, // TLS handshake timeout
+ ResponseHeaderTimeout: 3 * time.Minute, // time to wait for response headers (backend processing)
+ ExpectContinueTimeout: 0, // disable expect continue timeout
+
+ // HTTP/2 and performance settings
+ ForceAttemptHTTP2: true, // enable HTTP/2
+ DisableCompression: false, // keep compression enabled
+ DisableKeepAlives: false, // keep-alive is essential
+ }
+
+ // enable HTTP/2 support for better performance
+ err := http2.ConfigureTransport(transport)
+ if err != nil {
+ // HTTP/2 configuration failed, but HTTP/1.1 will still work
+ // in production, you might want to log this
+ }
+
+ return transport
+}
+
+// ticketData contains parsed ticket information and session keys
+type ticketData struct {
+ Key [16]byte
+ IV [16]byte
+ RequestID uuid.UUID
+ Nonce [16]byte
+ AllowedRPM uint16
+ WaitDelay uint16
+}
+
+func (c *callFunc) getTicket(cctx *callContext) (string, error) {
+ requestID := uuid.New()
+
+ sessionKey := [16]byte{}
+ sessionIV := [16]byte{}
+ _, err := rand.Read(sessionKey[:])
+ if err != nil {
+ return "", fmt.Errorf("failed to generate session key: %w", err)
+ }
+ _, err = rand.Read(sessionIV[:])
+ if err != nil {
+ return "", fmt.Errorf("failed to generate session IV: %w", err)
+ }
+
+ requestKeyHeader, err := c.createRequestKeyHeader(sessionKey, sessionIV, 0)
+ if err != nil {
+ return "", fmt.Errorf("failed to create request key header: %w", err)
+ }
+
+ httpReq, err := http.NewRequestWithContext(cctx, "GET", cctx.reqTicketURL.String(), nil)
+ if err != nil {
+ return "", fmt.Errorf("failed to create ticket request: %w", err)
+ }
+
+ httpReq.Header.Set(headerXInstallationID, uuid.UUID(c.sdk.installationID).String())
+ httpReq.Header.Set(headerXRequestID, requestID.String())
+ httpReq.Header.Set(headerXRequestKey, requestKeyHeader)
+ httpReq.Header.Set("User-Agent", c.createUserAgentHeader())
+ if c.sdk.licenseKey != emptyLicenseKey && c.sdk.licenseFP != emptyLicenseFP {
+ licenseKeyHeader, err := c.createLicenseKeyHeader(sessionKey, sessionIV)
+ if err != nil {
+ return "", fmt.Errorf("failed to create license key header: %w", err)
+ }
+ httpReq.Header.Set(headerXLicenseKey, licenseKeyHeader)
+ }
+
+ resp, err := c.sdk.client.Do(httpReq)
+ if err != nil {
+ return "", fmt.Errorf("ticket request failed: %w", err)
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", fmt.Errorf("failed to read ticket response: %w", err)
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return "", parseServerError(resp.StatusCode, body)
+ }
+
+ decryptedBody, err := DecryptBytes(body, sessionKey, sessionIV)
+ if err != nil {
+ return "", fmt.Errorf("failed to decrypt ticket response: %w", err)
+ }
+
+ return string(decryptedBody), nil
+}
+
+func (c *callFunc) solvePoW(cctx *callContext, ticket string) (*ticketData, error) {
+ ctx, cancel := context.WithTimeout(cctx, c.sdk.powTimeout)
+ defer cancel()
+
+ result, err := transmute(ctx, ticket, c.sdk.installationID[:])
+ cctx.restWaitTime = time.Duration(result.Recipe.RestTime) * time.Second
+ if err != nil {
+ return nil, fmt.Errorf("PoW solving failed: %w", err)
+ }
+
+ xor(result.Catalyst[0:16], c.sdk.installationID[0:16])
+
+ ticketData := &ticketData{
+ Key: [16]byte(result.Key),
+ IV: generateIV(c.sdk.installationID),
+ RequestID: uuid.UUID(result.Catalyst),
+ Nonce: [16]byte(result.Signature),
+ AllowedRPM: result.Recipe.Capacity,
+ WaitDelay: result.Recipe.RestTime,
+ }
+
+ return ticketData, nil
+}
+
+func (c *callFunc) createSignature(ticketData *ticketData, contentLength int64) (string, error) {
+ // nonce[16] + randPadding[11] + version[1] + timestamp[8] + contentLength[8] + crc32[4]
+ signData := make([]byte, 48)
+ copy(signData[0:16], ticketData.Nonce[0:16])
+ xor(signData[0:16], c.sdk.installationID[0:16])
+
+ _, err := rand.Read(signData[16:27])
+ if err != nil {
+ return "", fmt.Errorf("failed to generate random padding: %w", err)
+ }
+ signData[27] = sdkProtocolVersion
+
+ binary.BigEndian.PutUint64(signData[28:36], uint64(time.Now().UTC().UnixMicro()))
+ binary.BigEndian.PutUint64(signData[36:44], uint64(contentLength))
+
+ hash := crc32.ChecksumIEEE(signData[16:44])
+ binary.BigEndian.PutUint32(signData[44:48], hash)
+
+ sc, err := aes.NewCipher(ticketData.Key[:])
+ if err != nil {
+ return "", fmt.Errorf("failed to create AES cipher: %w", err)
+ }
+
+ scb := cipher.NewCBCEncrypter(sc, ticketData.IV[:])
+ scb.CryptBlocks(signData[16:48], signData[16:48])
+
+ return base64.StdEncoding.WithPadding(base64.NoPadding).EncodeToString(signData), nil
+}
+
+func (c *callFunc) createRequestKeyHeader(sessionKey, sessionIV [16]byte, contentLength int64) (string, error) {
+ var clientPublicKey [32]byte
+ copy(clientPublicKey[0:32], c.sdk.clientPublicKey[0:32])
+ xor(clientPublicKey[0:16], c.sdk.installationID[0:16])
+ xor(clientPublicKey[16:32], c.sdk.installationID[0:16])
+
+ var sharedKey [32]byte
+ box.Precompute(&sharedKey, c.sdk.serverPublicKey, c.sdk.clientPrivateKey)
+
+ // key[16] + iv[16] + timestamp[8] + contentLength[8]
+ payload := make([]byte, 48)
+ copy(payload[0:16], sessionKey[:])
+ copy(payload[16:32], sessionIV[:])
+ binary.BigEndian.PutUint64(payload[32:40], uint64(time.Now().UTC().UnixMicro()))
+ binary.BigEndian.PutUint64(payload[40:48], uint64(contentLength))
+
+ var nonce [24]byte
+ if _, err := rand.Read(nonce[:]); err != nil {
+ return "", fmt.Errorf("failed to generate nonce: %w", err)
+ }
+
+ encryptedPayload := box.SealAfterPrecomputation(nil, payload, &nonce, &sharedKey)
+
+ // clientPublicKey[32] + nonce[24] + encryptedPayload[64]
+ headerData := make([]byte, 120)
+ copy(headerData[0:32], clientPublicKey[:])
+ copy(headerData[32:56], nonce[:])
+ copy(headerData[56:120], encryptedPayload)
+
+ return base64.StdEncoding.WithPadding(base64.NoPadding).EncodeToString(headerData), nil
+}
+
+func (c *callFunc) createLicenseKeyHeader(sessionKey, sessionIV [16]byte) (string, error) {
+ payload := make([]byte, 32)
+ copy(payload[6:16], c.sdk.licenseKey[0:10])
+ copy(payload[16:32], c.sdk.licenseFP[0:16])
+
+ if _, err := rand.Read(payload[0:6]); err != nil {
+ return "", fmt.Errorf("failed to generate random padding: %w", err)
+ }
+
+ sc, err := aes.NewCipher(sessionKey[:])
+ if err != nil {
+ return "", fmt.Errorf("failed to create AES cipher: %w", err)
+ }
+
+ scb := cipher.NewCBCEncrypter(sc, sessionIV[:])
+ scb.CryptBlocks(payload[0:16], payload[0:16])
+ xor(payload[16:32], payload[0:16])
+
+ return base64.StdEncoding.WithPadding(base64.NoPadding).EncodeToString(payload), nil
+}
+
+func (c *callFunc) createUserAgentHeader() string {
+ userAgent := DefaultClientName + "/" + DefaultClientVersion
+ if c.sdk.clientName != DefaultClientName || c.sdk.clientVersion != DefaultClientVersion {
+ userAgent = c.sdk.clientName + "/" + c.sdk.clientVersion + " " + userAgent
+ }
+
+ return userAgent
+}
+
+// invokeRequest performs a complete PoW-protected request
+func (c *callFunc) invokeRequest(cctx *callContext) error {
+ // step 1: get PoW ticket
+ ticket, err := c.getTicket(cctx)
+ if err != nil {
+ return fmt.Errorf("failed to get ticket: %w", err)
+ }
+
+ // step 2: solve PoW challenge
+ ticketData, err := c.solvePoW(cctx, ticket)
+ if err != nil {
+ return fmt.Errorf("failed to solve PoW: %w", err)
+ }
+
+ // step 3: prepare encrypted request
+ var reqBody io.ReadCloser
+ if cctx.reqBodyReader != nil && cctx.reqBodyLength > 0 {
+ reqBody, err = EncryptStream(cctx.reqBodyReader, ticketData.Key, ticketData.IV)
+ if err != nil {
+ return fmt.Errorf("failed to encrypt request body: %w", err)
+ }
+ }
+
+ // step 4: create signature
+ signature, err := c.createSignature(ticketData, cctx.reqBodyLength)
+ if err != nil {
+ return fmt.Errorf("failed to create signature: %w", err)
+ }
+
+ // step 5: make HTTP target request
+ httpReq, err := http.NewRequestWithContext(cctx, cctx.reqMethod, cctx.reqCallURL.String(), reqBody)
+ if err != nil {
+ return fmt.Errorf("failed to create request: %w", err)
+ }
+
+ httpReq.Header.Set(headerXInstallationID, uuid.UUID(c.sdk.installationID).String())
+ httpReq.Header.Set(headerXRequestID, ticketData.RequestID.String())
+ httpReq.Header.Set(headerXRequestSign, signature)
+ httpReq.Header.Set("User-Agent", c.createUserAgentHeader())
+ if cctx.reqBodyReader != nil && cctx.reqBodyLength > 0 {
+ httpReq.Header.Set("Content-Type", "application/json")
+ }
+ if c.sdk.licenseKey != emptyLicenseKey && c.sdk.licenseFP != emptyLicenseFP {
+ licenseKeyHeader, err := c.createLicenseKeyHeader(ticketData.Key, ticketData.IV)
+ if err != nil {
+ return fmt.Errorf("failed to create license key header: %w", err)
+ }
+ httpReq.Header.Set(headerXLicenseKey, licenseKeyHeader)
+ }
+
+ resp, err := c.sdk.client.Do(httpReq)
+ if err != nil {
+ return fmt.Errorf("HTTP request failed: %w: %w", ErrClientInternal, err)
+ }
+ if resp == nil {
+ return fmt.Errorf("%w: unexpected response value", ErrClientInternal)
+ }
+
+ cctx.respStatusCode = resp.StatusCode
+ if cctx.respStatusCode == http.StatusOK {
+ // response body should be closed after decryption
+ if cctx.respBodyWriter != nil {
+ err = DecryptProxy(resp.Body, cctx.respBodyWriter, ticketData.Key, ticketData.IV)
+ } else {
+ cctx.respBodyReader, err = DecryptStream(resp.Body, ticketData.Key, ticketData.IV)
+ }
+ if err != nil {
+ return fmt.Errorf("failed to decrypt response body: %w", err)
+ }
+
+ return nil
+ }
+
+ defer resp.Body.Close()
+
+ responseBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return fmt.Errorf("failed to read response: %w", err)
+ }
+
+ return parseServerError(cctx.respStatusCode, responseBody)
+}
+
+func getServerPublicKey() *[32]byte {
+ rawPublic := [40]byte{
+ 0x8C, 0x00, 0x4F, 0xE0, 0xA4, 0xA5, 0x2C, 0x02,
+ 0xAD, 0xDC, 0x66, 0x4C, 0x52, 0x51, 0xA2, 0xC1,
+ 0x98, 0x7C, 0xF3, 0x7F, 0x7C, 0x04, 0x60, 0x44,
+ 0x73, 0x02, 0x2F, 0x89, 0x45, 0x2F, 0xCA, 0x06,
+ 0xEA, 0x9A, 0xFF, 0x68, 0x03, 0x40, 0x6B, 0x60,
+ }
+
+ result := [40]byte{rawPublic[0]>>4 | rawPublic[0]<<4}
+ for i := 1; i < 40; i++ {
+ result[i] = result[i-1] ^ rawPublic[i-1] ^ rawPublic[i]
+ }
+
+ key := [32]byte(result[result[37]:result[39]])
+ return &key
+}
+
+func generateIV(init [16]byte) [16]byte {
+ messSha256 := func(data [32]byte) [32]byte {
+ result := data
+ for range 7 {
+ result = sha256.Sum256(result[:])
+ }
+ return result
+ }
+
+ doubled := [32]byte{}
+ copy(doubled[0:16], init[:])
+ copy(doubled[16:32], init[:])
+ hash := messSha256(doubled)
+ xor(hash[0:16], hash[16:32])
+
+ return [16]byte(hash[0:16])
+}
+
+func xor(dst []byte, op []byte) {
+ for i := range min(len(dst), len(op)) {
+ dst[i] ^= op[i]
+ }
+}
diff --git a/sdk/transport_test.go b/sdk/transport_test.go
new file mode 100644
index 0000000..d1bf8df
--- /dev/null
+++ b/sdk/transport_test.go
@@ -0,0 +1,892 @@
+package sdk
+
+import (
+ "bytes"
+ "context"
+ "crypto/tls"
+ "errors"
+ "io"
+ "net/http"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+func TestMockServerBasic(t *testing.T) {
+ mockSrv := newMockServer()
+ server := mockSrv.createServer()
+ defer server.Close()
+
+ // test basic header validation
+ req, _ := http.NewRequest("GET", server.URL+defaultTicketPath+"test", nil)
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatalf("request failed: %v", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusBadRequest {
+ t.Errorf("expected 400 for missing headers, got %d", resp.StatusCode)
+ }
+
+ // test with proper basic headers
+ req.Header.Set(headerXInstallationID, uuid.New().String())
+ req.Header.Set(headerXRequestID, uuid.New().String())
+
+ resp, err = http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatalf("request with headers failed: %v", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusBadRequest {
+ t.Errorf("expected 400 for missing X-Request-Key, got %d", resp.StatusCode)
+ }
+}
+
+func TestGetTicketOnly(t *testing.T) {
+ mockSrv := newMockServer()
+ server := mockSrv.createServer()
+ defer server.Close()
+
+ serverPublicKey := mockSrv.getPublicKey()
+
+ err := Build([]CallConfig{}, withServerPublicKey(serverPublicKey))
+ if err != nil {
+ t.Fatalf("Build failed: %v", err)
+ }
+
+ s := newTestSDK()
+ s.serverPublicKey = serverPublicKey
+
+ cfn := &callFunc{sdk: s}
+
+ ctx := context.Background()
+ cctx := &callContext{Context: ctx}
+ cctx.reqTicketURL.Scheme = "http"
+ cctx.reqTicketURL.Host = strings.TrimPrefix(server.URL, "http://")
+ cctx.reqTicketURL.Path = defaultTicketPath + "valid_success"
+
+ ticket, err := cfn.getTicket(cctx)
+ if err != nil {
+ t.Errorf("getTicket failed: %v", err)
+ return
+ }
+
+ if ticket == "" {
+ t.Error("getTicket returned empty ticket")
+ return
+ }
+
+ testTicket, found := mockSrv.ticketsByName["valid_success"]
+ if !found || testTicket.Ticket != ticket {
+ t.Error("returned ticket does not match test data")
+ }
+}
+
+func TestTicketSuccessScenario(t *testing.T) {
+ mockSrv := newMockServer()
+
+ successTicket, exists := mockSrv.ticketsByName["valid_success"]
+ if !exists {
+ t.Fatal("valid_success ticket not found in test data")
+ }
+
+ installationID, err := uuid.Parse(successTicket.InstallationID)
+ if err != nil {
+ t.Fatalf("invalid InstallationID: %v", err)
+ }
+
+ // test PoW solving directly without full protocol encryption complexity
+ s := newTestSDK()
+ s.installationID = [16]byte(installationID)
+ s.powTimeout = 1 * time.Second
+
+ cfn := &callFunc{sdk: s}
+ ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+ defer cancel()
+
+ cctx := &callContext{Context: ctx}
+
+ // test PoW solving (should complete quickly)
+ ticketData, err := cfn.solvePoW(cctx, successTicket.Ticket)
+ if err != nil {
+ t.Errorf("Fast PoW solving failed: %v", err)
+ return
+ }
+
+ // validate ticket data
+ if ticketData.Key != successTicket.key {
+ t.Error("Fast PoW solving returned invalid key")
+ return
+ }
+ if ticketData.RequestID.String() != successTicket.RequestID {
+ t.Error("Fast PoW solving returned invalid RequestID")
+ return
+ }
+ if ticketData.Nonce != successTicket.nonce {
+ t.Error("Fast PoW solving returned invalid Nonce")
+ return
+ }
+
+ t.Log("Success scenario: Fast PoW solved successfully within 1s timeout")
+}
+
+func TestTicketTimeoutScenario(t *testing.T) {
+ mockSrv := newMockServer()
+ server := mockSrv.createTLSServer()
+ defer server.Close()
+
+ timeoutTicket, exists := mockSrv.ticketsByName["valid_timeout"]
+ if !exists {
+ t.Fatal("valid_timeout ticket not found in test data")
+ }
+
+ installationID, err := uuid.Parse(timeoutTicket.InstallationID)
+ if err != nil {
+ t.Fatalf("invalid InstallationID: %v", err)
+ }
+
+ var testCall CallReqRespBytes
+ configs := []CallConfig{{
+ Calls: []any{&testCall},
+ Host: strings.TrimPrefix(server.URL, "https://"),
+ Name: "valid_timeout",
+ Path: "/api/v1/test",
+ Method: CallMethodGET,
+ }}
+
+ transport := DefaultTransport()
+ transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+ err = Build(configs,
+ withServerPublicKey(mockSrv.getPublicKey()),
+ WithInstallationID([16]byte(installationID)),
+ WithPowTimeout(1*time.Second),
+ WithTransport(transport))
+ if err != nil {
+ t.Fatalf("Build failed: %v", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+ defer cancel()
+
+ if _, err = testCall(ctx); err == nil {
+ t.Error("timeout scenario should have failed")
+ return
+ }
+
+ if !errors.Is(err, ErrExperimentTimeout) {
+ t.Errorf("expected ErrExperimentTimeout, got: %v", err)
+ return
+ }
+
+ t.Log("Timeout scenario completed as expected")
+}
+
+func TestSimpleResponseData(t *testing.T) {
+ mockSrv := newMockServer()
+ server := mockSrv.createTLSServer()
+ defer server.Close()
+
+ testTicket, exists := mockSrv.ticketsByName["valid_success"]
+ if !exists {
+ t.Fatal("valid_success ticket not found in test data")
+ }
+
+ installationID, err := uuid.Parse(testTicket.InstallationID)
+ if err != nil {
+ t.Fatalf("invalid InstallationID: %v", err)
+ }
+
+ if len(mockSrv.testData.ResponseData) == 0 {
+ t.Skip("no response data found in test data")
+ }
+
+ responseData := mockSrv.testData.ResponseData[0]
+
+ var testCall CallReqRespBytes
+ configs := []CallConfig{{
+ Calls: []any{&testCall},
+ Host: strings.TrimPrefix(server.URL, "https://"),
+ Name: "valid_success",
+ Path: "/api/v1/call/" + responseData.Name,
+ Method: CallMethodGET,
+ }}
+
+ transport := DefaultTransport()
+ transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+ err = Build(configs,
+ withServerPublicKey(mockSrv.getPublicKey()),
+ WithInstallationID([16]byte(installationID)),
+ WithPowTimeout(1*time.Second),
+ WithTransport(transport))
+ if err != nil {
+ t.Fatalf("Build failed: %v", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+ defer cancel()
+
+ data, err := testCall(ctx)
+ if err != nil {
+ t.Error("response data scenario should have succeeded")
+ return
+ }
+
+ if string(data) != responseData.Data {
+ t.Errorf("response data mismatch: got %s, want %s", string(data), responseData.Data)
+ return
+ }
+
+ t.Log("Response data scenario completed as expected")
+}
+
+func TestProtocolSecurity(t *testing.T) {
+ t.Run("https_enforcement", func(t *testing.T) {
+ mockSrv := newMockServer()
+ server := mockSrv.createServer()
+ defer server.Close()
+
+ var testCall CallReqRespBytes
+ configs := []CallConfig{{
+ Calls: []any{&testCall},
+ Host: strings.TrimPrefix(server.URL, "http://"),
+ Name: "valid_success",
+ Path: "/api/v1/test",
+ Method: CallMethodGET,
+ }}
+
+ err := Build(configs, withServerPublicKey(mockSrv.getPublicKey()))
+ if err != nil {
+ t.Fatalf("Build failed: %v", err)
+ }
+
+ _, err = testCall(context.Background())
+ if err == nil || !strings.Contains(err.Error(), "HTTP response to HTTPS client") {
+ t.Error("protocol should enforce HTTPS")
+ }
+ })
+
+ t.Run("error_parsing", func(t *testing.T) {
+ tests := []struct {
+ errorJSON string
+ wantError error
+ }{
+ {`{"code":"TooManyRequestsRPM"}`, ErrTooManyRequestsRPM},
+ {`{"code":"BadGateway"}`, ErrBadGateway},
+ {`{"code":"Forbidden"}`, ErrForbidden},
+ }
+
+ for _, tt := range tests {
+ err := parseServerError(429, []byte(tt.errorJSON))
+ if err != tt.wantError {
+ t.Errorf("parseServerError() = %v, want %v", err, tt.wantError)
+ }
+ }
+ })
+}
+
+func TestProtocolUtilities(t *testing.T) {
+ t.Run("server_public_key", func(t *testing.T) {
+ key := getServerPublicKey()
+ if key == nil || *key == [32]byte{} {
+ t.Error("server public key invalid")
+ }
+ })
+
+ t.Run("generate_iv", func(t *testing.T) {
+ input := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ iv1 := generateIV(input)
+ iv2 := generateIV(input)
+
+ if iv1 != iv2 {
+ t.Error("generateIV should be deterministic")
+ }
+ })
+
+ t.Run("xor", func(t *testing.T) {
+ dst := []byte{0xFF, 0x00, 0xFF, 0x00}
+ op := []byte{0x0F, 0xF0, 0x0F, 0xF0}
+ expected := []byte{0xF0, 0xF0, 0xF0, 0xF0}
+
+ xor(dst, op)
+
+ for i, b := range dst {
+ if b != expected[i] {
+ t.Errorf("xor result[%d] = %02x, want %02x", i, b, expected[i])
+ }
+ }
+ })
+}
+
+// testCallSuite provides comprehensive testing for all call function types
+type testCallSuite struct {
+ // Basic patterns
+ CallBytes CallReqRespBytes
+ CallReader CallReqRespReader
+ CallWriter CallReqRespWriter
+
+ // Query patterns
+ CallQueryBytes CallReqQueryRespBytes
+ CallQueryReader CallReqQueryRespReader
+ CallQueryWriter CallReqQueryRespWriter
+
+ // Args patterns
+ CallArgsBytes CallReqWithArgsRespBytes
+ CallArgsReader CallReqWithArgsRespReader
+ CallArgsWriter CallReqWithArgsRespWriter
+
+ // Query + Args patterns
+ CallQueryArgsBytes CallReqQueryWithArgsRespBytes
+ CallQueryArgsReader CallReqQueryWithArgsRespReader
+ CallQueryArgsWriter CallReqQueryWithArgsRespWriter
+
+ // Body patterns
+ CallBodyBytes CallReqBytesRespBytes
+ CallBodyReader CallReqBytesRespReader
+ CallBodyWriter CallReqBytesRespWriter
+
+ // Reader body patterns
+ CallReaderBytes CallReqReaderRespBytes
+ CallReaderReader CallReqReaderRespReader
+ CallReaderWriter CallReqReaderRespWriter
+
+ // Body + Args patterns
+ CallBodyArgsBytes CallReqBytesWithArgsRespBytes
+ CallBodyArgsReader CallReqBytesWithArgsRespReader
+ CallBodyArgsWriter CallReqBytesWithArgsRespWriter
+
+ // Reader + Args patterns
+ CallReaderArgsBytes CallReqReaderWithArgsRespBytes
+ CallReaderArgsReader CallReqReaderWithArgsRespReader
+ CallReaderArgsWriter CallReqReaderWithArgsRespWriter
+}
+
+func (s *testCallSuite) getCallConfigs(host, basePath string) []CallConfig {
+ // All endpoints use the same valid ticket name to ensure they can get tickets
+ ticketName := "valid_success"
+ configs := []CallConfig{
+ // Basic patterns
+ {Calls: []any{&s.CallBytes}, Host: host, Name: ticketName, Path: basePath + "/basic", Method: CallMethodGET},
+ {Calls: []any{&s.CallReader}, Host: host, Name: ticketName, Path: basePath + "/basic", Method: CallMethodGET},
+ {Calls: []any{&s.CallWriter}, Host: host, Name: ticketName, Path: basePath + "/basic", Method: CallMethodGET},
+
+ // Query patterns
+ {Calls: []any{&s.CallQueryBytes}, Host: host, Name: ticketName, Path: basePath + "/query", Method: CallMethodGET},
+ {Calls: []any{&s.CallQueryReader}, Host: host, Name: ticketName, Path: basePath + "/query", Method: CallMethodGET},
+ {Calls: []any{&s.CallQueryWriter}, Host: host, Name: ticketName, Path: basePath + "/query", Method: CallMethodGET},
+
+ // Args patterns
+ {Calls: []any{&s.CallArgsBytes}, Host: host, Name: ticketName, Path: basePath + "/args/:id", Method: CallMethodGET},
+ {Calls: []any{&s.CallArgsReader}, Host: host, Name: ticketName, Path: basePath + "/args/:id", Method: CallMethodGET},
+ {Calls: []any{&s.CallArgsWriter}, Host: host, Name: ticketName, Path: basePath + "/args/:id", Method: CallMethodGET},
+
+ // Query + Args patterns
+ {Calls: []any{&s.CallQueryArgsBytes}, Host: host, Name: ticketName, Path: basePath + "/query/:id", Method: CallMethodGET},
+ {Calls: []any{&s.CallQueryArgsReader}, Host: host, Name: ticketName, Path: basePath + "/query/:id", Method: CallMethodGET},
+ {Calls: []any{&s.CallQueryArgsWriter}, Host: host, Name: ticketName, Path: basePath + "/query/:id", Method: CallMethodGET},
+
+ // Body patterns
+ {Calls: []any{&s.CallBodyBytes}, Host: host, Name: ticketName, Path: basePath + "/body", Method: CallMethodPOST},
+ {Calls: []any{&s.CallBodyReader}, Host: host, Name: ticketName, Path: basePath + "/body", Method: CallMethodPOST},
+ {Calls: []any{&s.CallBodyWriter}, Host: host, Name: ticketName, Path: basePath + "/body", Method: CallMethodPOST},
+
+ // Reader body patterns
+ {Calls: []any{&s.CallReaderBytes}, Host: host, Name: ticketName, Path: basePath + "/reader", Method: CallMethodPOST},
+ {Calls: []any{&s.CallReaderReader}, Host: host, Name: ticketName, Path: basePath + "/reader", Method: CallMethodPOST},
+ {Calls: []any{&s.CallReaderWriter}, Host: host, Name: ticketName, Path: basePath + "/reader", Method: CallMethodPOST},
+
+ // Body + Args patterns
+ {Calls: []any{&s.CallBodyArgsBytes}, Host: host, Name: ticketName, Path: basePath + "/body/:id", Method: CallMethodPOST},
+ {Calls: []any{&s.CallBodyArgsReader}, Host: host, Name: ticketName, Path: basePath + "/body/:id", Method: CallMethodPOST},
+ {Calls: []any{&s.CallBodyArgsWriter}, Host: host, Name: ticketName, Path: basePath + "/body/:id", Method: CallMethodPOST},
+
+ // Reader + Args patterns
+ {Calls: []any{&s.CallReaderArgsBytes}, Host: host, Name: ticketName, Path: basePath + "/reader/:id", Method: CallMethodPOST},
+ {Calls: []any{&s.CallReaderArgsReader}, Host: host, Name: ticketName, Path: basePath + "/reader/:id", Method: CallMethodPOST},
+ {Calls: []any{&s.CallReaderArgsWriter}, Host: host, Name: ticketName, Path: basePath + "/reader/:id", Method: CallMethodPOST},
+ }
+
+ return configs
+}
+
+func (s *testCallSuite) testCalls(ctx context.Context, expectedData string) error {
+ testData := []byte("test request body")
+
+ // Test basic patterns
+ if data, err := s.CallBytes(ctx); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallBytes: unexpected response")
+ }
+
+ if reader, err := s.CallReader(ctx); err != nil {
+ return err
+ } else {
+ defer reader.Close()
+ if data, err := io.ReadAll(reader); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallReader: unexpected response")
+ }
+ }
+
+ var buf bytes.Buffer
+ if err := s.CallWriter(ctx, &buf); err != nil {
+ return err
+ } else if buf.String() != expectedData {
+ return errors.New("CallWriter: unexpected response")
+ }
+
+ // Test query patterns
+ query := map[string]string{"limit": "10"}
+ if data, err := s.CallQueryBytes(ctx, query); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallQueryBytes: unexpected response")
+ }
+
+ if reader, err := s.CallQueryReader(ctx, query); err != nil {
+ return err
+ } else {
+ defer reader.Close()
+ if data, err := io.ReadAll(reader); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallQueryReader: unexpected response")
+ }
+ }
+
+ buf.Reset()
+ if err := s.CallQueryWriter(ctx, query, &buf); err != nil {
+ return err
+ } else if buf.String() != expectedData {
+ return errors.New("CallQueryWriter: unexpected response")
+ }
+
+ // Test args patterns
+ args := []string{"123"}
+ if data, err := s.CallArgsBytes(ctx, args); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallArgsBytes: unexpected response")
+ }
+
+ if reader, err := s.CallArgsReader(ctx, args); err != nil {
+ return err
+ } else {
+ defer reader.Close()
+ if data, err := io.ReadAll(reader); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallArgsReader: unexpected response")
+ }
+ }
+
+ buf.Reset()
+ if err := s.CallArgsWriter(ctx, args, &buf); err != nil {
+ return err
+ } else if buf.String() != expectedData {
+ return errors.New("CallArgsWriter: unexpected response")
+ }
+
+ // Test query + args patterns
+ if data, err := s.CallQueryArgsBytes(ctx, args, query); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallQueryArgsBytes: unexpected response")
+ }
+
+ if reader, err := s.CallQueryArgsReader(ctx, args, query); err != nil {
+ return err
+ } else {
+ defer reader.Close()
+ if data, err := io.ReadAll(reader); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallQueryArgsReader: unexpected response")
+ }
+ }
+
+ buf.Reset()
+ if err := s.CallQueryArgsWriter(ctx, args, query, &buf); err != nil {
+ return err
+ } else if buf.String() != expectedData {
+ return errors.New("CallQueryArgsWriter: unexpected response")
+ }
+
+ // Test body patterns
+ if data, err := s.CallBodyBytes(ctx, testData); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallBodyBytes: unexpected response")
+ }
+
+ if reader, err := s.CallBodyReader(ctx, testData); err != nil {
+ return err
+ } else {
+ defer reader.Close()
+ if data, err := io.ReadAll(reader); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallBodyReader: unexpected response")
+ }
+ }
+
+ buf.Reset()
+ if err := s.CallBodyWriter(ctx, testData, &buf); err != nil {
+ return err
+ } else if buf.String() != expectedData {
+ return errors.New("CallBodyWriter: unexpected response")
+ }
+
+ // Test reader body patterns
+ bodyReader := bytes.NewReader(testData)
+ if data, err := s.CallReaderBytes(ctx, bodyReader, int64(len(testData))); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallReaderBytes: unexpected response")
+ }
+
+ bodyReader = bytes.NewReader(testData)
+ if reader, err := s.CallReaderReader(ctx, bodyReader, int64(len(testData))); err != nil {
+ return err
+ } else {
+ defer reader.Close()
+ if data, err := io.ReadAll(reader); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallReaderReader: unexpected response")
+ }
+ }
+
+ bodyReader = bytes.NewReader(testData)
+ buf.Reset()
+ if err := s.CallReaderWriter(ctx, bodyReader, int64(len(testData)), &buf); err != nil {
+ return err
+ } else if buf.String() != expectedData {
+ return errors.New("CallReaderWriter: unexpected response")
+ }
+
+ // Test body + args patterns
+ if data, err := s.CallBodyArgsBytes(ctx, args, testData); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallBodyArgsBytes: unexpected response")
+ }
+
+ if reader, err := s.CallBodyArgsReader(ctx, args, testData); err != nil {
+ return err
+ } else {
+ defer reader.Close()
+ if data, err := io.ReadAll(reader); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallBodyArgsReader: unexpected response")
+ }
+ }
+
+ buf.Reset()
+ if err := s.CallBodyArgsWriter(ctx, args, testData, &buf); err != nil {
+ return err
+ } else if buf.String() != expectedData {
+ return errors.New("CallBodyArgsWriter: unexpected response")
+ }
+
+ // Test reader + args patterns
+ bodyReader = bytes.NewReader(testData)
+ if data, err := s.CallReaderArgsBytes(ctx, args, bodyReader, int64(len(testData))); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallReaderArgsBytes: unexpected response")
+ }
+
+ bodyReader = bytes.NewReader(testData)
+ if reader, err := s.CallReaderArgsReader(ctx, args, bodyReader, int64(len(testData))); err != nil {
+ return err
+ } else {
+ defer reader.Close()
+ if data, err := io.ReadAll(reader); err != nil {
+ return err
+ } else if string(data) != expectedData {
+ return errors.New("CallReaderArgsReader: unexpected response")
+ }
+ }
+
+ bodyReader = bytes.NewReader(testData)
+ buf.Reset()
+ if err := s.CallReaderArgsWriter(ctx, args, bodyReader, int64(len(testData)), &buf); err != nil {
+ return err
+ } else if buf.String() != expectedData {
+ return errors.New("CallReaderArgsWriter: unexpected response")
+ }
+
+ return nil
+}
+
+func TestAllCallTypes(t *testing.T) {
+ mockSrv := newMockServer()
+ server := mockSrv.createTLSServer()
+ defer server.Close()
+
+ testTicket, exists := mockSrv.ticketsByName["valid_success"]
+ if !exists {
+ t.Fatal("valid_success ticket not found in test data")
+ }
+
+ installationID, err := uuid.Parse(testTicket.InstallationID)
+ if err != nil {
+ t.Fatalf("invalid InstallationID: %v", err)
+ }
+
+ var callSuite testCallSuite
+ configs := callSuite.getCallConfigs(
+ strings.TrimPrefix(server.URL, "https://"),
+ "/api/v1/call",
+ )
+
+ transport := DefaultTransport()
+ transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+ err = Build(configs,
+ withServerPublicKey(mockSrv.getPublicKey()),
+ WithInstallationID([16]byte(installationID)),
+ WithPowTimeout(1*time.Second),
+ WithLicenseKey(encodeLicenseKey(testLicenseKey)),
+ WithTransport(transport))
+ if err != nil {
+ t.Fatalf("Build failed: %v", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ if err := callSuite.testCalls(ctx, testResponseData); err != nil {
+ t.Errorf("testCalls failed: %v", err)
+ }
+}
+
+func TestStreamingOperations(t *testing.T) {
+ mockSrv := newMockServer()
+ server := mockSrv.createTLSServer()
+ defer server.Close()
+
+ testTicket, exists := mockSrv.ticketsByName["valid_success"]
+ if !exists {
+ t.Fatal("valid_success ticket not found in test data")
+ }
+
+ installationID, err := uuid.Parse(testTicket.InstallationID)
+ if err != nil {
+ t.Fatalf("invalid InstallationID: %v", err)
+ }
+
+ var streamCall CallReqReaderRespReader
+ configs := []CallConfig{{
+ Calls: []any{&streamCall},
+ Host: strings.TrimPrefix(server.URL, "https://"),
+ Name: "valid_success",
+ Path: "/api/v1/call/echo",
+ Method: CallMethodPOST,
+ }}
+
+ transport := DefaultTransport()
+ transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+ err = Build(configs,
+ withServerPublicKey(mockSrv.getPublicKey()),
+ WithInstallationID([16]byte(installationID)),
+ WithPowTimeout(1*time.Second),
+ WithTransport(transport))
+ if err != nil {
+ t.Fatalf("Build failed: %v", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ // test streaming with large data
+ largeData := make([]byte, 10*1024) // 10KB
+ for i := range largeData {
+ largeData[i] = byte('a' + (i % 26))
+ }
+
+ reader, err := streamCall(ctx, bytes.NewReader(largeData), int64(len(largeData)))
+ if err != nil {
+ t.Errorf("streaming call failed: %v", err)
+ return
+ }
+ defer reader.Close()
+
+ response, err := io.ReadAll(reader)
+ if err != nil {
+ t.Errorf("failed to read streaming response: %v", err)
+ return
+ }
+
+ if string(response) != string(largeData) {
+ t.Errorf("unexpected streaming response: got %d bytes, want %d bytes", len(response), len(largeData))
+ }
+}
+
+func TestRetryLogic(t *testing.T) {
+ tests := []struct {
+ name string
+ errorType error
+ expectedRetry bool
+ expectedDelay time.Duration
+ }{
+ {"bad_gateway", ErrBadGateway, true, 3 * time.Second},
+ {"server_internal", ErrServerInternal, true, 3 * time.Second},
+ {"too_many_requests", ErrTooManyRequests, true, 5 * time.Second},
+ {"rpm_limit", ErrTooManyRequestsRPM, true, DefaultWaitTime},
+ {"experiment_timeout", ErrExperimentTimeout, true, DefaultWaitTime},
+ {"bad_request", ErrBadRequest, false, 0},
+ {"forbidden", ErrForbidden, false, 0},
+ {"not_found", ErrNotFound, false, 0},
+ }
+
+ cfn := &callFunc{}
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ retry := isTemporaryError(tt.errorType)
+ if retry != tt.expectedRetry {
+ t.Errorf("isTemporaryError() = %v, want %v", retry, tt.expectedRetry)
+ }
+
+ if tt.expectedRetry {
+ delay := cfn.calculateWaitTime(tt.errorType, nil)
+ if tt.errorType == ErrTooManyRequestsRPM || tt.errorType == ErrExperimentTimeout {
+ // these can vary based on context, just check it's reasonable
+ if delay <= 0 || delay > DefaultWaitTime {
+ t.Errorf("calculateWaitTime() = %v, expected <= %v", delay, DefaultWaitTime)
+ }
+ } else if delay != tt.expectedDelay {
+ t.Errorf("calculateWaitTime() = %v, want %v", delay, tt.expectedDelay)
+ }
+ }
+ })
+ }
+}
+
+func TestErrorScenarios(t *testing.T) {
+ mockSrv := newMockServer()
+ server := mockSrv.createTLSServer()
+ defer server.Close()
+
+ testTicket, exists := mockSrv.ticketsByName["invalid_corrupted"]
+ if !exists {
+ t.Fatal("invalid_corrupted ticket not found in test data")
+ }
+
+ installationID, err := uuid.Parse(testTicket.InstallationID)
+ if err != nil {
+ t.Fatalf("invalid InstallationID: %v", err)
+ }
+
+ var testCall CallReqRespBytes
+ configs := []CallConfig{{
+ Calls: []any{&testCall},
+ Host: strings.TrimPrefix(server.URL, "https://"),
+ Name: "invalid_corrupted",
+ Path: "/api/v1/call/test_json",
+ Method: CallMethodGET,
+ }}
+
+ transport := DefaultTransport()
+ transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+ err = Build(configs,
+ withServerPublicKey(mockSrv.getPublicKey()),
+ WithInstallationID([16]byte(installationID)),
+ WithPowTimeout(100*time.Millisecond), // short timeout to force error
+ WithTransport(transport))
+ if err != nil {
+ t.Fatalf("Build failed: %v", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+ defer cancel()
+
+ _, err = testCall(ctx)
+ if err == nil {
+ t.Error("expected error for corrupted ticket scenario")
+ return
+ }
+
+ // should get PoW timeout or invalid format error
+ if !errors.Is(err, ErrExperimentTimeout) && !strings.Contains(err.Error(), "PoW solving failed") {
+ t.Errorf("unexpected error type: %v", err)
+ }
+}
+
+func TestConcurrentCalls(t *testing.T) {
+ mockSrv := newMockServer()
+ server := mockSrv.createTLSServer()
+ defer server.Close()
+
+ testTicket, exists := mockSrv.ticketsByName["valid_success"]
+ if !exists {
+ t.Fatal("valid_success ticket not found in test data")
+ }
+
+ installationID, err := uuid.Parse(testTicket.InstallationID)
+ if err != nil {
+ t.Fatalf("invalid InstallationID: %v", err)
+ }
+
+ var testCall CallReqRespBytes
+ configs := []CallConfig{{
+ Calls: []any{&testCall},
+ Host: strings.TrimPrefix(server.URL, "https://"),
+ Name: "valid_success",
+ Path: "/api/v1/call/test_json",
+ Method: CallMethodGET,
+ }}
+
+ transport := DefaultTransport()
+ transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+ err = Build(configs,
+ withServerPublicKey(mockSrv.getPublicKey()),
+ WithInstallationID([16]byte(installationID)),
+ WithPowTimeout(5*time.Second),
+ WithTransport(transport))
+ if err != nil {
+ t.Fatalf("Build failed: %v", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ // test concurrent calls
+ var wg sync.WaitGroup
+ const numCalls = 20
+ results := make(chan error, numCalls*10)
+
+ for range numCalls {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for range 10 {
+ _, err := testCall(ctx)
+ results <- err
+ }
+ }()
+ }
+
+ // wait for all calls to complete
+ wg.Wait()
+
+ // collect results
+ for i := range numCalls {
+ if err := <-results; err != nil {
+ t.Errorf("concurrent call %d failed: %v", i, err)
+ }
+ }
+}
diff --git a/system/doc.go b/system/doc.go
new file mode 100644
index 0000000..01dc89e
--- /dev/null
+++ b/system/doc.go
@@ -0,0 +1,198 @@
+// Package system provides cross-platform utilities for stable machine identification and installation tracking.
+//
+// The system package implements secure, deterministic installation ID generation that remains
+// stable across application restarts on the same machine. It uses platform-specific machine
+// identification methods with graceful fallback to ensure reliable operation across different
+// environments and privilege levels.
+//
+// # Core Functionality
+//
+// The main function generates stable installation IDs for SDK usage:
+//
+// import "github.com/vxcontrol/cloud/system"
+//
+// // Generate stable installation ID
+// installationID := system.GetInstallationID()
+// // Returns same UUID for same machine across application restarts
+//
+// // Use with VXControl Cloud SDK
+// err := sdk.Build(configs,
+// sdk.WithClient("MyApp", "1.0.0"),
+// sdk.WithInstallationID(installationID),
+// )
+//
+// # Platform-Specific Implementation
+//
+// ## Linux Implementation
+//
+// Uses multiple identification sources for maximum stability:
+//
+// // Primary: D-Bus machine ID
+// machineID := readFile("/var/lib/dbus/machine-id")
+// // Fallback: Alternative location
+// machineID := readFile("/etc/machine-id")
+//
+// // Enhancement: SMBIOS data (when available)
+// systemUUID := getSMBIOSAttribute(SystemUUID)
+// systemManufacturer := getSMBIOSAttribute(SystemManufacturer)
+// systemProduct := getSMBIOSAttribute(SystemProductName)
+//
+// Linux Features:
+// - Primary: 32-character hex machine ID from systemd
+// - Enhancement: SMBIOS system information (requires root privileges)
+// - Fallback: hostname-based identification
+// - Graceful: Handles permission restrictions and missing files
+//
+// ## macOS Implementation
+//
+// Uses hardware platform UUID from system registry:
+//
+// // Hardware platform UUID via ioreg
+// ioregOutput := exec("ioreg", "-rd1", "-c", "IOPlatformExpertDevice")
+// platformUUID := extractIOPlatformUUID(ioregOutput)
+//
+// macOS Features:
+// - Hardware-based: IOPlatformUUID from system firmware
+// - Stable: Persists across OS reinstalls and updates
+// - Secure: No special privileges required
+// - Format: Standard UUID format (36 characters)
+//
+// ## Windows Implementation
+//
+// Uses registry machine GUID with system product information:
+//
+// // Registry machine GUID
+// machineGuid := registry.GetStringValue("SOFTWARE\\Microsoft\\Cryptography", "MachineGuid")
+//
+// // System product info via WMI
+// systemProduct := wmi.Query("SELECT * FROM Win32_ComputerSystemProduct")
+// combinedID := machineGuid + ":" + systemProduct.UUID
+//
+// Windows Features:
+// - Registry-based: Cryptographic machine GUID from Windows
+// - Enhanced: WMI system product information
+// - Stable: Survives most system changes and updates
+// - Comprehensive: Combines multiple identification sources
+//
+// # Fallback Strategy
+//
+// Graceful degradation when primary identification fails:
+//
+// func GetInstallationID() uuid.UUID {
+// machineID, err := getMachineID()
+// if err != nil || machineID == "" {
+// // Fallback to hostname
+// machineID = getHostname()
+// if machineID == "" {
+// // Final fallback to static value
+// machineID = "unknown-host"
+// }
+// }
+//
+// // Generate deterministic UUID
+// hash := md5.Sum([]byte(machineID + salt))
+// return uuid.NewMD5(uuid.NameSpaceURL, hash[:])
+// }
+//
+// Fallback Levels:
+// 1. Platform-specific machine ID (preferred)
+// 2. System hostname (degraded uniqueness)
+// 3. Static identifier (stable but not unique)
+//
+// # Security Considerations
+//
+// ## Cryptographic Properties
+//
+// // Deterministic UUID generation
+// salt := "*********************"
+// hash := md5.Sum([]byte(machineID + salt))
+// uuid := uuid.NewMD5(uuid.NameSpaceURL, hash[:])
+//
+// Security Features:
+// - Salt Protection: Fixed salt prevents rainbow table attacks
+// - MD5 Hashing: Sufficient for non-cryptographic identification
+// - UUID v3: RFC4122 compliant deterministic UUID generation
+// - NameSpace: URL namespace provides additional domain separation
+//
+// ## Privacy Protection
+//
+// // Machine identifiers are hashed before use
+// // Original machine ID is never transmitted to cloud services
+// // Only the derived UUID is sent as X-Installation-ID header
+//
+// Privacy Features:
+// - Hash-based: Original machine identifiers never exposed
+// - Irreversible: Cannot derive machine ID from installation UUID
+// - Stable: Same UUID for legitimate re-installations
+// - Unique: Different UUID for different machines (in most cases)
+//
+// # Performance Characteristics
+//
+// ## Timing Analysis
+//
+// Benchmark results (Apple M2 Max):
+// - Average generation time: ~17ms per call
+// - Variation: Depends on platform and system access speed
+// - Caching: No internal caching (called once per SDK initialization)
+//
+// Performance Factors:
+// - Linux: File system access + optional SMBIOS reads
+// - macOS: Process execution (ioreg command)
+// - Windows: Registry access + WMI queries
+// - Network: No network dependencies
+//
+// ## Memory Usage
+//
+// // Minimal memory footprint
+// // No persistent state or caching
+// // Platform-specific temporary allocations only
+//
+// Memory Characteristics:
+// - Zero persistent memory usage
+// - Temporary allocations for system calls
+// - No internal caching or state management
+// - Garbage collection friendly
+//
+// # Integration Guidelines
+//
+// ## SDK Integration
+//
+// Recommended usage pattern for VXControl Cloud SDK:
+//
+// // Initialize once per application lifecycle
+// installationID := system.GetInstallationID()
+//
+// // Use in SDK configuration
+// err := sdk.Build(configs,
+// sdk.WithInstallationID(installationID),
+// // ... other options
+// )
+//
+// ## Error Handling
+//
+// Robust error handling for system identification:
+//
+// // GetInstallationID never fails or panics
+// // Always returns valid UUID, even in degraded environments
+// installationID := system.GetInstallationID()
+//
+// // For diagnostic purposes, check machine ID separately
+// machineID, err := getMachineID()
+// if err != nil {
+// log.Printf("Machine ID unavailable (using fallback): %v", err)
+// } else {
+// log.Printf("Machine ID: %s", machineID)
+// }
+//
+// ## Threading Considerations
+//
+// // GetInstallationID is thread-safe
+// // Can be called concurrently from multiple goroutines
+// // No internal state or synchronization required
+//
+// Thread Safety:
+// - Concurrent access safe
+// - No shared state
+// - Idempotent operation
+// - No race conditions
+package system
diff --git a/system/installation_id.go b/system/installation_id.go
new file mode 100644
index 0000000..3c3e205
--- /dev/null
+++ b/system/installation_id.go
@@ -0,0 +1,22 @@
+package system
+
+import (
+ "crypto/md5" //nolint:gosec
+
+ "github.com/google/uuid"
+)
+
+func GetInstallationID() uuid.UUID {
+ salt := "49564c1fb63a7d2dd88f20a849be41ac439c2d8d3e433b2556364d9be9ae96f4"
+ id, err := getMachineID()
+ if err != nil || id == "" {
+ // fallback to hostname-based ID when machine ID is not available
+ id = getHostname()
+ if id == "" {
+ // final fallback to static value to ensure stability
+ id = "unknown-host"
+ }
+ }
+ hash := md5.Sum([]byte(id + salt)) //nolint:gosec
+ return uuid.NewMD5(uuid.NameSpaceURL, hash[:])
+}
diff --git a/system/installation_test.go b/system/installation_test.go
new file mode 100644
index 0000000..22241fe
--- /dev/null
+++ b/system/installation_test.go
@@ -0,0 +1,183 @@
+package system
+
+import (
+ "runtime"
+ "strings"
+ "testing"
+
+ "github.com/google/uuid"
+)
+
+func TestGetInstallationID_Stability(t *testing.T) {
+ // test that installation ID is stable across multiple calls
+ id1 := GetInstallationID()
+ id2 := GetInstallationID()
+
+ if id1 != id2 {
+ t.Errorf("installation ID is not stable: %s != %s", id1, id2)
+ }
+
+ // verify it's a valid UUID
+ if id1 == uuid.Nil {
+ t.Error("installation ID should not be nil UUID")
+ }
+
+ // test multiple calls to ensure consistency
+ for i := range 10 {
+ idN := GetInstallationID()
+ if idN != id1 {
+ t.Errorf("installation ID changed on call %d: %s != %s", i, idN, id1)
+ }
+ }
+}
+
+func TestGetInstallationID_ValidFormat(t *testing.T) {
+ id := GetInstallationID()
+
+ // verify UUID format
+ idStr := id.String()
+ if len(idStr) != 36 {
+ t.Errorf("invalid UUID length: expected 36, got %d", len(idStr))
+ }
+
+ // verify it can be parsed back
+ parsedID, err := uuid.Parse(idStr)
+ if err != nil {
+ t.Errorf("invalid UUID format: %v", err)
+ }
+
+ if parsedID != id {
+ t.Errorf("UUID parsing mismatch: %s != %s", parsedID, id)
+ }
+}
+
+func TestGetMachineID_NoError(t *testing.T) {
+ // test that getMachineID doesn't panic
+ defer func() {
+ if r := recover(); r != nil {
+ t.Errorf("getMachineID panicked: %v", r)
+ }
+ }()
+
+ machineID, err := getMachineID()
+
+ t.Logf("Platform: %s", runtime.GOOS)
+ t.Logf("Machine ID: %s", machineID)
+ t.Logf("Error: %v", err)
+
+ // on any platform, we should either get an ID or handle the error gracefully
+ if err != nil {
+ t.Logf("getMachineID failed (this is acceptable): %v", err)
+ } else if machineID == "" {
+ t.Log("getMachineID returned empty string (this is acceptable)")
+ }
+}
+
+func TestGetHostname_NoError(t *testing.T) {
+ // test that getHostname doesn't panic
+ defer func() {
+ if r := recover(); r != nil {
+ t.Errorf("getHostname panicked: %v", r)
+ }
+ }()
+
+ hostname := getHostname()
+ t.Logf("Hostname: %s", hostname)
+
+ // hostname can be empty in some environments, that's acceptable
+}
+
+func BenchmarkGetInstallationID(b *testing.B) {
+ for b.Loop() {
+ _ = GetInstallationID()
+ }
+}
+
+func TestInstallationID_CrossPlatformConsistency(t *testing.T) {
+ // test that installation ID generation doesn't panic on any platform
+ defer func() {
+ if r := recover(); r != nil {
+ t.Errorf("GetInstallationID panicked: %v", r)
+ }
+ }()
+
+ id := GetInstallationID()
+ t.Logf("Platform: %s", runtime.GOOS)
+ t.Logf("Installation ID: %s", id.String())
+
+ // verify UUID version (should be version 3 for MD5-based UUID)
+ if id.Version() != 3 {
+ t.Errorf("expected UUID version 3 (MD5), got version %d", id.Version())
+ }
+
+ // verify UUID variant
+ if id.Variant() != uuid.RFC4122 {
+ t.Errorf("expected RFC4122 variant, got %d", id.Variant())
+ }
+}
+
+func TestMachineID_PlatformSpecific(t *testing.T) {
+ machineID, err := getMachineID()
+
+ t.Logf("Platform: %s", runtime.GOOS)
+ t.Logf("Machine ID: %s", machineID)
+ t.Logf("Error: %v", err)
+
+ // Platform-specific validation
+ switch runtime.GOOS {
+ case "darwin":
+ // on macOS, we expect either a valid UUID or an error
+ if err == nil && machineID != "" {
+ // should be a valid UUID format from IOPlatformUUID
+ if len(machineID) != 36 {
+ t.Logf("macOS machine ID has unexpected length: %d (expected 36)", len(machineID))
+ }
+ }
+
+ case "linux":
+ // on Linux, we expect either machine-id or an error
+ if err == nil && machineID != "" {
+ // machine-id is usually 32 hex characters, but can be extended with SMBIOS
+ if len(machineID) < 32 {
+ t.Logf("Linux machine ID shorter than expected: %d", len(machineID))
+ }
+ }
+
+ case "windows":
+ // on Windows, we expect either MachineGuid or an error
+ if err == nil && machineID != "" {
+ // should contain both MachineGuid and system product info
+ if !strings.Contains(machineID, ":") {
+ t.Logf("Windows machine ID missing system product info")
+ }
+ }
+ }
+
+ // test that fallback logic works correctly
+ // even if machine ID fails, GetInstallationID should still work
+ installationID := GetInstallationID()
+ if installationID == uuid.Nil {
+ t.Error("installation ID should not be nil even when machine ID fails")
+ }
+}
+
+func TestInstallationID_UniqueAcrossSessions(t *testing.T) {
+ // simulate getting installation ID multiple times
+ // to ensure it's deterministic
+
+ ids := make(map[uuid.UUID]bool)
+
+ // get installation ID 100 times
+ for range 100 {
+ id := GetInstallationID()
+ ids[id] = true
+ }
+
+ // should always be exactly 1 unique ID
+ if len(ids) != 1 {
+ t.Errorf("installation ID not deterministic: got %d unique IDs", len(ids))
+ for id := range ids {
+ t.Logf("ID: %s", id.String())
+ }
+ }
+}
diff --git a/system/utils.go b/system/utils.go
new file mode 100644
index 0000000..bc1dcb4
--- /dev/null
+++ b/system/utils.go
@@ -0,0 +1,14 @@
+package system
+
+import (
+ "os"
+)
+
+func getHostname() string {
+ hn, err := os.Hostname()
+ if err != nil {
+ return ""
+ }
+
+ return hn
+}
diff --git a/system/utils_darwin.go b/system/utils_darwin.go
new file mode 100644
index 0000000..f0b240d
--- /dev/null
+++ b/system/utils_darwin.go
@@ -0,0 +1,58 @@
+//go:build darwin
+// +build darwin
+
+package system
+
+import (
+ "bytes"
+ "fmt"
+ "os/exec"
+ "strings"
+ "sync"
+)
+
+var execLock sync.Mutex
+
+func getMachineID() (string, error) {
+ out, err := execCmd("ioreg", "-rd1", "-c", "IOPlatformExpertDevice")
+ if err != nil {
+ return "", err
+ }
+ id, err := extractID(out)
+ if err != nil {
+ return "", err
+ }
+ return strings.TrimSpace(strings.Trim(id, "\n")), nil
+}
+
+func extractID(lines string) (string, error) {
+ const uuidParamName = "IOPlatformUUID"
+ for _, line := range strings.Split(lines, "\n") {
+ if strings.Contains(line, uuidParamName) {
+ parts := strings.SplitAfter(line, `" = "`)
+ if len(parts) == 2 {
+ return strings.TrimRight(parts[1], `"`), nil
+ }
+ }
+ }
+ return "", fmt.Errorf("failed to extract the '%s' value from the `ioreg` output", uuidParamName)
+}
+
+func execCmd(scmd string, args ...string) (string, error) {
+ execLock.Lock()
+ defer execLock.Unlock()
+
+ var stdout bytes.Buffer
+ var stderr bytes.Buffer
+ cmd := exec.Command(scmd, args...)
+ cmd.Stdin = strings.NewReader("")
+ cmd.Stdout = &stdout
+ cmd.Stderr = &stderr
+
+ err := cmd.Run()
+ if err != nil {
+ return "", err
+ }
+
+ return stdout.String(), nil
+}
diff --git a/system/utils_linux.go b/system/utils_linux.go
new file mode 100644
index 0000000..887eaa6
--- /dev/null
+++ b/system/utils_linux.go
@@ -0,0 +1,151 @@
+//go:build linux
+// +build linux
+
+package system
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "sync"
+
+ "github.com/digitalocean/go-smbios/smbios"
+)
+
+type Feature int
+
+const (
+ _ Feature = iota
+
+ // System manufacturer. Requires access to SMBIOS data via DMI (i.e. root privileges)
+ SystemManufacturer
+ // System product name. Requires access to SMBIOS data via DMI (i.e. root privileges)
+ SystemProductName
+ // System UUID. Makes sense for virtual machines. Requires access to SMBIOS data via DMI (i.e. root privileges)
+ SystemUUID
+)
+
+var (
+ readSMBIOSOnce sync.Once
+ smbiosReadingErr error
+ smbiosAttrValues = make(map[Feature]string)
+
+ // See SMBIOS specification https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.3.0.pdf
+ smbiosAttrTable = [...]smbiosAttribute{
+ // System 0x01
+ {0x01, 0x08, 0x04, SystemManufacturer, nil},
+ {0x01, 0x08, 0x05, SystemProductName, nil},
+ {0x01, 0x14, 0x08, SystemUUID, formatUUID},
+ }
+)
+
+func formatUUID(b []byte) (string, error) {
+ return fmt.Sprintf("%0x-%0x-%0x-%0x-%0x", b[:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil
+}
+
+func getSMBIOSAttr(feat Feature) (string, error) {
+ readSMBIOSOnce.Do(func() {
+ smbiosReadingErr = readSMBIOSAttributes()
+ })
+ if smbiosReadingErr != nil {
+ return "", smbiosReadingErr
+ }
+ return smbiosAttrValues[feat], nil
+}
+
+func readSMBIOSAttributes() error {
+ smbiosAttrIndex := buildSMBIOSAttrIndex()
+ rc, _, err := smbios.Stream()
+ if err != nil {
+ return fmt.Errorf("unable to open SMBIOS info: %v", err)
+ }
+ defer rc.Close()
+ structures, err := smbios.NewDecoder(rc).Decode()
+ if err != nil {
+ return fmt.Errorf("unable to decode SMBIOS info: %v", err)
+ }
+ for _, s := range structures {
+ attrList := smbiosAttrIndex[int(s.Header.Type)]
+ for _, attr := range attrList {
+ val, err := attr.readValueString(s)
+ if err != nil {
+ return fmt.Errorf("unable to read SMBIOS attribute '%v' of structure type 0x%0x: %v",
+ attr.feature, s.Header.Type, err)
+ }
+ smbiosAttrValues[attr.feature] = val
+ }
+ }
+ return nil
+}
+
+func buildSMBIOSAttrIndex() map[int][]*smbiosAttribute {
+ res := make(map[int][]*smbiosAttribute)
+ for i := range smbiosAttrTable {
+ attr := &smbiosAttrTable[i]
+ res[attr.structType] = append(res[attr.structType], attr)
+ }
+ return res
+}
+
+type smbiosAttribute struct {
+ structType int
+ structMinLength int
+ offset int
+
+ feature Feature
+
+ format func(data []byte) (string, error)
+}
+
+func (attr *smbiosAttribute) readValueString(s *smbios.Structure) (string, error) {
+ if len(s.Formatted) < attr.structMinLength {
+ return "", nil
+ }
+ if attr.format != nil {
+ const headerSize = 4
+ return attr.format(s.Formatted[attr.offset-headerSize:])
+ }
+ return attr.getString(s)
+}
+
+func (attr *smbiosAttribute) getString(s *smbios.Structure) (string, error) {
+ const headerSize = 4
+ strNo := int(s.Formatted[attr.offset-headerSize])
+ strNo -= 1
+ if strNo < 0 || strNo >= len(s.Strings) {
+ return "", fmt.Errorf("invalid string no")
+ }
+ return s.Strings[strNo], nil
+}
+
+func getMachineID() (string, error) {
+ const (
+ // dbusPath is the default path for dbus machine id.
+ dbusPath = "/var/lib/dbus/machine-id"
+ // dbusPathEtc is the default path for dbus machine id located in /etc.
+ // Some systems (like Fedora 20) only know this path.
+ // Sometimes it's the other way round.
+ dbusPathEtc = "/etc/machine-id"
+ )
+
+ id, err := os.ReadFile(dbusPath)
+ if err != nil {
+ id, err = os.ReadFile(dbusPathEtc)
+ }
+ if err != nil {
+ return "", err
+ }
+ machineID := strings.TrimSpace(strings.Trim(string(id), "\n"))
+
+ // root privileges are required to access attributes, the process will be skipped in case of insufficient privileges
+ smbiosAttrs := [...]Feature{SystemUUID, SystemManufacturer, SystemProductName}
+ for _, attr := range smbiosAttrs {
+ attrVal, err := getSMBIOSAttr(attr)
+ if err != nil || strings.TrimSpace(attrVal) == "" {
+ continue
+ }
+ machineID = fmt.Sprintf("%s:%s", machineID, strings.ToLower(attrVal))
+ }
+
+ return machineID, nil
+}
diff --git a/system/utils_windows.go b/system/utils_windows.go
new file mode 100644
index 0000000..1cd4efe
--- /dev/null
+++ b/system/utils_windows.go
@@ -0,0 +1,126 @@
+//go:build windows
+// +build windows
+
+package system
+
+import (
+ "fmt"
+
+ "github.com/go-ole/go-ole"
+ "golang.org/x/sys/windows/registry"
+)
+
+func getMachineID() (string, error) {
+ sp, _ := getSystemProduct()
+ k, err := registry.OpenKey(registry.LOCAL_MACHINE,
+ `SOFTWARE\Microsoft\Cryptography`,
+ registry.QUERY_VALUE|registry.WOW64_64KEY)
+ if err != nil {
+ return sp, err
+ }
+ defer k.Close()
+
+ s, _, err := k.GetStringValue("MachineGuid")
+ if err != nil {
+ return sp, err
+ }
+ return s + ":" + sp, nil
+}
+
+func getSystemProduct() (string, error) {
+ var err error
+ var classID *ole.GUID
+
+ IID_ISWbemLocator, _ := ole.CLSIDFromString("{76A6415B-CB41-11D1-8B02-00600806D9B6}")
+
+ err = ole.CoInitialize(0)
+ if err != nil {
+ return "", fmt.Errorf("OLE initialize error: %v", err)
+ }
+ defer ole.CoUninitialize()
+
+ classID, err = ole.ClassIDFrom("WbemScripting.SWbemLocator")
+ if err != nil {
+ return "", fmt.Errorf("CreateObject WbemScripting.SWbemLocator returned with %v", err)
+ }
+
+ comserver, err := ole.CreateInstance(classID, ole.IID_IUnknown)
+ if err != nil {
+ return "", fmt.Errorf("CreateInstance WbemScripting.SWbemLocator returned with %v", err)
+ }
+ if comserver == nil {
+ return "", fmt.Errorf("CreateObject WbemScripting.SWbemLocator not an object")
+ }
+ defer comserver.Release()
+
+ dispatch, err := comserver.QueryInterface(IID_ISWbemLocator)
+ if err != nil {
+ return "", fmt.Errorf("context.iunknown.QueryInterface returned with %v", err)
+ }
+ defer dispatch.Release()
+
+ wbemServices, err := dispatch.CallMethod("ConnectServer")
+ if err != nil {
+ return "", fmt.Errorf("ConnectServer failed with %v", err)
+ }
+ defer wbemServices.Clear()
+
+ query := "SELECT * FROM Win32_ComputerSystemProduct"
+ objectset, err := wbemServices.ToIDispatch().CallMethod("ExecQuery", query)
+ if err != nil {
+ return "", fmt.Errorf("ExecQuery failed with %v", err)
+ }
+ defer objectset.Clear()
+
+ enum_property, err := objectset.ToIDispatch().GetProperty("_NewEnum")
+ if err != nil {
+ return "", fmt.Errorf("Get _NewEnum property failed with %v", err)
+ }
+ defer enum_property.Clear()
+
+ enum, err := enum_property.ToIUnknown().IEnumVARIANT(ole.IID_IEnumVariant)
+ if err != nil {
+ return "", fmt.Errorf("IEnumVARIANT() returned with %v", err)
+ }
+ if enum == nil {
+ return "", fmt.Errorf("Enum is nil")
+ }
+ defer enum.Release()
+
+ for tmp, length, err := enum.Next(1); length > 0; tmp, length, err = enum.Next(1) {
+ if err != nil {
+ return "", fmt.Errorf("Next() returned with %v", err)
+ }
+ tmp_dispatch := tmp.ToIDispatch()
+ defer tmp_dispatch.Release()
+
+ props, err := tmp_dispatch.GetProperty("Properties_")
+ if err != nil {
+ return "", fmt.Errorf("Get Properties_ property failed with %v", err)
+ }
+ defer props.Clear()
+
+ props_enum_property, err := props.ToIDispatch().GetProperty("_NewEnum")
+ if err != nil {
+ return "", fmt.Errorf("Get _NewEnum property failed with %v", err)
+ }
+ defer props_enum_property.Clear()
+
+ props_enum, err := props_enum_property.ToIUnknown().IEnumVARIANT(ole.IID_IEnumVariant)
+ if err != nil {
+ return "", fmt.Errorf("IEnumVARIANT failed with %v", err)
+ }
+ defer props_enum.Release()
+
+ class_variant, err := tmp_dispatch.GetProperty("UUID")
+ if err != nil {
+ return "", fmt.Errorf("Get UUID property failed with %v", err)
+ }
+ defer class_variant.Clear()
+
+ class_name := class_variant.ToString()
+ return class_name, nil
+ }
+
+ return "", fmt.Errorf("not found")
+}