From 56abe8a97c72419c07a6daf263ba6f4a9b5fe4b1 Mon Sep 17 00:00:00 2001 From: magdev Date: Wed, 28 Jan 2026 11:56:47 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 14 +++ CLAUDE.md | 20 ++++ docs/server-implementation.md | 178 ++++++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1148b2..6a0c451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index b6744be..28365fb 100644 --- a/CLAUDE.md +++ b/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) - Package hash format uses `sha256:hexstring` prefix for integrity verification - 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 diff --git a/docs/server-implementation.md b/docs/server-implementation.md index 644653a..b8be392 100644 --- a/docs/server-implementation.md +++ b/docs/server-implementation.md @@ -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 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 | | `max_activations_reached` | 403 | Maximum activations limit exceeded | | `activation_failed` | 500 | Server error during activation | +| `product_not_found` | 404 | Licensed product not found | | `rate_limit_exceeded` | 429 | Too many requests | ### Rate Limit Response @@ -366,6 +425,13 @@ final class LicenseApi 'permission_callback' => [$this, 'checkRateLimit'], '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 } + 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 { return is_string($value) && strlen($value) <= 64 && strlen($value) >= 8; @@ -598,6 +692,62 @@ final class LicenseApi ], 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 { $data = [ @@ -732,6 +882,34 @@ final class LicenseApi 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 { if ($license === null || empty($license['expires_at'])) {