diff --git a/docs/code-bridge/archive/RES-2026-04-12-phase11bc-deploy.md b/docs/code-bridge/archive/RES-2026-04-12-phase11bc-deploy.md new file mode 100644 index 0000000..6a44d9f --- /dev/null +++ b/docs/code-bridge/archive/RES-2026-04-12-phase11bc-deploy.md @@ -0,0 +1,35 @@ +# Architectural Response + +**Re:** Phase 11B/C Deployment Complete +**Date:** 2026-04-12 +**From:** Chronicler #84 — The Meridian + +--- + +## Status: DEPLOYED ✅ + +### Discord Role Created +- Name: **ModpackChecker Customer** +- Color: Frost teal (#1A9ECF) +- ID: `1493061127423262870` + +### Env Var Added +``` +MVC_CUSTOMER_ROLE_ID=1493061127423262870 +``` + +### Code Deployed +- `src/discord/verifymvc.js` ✅ +- `src/discord/events.js` ✅ +- `src/index.js` ✅ + +### Verification +- Health: `{"status":"online","bot":"The Arbiter#6636"}` ✅ +- Logs: `✅ Successfully reloaded application (/) commands` ✅ +- `/verify-mvc` is live in Discord + +--- + +Proceed to Phase 11D. + +*— Chronicler #84, The Meridian* diff --git a/docs/code-bridge/status/ACTIVE_CONTEXT.md b/docs/code-bridge/status/ACTIVE_CONTEXT.md index 183957b..a9f6e11 100644 --- a/docs/code-bridge/status/ACTIVE_CONTEXT.md +++ b/docs/code-bridge/status/ACTIVE_CONTEXT.md @@ -1,19 +1,19 @@ # Code Status Update -**Last Updated:** 2026-04-12 20:45 CDT +**Last Updated:** 2026-04-12 21:15 CDT ## Current Focus -Phase 11B/C complete — /verify-mvc slash command written. Ready for Chronicler deployment + role creation. +Phase 11D complete — Blueprint extension licensing integration written. Ready for testing. ## Recently Completed -- Phase 11A: DEPLOYED ✅ — migration ran, API routes live, health check passed -- Phase 11B/C: Created `src/discord/verifymvc.js` — /verify-mvc slash command -- Phase 11B/C: Wired into events.js handler + index.js command registration -- Command assigns MVC_CUSTOMER_ROLE_ID role on successful verification +- Phase 11A: DEPLOYED ✅ — DB + API routes live on Command Center +- Phase 11B/C: DEPLOYED ✅ — /verify-mvc live, customer role created (1493061127423262870) +- Phase 11D: Created LicenseService.php — activate, validate, deactivate, grace period, tier check +- Phase 11D: Created ValidateLicense.php — `mvc:validate` Artisan command (daily phone-home) +- Phase 11D: Updated admin controller.php — license activation/deactivation in update handler +- Phase 11D: Updated admin view.blade.php — order ID input, status indicator, grace/expired banners, dynamic tier gating ## Next Steps Pending -- **DEPLOY: Chronicler restarts Arbiter + creates ModpackChecker Customer role on Discord** -- **Chronicler needs to:** set MVC_CUSTOMER_ROLE_ID in .env after creating role -- Phase 11D: Blueprint extension — license activation UI, phone-home cron, tier gating +- Phase 11D testing: build .blueprint, install, verify license flow end-to-end - Phase 11E: GitBook knowledge base migration - Phase 11F: BuiltByBit listing creation (Standard $14.99, Professional $24.99) - Phase 11G: Business hours & support boundaries diff --git a/services/modpack-version-checker/blueprint-extension/admin/controller.php b/services/modpack-version-checker/blueprint-extension/admin/controller.php index 8cac306..bce24eb 100644 --- a/services/modpack-version-checker/blueprint-extension/admin/controller.php +++ b/services/modpack-version-checker/blueprint-extension/admin/controller.php @@ -7,6 +7,7 @@ use Illuminate\View\Factory as ViewFactory; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Admin\BlueprintAdminLibrary as BlueprintExtensionLibrary; use Pterodactyl\Http\Requests\Admin\AdminFormRequest; +use Pterodactyl\Services\LicenseService; use Illuminate\Http\RedirectResponse; class modpackcheckerExtensionController extends Controller @@ -14,54 +15,70 @@ class modpackcheckerExtensionController extends Controller public function __construct( private ViewFactory $view, private BlueprintExtensionLibrary $blueprint, + private LicenseService $licenseService, ) {} public function index(): View { - // Get current settings $curseforge_api_key = $this->blueprint->dbGet('modpackchecker', 'curseforge_api_key'); $discord_webhook_url = $this->blueprint->dbGet('modpackchecker', 'discord_webhook_url'); $check_interval = $this->blueprint->dbGet('modpackchecker', 'check_interval'); - $tier = $this->blueprint->dbGet('modpackchecker', 'tier'); - // Set defaults if empty if ($check_interval == '') { $this->blueprint->dbSet('modpackchecker', 'check_interval', 'daily'); $check_interval = 'daily'; } - if ($tier == '') { - $this->blueprint->dbSet('modpackchecker', 'tier', 'standard'); - $tier = 'standard'; - } + + $license = $this->licenseService->getState(); return $this->view->make( 'admin.extensions.modpackchecker.index', [ 'curseforge_api_key' => $curseforge_api_key, 'discord_webhook_url' => $discord_webhook_url, 'check_interval' => $check_interval, - 'tier' => $tier, + 'license' => $license, 'root' => '/admin/extensions/modpackchecker', 'blueprint' => $this->blueprint, ] ); } - /** - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ public function update(modpackcheckerSettingsFormRequest $request): RedirectResponse { + // Handle license activation + $orderId = $request->input('order_id'); + if (!empty($orderId)) { + $currentOrderId = $this->blueprint->dbGet('modpackchecker', 'order_id'); + if ($orderId !== $currentOrderId) { + $result = $this->licenseService->activate($orderId); + if (!$result['success']) { + return redirect() + ->route('admin.extensions.modpackchecker.index') + ->with('error', 'License activation failed: ' . ($result['error'] ?? 'Unknown error')); + } + } + } + + // Handle license deactivation + if ($request->input('deactivate_license') === '1') { + $this->licenseService->deactivate(); + return redirect() + ->route('admin.extensions.modpackchecker.index') + ->with('success', 'License deactivated.'); + } + + // Save standard settings $this->blueprint->dbSet('modpackchecker', 'curseforge_api_key', $request->input('curseforge_api_key') ?? ''); - // Only save PRO-tier fields if the user is on the pro tier - $tier = $this->blueprint->dbGet('modpackchecker', 'tier') ?: 'standard'; - if ($tier === 'pro') { + // Only save PRO-tier fields if licensed as professional + if ($this->licenseService->isProFeatureAllowed()) { $this->blueprint->dbSet('modpackchecker', 'discord_webhook_url', $request->input('discord_webhook_url') ?? ''); $this->blueprint->dbSet('modpackchecker', 'check_interval', $request->input('check_interval') ?? 'daily'); } - return redirect()->route('admin.extensions.modpackchecker.index')->with('success', 'Settings saved successfully.'); + return redirect() + ->route('admin.extensions.modpackchecker.index') + ->with('success', 'Settings saved successfully.'); } } @@ -70,6 +87,8 @@ class modpackcheckerSettingsFormRequest extends AdminFormRequest public function rules(): array { return [ + 'order_id' => 'nullable|string|max:64', + 'deactivate_license' => 'nullable|string', 'curseforge_api_key' => 'nullable|string|max:500', 'discord_webhook_url' => 'nullable|url|max:500', 'check_interval' => 'nullable|in:daily,12h,6h', @@ -79,6 +98,7 @@ class modpackcheckerSettingsFormRequest extends AdminFormRequest public function attributes(): array { return [ + 'order_id' => 'Order ID', 'curseforge_api_key' => 'CurseForge API Key', 'discord_webhook_url' => 'Discord Webhook URL', 'check_interval' => 'Check Interval', diff --git a/services/modpack-version-checker/blueprint-extension/admin/view.blade.php b/services/modpack-version-checker/blueprint-extension/admin/view.blade.php index 99e36f8..8471aaa 100644 --- a/services/modpack-version-checker/blueprint-extension/admin/view.blade.php +++ b/services/modpack-version-checker/blueprint-extension/admin/view.blade.php @@ -61,7 +61,81 @@ + + @if($license['status'] === 'expired') +
+
+
+

