You've already forked wc-licensed-product-client
Add update-check endpoint documentation (v0.2.2)
- Add /update-check endpoint documentation to server-implementation.md - Add product_not_found error code to error codes table - Add handleUpdateCheck() handler example in WordPress plugin - Add findProduct() method stub for product lookups - Verified client implementation aligns with server documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
14
CHANGELOG.md
14
CHANGELOG.md
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.2.2] - 2026-01-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `/update-check` endpoint documentation in server-implementation.md
|
||||||
|
- `product_not_found` error code to error codes table
|
||||||
|
- `handleUpdateCheck()` handler example in WordPress plugin implementation
|
||||||
|
- `findProduct()` method stub for product lookups
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Verified client implementation aligns with updated server documentation
|
||||||
|
- All signature algorithms, key derivation, and JSON canonicalization match server
|
||||||
|
|
||||||
## [0.2.1] - 2026-01-27
|
## [0.2.1] - 2026-01-27
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
20
CLAUDE.md
20
CLAUDE.md
@@ -257,3 +257,23 @@ When editing CLAUDE.md or other markdown files, follow these rules to avoid lint
|
|||||||
- `UpdateInfo` DTO supports both `download_url` and `package` fields (WordPress compatibility)
|
- `UpdateInfo` DTO supports both `download_url` and `package` fields (WordPress compatibility)
|
||||||
- Package hash format uses `sha256:hexstring` prefix for integrity verification
|
- Package hash format uses `sha256:hexstring` prefix for integrity verification
|
||||||
- StringEncoder can generate encoded constants for obfuscated endpoint names
|
- StringEncoder can generate encoded constants for obfuscated endpoint names
|
||||||
|
|
||||||
|
### 2026-01-28 - Version 0.2.2 (Server Documentation Alignment)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Fetched and compared remote server-implementation.md against client implementation
|
||||||
|
- Verified client signature algorithm matches server exactly (HKDF, recursive key sorting, JSON flags)
|
||||||
|
- Added `/update-check` endpoint documentation to `docs/server-implementation.md`
|
||||||
|
- Added `product_not_found` error code to error codes table
|
||||||
|
- Added `handleUpdateCheck()` handler example in WordPress plugin implementation
|
||||||
|
- Added `findProduct()` method stub for product lookups
|
||||||
|
- Added `getUpdateCheckArgs()` for request validation
|
||||||
|
- All 66 tests pass
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- Client implementation was already fully aligned with server documentation
|
||||||
|
- Server-implementation.md needed update-check endpoint documentation (added in v0.2.1 client but not server docs)
|
||||||
|
- Documentation alignment is as important as code alignment for maintainability
|
||||||
|
- Remote server docs fetched successfully via WebFetch tool
|
||||||
|
|||||||
@@ -153,6 +153,64 @@ Activate a license on a specific domain.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### POST /update-check
|
||||||
|
|
||||||
|
Check for plugin updates for a licensed product.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"license_key": "ABCD-1234-EFGH-5678",
|
||||||
|
"domain": "example.com",
|
||||||
|
"plugin_slug": "my-licensed-plugin",
|
||||||
|
"current_version": "1.0.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success Response (200) - Update Available:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"update_available": true,
|
||||||
|
"version": "1.2.0",
|
||||||
|
"slug": "my-licensed-plugin",
|
||||||
|
"plugin": "my-licensed-plugin/my-licensed-plugin.php",
|
||||||
|
"download_url": "https://example.com/license-download/123-456-abc123",
|
||||||
|
"package": "https://example.com/license-download/123-456-abc123",
|
||||||
|
"last_updated": "2026-01-27",
|
||||||
|
"tested": "6.7",
|
||||||
|
"requires": "6.0",
|
||||||
|
"requires_php": "8.3",
|
||||||
|
"changelog": "## 1.2.0\n- New feature added\n- Bug fixes",
|
||||||
|
"package_hash": "sha256:abc123def456...",
|
||||||
|
"name": "My Licensed Plugin",
|
||||||
|
"homepage": "https://example.com/product/my-plugin"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success Response (200) - No Update:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"update_available": false,
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Response (404):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"update_available": false,
|
||||||
|
"error": "product_not_found",
|
||||||
|
"message": "Licensed product not found."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Error Codes
|
## Error Codes
|
||||||
|
|
||||||
The API uses consistent error codes for programmatic handling:
|
The API uses consistent error codes for programmatic handling:
|
||||||
@@ -167,6 +225,7 @@ The API uses consistent error codes for programmatic handling:
|
|||||||
| `domain_mismatch` | 403 | License not authorized for this domain |
|
| `domain_mismatch` | 403 | License not authorized for this domain |
|
||||||
| `max_activations_reached` | 403 | Maximum activations limit exceeded |
|
| `max_activations_reached` | 403 | Maximum activations limit exceeded |
|
||||||
| `activation_failed` | 500 | Server error during activation |
|
| `activation_failed` | 500 | Server error during activation |
|
||||||
|
| `product_not_found` | 404 | Licensed product not found |
|
||||||
| `rate_limit_exceeded` | 429 | Too many requests |
|
| `rate_limit_exceeded` | 429 | Too many requests |
|
||||||
|
|
||||||
### Rate Limit Response
|
### Rate Limit Response
|
||||||
@@ -366,6 +425,13 @@ final class LicenseApi
|
|||||||
'permission_callback' => [$this, 'checkRateLimit'],
|
'permission_callback' => [$this, 'checkRateLimit'],
|
||||||
'args' => $this->getActivateArgs(),
|
'args' => $this->getActivateArgs(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
register_rest_route(self::API_NAMESPACE, '/update-check', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => [$this, 'handleUpdateCheck'],
|
||||||
|
'permission_callback' => [$this, 'checkRateLimit'],
|
||||||
|
'args' => $this->getUpdateCheckArgs(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -407,6 +473,34 @@ final class LicenseApi
|
|||||||
return $this->getValidateArgs(); // Same as validate
|
return $this->getValidateArgs(); // Same as validate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getUpdateCheckArgs(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'license_key' => [
|
||||||
|
'required' => true,
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'validate_callback' => [$this, 'validateLicenseKeyFormat'],
|
||||||
|
],
|
||||||
|
'domain' => [
|
||||||
|
'required' => true,
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'validate_callback' => [$this, 'validateDomainFormat'],
|
||||||
|
],
|
||||||
|
'plugin_slug' => [
|
||||||
|
'required' => false,
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
],
|
||||||
|
'current_version' => [
|
||||||
|
'required' => false,
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function validateLicenseKeyFormat($value): bool
|
public function validateLicenseKeyFormat($value): bool
|
||||||
{
|
{
|
||||||
return is_string($value) && strlen($value) <= 64 && strlen($value) >= 8;
|
return is_string($value) && strlen($value) <= 64 && strlen($value) >= 8;
|
||||||
@@ -598,6 +692,62 @@ final class LicenseApi
|
|||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function handleUpdateCheck(\WP_REST_Request $request): \WP_REST_Response|\WP_Error
|
||||||
|
{
|
||||||
|
$licenseKey = $request->get_param('license_key');
|
||||||
|
$domain = $request->get_param('domain');
|
||||||
|
$pluginSlug = $request->get_param('plugin_slug');
|
||||||
|
$currentVersion = $request->get_param('current_version');
|
||||||
|
|
||||||
|
$license = $this->findLicense($licenseKey);
|
||||||
|
|
||||||
|
if ($license === null) {
|
||||||
|
return $this->errorResponse('license_not_found', 'License key not found.', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($license['status'] !== 'active' || $this->isExpired($license)) {
|
||||||
|
return $this->errorResponse('license_invalid', 'License validation failed.', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($license['domain']) && $license['domain'] !== $domain) {
|
||||||
|
return $this->errorResponse('domain_mismatch', 'This license is not valid for this domain.', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Replace with your product lookup logic
|
||||||
|
$product = $this->findProduct($license['product_id']);
|
||||||
|
|
||||||
|
if ($product === null) {
|
||||||
|
return $this->errorResponse('product_not_found', 'Licensed product not found.', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$latestVersion = $product['version'] ?? '1.0.0';
|
||||||
|
$updateAvailable = $currentVersion !== null
|
||||||
|
&& version_compare($currentVersion, $latestVersion, '<');
|
||||||
|
|
||||||
|
$response = [
|
||||||
|
'success' => true,
|
||||||
|
'update_available' => $updateAvailable,
|
||||||
|
'version' => $latestVersion,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($updateAvailable) {
|
||||||
|
$response['slug'] = $product['slug'] ?? $pluginSlug;
|
||||||
|
$response['plugin'] = ($product['slug'] ?? $pluginSlug) . '/' . ($product['slug'] ?? $pluginSlug) . '.php';
|
||||||
|
$response['download_url'] = $product['download_url'] ?? null;
|
||||||
|
$response['package'] = $product['download_url'] ?? null;
|
||||||
|
$response['last_updated'] = $product['last_updated'] ?? null;
|
||||||
|
$response['tested'] = $product['tested'] ?? null;
|
||||||
|
$response['requires'] = $product['requires'] ?? null;
|
||||||
|
$response['requires_php'] = $product['requires_php'] ?? null;
|
||||||
|
$response['changelog'] = $product['changelog'] ?? null;
|
||||||
|
$response['package_hash'] = $product['package_hash'] ?? null;
|
||||||
|
$response['name'] = $product['name'] ?? null;
|
||||||
|
$response['homepage'] = $product['homepage'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new \WP_REST_Response($response, 200);
|
||||||
|
}
|
||||||
|
|
||||||
private function errorResponse(string $code, string $message, int $status): \WP_REST_Response
|
private function errorResponse(string $code, string $message, int $status): \WP_REST_Response
|
||||||
{
|
{
|
||||||
$data = [
|
$data = [
|
||||||
@@ -732,6 +882,34 @@ final class LicenseApi
|
|||||||
return false; // Replace with actual implementation
|
return false; // Replace with actual implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a product by ID.
|
||||||
|
*
|
||||||
|
* TODO: Replace with your WooCommerce product lookup
|
||||||
|
*
|
||||||
|
* @return array|null Product data or null if not found
|
||||||
|
*/
|
||||||
|
private function findProduct(int $productId): ?array
|
||||||
|
{
|
||||||
|
// Example structure - replace with actual database lookup
|
||||||
|
// return [
|
||||||
|
// 'product_id' => 123,
|
||||||
|
// 'name' => 'My Licensed Plugin',
|
||||||
|
// 'slug' => 'my-licensed-plugin',
|
||||||
|
// 'version' => '1.2.0',
|
||||||
|
// 'download_url' => 'https://example.com/license-download/123-abc',
|
||||||
|
// 'last_updated' => '2026-01-27',
|
||||||
|
// 'tested' => '6.7',
|
||||||
|
// 'requires' => '6.0',
|
||||||
|
// 'requires_php' => '8.3',
|
||||||
|
// 'changelog' => '## 1.2.0\n- New feature\n- Bug fixes',
|
||||||
|
// 'package_hash' => 'sha256:abc123...',
|
||||||
|
// 'homepage' => 'https://example.com/product/my-plugin',
|
||||||
|
// ];
|
||||||
|
|
||||||
|
return null; // Replace with actual implementation
|
||||||
|
}
|
||||||
|
|
||||||
private function isExpired(?array $license): bool
|
private function isExpired(?array $license): bool
|
||||||
{
|
{
|
||||||
if ($license === null || empty($license['expires_at'])) {
|
if ($license === null || empty($license['expires_at'])) {
|
||||||
|
|||||||
Reference in New Issue
Block a user