Files
wc-licensed-product-client/docs/client-implementation.md

1019 lines
28 KiB
Markdown
Raw Normal View History

# Client-Side Implementation Guide
This document provides a comprehensive guide for integrating the WooCommerce Licensed Product Client into existing PHP projects. It covers architecture, classes, configuration, and best practices.
## Overview
The `magdev/wc-licensed-product-client` library provides a complete PHP client for interacting with the WooCommerce Licensed Product REST API. It supports:
- License validation, status checking, and activation
- Response signature verification (HMAC-SHA256)
- PSR-3 logging integration
- PSR-6 caching integration
- Comprehensive exception handling
- Optional code integrity verification
## Requirements
- **PHP:** 8.3 or higher
- **Dependencies:**
- `symfony/http-client` ^7.0
- `psr/log` ^3.0
- `psr/cache` ^3.0
- `psr/http-client` ^1.0
## Installation
```bash
composer require magdev/wc-licensed-product-client
```
## Architecture
### Class Diagram
```text
┌─────────────────────────────┐
│ LicenseClientInterface │
│ (Public API Contract) │
├─────────────────────────────┤
│ + validate() │
│ + status() │
│ + activate() │
└─────────────┬───────────────┘
│ implements
├───────────────────────┐
▼ ▼
┌─────────────────────┐ ┌──────────────────────┐
│ LicenseClient │ │ SecureLicenseClient │
│ (Basic Client) │ │ (With Signatures) │
└─────────────────────┘ └──────────────────────┘
│ │
└───────────┬───────────┘
┌─────────────────────┐
│ DTOs │
├─────────────────────┤
│ LicenseInfo │
│ LicenseStatus │
│ ActivationResult │
│ LicenseState (enum) │
└─────────────────────┘
```
### Directory Structure
```text
src/
├── LicenseClientInterface.php # Public API contract
├── LicenseClient.php # Basic implementation
├── SecureLicenseClient.php # Secure implementation with signatures
├── Dto/
│ ├── LicenseInfo.php # Validation response DTO
│ ├── LicenseStatus.php # Status response DTO + LicenseState enum
│ └── ActivationResult.php # Activation response DTO
├── Exception/
│ ├── LicenseException.php # Base exception
│ ├── LicenseNotFoundException.php
│ ├── LicenseExpiredException.php
│ ├── LicenseRevokedException.php
│ ├── LicenseInactiveException.php
│ ├── LicenseInvalidException.php
│ ├── DomainMismatchException.php
│ ├── MaxActivationsReachedException.php
│ ├── ActivationFailedException.php
│ └── RateLimitExceededException.php
└── Security/
├── ResponseSignature.php # HMAC signature verification
├── SignatureException.php # Signature verification error
├── StringEncoder.php # XOR-based string obfuscation
├── IntegrityChecker.php # Source file hash verification
└── IntegrityException.php # Integrity check failure
```
## Client Classes
### LicenseClientInterface
The public API contract that both client implementations follow:
```php
interface LicenseClientInterface
{
/**
* Validate a license key for a specific domain.
*
* @throws LicenseException When validation fails
*/
public function validate(string $licenseKey, string $domain): LicenseInfo;
/**
* Get detailed status information for a license key.
*
* @throws LicenseException When status check fails
*/
public function status(string $licenseKey): LicenseStatus;
/**
* Activate a license on a specific domain.
*
* @throws LicenseException When activation fails
*/
public function activate(string $licenseKey, string $domain): ActivationResult;
}
```
### LicenseClient (Basic)
The basic client without signature verification. Use this when:
- The server does not support response signing
- You're in a development/testing environment
- Security is handled at another layer
**Constructor Parameters:**
| Parameter | Type | Required | Default | Description |
| --------- | ---- | -------- | ------- | ----------- |
| `httpClient` | `HttpClientInterface` | Yes | - | Symfony HTTP client instance |
| `baseUrl` | `string` | Yes | - | WordPress site URL (e.g., `https://example.com`) |
| `logger` | `LoggerInterface\|null` | No | `NullLogger` | PSR-3 logger for debugging |
| `cache` | `CacheItemPoolInterface\|null` | No | `null` | PSR-6 cache for responses |
| `cacheTtl` | `int` | No | `300` | Cache TTL in seconds (5 minutes) |
**Example:**
```php
use Magdev\WcLicensedProductClient\LicenseClient;
use Symfony\Component\HttpClient\HttpClient;
$client = new LicenseClient(
httpClient: HttpClient::create(),
baseUrl: 'https://your-wordpress-site.com',
);
```
### SecureLicenseClient (Recommended)
The secure client with response signature verification. Use this for production environments.
**Constructor Parameters:**
| Parameter | Type | Required | Default | Description |
| --------- | ---- | -------- | ------- | ----------- |
| `httpClient` | `HttpClientInterface` | Yes | - | Symfony HTTP client instance |
| `baseUrl` | `string` | Yes | - | WordPress site URL |
| `serverSecret` | `string` | Yes | - | Shared secret (same as server) |
| `logger` | `LoggerInterface\|null` | No | `NullLogger` | PSR-3 logger |
| `cache` | `CacheItemPoolInterface\|null` | No | `null` | PSR-6 cache |
| `cacheTtl` | `int` | No | `300` | Cache TTL in seconds |
| `verifyIntegrity` | `bool` | No | `false` | Enable source file integrity checks |
| `encoder` | `StringEncoder\|null` | No | Default instance | Custom string encoder |
**Example:**
```php
use Magdev\WcLicensedProductClient\SecureLicenseClient;
use Symfony\Component\HttpClient\HttpClient;
$client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: 'https://your-wordpress-site.com',
serverSecret: 'your-shared-server-secret-min-32-chars',
);
```
**Security Features:**
1. **Response Signature Verification**: Every response is verified using HMAC-SHA256
2. **Timestamp Validation**: Prevents replay attacks (5-minute tolerance)
3. **Per-License Key Derivation**: Each license gets a unique signing key
4. **Obfuscated API Paths**: API endpoint paths are XOR-encoded in source code
5. **Optional Integrity Checking**: Verify critical source files haven't been modified
## Data Transfer Objects (DTOs)
### LicenseInfo
Returned by `validate()`. Contains license details for a valid license.
```php
final readonly class LicenseInfo
{
public function __construct(
public int $productId, // WooCommerce product ID
public ?\DateTimeImmutable $expiresAt, // null = lifetime license
public ?int $versionId, // Product version (if applicable)
) {}
public static function fromArray(array $data): self;
public function isLifetime(): bool; // Returns true if no expiration
}
```
**Usage:**
```php
$info = $client->validate('ABCD-1234-EFGH-5678', 'example.com');
echo "Product ID: " . $info->productId;
echo "Lifetime: " . ($info->isLifetime() ? 'Yes' : 'No');
if (!$info->isLifetime()) {
echo "Expires: " . $info->expiresAt->format('Y-m-d');
}
```
### LicenseStatus
Returned by `status()`. Contains comprehensive license information.
```php
final readonly class LicenseStatus
{
public function __construct(
public bool $valid, // Is license currently valid
public LicenseState $status, // Status enum value
public string $domain, // Bound domain
public ?\DateTimeImmutable $expiresAt, // Expiration date
public int $activationsCount, // Current activations
public int $maxActivations, // Maximum allowed
) {}
public static function fromArray(array $data): self;
public function isLifetime(): bool;
public function hasAvailableActivations(): bool;
}
```
**Usage:**
```php
$status = $client->status('ABCD-1234-EFGH-5678');
echo "Valid: " . ($status->valid ? 'Yes' : 'No');
echo "Status: " . $status->status->value; // active, inactive, expired, revoked
echo "Domain: " . $status->domain;
echo "Activations: " . $status->activationsCount . "/" . $status->maxActivations;
if ($status->hasAvailableActivations()) {
echo "More activations available!";
}
```
### LicenseState (Enum)
Represents possible license states:
```php
enum LicenseState: string
{
case Active = 'active'; // License is active and valid
case Inactive = 'inactive'; // License exists but not activated
case Expired = 'expired'; // License has passed expiration date
case Revoked = 'revoked'; // License was manually revoked
}
```
### ActivationResult
Returned by `activate()`. Contains activation outcome.
```php
final readonly class ActivationResult
{
public function __construct(
public bool $success, // Whether activation succeeded
public string $message, // Human-readable message
) {}
public static function fromArray(array $data): self;
}
```
**Usage:**
```php
$result = $client->activate('ABCD-1234-EFGH-5678', 'newdomain.com');
if ($result->success) {
echo "Activated: " . $result->message;
} else {
echo "Failed: " . $result->message;
}
```
## Exception Hierarchy
All exceptions extend `LicenseException` and include an `errorCode` property for programmatic handling.
```text
LicenseException (base)
├── LicenseNotFoundException // error: license_not_found
├── LicenseExpiredException // error: license_expired
├── LicenseRevokedException // error: license_revoked
├── LicenseInactiveException // error: license_inactive
├── LicenseInvalidException // error: license_invalid
├── DomainMismatchException // error: domain_mismatch
├── MaxActivationsReachedException // error: max_activations_reached
├── ActivationFailedException // error: activation_failed
├── RateLimitExceededException // error: rate_limit_exceeded (has retryAfter)
└── Security\SignatureException // error: signature_invalid
└── Security\IntegrityException // error: integrity_check_failed
```
### Exception Properties
**LicenseException (Base):**
```php
class LicenseException extends \RuntimeException
{
public readonly ?string $errorCode; // API error code for programmatic handling
}
```
**RateLimitExceededException:**
```php
final class RateLimitExceededException extends LicenseException
{
public readonly ?int $retryAfter; // Seconds until rate limit resets
}
```
**IntegrityException:**
```php
final class IntegrityException extends LicenseException
{
public readonly array $failures; // List of failed integrity checks
}
```
### Catching Exceptions
```php
use Magdev\WcLicensedProductClient\Exception\{
LicenseException,
LicenseNotFoundException,
LicenseExpiredException,
DomainMismatchException,
RateLimitExceededException,
};
use Magdev\WcLicensedProductClient\Security\SignatureException;
try {
$info = $client->validate($licenseKey, $domain);
} catch (SignatureException $e) {
// Response was tampered with or server secret mismatch
error_log("SECURITY: Response signature invalid");
die("License verification failed. Please contact support.");
} catch (LicenseNotFoundException $e) {
echo "License key not found.";
} catch (LicenseExpiredException $e) {
echo "Your license has expired. Please renew.";
} catch (DomainMismatchException $e) {
echo "This license is not valid for this domain.";
} catch (RateLimitExceededException $e) {
echo "Too many requests. Try again in {$e->retryAfter} seconds.";
} catch (LicenseException $e) {
// Catch-all for any license error
echo "License error: " . $e->getMessage();
error_log("License error [{$e->errorCode}]: " . $e->getMessage());
}
```
## Security Classes
### ResponseSignature
Verifies HMAC-SHA256 signatures on API responses to prevent tampering.
**Key Derivation:**
Each license key derives a unique signing key from the server secret:
```php
// HKDF-like key derivation (internal implementation)
$prk = hash_hmac('sha256', $licenseKey, $serverSecret, true);
$derivedKey = hash_hmac('sha256', $prk . "\x01", $serverSecret);
```
**Signature Format:**
```text
signature = HMAC-SHA256(
key = derived_key,
message = timestamp + ":" + canonical_json(response_body)
)
```
Where:
- `canonical_json` sorts keys alphabetically
- JSON uses `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE` flags
- Result is hex-encoded (64 characters)
**Response Headers:**
| Header | Description | Example |
| ------ | ----------- | ------- |
| `X-License-Signature` | HMAC-SHA256 signature (hex) | `a1b2c3...` (64 chars) |
| `X-License-Timestamp` | Unix timestamp when signed | `1706000000` |
**Custom Timestamp Tolerance:**
```php
use Magdev\WcLicensedProductClient\Security\ResponseSignature;
// Default is 300 seconds (5 minutes)
$signature = new ResponseSignature($secretKey, timestampTolerance: 600); // 10 minutes
```
### StringEncoder
Simple XOR-based string obfuscation for hiding API paths in source code.
**WARNING:** This is NOT encryption. It only prevents casual inspection.
```php
use Magdev\WcLicensedProductClient\Security\StringEncoder;
$encoder = new StringEncoder();
// Encode strings for embedding in source code
$encoded = $encoder->encode('/wp-json/wc-licensed-product/v1');
// Result: "MR4xBi8rPDUcHhk2EQ0TICsvMBo9Mw0HJRE="
// Decode at runtime
$decoded = $encoder->decode('MR4xBi8rPDUcHhk2EQ0TICsvMBo9Mw0HJRE=');
// Result: "/wp-json/wc-licensed-product/v1"
// Generate multiple encoded constants
$constants = $encoder->generateEncodedConstants([
'API_PATH' => '/wp-json/wc-licensed-product/v1',
'VALIDATE' => 'validate',
'STATUS' => 'status',
]);
```
**Custom Encoder Key:**
```php
$encoder = new StringEncoder('my_custom_encoder_key');
```
### IntegrityChecker
Verifies that critical source files haven't been modified by comparing SHA256 hashes.
```php
use Magdev\WcLicensedProductClient\Security\IntegrityChecker;
// Define expected hashes (generated during build)
$expectedHashes = [
'src/SecureLicenseClient.php' => 'abc123...',
'src/Security/ResponseSignature.php' => 'def456...',
];
$checker = new IntegrityChecker($expectedHashes, '/path/to/project');
// Verify all files
try {
$checker->verify();
echo "All files intact";
} catch (IntegrityException $e) {
echo "Tampered files: " . implode(', ', $e->failures);
}
// Or check silently
if (!$checker->isValid()) {
die("Critical files have been modified!");
}
```
**Generating Hashes During Build:**
```php
$checker = new IntegrityChecker([], '/path/to/project');
$hashes = $checker->generateHashes([
'src/SecureLicenseClient.php',
'src/Security/ResponseSignature.php',
'src/Security/SignatureException.php',
]);
// Output as PHP code for embedding
echo $checker->generateHashesAsPhpCode([
'src/SecureLicenseClient.php',
'src/Security/ResponseSignature.php',
]);
```
## Caching
Both clients support PSR-6 caching to reduce API calls. Cache keys use SHA256 hashes of license keys to avoid exposing sensitive data.
**Cache Key Format:**
```text
wc_license_{operation}_{sha256(license_key)}[_{sha256(domain)}]
```
**Example with Symfony Cache:**
```php
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
$cache = new FilesystemAdapter(
namespace: 'license_client',
defaultLifetime: 300,
directory: '/tmp/license-cache'
);
$client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: 'https://example.com',
serverSecret: 'your-secret',
cache: $cache,
cacheTtl: 600, // 10 minutes
);
```
**Cache Behavior:**
- `validate()` and `status()` responses are cached
- `activate()` responses are NOT cached
- After `activate()`, related cache entries are automatically invalidated
## Logging
Both clients support PSR-3 logging for debugging and monitoring.
**Log Levels Used:**
| Level | When Used |
| ----- | --------- |
| `debug` | Cache hits, signature verification success |
| `info` | API calls, successful operations |
| `warning` | API error responses, missing signatures |
| `error` | Request failures, network errors |
| `critical` | Integrity check failures |
**Example with Monolog:**
```php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$logger = new Logger('license');
$logger->pushHandler(new StreamHandler('/var/log/license.log', Logger::DEBUG));
$client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: 'https://example.com',
serverSecret: 'your-secret',
logger: $logger,
);
```
## Integration Guide
### Basic Integration
The simplest integration for existing PHP projects:
```php
<?php
// license-check.php
use Magdev\WcLicensedProductClient\SecureLicenseClient;
use Magdev\WcLicensedProductClient\Exception\LicenseException;
use Symfony\Component\HttpClient\HttpClient;
// Configuration (load from environment/config file)
$licenseKey = getenv('MY_PRODUCT_LICENSE_KEY');
$serverSecret = getenv('LICENSE_SERVER_SECRET');
$licenseServer = 'https://your-license-server.com';
$currentDomain = $_SERVER['HTTP_HOST'] ?? php_uname('n');
// Create client
$client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: $licenseServer,
serverSecret: $serverSecret,
);
// Validate license
try {
$info = $client->validate($licenseKey, $currentDomain);
// Define constant for use throughout application
define('MY_PRODUCT_LICENSED', true);
define('MY_PRODUCT_ID', $info->productId);
} catch (LicenseException $e) {
define('MY_PRODUCT_LICENSED', false);
define('MY_PRODUCT_ID', null);
// Log the error but don't expose details to users
error_log("License validation failed: " . $e->getMessage());
}
```
### WordPress Plugin Integration
```php
<?php
/**
* Plugin Name: My Licensed Plugin
*/
namespace MyPlugin;
use Magdev\WcLicensedProductClient\SecureLicenseClient;
use Magdev\WcLicensedProductClient\Exception\LicenseException;
use Symfony\Component\HttpClient\HttpClient;
class LicenseManager
{
private const LICENSE_OPTION = 'my_plugin_license_key';
private const SERVER_URL = 'https://your-license-server.com';
private const SERVER_SECRET = 'your-server-secret'; // Or load from wp-config.php
private ?SecureLicenseClient $client = null;
public function __construct()
{
$this->client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: self::SERVER_URL,
serverSecret: self::SERVER_SECRET,
);
}
public function isLicenseValid(): bool
{
$licenseKey = get_option(self::LICENSE_OPTION);
if (empty($licenseKey)) {
return false;
}
// Use transient for caching
$cacheKey = 'my_plugin_license_valid_' . md5($licenseKey);
$cached = get_transient($cacheKey);
if ($cached !== false) {
return $cached === 'valid';
}
try {
$domain = wp_parse_url(home_url(), PHP_URL_HOST);
$this->client->validate($licenseKey, $domain);
set_transient($cacheKey, 'valid', HOUR_IN_SECONDS);
return true;
} catch (LicenseException $e) {
set_transient($cacheKey, 'invalid', 5 * MINUTE_IN_SECONDS);
return false;
}
}
public function activateLicense(string $licenseKey): array
{
try {
$domain = wp_parse_url(home_url(), PHP_URL_HOST);
$result = $this->client->activate($licenseKey, $domain);
if ($result->success) {
update_option(self::LICENSE_OPTION, $licenseKey);
delete_transient('my_plugin_license_valid_' . md5($licenseKey));
}
return [
'success' => $result->success,
'message' => $result->message,
];
} catch (LicenseException $e) {
return [
'success' => false,
'message' => $e->getMessage(),
];
}
}
}
```
### Laravel Integration
**Service Provider:**
```php
<?php
// app/Providers/LicenseServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Magdev\WcLicensedProductClient\LicenseClientInterface;
use Magdev\WcLicensedProductClient\SecureLicenseClient;
use Symfony\Component\HttpClient\HttpClient;
use Illuminate\Log\Logger;
use Illuminate\Cache\CacheManager;
class LicenseServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(LicenseClientInterface::class, function ($app) {
return new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: config('services.license.url'),
serverSecret: config('services.license.secret'),
logger: $app->make(Logger::class),
cache: $app->make(CacheManager::class)->store('file')->getStore(),
);
});
}
}
```
**Configuration (config/services.php):**
```php
return [
// ...
'license' => [
'url' => env('LICENSE_SERVER_URL'),
'secret' => env('LICENSE_SERVER_SECRET'),
],
];
```
**Usage in Controller:**
```php
<?php
namespace App\Http\Controllers;
use Magdev\WcLicensedProductClient\LicenseClientInterface;
use Magdev\WcLicensedProductClient\Exception\LicenseException;
class LicenseController extends Controller
{
public function __construct(
private readonly LicenseClientInterface $licenseClient,
) {}
public function validate(Request $request)
{
try {
$info = $this->licenseClient->validate(
$request->input('license_key'),
$request->getHost(),
);
return response()->json([
'valid' => true,
'product_id' => $info->productId,
'expires_at' => $info->expiresAt?->format('Y-m-d'),
]);
} catch (LicenseException $e) {
return response()->json([
'valid' => false,
'error' => $e->errorCode,
'message' => $e->getMessage(),
], 403);
}
}
}
```
### Symfony Integration
**Service Configuration (config/services.yaml):**
```yaml
services:
Magdev\WcLicensedProductClient\LicenseClientInterface:
class: Magdev\WcLicensedProductClient\SecureLicenseClient
arguments:
$httpClient: '@Symfony\Contracts\HttpClient\HttpClientInterface'
$baseUrl: '%env(LICENSE_SERVER_URL)%'
$serverSecret: '%env(LICENSE_SERVER_SECRET)%'
$logger: '@logger'
$cache: '@cache.app'
```
## Best Practices
### 1. Use the Secure Client in Production
Always use `SecureLicenseClient` in production to verify response signatures:
```php
// Development
$client = new LicenseClient(...);
// Production
$client = new SecureLicenseClient(..., serverSecret: $secret);
```
### 2. Handle All Exception Types
Don't catch just the base exception. Handle specific cases:
```php
try {
$client->validate($key, $domain);
} catch (SignatureException $e) {
// Critical: possible tampering
notifyAdmin("Signature verification failed!");
die("Security error");
} catch (RateLimitExceededException $e) {
// Retry later
sleep($e->retryAfter ?? 60);
retry();
} catch (LicenseExpiredException $e) {
// Prompt renewal
showRenewalForm();
} catch (LicenseException $e) {
// Generic handling
showError($e->getMessage());
}
```
### 3. Cache Validation Results
Use caching to minimize API calls:
```php
$cache = new FilesystemAdapter('licenses', 300);
$client = new SecureLicenseClient(..., cache: $cache, cacheTtl: 300);
```
### 4. Store Secrets Securely
Never hardcode secrets in source code:
```php
// Bad
$client = new SecureLicenseClient(..., serverSecret: 'abc123');
// Good
$client = new SecureLicenseClient(..., serverSecret: getenv('LICENSE_SECRET'));
```
### 5. Validate on Application Startup
Check license validity early, not on every request:
```php
// bootstrap.php
$isLicensed = checkLicense();
define('APP_LICENSED', $isLicensed);
// Later in code
if (APP_LICENSED) {
showPremiumFeatures();
}
```
### 6. Log License Events
Enable logging for debugging and audit trails:
```php
$client = new SecureLicenseClient(
...,
logger: new Logger('license'),
);
```
### 7. Implement Graceful Degradation
Don't crash the entire application on license failure:
```php
try {
$info = $client->validate($key, $domain);
enablePremiumFeatures($info);
} catch (LicenseException $e) {
// Fall back to free tier
enableFreeFeatures();
logLicenseIssue($e);
}
```
## API Reference
### API Endpoints
The client communicates with these REST API endpoints:
| Endpoint | Method | Description |
| -------- | ------ | ----------- |
| `/wp-json/wc-licensed-product/v1/validate` | POST | Validate license for domain |
| `/wp-json/wc-licensed-product/v1/status` | POST | Get license status details |
| `/wp-json/wc-licensed-product/v1/activate` | POST | Activate license on domain |
### Request Format
All requests use JSON bodies:
```json
{
"license_key": "ABCD-1234-EFGH-5678",
"domain": "example.com"
}
```
### Response Format
**Successful Validation:**
```json
{
"valid": true,
"license": {
"product_id": 123,
"expires_at": "2027-01-21",
"version_id": null
}
}
```
**Error Response:**
```json
{
"valid": false,
"error": "license_expired",
"message": "This license has expired."
}
```
### Error Codes
| Error Code | Exception Class | Description |
| ---------- | --------------- | ----------- |
| `license_not_found` | `LicenseNotFoundException` | License key doesn't exist |
| `license_expired` | `LicenseExpiredException` | License past expiration date |
| `license_revoked` | `LicenseRevokedException` | License manually revoked |
| `license_inactive` | `LicenseInactiveException` | License not activated |
| `license_invalid` | `LicenseInvalidException` | License invalid for operation |
| `domain_mismatch` | `DomainMismatchException` | Domain not authorized |
| `max_activations_reached` | `MaxActivationsReachedException` | Too many activations |
| `activation_failed` | `ActivationFailedException` | Server error during activation |
| `rate_limit_exceeded` | `RateLimitExceededException` | Too many requests |
| `signature_invalid` | `SignatureException` | Response signature invalid |
| `integrity_check_failed` | `IntegrityException` | Source files modified |
## Troubleshooting
### "Response is not signed by the server"
- Server not configured with `WC_LICENSE_SERVER_SECRET`
- Using `SecureLicenseClient` with non-signing server
- **Solution:** Configure server secret or use `LicenseClient`
### "Response signature verification failed"
- Different secrets on server/client
- Clock skew > 5 minutes between server and client
- Response modified by proxy/cache
- **Solution:** Verify secrets match, sync clocks, disable response caching
### Connection Timeouts
- Network issues between client and license server
- **Solution:** Configure HTTP client timeout
```php
$httpClient = HttpClient::create([
'timeout' => 30,
'max_duration' => 60,
]);
```
### Rate Limiting
- Too many requests from same IP
- **Solution:** Implement caching, handle `RateLimitExceededException`
```php
catch (RateLimitExceededException $e) {
$waitSeconds = $e->retryAfter ?? 60;
// Implement exponential backoff
}
```