License Expired

+

Your license could not be validated. Pro features are disabled. Please check your order ID or contact support.

+
+
+
+ @elseif($license['status'] === 'grace') +
+
+
+

License Validation Warning

+

Could not reach license server. Grace period expires: {{ $license['grace_expires'] }}

+
+
+
+ @endif + +
+
+
+
+

+ License + @if($license['status'] === 'active') + Active + @elseif($license['status'] === 'grace') + Grace Period + @elseif($license['status'] === 'expired') + Expired + @else + Not Activated + @endif +

+
+
+
+ + +

+ Enter the order ID from your BuiltByBit purchase to activate. +

+
+ @if($license['status'] === 'active' || $license['status'] === 'grace') +
+

+ Tier: {{ ucfirst($license['tier']) }} +  |  + Last Validated: {{ $license['last_validated'] ?: 'Never' }} +

+
+ @endif + @if(!empty($license['order_id'])) +
+ +
+ @endif +
+
+
+
@@ -83,7 +157,7 @@ autocomplete="off" />

- Get your free API key from + Get your free API key from console.curseforge.com. Required for CurseForge modpack detection.

@@ -91,7 +165,9 @@
+ +
@@ -104,22 +180,25 @@
-

- Standard tier is locked to daily cron checks. - Upgrade to Professional for more frequent automated checks. + @if($license['tier'] === 'professional' && $license['status'] !== 'expired') + Professional tier — custom check intervals enabled. + @else + Standard tier is locked to daily checks. + Upgrade to Professional for more frequent automated checks. + @endif

-
-
@@ -139,16 +218,22 @@ value="{{ $discord_webhook_url }}" placeholder="https://discord.com/api/webhooks/..." class="form-control" - disabled + {{ $license['tier'] !== 'professional' || $license['status'] === 'expired' ? 'disabled' : '' }} />

- Upgrade to Professional to receive automated update alerts in your Discord server. + @if($license['tier'] === 'professional' && $license['status'] !== 'expired') + Professional tier — Discord webhook alerts enabled. + @else + Upgrade to Professional to receive automated update alerts in your Discord server. + @endif

+ +
@@ -177,12 +262,37 @@
  • FTB (modpacks.ch) - (No key required) + + @if($license['tier'] === 'professional') + (Enabled — Professional) + @else + (PRO TIER only) + @endif +
  • + + + @if(!empty($license['latest_version']) && $license['latest_version'] !== '1.0.0') +
    +
    +
    +

    + Update Available +

    +
    +
    +

    A new version ({{ $license['latest_version'] }}) is available.

    + + Download from BuiltByBit + +
    +
    +
    + @endif @@ -192,10 +302,10 @@

    How It Works

    ModpackChecker automatically detects modpacks via Egg Variables or file fingerprinting. - Set MODPACK_PLATFORM and + Set MODPACK_PLATFORM and MODPACK_ID in your server's startup variables - for the most reliable detection, or let the extension scan for - manifest.json / + for the most reliable detection, or let the extension scan for + manifest.json / modrinth.index.json files.

    diff --git a/services/modpack-version-checker/blueprint-extension/app/Console/Commands/ValidateLicense.php b/services/modpack-version-checker/blueprint-extension/app/Console/Commands/ValidateLicense.php new file mode 100644 index 0000000..662d7aa --- /dev/null +++ b/services/modpack-version-checker/blueprint-extension/app/Console/Commands/ValidateLicense.php @@ -0,0 +1,47 @@ +> /dev/null 2>&1 + * + * Calls Arbiter /api/mvc/validate to confirm license is still active. + * On failure, enters 7-day grace period. After grace expires, marks expired. + */ + +namespace Pterodactyl\Console\Commands; + +use Illuminate\Console\Command; +use Pterodactyl\Services\LicenseService; + +class ValidateLicense extends Command +{ + protected $signature = 'mvc:validate'; + protected $description = 'Validate ModpackChecker license (daily phone-home)'; + + public function __construct(private LicenseService $licenseService) + { + parent::__construct(); + } + + public function handle(): int + { + $this->info('Validating ModpackChecker license...'); + + $result = $this->licenseService->validate(); + + match ($result['status']) { + 'active' => $this->info("✅ License active — tier: {$result['tier']}"), + 'grace' => $this->warn("⚠️ Grace period — expires: {$result['expires']}"), + 'expired' => $this->error("❌ License expired"), + 'inactive' => $this->line("No license configured"), + default => $this->line("Status: {$result['status']}"), + }; + + return 0; + } +} diff --git a/services/modpack-version-checker/blueprint-extension/app/Services/LicenseService.php b/services/modpack-version-checker/blueprint-extension/app/Services/LicenseService.php new file mode 100644 index 0000000..ea0ac3f --- /dev/null +++ b/services/modpack-version-checker/blueprint-extension/app/Services/LicenseService.php @@ -0,0 +1,195 @@ +post("{$this->arbiterBase}/activate", [ + 'order_id' => $orderId, + 'domain' => $domain, + 'ip' => request()->server('SERVER_ADDR', ''), + ]); + + $data = $response->json(); + + if ($response->successful() && ($data['status'] ?? '') === 'active') { + $this->blueprint->dbSet('modpackchecker', 'order_id', $orderId); + $this->blueprint->dbSet('modpackchecker', 'license_status', 'active'); + $this->blueprint->dbSet('modpackchecker', 'tier', $data['tier'] ?? 'standard'); + $this->blueprint->dbSet('modpackchecker', 'last_validated', now()->toISOString()); + $this->blueprint->dbSet('modpackchecker', 'grace_expires', ''); + + return ['success' => true, 'tier' => $data['tier'] ?? 'standard']; + } + + return ['success' => false, 'error' => $data['error'] ?? 'Activation failed']; + + } catch (\Exception $e) { + Log::error('[MVC License] Activation error: ' . $e->getMessage()); + return ['success' => false, 'error' => 'Could not reach license server. Try again later.']; + } + } + + /** + * Validate (phone-home). Called daily by mvc:validate Artisan command. + * On failure, enters grace period. After 7 days, marks expired. + */ + public function validate(): array + { + $orderId = $this->blueprint->dbGet('modpackchecker', 'order_id'); + if (empty($orderId)) { + return ['status' => 'inactive', 'message' => 'No license configured']; + } + + $domain = config('app.url'); + + try { + $response = Http::timeout(15)->post("{$this->arbiterBase}/validate", [ + 'order_id' => $orderId, + 'domain' => $domain, + 'version' => config('app.version', '1.0.0'), + 'php_version' => PHP_VERSION, + ]); + + $data = $response->json(); + + if ($response->successful() && ($data['status'] ?? '') === 'active') { + $this->blueprint->dbSet('modpackchecker', 'license_status', 'active'); + $this->blueprint->dbSet('modpackchecker', 'tier', $data['tier'] ?? 'standard'); + $this->blueprint->dbSet('modpackchecker', 'last_validated', now()->toISOString()); + $this->blueprint->dbSet('modpackchecker', 'grace_expires', ''); + + if (!empty($data['latest_version'])) { + $this->blueprint->dbSet('modpackchecker', 'latest_version', $data['latest_version']); + } + + return ['status' => 'active', 'tier' => $data['tier'] ?? 'standard']; + } + + return $this->enterGracePeriod($data['error'] ?? 'Validation failed'); + + } catch (\Exception $e) { + Log::warning('[MVC License] Validation failed: ' . $e->getMessage()); + return $this->enterGracePeriod('Could not reach license server'); + } + } + + /** + * Enter or continue grace period. After 7 days, expire. + */ + private function enterGracePeriod(string $reason): array + { + $graceExpires = $this->blueprint->dbGet('modpackchecker', 'grace_expires'); + + if (empty($graceExpires)) { + // First failure — start grace period + $expires = now()->addDays($this->graceDays)->toISOString(); + $this->blueprint->dbSet('modpackchecker', 'license_status', 'grace'); + $this->blueprint->dbSet('modpackchecker', 'grace_expires', $expires); + + Log::warning("[MVC License] Grace period started. Expires: {$expires}. Reason: {$reason}"); + return ['status' => 'grace', 'expires' => $expires, 'reason' => $reason]; + } + + // Check if grace period expired + if (now()->greaterThan($graceExpires)) { + $this->blueprint->dbSet('modpackchecker', 'license_status', 'expired'); + Log::error('[MVC License] Grace period expired. License marked as expired.'); + return ['status' => 'expired', 'reason' => $reason]; + } + + return ['status' => 'grace', 'expires' => $graceExpires, 'reason' => $reason]; + } + + /** + * Deactivate license on this panel. + */ + public function deactivate(): array + { + $orderId = $this->blueprint->dbGet('modpackchecker', 'order_id'); + if (empty($orderId)) { + return ['success' => true]; + } + + try { + Http::timeout(15)->post("{$this->arbiterBase}/deactivate", [ + 'order_id' => $orderId, + 'domain' => config('app.url'), + ]); + } catch (\Exception $e) { + // Best-effort — clear local state regardless + } + + $this->blueprint->dbSet('modpackchecker', 'order_id', ''); + $this->blueprint->dbSet('modpackchecker', 'license_status', 'inactive'); + $this->blueprint->dbSet('modpackchecker', 'tier', 'standard'); + $this->blueprint->dbSet('modpackchecker', 'grace_expires', ''); + + return ['success' => true]; + } + + /** + * Check if a pro feature is allowed based on current tier + license status. + */ + public function isProFeatureAllowed(): bool + { + $status = $this->blueprint->dbGet('modpackchecker', 'license_status'); + $tier = $this->blueprint->dbGet('modpackchecker', 'tier'); + + if ($status === 'expired') { + return false; + } + + return $tier === 'professional'; + } + + /** + * Get full license state for the admin UI. + */ + public function getState(): array + { + return [ + 'order_id' => $this->blueprint->dbGet('modpackchecker', 'order_id') ?: '', + 'status' => $this->blueprint->dbGet('modpackchecker', 'license_status') ?: 'inactive', + 'tier' => $this->blueprint->dbGet('modpackchecker', 'tier') ?: 'standard', + 'grace_expires' => $this->blueprint->dbGet('modpackchecker', 'grace_expires') ?: '', + 'last_validated' => $this->blueprint->dbGet('modpackchecker', 'last_validated') ?: '', + 'latest_version' => $this->blueprint->dbGet('modpackchecker', 'latest_version') ?: '', + ]; + } +}