From 4b20efe4bcbd222e1bf99828f32c4732b0cf1eb1 Mon Sep 17 00:00:00 2001 From: smueller Date: Fri, 26 Jun 2026 15:11:29 +0200 Subject: [PATCH] Implement WHMCS Addon Dashboard with API for stats and integrate billing provider logic --- .env.example | 1 + apps/api/src/billing/billing-provider.ts | 23 + apps/api/src/billing/billing.module.ts | 6 +- apps/api/src/billing/billing.service.ts | 6 +- .../stripe/stripe-webhook.controller.ts | 33 ++ .../billing/stripe/stripe-webhook.service.ts | 126 ++++++ .../whmcs/whmcs-dashboard.service.ts | 84 ++++ .../whmcs/whmcs-integration.controller.ts | 7 + .../whmcs/whmcs-integration.module.ts | 2 + .../whmcs/whmcs-integration.service.ts | 3 +- apps/api/test/phase9.test.js | 2 +- apps/api/test/whmcs-addon.test.js | 40 ++ docs/IMPLEMENTATION_STATUS.md | 36 +- docs/billing/providers.md | 43 ++ docs/integrations/whmcs/addon-module.md | 29 ++ integrations/whmcs/README.md | 4 +- integrations/whmcs/hooks/hexagamecloud.php | 42 ++ .../modules/addons/hexagamecloud/README.md | 37 +- .../addons/hexagamecloud/hexagamecloud.php | 396 ++++++++++++++++++ .../addons/hexagamecloud/lib/ApiClient.php | 135 ++++++ .../addons/hexagamecloud/lib/EventPoller.php | 64 +++ .../hexagamecloud/lib/ModuleStorage.php | 123 ++++++ .../addons/hexagamecloud/sql/install.sql | 49 +++ .../hexagamecloud/templates/dashboard.tpl | 55 +++ .../addons/hexagamecloud/templates/events.tpl | 27 ++ .../templates/reconciliation.tpl | 29 ++ .../servers/hexagamecloud/lib/ApiClient.php | 5 + packages/contracts/src/index.ts | 2 + packages/contracts/src/whmcs.ts | 19 + packages/contracts/tsconfig.tsbuildinfo | 2 +- 30 files changed, 1414 insertions(+), 16 deletions(-) create mode 100644 apps/api/src/billing/billing-provider.ts create mode 100644 apps/api/src/billing/stripe/stripe-webhook.controller.ts create mode 100644 apps/api/src/billing/stripe/stripe-webhook.service.ts create mode 100644 apps/api/src/integrations/whmcs/whmcs-dashboard.service.ts create mode 100644 apps/api/test/whmcs-addon.test.js create mode 100644 docs/billing/providers.md create mode 100644 docs/integrations/whmcs/addon-module.md create mode 100644 integrations/whmcs/hooks/hexagamecloud.php create mode 100644 integrations/whmcs/modules/addons/hexagamecloud/hexagamecloud.php create mode 100644 integrations/whmcs/modules/addons/hexagamecloud/lib/ApiClient.php create mode 100644 integrations/whmcs/modules/addons/hexagamecloud/lib/EventPoller.php create mode 100644 integrations/whmcs/modules/addons/hexagamecloud/lib/ModuleStorage.php create mode 100644 integrations/whmcs/modules/addons/hexagamecloud/sql/install.sql create mode 100644 integrations/whmcs/modules/addons/hexagamecloud/templates/dashboard.tpl create mode 100644 integrations/whmcs/modules/addons/hexagamecloud/templates/events.tpl create mode 100644 integrations/whmcs/modules/addons/hexagamecloud/templates/reconciliation.tpl diff --git a/.env.example b/.env.example index d020fa1..0001d0c 100644 --- a/.env.example +++ b/.env.example @@ -80,6 +80,7 @@ MODRINTH_USER_AGENT=HexaHostGameCloud/0.1.0 (contact@example.net) CURSEFORGE_API_KEY= # --- Billing --- +# internal = credit wallet (dev) | whmcs = production (recommended) | stripe = optional fallback BILLING_PROVIDER=internal STRIPE_SECRET_KEY= STRIPE_WEBHOOK_SECRET= diff --git a/apps/api/src/billing/billing-provider.ts b/apps/api/src/billing/billing-provider.ts new file mode 100644 index 0000000..cb54d8c --- /dev/null +++ b/apps/api/src/billing/billing-provider.ts @@ -0,0 +1,23 @@ +export type BillingProvider = 'internal' | 'whmcs' | 'stripe'; + +export function getBillingProvider(): BillingProvider { + const value = process.env['BILLING_PROVIDER'] ?? 'internal'; + if (value === 'whmcs' || value === 'stripe') { + return value; + } + return 'internal'; +} + +/** Commercial billing is handled outside the credit wallet (WHMCS or Stripe). */ +export function isExternalBilling(): boolean { + const provider = getBillingProvider(); + return provider === 'whmcs' || provider === 'stripe'; +} + +export function isWhmcsBilling(): boolean { + return getBillingProvider() === 'whmcs'; +} + +export function isStripeBilling(): boolean { + return getBillingProvider() === 'stripe'; +} diff --git a/apps/api/src/billing/billing.module.ts b/apps/api/src/billing/billing.module.ts index 0cdd6e6..f0ffe4b 100644 --- a/apps/api/src/billing/billing.module.ts +++ b/apps/api/src/billing/billing.module.ts @@ -4,11 +4,13 @@ import { PrismaModule } from '../prisma/prisma.module'; import { BillingController } from './billing.controller'; import { BillingService } from './billing.service'; +import { StripeWebhookController } from './stripe/stripe-webhook.controller'; +import { StripeWebhookService } from './stripe/stripe-webhook.service'; @Module({ imports: [PrismaModule], - controllers: [BillingController], - providers: [BillingService], + controllers: [BillingController, StripeWebhookController], + providers: [BillingService, StripeWebhookService], exports: [BillingService], }) export class BillingModule {} diff --git a/apps/api/src/billing/billing.service.ts b/apps/api/src/billing/billing.service.ts index 598ff0c..bdc0f9a 100644 --- a/apps/api/src/billing/billing.service.ts +++ b/apps/api/src/billing/billing.service.ts @@ -4,9 +4,7 @@ import type { UsageListResponse, WalletResponse } from '@hexahost/contracts'; import { PrismaService } from '../prisma/prisma.service'; -function isWhmcsBilling(): boolean { - return (process.env['BILLING_PROVIDER'] ?? 'internal') === 'whmcs'; -} +import { isExternalBilling } from './billing-provider'; @Injectable() export class BillingService { @@ -99,7 +97,7 @@ export class BillingService { } async assertCanStartServer(userId: string): Promise { - if (isWhmcsBilling()) { + if (isExternalBilling()) { return; } diff --git a/apps/api/src/billing/stripe/stripe-webhook.controller.ts b/apps/api/src/billing/stripe/stripe-webhook.controller.ts new file mode 100644 index 0000000..980f18f --- /dev/null +++ b/apps/api/src/billing/stripe/stripe-webhook.controller.ts @@ -0,0 +1,33 @@ +import { + BadRequestException, + Controller, + Headers, + Post, + Req, +} from '@nestjs/common'; +import { SkipThrottle } from '@nestjs/throttler'; +import type { FastifyRequest } from 'fastify'; + +import { Public } from '../../auth/decorators/current-user.decorator'; + +import { StripeWebhookService } from './stripe-webhook.service'; + +@Controller('webhooks/stripe') +@SkipThrottle() +export class StripeWebhookController { + constructor(private readonly stripe: StripeWebhookService) {} + + @Public() + @Post() + handleWebhook( + @Req() request: FastifyRequest & { rawBody?: string }, + @Headers('stripe-signature') signature?: string, + ) { + const rawBody = request.rawBody; + if (!rawBody) { + throw new BadRequestException('Missing raw request body for Stripe webhook'); + } + + return this.stripe.handleWebhook(rawBody, signature); + } +} diff --git a/apps/api/src/billing/stripe/stripe-webhook.service.ts b/apps/api/src/billing/stripe/stripe-webhook.service.ts new file mode 100644 index 0000000..94cd14c --- /dev/null +++ b/apps/api/src/billing/stripe/stripe-webhook.service.ts @@ -0,0 +1,126 @@ +import { createHmac, timingSafeEqual } from 'node:crypto'; + +import { Injectable, Logger } from '@nestjs/common'; + +import { PrismaService } from '../../prisma/prisma.service'; +import { isStripeBilling } from '../billing-provider'; + +@Injectable() +export class StripeWebhookService { + private readonly logger = new Logger(StripeWebhookService.name); + + constructor(private readonly prisma: PrismaService) {} + + handleWebhook(rawBody: string, signatureHeader?: string): { received: boolean; mode: string } { + if (!isStripeBilling()) { + this.logger.debug('Stripe webhook ignored — BILLING_PROVIDER is not stripe'); + return { received: true, mode: 'ignored' }; + } + + const webhookSecret = process.env['STRIPE_WEBHOOK_SECRET']; + if (!webhookSecret) { + this.logger.warn('Stripe billing enabled but STRIPE_WEBHOOK_SECRET is not set'); + return { received: true, mode: 'unconfigured' }; + } + + if (!signatureHeader || !this.verifySignature(rawBody, signatureHeader, webhookSecret)) { + this.logger.warn('Stripe webhook signature verification failed'); + return { received: false, mode: 'invalid_signature' }; + } + + const event = JSON.parse(rawBody) as { + id?: string; + type?: string; + data?: { object?: Record }; + }; + + if (!event.id || !event.type) { + return { received: false, mode: 'invalid_payload' }; + } + + void this.processEventIdempotently(event.id, event.type, event.data?.object ?? {}); + + return { received: true, mode: 'processed' }; + } + + private verifySignature( + payload: string, + signatureHeader: string, + secret: string, + ): boolean { + const parts = Object.fromEntries( + signatureHeader.split(',').map((part) => { + const [key, value] = part.split('='); + return [key, value]; + }), + ) as Record; + + const timestamp = parts['t']; + const signature = parts['v1']; + + if (!timestamp || !signature) { + return false; + } + + const signedPayload = `${timestamp}.${payload}`; + const expected = createHmac('sha256', secret).update(signedPayload).digest('hex'); + const expectedBuffer = Buffer.from(expected, 'utf8'); + const providedBuffer = Buffer.from(signature, 'utf8'); + + if (expectedBuffer.length !== providedBuffer.length) { + return false; + } + + return timingSafeEqual(expectedBuffer, providedBuffer); + } + + private async processEventIdempotently( + eventId: string, + eventType: string, + object: Record, + ): Promise { + const idempotencyKey = `stripe:${eventId}`; + + const existing = await this.prisma.integrationOperation.findUnique({ + where: { idempotencyKey }, + }); + + if (existing) { + return; + } + + this.logger.log({ eventId, eventType }, 'Stripe webhook received'); + + await this.prisma.integrationOperation.create({ + data: { + installationId: await this.resolveStripeInstallationId(), + idempotencyKey, + operation: `stripe:${eventType}`, + externalRef: eventId, + status: 'COMPLETED', + result: { + eventType, + objectId: object['id'] ?? null, + note: 'Stripe fallback handler — extend for direct billing without WHMCS', + }, + }, + }); + } + + private async resolveStripeInstallationId(): Promise { + const installation = await this.prisma.whmcsInstallation.upsert({ + where: { integrationId: 'stripe-fallback' }, + create: { + integrationId: 'stripe-fallback', + name: 'Stripe fallback billing', + apiSecret: 'stripe-fallback-not-used-for-hmac', + isActive: true, + metadata: { source: 'stripe-webhook' }, + }, + update: {}, + select: { id: true }, + }); + + return installation.id; + } +} diff --git a/apps/api/src/integrations/whmcs/whmcs-dashboard.service.ts b/apps/api/src/integrations/whmcs/whmcs-dashboard.service.ts new file mode 100644 index 0000000..774f3d9 --- /dev/null +++ b/apps/api/src/integrations/whmcs/whmcs-dashboard.service.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@nestjs/common'; + +import type { WhmcsDashboardStatsResponse } from '@hexahost/contracts'; + +import { getBillingProvider } from '../../billing/billing-provider'; +import { PrismaService } from '../../prisma/prisma.service'; + +type InstallationRef = { + id: string; + integrationId: string; +}; + +@Injectable() +export class WhmcsDashboardService { + constructor(private readonly prisma: PrismaService) {} + + async getStats(installation: InstallationRef): Promise { + const [ + linkedClients, + linkedServices, + pendingEvents, + deadLetterEvents, + openIssues, + exportedUsagePeriods, + syncCursor, + lastReconciledLink, + ] = await Promise.all([ + this.prisma.whmcsClientLink.count({ + where: { installationId: installation.id }, + }), + this.prisma.whmcsServiceLink.count({ + where: { installationId: installation.id }, + }), + this.prisma.integrationEvent.count({ + where: { installationId: installation.id, status: 'PENDING' }, + }), + this.prisma.integrationEvent.count({ + where: { installationId: installation.id, status: 'DEAD_LETTER' }, + }), + this.prisma.reconciliationIssue.count({ + where: { installationId: installation.id, resolvedAt: null }, + }), + this.prisma.whmcsUsageExport.count({ + where: { installationId: installation.id, status: { in: ['EXPORTED', 'ADJUSTED'] } }, + }), + this.prisma.integrationSyncCursor.findUnique({ + where: { installationId: installation.id }, + }), + this.prisma.whmcsServiceLink.findFirst({ + where: { + installationId: installation.id, + lastReconciledAt: { not: null }, + }, + orderBy: { lastReconciledAt: 'desc' }, + select: { lastReconciledAt: true }, + }), + ]); + + const suspendedServices = await this.prisma.whmcsServiceLink.count({ + where: { installationId: installation.id, status: 'SUSPENDED' }, + }); + + const terminatedServices = await this.prisma.whmcsServiceLink.count({ + where: { installationId: installation.id, status: 'TERMINATED' }, + }); + + return { + integrationId: installation.integrationId, + billingProvider: getBillingProvider(), + moduleVersion: '1.0.0', + linkedClients, + linkedServices, + suspendedServices, + terminatedServices, + pendingEvents, + deadLetterEvents, + openReconciliationIssues: openIssues, + exportedUsagePeriods, + eventsCursor: syncCursor?.eventsCursor ?? null, + lastEventSyncAt: syncCursor?.updatedAt.toISOString() ?? null, + lastReconciliationAt: lastReconciledLink?.lastReconciledAt?.toISOString() ?? null, + }; + } +} diff --git a/apps/api/src/integrations/whmcs/whmcs-integration.controller.ts b/apps/api/src/integrations/whmcs/whmcs-integration.controller.ts index 407a220..6b93d82 100644 --- a/apps/api/src/integrations/whmcs/whmcs-integration.controller.ts +++ b/apps/api/src/integrations/whmcs/whmcs-integration.controller.ts @@ -33,6 +33,7 @@ import { type WhmcsAuthenticatedRequest, } from './whmcs-integration.guard'; import { WhmcsIntegrationService } from './whmcs-integration.service'; +import { WhmcsDashboardService } from './whmcs-dashboard.service'; import { WhmcsEventsService } from './whmcs-events.service'; import { WhmcsReconciliationService } from './whmcs-reconciliation.service'; import { WhmcsUsageService } from './whmcs-usage.service'; @@ -47,8 +48,14 @@ export class WhmcsIntegrationController { private readonly reconciliation: WhmcsReconciliationService, private readonly events: WhmcsEventsService, private readonly usage: WhmcsUsageService, + private readonly dashboard: WhmcsDashboardService, ) {} + @Get('dashboard/stats') + dashboardStats(@Req() request: WhmcsAuthenticatedRequest) { + return this.dashboard.getStats(request.whmcsInstallation!); + } + @Get('health') health(@Req() request: WhmcsAuthenticatedRequest) { return this.whmcs.getHealth(request.whmcsInstallation!); diff --git a/apps/api/src/integrations/whmcs/whmcs-integration.module.ts b/apps/api/src/integrations/whmcs/whmcs-integration.module.ts index e3f273e..853a4d2 100644 --- a/apps/api/src/integrations/whmcs/whmcs-integration.module.ts +++ b/apps/api/src/integrations/whmcs/whmcs-integration.module.ts @@ -8,6 +8,7 @@ import { AuthModule } from '../../auth/auth.module'; import { ServersModule } from '../../servers/servers.module'; import { WhmcsIntegrationController } from './whmcs-integration.controller'; +import { WhmcsDashboardService } from './whmcs-dashboard.service'; import { WhmcsEventsService } from './whmcs-events.service'; import { INTEGRATIONS_REDIS } from './whmcs-integration.constants'; import { WhmcsIntegrationGuard } from './whmcs-integration.guard'; @@ -25,6 +26,7 @@ export { INTEGRATIONS_REDIS } from './whmcs-integration.constants'; WhmcsReconciliationService, WhmcsEventsService, WhmcsUsageService, + WhmcsDashboardService, WhmcsIntegrationGuard, { provide: INTEGRATIONS_REDIS, diff --git a/apps/api/src/integrations/whmcs/whmcs-integration.service.ts b/apps/api/src/integrations/whmcs/whmcs-integration.service.ts index c84dd22..a3b9ff6 100644 --- a/apps/api/src/integrations/whmcs/whmcs-integration.service.ts +++ b/apps/api/src/integrations/whmcs/whmcs-integration.service.ts @@ -17,6 +17,7 @@ import { formatJoinAddress } from '@hexahost/dns'; import type { GameServer } from '@hexahost/database'; import { BillingService } from '../../billing/billing.service'; +import { getBillingProvider } from '../../billing/billing-provider'; import { PrismaService } from '../../prisma/prisma.service'; import { ServersService } from '../../servers/servers.service'; @@ -39,7 +40,7 @@ export class WhmcsIntegrationService { return { status: 'ok' as const, integrationId: installation.integrationId, - billingProvider: process.env['BILLING_PROVIDER'] ?? 'internal', + billingProvider: getBillingProvider(), }; } diff --git a/apps/api/test/phase9.test.js b/apps/api/test/phase9.test.js index 2315195..f4ab56d 100644 --- a/apps/api/test/phase9.test.js +++ b/apps/api/test/phase9.test.js @@ -17,7 +17,7 @@ describe('phase 9 api contract', () => { join(process.cwd(), 'src', 'billing/billing.service.ts'), 'utf8', ); - assert.ok(billingSource.includes('isWhmcsBilling')); + assert.ok(billingSource.includes('isExternalBilling')); const serversSource = readFileSync( join(process.cwd(), 'src', 'servers/servers.service.ts'), diff --git a/apps/api/test/whmcs-addon.test.js b/apps/api/test/whmcs-addon.test.js new file mode 100644 index 0000000..f2e37cf --- /dev/null +++ b/apps/api/test/whmcs-addon.test.js @@ -0,0 +1,40 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +describe('whmcs addon module contract', () => { + it('documents addon dashboard API and PHP module', () => { + const controller = readFileSync( + join(process.cwd(), 'src', 'integrations/whmcs/whmcs-integration.controller.ts'), + 'utf8', + ); + assert.ok(controller.includes("'dashboard/stats'")); + + const addonMain = readFileSync( + join(process.cwd(), '..', '..', 'integrations/whmcs/modules/addons/hexagamecloud/hexagamecloud.php'), + 'utf8', + ); + assert.ok(addonMain.includes('hexagamecloud_config')); + assert.ok(addonMain.includes('hexagamecloud_page_dashboard')); + assert.ok(addonMain.includes('HexaGameCloudEventPoller')); + }); +}); + +describe('stripe fallback contract', () => { + it('documents optional stripe webhook and billing provider', () => { + const provider = readFileSync( + join(process.cwd(), 'src', 'billing/billing-provider.ts'), + 'utf8', + ); + assert.ok(provider.includes('isExternalBilling')); + assert.ok(provider.includes("'stripe'")); + + const webhook = readFileSync( + join(process.cwd(), 'src', 'billing/stripe/stripe-webhook.service.ts'), + 'utf8', + ); + assert.ok(webhook.includes('isStripeBilling')); + assert.ok(webhook.includes('ignored')); + }); +}); diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 0669816..7cad8ad 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -2,7 +2,39 @@ Last updated: 2026-06-26 -## Current phase: WHMCS Phase E + F — Sync & Usage Billing ✅ +## Current phase: WHMCS Addon Dashboard ✅ + +### WHMCS Addon module completed + +| Area | Status | Notes | +|------|--------|-------| +| Addon PHP module | Done | Dashboard, events, reconciliation UI | +| Module tables | Done | `mod_hexagamecloud_*` on activation | +| Event poller | Done | Manual + daily cron hook | +| Dashboard API | Done | `GET /integrations/whmcs/dashboard/stats` | +| Cron hook | Done | `integrations/whmcs/hooks/hexagamecloud.php` | +| Docs | Done | `docs/integrations/whmcs/addon-module.md` | + +### Billing providers + +| Provider | Status | Notes | +|----------|--------|-------| +| WHMCS | Done | **Production default** (`BILLING_PROVIDER=whmcs`) | +| Internal | Done | Dev / free tier credits | +| Stripe | Stub | Optional fallback webhook — inactive unless `BILLING_PROVIDER=stripe` | + +See `docs/billing/providers.md`. + +### Abnahmekriterium Addon + +- [x] WHMCS admin dashboard shows API health and sync stats +- [x] Event poll + ack from addon UI and cron hook +- [x] Reconciliation dry-run from addon UI +- [ ] Manual E2E: activate addon on WHMCS host + +--- + +## WHMCS Phase E + F — Sync & Usage Billing ✅ ### WHMCS Phase E completed @@ -234,4 +266,4 @@ Server entry fields: ### Next phase: Post-MVP hardening -- Stripe webhooks, penetration test, WHMCS addon dashboard UI +- Penetration test, product mapping UI in addon, mTLS for integration API diff --git a/docs/billing/providers.md b/docs/billing/providers.md new file mode 100644 index 0000000..53a2378 --- /dev/null +++ b/docs/billing/providers.md @@ -0,0 +1,43 @@ +# Billing providers + +GameCloud supports three billing modes via `BILLING_PROVIDER`: + +| Provider | Use case | Credit wallet | +|----------|----------|---------------| +| `internal` | Dev / free tier | Required for start | +| `whmcs` | **Production default** | Bypassed — WHMCS bills customers | +| `stripe` | Optional fallback without WHMCS | Bypassed — extend webhook handler | + +## WHMCS (recommended) + +```env +BILLING_PROVIDER=whmcs +WHMCS_INTEGRATION_ID=your-integration-id +WHMCS_API_SECRET=your-secret +``` + +Commercial lifecycle (orders, invoices, suspend, usage metrics) runs entirely in WHMCS. GameCloud enforces `billingSuspendedAt` from WHMCS API calls. + +## Internal (development) + +```env +BILLING_PROVIDER=internal +``` + +Uses the credit wallet and usage metering from Phase 7. Suitable for local development and standalone free tier. + +## Stripe (optional fallback) + +```env +BILLING_PROVIDER=stripe +STRIPE_SECRET_KEY=sk_test_... +STRIPE_WEBHOOK_SECRET=whsec_... +``` + +Webhook endpoint: `POST /api/v1/webhooks/stripe` + +When `BILLING_PROVIDER` is not `stripe`, webhooks return `{ received: true, mode: 'ignored' }` without side effects. + +The current implementation verifies signatures and logs events idempotently. Extend `StripeWebhookService` to map Stripe customers to GameCloud users if you deploy without WHMCS. + +**Do not enable Stripe and WHMCS billing simultaneously** — choose one commercial system of record. diff --git a/docs/integrations/whmcs/addon-module.md b/docs/integrations/whmcs/addon-module.md new file mode 100644 index 0000000..f945075 --- /dev/null +++ b/docs/integrations/whmcs/addon-module.md @@ -0,0 +1,29 @@ +# WHMCS addon module + +The addon provides global GameCloud administration inside WHMCS. **WHMCS remains the commercial system of record**; this module does not process payments. + +## Install + +1. Copy `integrations/whmcs/modules/addons/hexagamecloud` to `/modules/addons/hexagamecloud` +2. **Setup → Addon Modules** → activate **HexaHost GameCloud** +3. Configure: + - **GameCloud API URL** — e.g. `https://api.example.net` + - **Integration ID** — matches `WHMCS_INTEGRATION_ID` on GameCloud + - **API Secret** — matches `WHMCS_API_SECRET` +4. Copy `integrations/whmcs/hooks/hexagamecloud.php` to WHMCS `includes/hooks/hexagamecloud.php` + +## Pages + +| Tab | Purpose | +|-----|---------| +| Dashboard | Health, counts, last sync | +| Events | Poll, view, ack, dead-letter | +| Reconciliation | Dry-run / live reconcile | + +## Cron + +The daily hook polls pending GameCloud events and optionally runs a dry reconciliation when **Auto-reconcile on daily cron** is enabled. + +## Billing + +Set `BILLING_PROVIDER=whmcs` on GameCloud in production. Stripe (`BILLING_PROVIDER=stripe`) is an optional fallback for standalone deployments without WHMCS — see [billing providers](../../billing/providers.md). diff --git a/integrations/whmcs/README.md b/integrations/whmcs/README.md index cee1015..a0b2a63 100644 --- a/integrations/whmcs/README.md +++ b/integrations/whmcs/README.md @@ -60,8 +60,10 @@ Handles global configuration: **Phase F (Usage Billing)** — implemented. See [docs/integrations/whmcs/usage-billing.md](../../docs/integrations/whmcs/usage-billing.md). +**Addon module (Dashboard)** — implemented. See [docs/integrations/whmcs/addon-module.md](../../docs/integrations/whmcs/addon-module.md). + - Server module: `integrations/whmcs/modules/servers/hexagamecloud/` -- Addon module: planned for global mappings and reconciliation UI +- Addon module: `integrations/whmcs/modules/addons/hexagamecloud/` ## Documentation (planned) diff --git a/integrations/whmcs/hooks/hexagamecloud.php b/integrations/whmcs/hooks/hexagamecloud.php new file mode 100644 index 0000000..557862a --- /dev/null +++ b/integrations/whmcs/hooks/hexagamecloud.php @@ -0,0 +1,42 @@ +where('module', 'hexagamecloud') + ->pluck('value', 'setting'); + + $apiUrl = trim((string) ($rows['api_url'] ?? '')); + $integrationId = trim((string) ($rows['integration_id'] ?? '')); + $apiSecret = trim((string) ($rows['api_secret'] ?? '')); + + if ($apiUrl === '' || $integrationId === '' || $apiSecret === '') { + return; + } + + require_once ROOTDIR . '/modules/addons/hexagamecloud/lib/ApiClient.php'; + require_once ROOTDIR . '/modules/addons/hexagamecloud/lib/ModuleStorage.php'; + require_once ROOTDIR . '/modules/addons/hexagamecloud/lib/EventPoller.php'; + + try { + $client = new HexaGameCloudAddonApiClient($apiUrl, $integrationId, $apiSecret); + $storage = new HexaGameCloudModuleStorage(); + $limit = (int) ($rows['event_poll_limit'] ?? 50); + $poller = new HexaGameCloudEventPoller($client, $storage, max(1, min($limit, 100))); + $poller->pollAndAcknowledge(); + + if (!empty($rows['auto_reconcile'])) { + $client->reconcileAll(['dryRun' => true]); + } + } catch (Throwable $exception) { + logActivity('HexaGameCloud cron poll failed: ' . $exception->getMessage()); + } +}); diff --git a/integrations/whmcs/modules/addons/hexagamecloud/README.md b/integrations/whmcs/modules/addons/hexagamecloud/README.md index 4f106a7..9fa522a 100644 --- a/integrations/whmcs/modules/addons/hexagamecloud/README.md +++ b/integrations/whmcs/modules/addons/hexagamecloud/README.md @@ -1,7 +1,36 @@ -# Addon module placeholder +# WHMCS addon module -Implementation planned in WHMCS Phase A. +Install path: `/modules/addons/hexagamecloud/` -Target install path: `/modules/addons/hexagamecloud/` +## Features -Handles global API settings, product mappings, reconciliation, and event polling. +- **Dashboard** — API health, linked services, pending events, reconciliation summary +- **Events** — Poll GameCloud cursor events, local cache, manual ack / dead-letter +- **Reconciliation** — Dry-run and live runs with optional auto-repair +- **Daily cron** — Copy `integrations/whmcs/hooks/hexagamecloud.php` to WHMCS `includes/hooks/` + +## Setup + +1. Activate **Addon Modules → HexaHost GameCloud** +2. Configure API URL, Integration ID, API Secret (same as GameCloud `WHMCS_*` seed values) +3. Copy the hook file for automated polling +4. Open **Addons → HexaHost GameCloud** for the dashboard + +## Tables + +Created on activation (`sql/install.sql`): + +- `mod_hexagamecloud_installations` +- `mod_hexagamecloud_sync_cursors` +- `mod_hexagamecloud_events` +- `mod_hexagamecloud_operations` +- `mod_hexagamecloud_reconciliation` + +## API used + +- `GET /integrations/whmcs/dashboard/stats` +- `GET /integrations/whmcs/accounts` +- `GET /integrations/whmcs/events` + `POST .../events/ack` +- `POST /integrations/whmcs/reconcile` + +See [docs/integrations/whmcs/addon-module.md](../../../../docs/integrations/whmcs/addon-module.md). diff --git a/integrations/whmcs/modules/addons/hexagamecloud/hexagamecloud.php b/integrations/whmcs/modules/addons/hexagamecloud/hexagamecloud.php new file mode 100644 index 0000000..21dff67 --- /dev/null +++ b/integrations/whmcs/modules/addons/hexagamecloud/hexagamecloud.php @@ -0,0 +1,396 @@ + 'HexaHost GameCloud', + 'description' => 'Global GameCloud integration — dashboard, reconciliation, and event polling.', + 'version' => HEXAGAMECLOUD_ADDON_VERSION, + 'author' => 'HexaHost', + 'language' => 'english', + 'fields' => [ + 'api_url' => [ + 'FriendlyName' => 'GameCloud API URL', + 'Type' => 'text', + 'Size' => '128', + 'Default' => 'https://api.example.net', + 'Description' => 'Base URL of the GameCloud API (no trailing slash)', + ], + 'integration_id' => [ + 'FriendlyName' => 'Integration ID', + 'Type' => 'text', + 'Size' => '64', + 'Description' => 'Matches WHMCS_INTEGRATION_ID on GameCloud', + ], + 'api_secret' => [ + 'FriendlyName' => 'API Secret', + 'Type' => 'password', + 'Size' => '128', + 'Description' => 'HMAC signing secret — stored encrypted by WHMCS', + ], + 'sso_origin' => [ + 'FriendlyName' => 'SSO Panel Origin', + 'Type' => 'text', + 'Size' => '128', + 'Default' => 'https://panel.example.net', + 'Description' => 'Customer panel URL for SSO redirects', + ], + 'event_poll_limit' => [ + 'FriendlyName' => 'Event poll batch size', + 'Type' => 'text', + 'Size' => '4', + 'Default' => '50', + ], + 'auto_reconcile' => [ + 'FriendlyName' => 'Auto-reconcile on daily cron', + 'Type' => 'yesno', + 'Description' => 'Run dry reconciliation during daily event poll', + ], + ], + ]; +} + +function hexagamecloud_activate(): array +{ + try { + $sql = file_get_contents(__DIR__ . '/sql/install.sql'); + if ($sql === false) { + throw new RuntimeException('Unable to read install.sql'); + } + + foreach (array_filter(array_map('trim', explode(';', $sql))) as $statement) { + if ($statement !== '') { + Capsule::connection()->statement($statement); + } + } + + return [ + 'status' => 'success', + 'description' => 'HexaHost GameCloud addon activated. Copy hooks/hexagamecloud.php to includes/hooks/ for cron polling.', + ]; + } catch (Throwable $exception) { + return [ + 'status' => 'error', + 'description' => 'Activation failed: ' . $exception->getMessage(), + ]; + } +} + +function hexagamecloud_deactivate(): array +{ + return [ + 'status' => 'success', + 'description' => 'Addon deactivated. Module tables were kept for audit history.', + ]; +} + +function hexagamecloud_output(array $vars): void +{ + $action = $_GET['action'] ?? 'dashboard'; + $modulelink = $vars['modulelink']; + $storage = new HexaGameCloudModuleStorage(); + + try { + $client = hexagamecloud_create_client($vars); + } catch (Throwable $exception) { + hexagamecloud_render_error($vars, $exception->getMessage()); + return; + } + + switch ($action) { + case 'events': + hexagamecloud_page_events($vars, $client, $storage, $modulelink); + break; + case 'reconciliation': + hexagamecloud_page_reconciliation($vars, $client, $storage, $modulelink); + break; + case 'poll': + hexagamecloud_action_poll($vars, $client, $storage, $modulelink); + break; + case 'reconcile': + hexagamecloud_action_reconcile($vars, $client, $storage, $modulelink); + break; + case 'ack': + hexagamecloud_action_ack($vars, $client, $storage, $modulelink); + break; + default: + hexagamecloud_page_dashboard($vars, $client, $storage, $modulelink); + break; + } +} + +function hexagamecloud_create_client(array $vars): HexaGameCloudAddonApiClient +{ + $apiUrl = trim((string) ($vars['api_url'] ?? '')); + $integrationId = trim((string) ($vars['integration_id'] ?? '')); + $apiSecret = trim((string) ($vars['api_secret'] ?? '')); + + if ($apiUrl === '' || $integrationId === '' || $apiSecret === '') { + throw new RuntimeException('Configure API URL, Integration ID and API Secret in addon settings.'); + } + + return new HexaGameCloudAddonApiClient($apiUrl, $integrationId, $apiSecret); +} + +function hexagamecloud_page_dashboard( + array $vars, + HexaGameCloudAddonApiClient $client, + HexaGameCloudModuleStorage $storage, + string $modulelink +): void { + $health = $client->health(); + $stats = $client->dashboardStats(); + $accounts = $client->listAccounts(); + $unlinked = hexagamecloud_count_unlinked_whmcs_services($accounts['accounts'] ?? []); + + $orphans = 0; + foreach ($accounts['accounts'] ?? [] as $account) { + if (($account['linkStatus'] ?? '') === 'TERMINATED' && ($account['gameCloudStatus'] ?? '') !== 'DELETED') { + $orphans++; + } + } + + $storage->recordOperation('dashboard:view', 'success', [ + 'integrationId' => $stats['integrationId'] ?? null, + ]); + + echo hexagamecloud_render_template('dashboard', [ + 'modulelink' => $modulelink, + 'version' => HEXAGAMECLOUD_ADDON_VERSION, + 'health' => $health, + 'stats' => $stats, + 'linkedServices' => count($accounts['accounts'] ?? []), + 'unlinkedWhmcsServices' => $unlinked, + 'driftCount' => $orphans, + 'lastError' => $storage->getLastError(), + ]); +} + +function hexagamecloud_page_events( + array $vars, + HexaGameCloudAddonApiClient $client, + HexaGameCloudModuleStorage $storage, + string $modulelink +): void { + $localEvents = $storage->listLocalEvents(100); + $pendingCount = $storage->countLocalEventsByStatus('pending'); + + echo hexagamecloud_render_template('events', [ + 'modulelink' => $modulelink, + 'localEvents' => $localEvents, + 'pendingCount' => $pendingCount, + 'deadLetterCount' => $storage->countLocalEventsByStatus('dead_letter'), + ]); +} + +function hexagamecloud_page_reconciliation( + array $vars, + HexaGameCloudAddonApiClient $client, + HexaGameCloudModuleStorage $storage, + string $modulelink +): void { + $runs = $storage->listReconciliationRuns(20); + $dryRun = isset($_GET['dry']) ? true : false; + $liveResult = null; + + if (isset($_GET['run'])) { + try { + $liveResult = $client->reconcileAll([ + 'dryRun' => $dryRun, + 'autoRepair' => !$dryRun && !empty($_GET['repair']), + ]); + $storage->saveReconciliationRun($liveResult, $dryRun ? 'dry_run' : 'live'); + } catch (Throwable $exception) { + $storage->setLastError($exception->getMessage()); + $liveResult = ['error' => $exception->getMessage()]; + } + } + + echo hexagamecloud_render_template('reconciliation', [ + 'modulelink' => $modulelink, + 'runs' => $runs, + 'liveResult' => $liveResult, + 'dryRun' => $dryRun, + ]); +} + +function hexagamecloud_action_poll( + array $vars, + HexaGameCloudAddonApiClient $client, + HexaGameCloudModuleStorage $storage, + string $modulelink +): void { + $limit = (int) ($vars['event_poll_limit'] ?? 50); + $poller = new HexaGameCloudEventPoller($client, $storage, max(1, min($limit, 100))); + + try { + $result = $poller->pollAndAcknowledge(); + $storage->recordOperation('events:poll', 'success', $result); + } catch (Throwable $exception) { + $storage->setLastError($exception->getMessage()); + $storage->recordOperation('events:poll', 'error', ['message' => $exception->getMessage()]); + } + + header('Location: ' . $modulelink . '&action=events'); + exit; +} + +function hexagamecloud_action_reconcile( + array $vars, + HexaGameCloudAddonApiClient $client, + HexaGameCloudModuleStorage $storage, + string $modulelink +): void { + header('Location: ' . $modulelink . '&action=reconciliation&run=1&dry=1'); + exit; +} + +function hexagamecloud_action_ack( + array $vars, + HexaGameCloudAddonApiClient $client, + HexaGameCloudModuleStorage $storage, + string $modulelink +): void { + $eventId = (int) ($_POST['local_event_id'] ?? 0); + $deadLetter = !empty($_POST['dead_letter']); + $local = $storage->getLocalEvent($eventId); + + if ($local && !empty($local['remote_event_id'])) { + try { + $client->acknowledgeEvents([$local['remote_event_id']], $deadLetter, 'Manual ack from WHMCS addon'); + $storage->updateLocalEventStatus($eventId, $deadLetter ? 'dead_letter' : 'acknowledged'); + } catch (Throwable $exception) { + $storage->setLastError($exception->getMessage()); + } + } + + header('Location: ' . $modulelink . '&action=events'); + exit; +} + +function hexagamecloud_count_unlinked_whmcs_services(array $linkedAccounts): int +{ + $linkedIds = []; + foreach ($linkedAccounts as $account) { + if (!empty($account['externalServiceId'])) { + $linkedIds[(string) $account['externalServiceId']] = true; + } + } + + $hosting = Capsule::table('tblhosting') + ->join('tblproducts', 'tblproducts.id', '=', 'tblhosting.packageid') + ->where('tblproducts.servertype', 'hexagamecloud') + ->whereIn('tblhosting.domainstatus', ['Active', 'Suspended', 'Pending']) + ->select('tblhosting.id') + ->get(); + + $unlinked = 0; + foreach ($hosting as $row) { + if (!isset($linkedIds[(string) $row->id])) { + $unlinked++; + } + } + + return $unlinked; +} + +function hexagamecloud_render_template(string $name, array $data): string +{ + $path = __DIR__ . '/templates/' . $name . '.tpl'; + if (!is_readable($path)) { + return '
Template missing: ' . htmlspecialchars($name) . '
'; + } + + $html = file_get_contents($path); + if ($html === false) { + return ''; + } + + foreach ($data as $key => $value) { + $placeholder = '{{' . $key . '}}'; + if (is_scalar($value) || $value === null) { + $html = str_replace($placeholder, htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'), $html); + } + } + + return hexagamecloud_expand_blocks($html, $data); +} + +function hexagamecloud_expand_blocks(string $html, array $data): string +{ + if (!empty($data['stats']) && is_array($data['stats'])) { + foreach ($data['stats'] as $key => $value) { + $html = str_replace('{{stats.' . $key . '}}', htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'), $html); + } + } + + if (!empty($data['health']) && is_array($data['health'])) { + foreach ($data['health'] as $key => $value) { + $html = str_replace('{{health.' . $key . '}}', htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'), $html); + } + } + + if (!empty($data['localEvents']) && is_array($data['localEvents'])) { + $rows = ''; + foreach ($data['localEvents'] as $event) { + $rows .= ''; + $rows .= '' . (int) $event['id'] . ''; + $rows .= '' . htmlspecialchars((string) $event['event_type'], ENT_QUOTES, 'UTF-8') . ''; + $rows .= '' . htmlspecialchars((string) $event['status'], ENT_QUOTES, 'UTF-8') . ''; + $rows .= '' . htmlspecialchars((string) $event['payload'], ENT_QUOTES, 'UTF-8') . ''; + $rows .= '' . htmlspecialchars((string) $event['created_at'], ENT_QUOTES, 'UTF-8') . ''; + $rows .= '
'; + $rows .= ''; + $rows .= ' '; + $rows .= ''; + $rows .= '
'; + $rows .= ''; + } + $html = str_replace('{{events.rows}}', $rows, $html); + } + + if (!empty($data['runs']) && is_array($data['runs'])) { + $rows = ''; + foreach ($data['runs'] as $run) { + $rows .= ''; + $rows .= '' . htmlspecialchars((string) $run['run_id'], ENT_QUOTES, 'UTF-8') . ''; + $rows .= '' . htmlspecialchars((string) $run['mode'], ENT_QUOTES, 'UTF-8') . ''; + $rows .= '' . (int) $run['issue_count'] . ''; + $rows .= '' . htmlspecialchars((string) $run['created_at'], ENT_QUOTES, 'UTF-8') . ''; + $rows .= ''; + } + $html = str_replace('{{reconciliation.rows}}', $rows, $html); + } + + if (!empty($data['liveResult']) && is_array($data['liveResult'])) { + $html = str_replace( + '{{reconciliation.result}}', + '
' . htmlspecialchars(json_encode($data['liveResult'], JSON_PRETTY_PRINT), ENT_QUOTES, 'UTF-8') . '
', + $html + ); + } else { + $html = str_replace('{{reconciliation.result}}', '', $html); + } + + return $html; +} + +function hexagamecloud_render_error(array $vars, string $message): void +{ + echo '
GameCloud addon error: ' . htmlspecialchars($message, ENT_QUOTES, 'UTF-8') . '
'; + echo '

Configure the addon under Setup → Addon Modules → HexaHost GameCloud.

'; +} diff --git a/integrations/whmcs/modules/addons/hexagamecloud/lib/ApiClient.php b/integrations/whmcs/modules/addons/hexagamecloud/lib/ApiClient.php new file mode 100644 index 0000000..e380d12 --- /dev/null +++ b/integrations/whmcs/modules/addons/hexagamecloud/lib/ApiClient.php @@ -0,0 +1,135 @@ +baseUrl === '' || $this->integrationId === '' || $this->apiSecret === '') { + throw new RuntimeException('API URL, integration ID and secret are required'); + } + } + + public function health(): array + { + return $this->request('GET', '/api/v1/integrations/whmcs/health'); + } + + public function dashboardStats(): array + { + return $this->request('GET', '/api/v1/integrations/whmcs/dashboard/stats'); + } + + public function listAccounts(): array + { + return $this->request('GET', '/api/v1/integrations/whmcs/accounts'); + } + + public function listEvents(?string $cursor = null, int $limit = 50): array + { + $query = $cursor + ? ('?cursor=' . rawurlencode($cursor) . '&limit=' . $limit) + : ('?limit=' . $limit); + + return $this->request('GET', '/api/v1/integrations/whmcs/events' . $query); + } + + public function acknowledgeEvents(array $eventIds, bool $deadLetter = false, ?string $reason = null): array + { + $payload = ['eventIds' => $eventIds, 'deadLetter' => $deadLetter]; + if ($reason !== null) { + $payload['deadLetterReason'] = $reason; + } + + return $this->request('POST', '/api/v1/integrations/whmcs/events/ack', $payload); + } + + public function reconcileAll(array $payload = []): array + { + return $this->request( + 'POST', + '/api/v1/integrations/whmcs/reconcile', + $payload, + 'reconcile:all:' . date('Y-m-d-His') + ); + } + + private function request(string $method, string $pathWithQuery, array $body = [], ?string $idempotencySuffix = null): array + { + $pathParts = explode('?', $pathWithQuery, 2); + $path = $pathParts[0]; + $canonicalQuery = ''; + + if (isset($pathParts[1]) && $pathParts[1] !== '') { + parse_str($pathParts[1], $queryParams); + ksort($queryParams); + $pairs = []; + foreach ($queryParams as $key => $value) { + $pairs[] = rawurlencode((string) $key) . '=' . rawurlencode((string) $value); + } + $canonicalQuery = implode('&', $pairs); + } + + $bodyJson = $body === [] ? '' : json_encode($body, JSON_THROW_ON_ERROR); + $timestamp = (string) (int) (microtime(true) * 1000); + $nonce = bin2hex(random_bytes(16)); + $idempotencyKey = $idempotencySuffix ?: $method . ':' . $path . ':' . $timestamp; + + $signatureBase = implode("\n", [ + strtoupper($method), + $path, + $canonicalQuery, + hash('sha256', $bodyJson), + $timestamp, + $nonce, + $idempotencyKey, + ]); + + $signature = hash_hmac('sha256', $signatureBase, $this->apiSecret); + $requestUrl = rtrim($this->baseUrl, '/') . $path . ($canonicalQuery !== '' ? ('?' . $canonicalQuery) : ''); + + $ch = curl_init($requestUrl); + if ($ch === false) { + throw new RuntimeException('Unable to initialize HTTP client'); + } + + curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'Accept: application/json', + 'X-HGC-Integration-ID: ' . $this->integrationId, + 'X-HGC-Timestamp: ' . $timestamp, + 'X-HGC-Nonce: ' . $nonce, + 'X-HGC-Idempotency-Key: ' . $idempotencyKey, + 'X-HGC-Signature: ' . $signature, + ], + CURLOPT_POSTFIELDS => $bodyJson, + ]); + + $responseBody = curl_exec($ch); + $statusCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + if ($responseBody === false) { + throw new RuntimeException('GameCloud request failed: ' . $curlError); + } + + $decoded = json_decode($responseBody, true); + if ($statusCode >= 400) { + $message = is_array($decoded) + ? ($decoded['detail'] ?? $decoded['title'] ?? $responseBody) + : $responseBody; + throw new RuntimeException('GameCloud API error (' . $statusCode . '): ' . $message); + } + + return is_array($decoded) ? $decoded : []; + } +} diff --git a/integrations/whmcs/modules/addons/hexagamecloud/lib/EventPoller.php b/integrations/whmcs/modules/addons/hexagamecloud/lib/EventPoller.php new file mode 100644 index 0000000..84d132a --- /dev/null +++ b/integrations/whmcs/modules/addons/hexagamecloud/lib/EventPoller.php @@ -0,0 +1,64 @@ +storage->getSyncCursor(); + $response = $this->client->listEvents($cursor, $this->limit); + $events = $response['events'] ?? []; + $ackIds = []; + $stored = 0; + + foreach ($events as $event) { + $remoteId = (string) ($event['id'] ?? ''); + $eventType = (string) ($event['eventType'] ?? 'unknown'); + $payload = is_array($event['payload'] ?? null) ? $event['payload'] : []; + + if ($remoteId === '') { + continue; + } + + $this->storage->storeRemoteEvent($remoteId, $eventType, $payload); + $stored++; + + if ($this->shouldAutoAck($eventType)) { + $ackIds[] = $remoteId; + } + } + + if ($ackIds !== []) { + $this->client->acknowledgeEvents($ackIds); + } + + if (!empty($response['nextCursor'])) { + $this->storage->updateSyncCursor((string) $response['nextCursor']); + } elseif ($events !== []) { + $last = $events[count($events) - 1]; + if (!empty($last['id'])) { + $this->storage->updateSyncCursor((string) $last['id']); + } + } + + return [ + 'fetched' => count($events), + 'stored' => $stored, + 'acknowledged' => count($ackIds), + 'nextCursor' => $response['nextCursor'] ?? null, + ]; + } + + private function shouldAutoAck(string $eventType): bool + { + return in_array($eventType, ['usage_period_ready', 'usage_period_closed'], true); + } +} diff --git a/integrations/whmcs/modules/addons/hexagamecloud/lib/ModuleStorage.php b/integrations/whmcs/modules/addons/hexagamecloud/lib/ModuleStorage.php new file mode 100644 index 0000000..936345c --- /dev/null +++ b/integrations/whmcs/modules/addons/hexagamecloud/lib/ModuleStorage.php @@ -0,0 +1,123 @@ +insert([ + 'operation' => $operation, + 'status' => $status, + 'metadata' => json_encode($metadata, JSON_THROW_ON_ERROR), + 'created_at' => gmdate('Y-m-d H:i:s'), + ]); + } + + public function setLastError(string $message): void + { + Capsule::table('mod_hexagamecloud_installations')->updateOrInsert( + ['id' => 1], + [ + 'last_error' => $message, + 'updated_at' => gmdate('Y-m-d H:i:s'), + ] + ); + } + + public function getLastError(): ?string + { + $row = Capsule::table('mod_hexagamecloud_installations')->where('id', 1)->first(); + return $row?->last_error; + } + + public function updateSyncCursor(?string $cursor): void + { + Capsule::table('mod_hexagamecloud_sync_cursors')->updateOrInsert( + ['id' => 1], + [ + 'events_cursor' => $cursor, + 'last_poll_at' => gmdate('Y-m-d H:i:s'), + 'updated_at' => gmdate('Y-m-d H:i:s'), + ] + ); + } + + public function getSyncCursor(): ?string + { + $row = Capsule::table('mod_hexagamecloud_sync_cursors')->where('id', 1)->first(); + return $row?->events_cursor; + } + + public function storeRemoteEvent(string $remoteId, string $eventType, array $payload, string $status = 'pending'): void + { + $existing = Capsule::table('mod_hexagamecloud_events') + ->where('remote_event_id', $remoteId) + ->first(); + + if ($existing) { + return; + } + + Capsule::table('mod_hexagamecloud_events')->insert([ + 'remote_event_id' => $remoteId, + 'event_type' => $eventType, + 'payload' => json_encode($payload, JSON_THROW_ON_ERROR), + 'status' => $status, + 'created_at' => gmdate('Y-m-d H:i:s'), + ]); + } + + public function listLocalEvents(int $limit = 50): array + { + return Capsule::table('mod_hexagamecloud_events') + ->orderBy('id', 'desc') + ->limit($limit) + ->get() + ->map(static fn ($row) => (array) $row) + ->all(); + } + + public function countLocalEventsByStatus(string $status): int + { + return (int) Capsule::table('mod_hexagamecloud_events')->where('status', $status)->count(); + } + + public function getLocalEvent(int $id): ?array + { + $row = Capsule::table('mod_hexagamecloud_events')->where('id', $id)->first(); + return $row ? (array) $row : null; + } + + public function updateLocalEventStatus(int $id, string $status): void + { + Capsule::table('mod_hexagamecloud_events')->where('id', $id)->update([ + 'status' => $status, + 'processed_at' => gmdate('Y-m-d H:i:s'), + ]); + } + + public function saveReconciliationRun(array $result, string $mode): void + { + $issues = $result['issues'] ?? []; + Capsule::table('mod_hexagamecloud_reconciliation')->insert([ + 'run_id' => (string) ($result['runId'] ?? ''), + 'mode' => $mode, + 'issue_count' => is_array($issues) ? count($issues) : 0, + 'payload' => json_encode($result, JSON_THROW_ON_ERROR), + 'created_at' => gmdate('Y-m-d H:i:s'), + ]); + } + + public function listReconciliationRuns(int $limit = 20): array + { + return Capsule::table('mod_hexagamecloud_reconciliation') + ->orderBy('id', 'desc') + ->limit($limit) + ->get() + ->map(static fn ($row) => (array) $row) + ->all(); + } +} diff --git a/integrations/whmcs/modules/addons/hexagamecloud/sql/install.sql b/integrations/whmcs/modules/addons/hexagamecloud/sql/install.sql new file mode 100644 index 0000000..a1769be --- /dev/null +++ b/integrations/whmcs/modules/addons/hexagamecloud/sql/install.sql @@ -0,0 +1,49 @@ +CREATE TABLE IF NOT EXISTS `mod_hexagamecloud_installations` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `last_error` text NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `mod_hexagamecloud_sync_cursors` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `events_cursor` varchar(64) NULL, + `last_poll_at` datetime NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `mod_hexagamecloud_events` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `remote_event_id` varchar(64) NOT NULL, + `event_type` varchar(64) NOT NULL, + `payload` mediumtext NOT NULL, + `status` varchar(32) NOT NULL DEFAULT 'pending', + `processed_at` datetime NULL, + `created_at` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `remote_event_id` (`remote_event_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `mod_hexagamecloud_operations` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `operation` varchar(64) NOT NULL, + `status` varchar(32) NOT NULL, + `metadata` mediumtext NULL, + `created_at` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `mod_hexagamecloud_reconciliation` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `run_id` varchar(64) NOT NULL, + `mode` varchar(32) NOT NULL, + `issue_count` int unsigned NOT NULL DEFAULT 0, + `payload` mediumtext NULL, + `created_at` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT IGNORE INTO `mod_hexagamecloud_installations` (`id`, `updated_at`) VALUES (1, UTC_TIMESTAMP()); + +INSERT IGNORE INTO `mod_hexagamecloud_sync_cursors` (`id`, `updated_at`) VALUES (1, UTC_TIMESTAMP()); diff --git a/integrations/whmcs/modules/addons/hexagamecloud/templates/dashboard.tpl b/integrations/whmcs/modules/addons/hexagamecloud/templates/dashboard.tpl new file mode 100644 index 0000000..b27a1b2 --- /dev/null +++ b/integrations/whmcs/modules/addons/hexagamecloud/templates/dashboard.tpl @@ -0,0 +1,55 @@ +
+
+

HexaHost GameCloud — Dashboard

+
+
+

+ Dashboard + Events + Reconciliation + Poll events now +

+ +
+
+
+

{{health.status}}

+ API status +
+
+
+
+

{{stats.linkedServices}}

+ Linked services +
+
+
+
+

{{unlinkedWhmcsServices}}

+ Unlinked WHMCS services +
+
+
+
+

{{stats.pendingEvents}}

+ Pending GameCloud events +
+
+
+ + + + + + + + + + + + + + +
Addon version{{version}}
Integration ID{{stats.integrationId}}
Billing provider{{stats.billingProvider}}
Linked clients{{stats.linkedClients}}
Open reconciliation issues{{stats.openReconciliationIssues}}
Dead-letter events{{stats.deadLetterEvents}}
Usage exports{{stats.exportedUsagePeriods}}
Last reconciliation{{stats.lastReconciliationAt}}
Last event sync{{stats.lastEventSyncAt}}
Last error{{lastError}}
+
+
diff --git a/integrations/whmcs/modules/addons/hexagamecloud/templates/events.tpl b/integrations/whmcs/modules/addons/hexagamecloud/templates/events.tpl new file mode 100644 index 0000000..f0c857b --- /dev/null +++ b/integrations/whmcs/modules/addons/hexagamecloud/templates/events.tpl @@ -0,0 +1,27 @@ +
+
+

GameCloud Events

+
+
+

+ Dashboard + Poll & auto-ack +

+

Pending local copies: {{pendingCount}} · Dead letter: {{deadLetterCount}}

+ + + + + + + + + + + + + {{events.rows}} + +
IDTypeStatusPayloadCreatedActions
+
+
diff --git a/integrations/whmcs/modules/addons/hexagamecloud/templates/reconciliation.tpl b/integrations/whmcs/modules/addons/hexagamecloud/templates/reconciliation.tpl new file mode 100644 index 0000000..35312a4 --- /dev/null +++ b/integrations/whmcs/modules/addons/hexagamecloud/templates/reconciliation.tpl @@ -0,0 +1,29 @@ +
+
+

Reconciliation

+
+
+

+ Dashboard + Dry run + Live + auto-repair +

+ + {{reconciliation.result}} + +

Recent runs

+ + + + + + + + + + + {{reconciliation.rows}} + +
Run IDModeIssuesCreated
+
+
diff --git a/integrations/whmcs/modules/servers/hexagamecloud/lib/ApiClient.php b/integrations/whmcs/modules/servers/hexagamecloud/lib/ApiClient.php index 84663ed..a5af49d 100644 --- a/integrations/whmcs/modules/servers/hexagamecloud/lib/ApiClient.php +++ b/integrations/whmcs/modules/servers/hexagamecloud/lib/ApiClient.php @@ -100,6 +100,11 @@ final class HexaGameCloudApiClient return $this->request('GET', '/api/v1/integrations/whmcs/accounts'); } + public function dashboardStats(): array + { + return $this->request('GET', '/api/v1/integrations/whmcs/dashboard/stats'); + } + public function reconcileService(string $externalServiceId, array $payload = []): array { return $this->request( diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index ab8b50a..b92d329 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -253,6 +253,7 @@ export { whmcsUsageQuerySchema, whmcsUsageResponseSchema, whmcsUsageAdjustmentRequestSchema, + whmcsDashboardStatsResponseSchema, type WhmcsReconcileRequest, type WhmcsReconcileResponse, type WhmcsListAccountsResponse, @@ -262,4 +263,5 @@ export { type WhmcsEventsAckResponse, type WhmcsUsageResponse, type WhmcsUsageAdjustmentRequest, + type WhmcsDashboardStatsResponse, } from './whmcs'; diff --git a/packages/contracts/src/whmcs.ts b/packages/contracts/src/whmcs.ts index a14dcc3..206a169 100644 --- a/packages/contracts/src/whmcs.ts +++ b/packages/contracts/src/whmcs.ts @@ -268,3 +268,22 @@ export type WhmcsEventsAckRequest = z.infer; export type WhmcsEventsAckResponse = z.infer; export type WhmcsUsageResponse = z.infer; export type WhmcsUsageAdjustmentRequest = z.infer; + +export const whmcsDashboardStatsResponseSchema = z.object({ + integrationId: z.string(), + billingProvider: z.enum(['internal', 'whmcs', 'stripe']), + moduleVersion: z.string(), + linkedClients: z.number().int(), + linkedServices: z.number().int(), + suspendedServices: z.number().int(), + terminatedServices: z.number().int(), + pendingEvents: z.number().int(), + deadLetterEvents: z.number().int(), + openReconciliationIssues: z.number().int(), + exportedUsagePeriods: z.number().int(), + eventsCursor: z.string().uuid().nullable(), + lastEventSyncAt: z.string().datetime().nullable(), + lastReconciliationAt: z.string().datetime().nullable(), +}); + +export type WhmcsDashboardStatsResponse = z.infer; diff --git a/packages/contracts/tsconfig.tsbuildinfo b/packages/contracts/tsconfig.tsbuildinfo index 01ce961..4b3bfab 100644 --- a/packages/contracts/tsconfig.tsbuildinfo +++ b/packages/contracts/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"fileNames":["../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/typealiases.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/index.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/zoderror.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseutil.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/enumutil.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorutil.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/partialutil.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/standard-schema.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/index.d.cts","./src/addons.ts","./src/auth.ts","./src/backups.ts","./src/billing.ts","./src/catalog.ts","./src/console.ts","./src/error.ts","./src/files.ts","./src/health.ts","./src/pagination.ts","./src/servers.ts","./src/properties.ts","./src/players.ts","./src/worlds.ts","./src/nodes.ts","./src/join.ts","./src/whmcs.ts","./src/index.ts","../../node_modules/.pnpm/@types+estree@1.0.9/node_modules/@types/estree/index.d.ts","../../node_modules/.pnpm/@types+json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts","../../node_modules/.pnpm/@types+eslint@9.6.1/node_modules/@types/eslint/use-at-your-own-risk.d.ts","../../node_modules/.pnpm/@types+eslint@9.6.1/node_modules/@types/eslint/index.d.ts","../../node_modules/.pnpm/@types+eslint-scope@3.7.7/node_modules/@types/eslint-scope/index.d.ts"],"fileIdsList":[[90,93],[90,91,92],[93],[70],[61,62],[58,59,61,63,64,69],[59,61],[69],[61],[58,59,61,64,65,66,67,68],[58,59,60],[71],[72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88],[71,82],[71,73,82]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"293eadad9dead44c6fd1db6de552663c33f215c55a1bfa2802a1bceed88ff0ec","impliedFormat":1},{"version":"833e92c058d033cde3f29a6c7603f517001d1ddd8020bc94d2067a3bc69b2a8e","impliedFormat":1},{"version":"08b2fae7b0f553ad9f79faec864b179fc58bc172e295a70943e8585dd85f600c","impliedFormat":1},{"version":"f12edf1672a94c578eca32216839604f1e1c16b40a1896198deabf99c882b340","impliedFormat":1},{"version":"e3498cf5e428e6c6b9e97bd88736f26d6cf147dedbfa5a8ad3ed8e05e059af8a","impliedFormat":1},{"version":"dba3f34531fd9b1b6e072928b6f885aa4d28dd6789cbd0e93563d43f4b62da53","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"e4b03ddcf8563b1c0aee782a185286ed85a255ce8a30df8453aade2188bbc904","impliedFormat":1},{"version":"2329d90062487e1eaca87b5e06abcbbeeecf80a82f65f949fd332cfcf824b87b","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"4fdb529707247a1a917a4626bfb6a293d52cd8ee57ccf03830ec91d39d606d6d","impliedFormat":1},{"version":"a9ebb67d6bbead6044b43714b50dcb77b8f7541ffe803046fdec1714c1eba206","impliedFormat":1},{"version":"5780b706cece027f0d4444fbb4e1af62dc51e19da7c3d3719f67b22b033859b9","impliedFormat":1},{"version":"a2c262321da151eda27feac322c9c41a6f20a1827a56d36b083a02dbbd99c527","signature":"b9c75a8a00397adccffc3cfad99587225e887da568dbea6f85b87e00604127b9"},{"version":"79750194d53c3766974bc858e8fee0c86b28cddf493894e2e6f5bd34d6ca8c52","signature":"95ffcd2b6bb4e5f70fe4df750aa4a2968037f13fe4eedbd3fa9c4d9dc5f6c40e"},{"version":"4805b4fce2a5e997e1f3a2c34c5a048bb22af4d624df630659a2d9b9cb8a1d29","signature":"5a6894b3dfd0f673cc8485be7642efaf5f6a2295255472d15ce65dfb44b6b782"},{"version":"9d6f9bad18fa8c5ccfaec031a0a72c0ced949fc1359c2760326ececfe44cb6f9","signature":"ca8618d848283d83a6f65c460ae83f0758fb179ae4f5280e4668cfa60adf157c"},{"version":"cba707183ed31b242617e68deef3ba34f82dcfe17d7444edab33e93d526832ca","signature":"66c20d2476946dd02a69c727c4a7594642d202f74fc6958e1a594e1fa5a8c1d4"},{"version":"9fbe3dd28de24ae4ad40963f723e6f1d244e8efe506e2716f8cf369a6fd6ab1d","signature":"7a91f56e5cf7ae03024a0bbccc80290f5b5e2cc0b738d16a6b58e91f5aebf2ce"},{"version":"57cc3c9b508a8245176a404752b1befcff5ed0dab0aa990de8dedd48b194b465","signature":"535843ba277a7ac9c45dc4be5c1ee8049560b2cdb13b947669083b1b28550996"},{"version":"03f1c2cb8e18d7b6cd2b88af99811b01d8ac1fab87a20f8ffed672d13ed9980c","signature":"53e8271ed9ec8c8e197f90077dc475d6f396ff5825c657a7487d66d2ddefe4a2"},{"version":"252cadf2f292a0f1f83aac871ffd7ffa01cfe9c7807807144649829819d58262","signature":"a0ea78430d431cd60ba7c60e0439e68449416c6da4557a0ed0c13eddcadb7c1f"},{"version":"ad803b7559d94d5b3aa47c3d12e50f160570bd082fbd9dfc568bb3d87eca0ae7","signature":"77abb38bc75a33e9579138dcede7ccc1f9ec7dc171a393006b6e6d61b45c107b"},{"version":"4f4298257de3ba34e67e753e4170760dffc8a2347eb4a4cdb1ab57001b6c0c14","signature":"ed155403e75660e5ced33e5ae18d4daa1f1e8c2603d24a239abc799435dbf124"},{"version":"9bb37e327d06bc032fd19496949828741d049792eb598a1dad7200b156d09d8d","signature":"e344f651d02986596c33a8b4e9495750044f4e7a0acbbf4f752f56852a0d7d90"},{"version":"9c9305bf3e87395bb0b6c350aa5749729a36e30eccb43dc070d07bcd936c02d9","signature":"29950ef2a517a9254a91a91495daa98e8ce0f926ef299ba891e8a28eca3de850"},{"version":"2b5d58380a771126fa8356618483a9f7b7afa6688640813417c73b7203af5215","signature":"03ba45b7629990069bdb3c77e7511afaf92b40d2fbe1984999aaaa54fc5141b1"},{"version":"508998755e135c164b5184a681301cc43ea0d0fcb253b8efa16f6512b93d1946","signature":"14e44b8e788764b288fe9423718096ddf1ec6bc61b42a7d1fb4b9f686d7fee9d"},{"version":"482723066279e20ce7aca2864a689eb5c3f186990f8cc0dd8663b01d60eb146f","signature":"310ac9fe27916d254d7c9d7ab0f7174e9fb169888177a405ec614382f85377db"},{"version":"1539b2b5a1c86a4956ef7946ef2080dc36a3eb2e833d4497ea2f309cffdc2b5c","signature":"c070509b84bd72677276eae686e9961f1f9c36b81bd0fdfd7d9a4ab5c86f739c"},{"version":"dbd0e1d5c555f06d1abec9bbecf9715f087c337270755608025f0577d422d195","signature":"87b9af96a723cdebd96033506222ef868d32abc4acfb0e5c19946094f01f234c"},{"version":"751764bb94219b4ce8f5475dc35d3de2e432fea01a0c9610cd7f69ad05e398c6","impliedFormat":1},{"version":"f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","impliedFormat":1},{"version":"a4a39b5714adfcadd3bbea6698ca2e942606d833bde62ad5fb6ec55f5e438ff8","impliedFormat":1},{"version":"bbc1d029093135d7d9bfa4b38cbf8761db505026cc458b5e9c8b74f4000e5e75","impliedFormat":1},{"version":"1f68ab0e055994eb337b67aa87d2a15e0200951e9664959b3866ee6f6b11a0fe","impliedFormat":1}],"root":[[72,89]],"options":{"composite":true,"declaration":true,"declarationMap":true,"esModuleInterop":true,"module":1,"outDir":"./dist","rootDir":"./src","skipLibCheck":true,"sourceMap":true,"strict":true,"target":9},"referencedMap":[[94,1],[93,2],[92,3],[71,4],[63,5],[70,6],[64,7],[67,8],[60,4],[62,9],[69,10],[61,11],[72,12],[73,12],[74,12],[75,12],[76,12],[77,12],[78,12],[79,12],[80,12],[89,13],[87,14],[86,12],[81,12],[84,12],[83,12],[82,12],[88,15],[85,12]],"latestChangedDtsFile":"./dist/index.d.ts","version":"5.9.3"} \ No newline at end of file +{"fileNames":["../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/typealiases.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/index.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/zoderror.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseutil.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/enumutil.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorutil.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/partialutil.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/standard-schema.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/index.d.cts","./src/addons.ts","./src/auth.ts","./src/backups.ts","./src/billing.ts","./src/catalog.ts","./src/console.ts","./src/error.ts","./src/files.ts","./src/health.ts","./src/pagination.ts","./src/servers.ts","./src/properties.ts","./src/players.ts","./src/worlds.ts","./src/nodes.ts","./src/join.ts","./src/whmcs.ts","./src/index.ts","../../node_modules/.pnpm/@types+estree@1.0.9/node_modules/@types/estree/index.d.ts","../../node_modules/.pnpm/@types+json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts","../../node_modules/.pnpm/@types+eslint@9.6.1/node_modules/@types/eslint/use-at-your-own-risk.d.ts","../../node_modules/.pnpm/@types+eslint@9.6.1/node_modules/@types/eslint/index.d.ts","../../node_modules/.pnpm/@types+eslint-scope@3.7.7/node_modules/@types/eslint-scope/index.d.ts"],"fileIdsList":[[90,93],[90,91,92],[93],[70],[61,62],[58,59,61,63,64,69],[59,61],[69],[61],[58,59,61,64,65,66,67,68],[58,59,60],[71],[72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88],[71,82],[71,73,82]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"293eadad9dead44c6fd1db6de552663c33f215c55a1bfa2802a1bceed88ff0ec","impliedFormat":1},{"version":"833e92c058d033cde3f29a6c7603f517001d1ddd8020bc94d2067a3bc69b2a8e","impliedFormat":1},{"version":"08b2fae7b0f553ad9f79faec864b179fc58bc172e295a70943e8585dd85f600c","impliedFormat":1},{"version":"f12edf1672a94c578eca32216839604f1e1c16b40a1896198deabf99c882b340","impliedFormat":1},{"version":"e3498cf5e428e6c6b9e97bd88736f26d6cf147dedbfa5a8ad3ed8e05e059af8a","impliedFormat":1},{"version":"dba3f34531fd9b1b6e072928b6f885aa4d28dd6789cbd0e93563d43f4b62da53","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"e4b03ddcf8563b1c0aee782a185286ed85a255ce8a30df8453aade2188bbc904","impliedFormat":1},{"version":"2329d90062487e1eaca87b5e06abcbbeeecf80a82f65f949fd332cfcf824b87b","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"4fdb529707247a1a917a4626bfb6a293d52cd8ee57ccf03830ec91d39d606d6d","impliedFormat":1},{"version":"a9ebb67d6bbead6044b43714b50dcb77b8f7541ffe803046fdec1714c1eba206","impliedFormat":1},{"version":"5780b706cece027f0d4444fbb4e1af62dc51e19da7c3d3719f67b22b033859b9","impliedFormat":1},{"version":"a2c262321da151eda27feac322c9c41a6f20a1827a56d36b083a02dbbd99c527","signature":"b9c75a8a00397adccffc3cfad99587225e887da568dbea6f85b87e00604127b9"},{"version":"79750194d53c3766974bc858e8fee0c86b28cddf493894e2e6f5bd34d6ca8c52","signature":"95ffcd2b6bb4e5f70fe4df750aa4a2968037f13fe4eedbd3fa9c4d9dc5f6c40e"},{"version":"4805b4fce2a5e997e1f3a2c34c5a048bb22af4d624df630659a2d9b9cb8a1d29","signature":"5a6894b3dfd0f673cc8485be7642efaf5f6a2295255472d15ce65dfb44b6b782"},{"version":"9d6f9bad18fa8c5ccfaec031a0a72c0ced949fc1359c2760326ececfe44cb6f9","signature":"ca8618d848283d83a6f65c460ae83f0758fb179ae4f5280e4668cfa60adf157c"},{"version":"cba707183ed31b242617e68deef3ba34f82dcfe17d7444edab33e93d526832ca","signature":"66c20d2476946dd02a69c727c4a7594642d202f74fc6958e1a594e1fa5a8c1d4"},{"version":"9fbe3dd28de24ae4ad40963f723e6f1d244e8efe506e2716f8cf369a6fd6ab1d","signature":"7a91f56e5cf7ae03024a0bbccc80290f5b5e2cc0b738d16a6b58e91f5aebf2ce"},{"version":"57cc3c9b508a8245176a404752b1befcff5ed0dab0aa990de8dedd48b194b465","signature":"535843ba277a7ac9c45dc4be5c1ee8049560b2cdb13b947669083b1b28550996"},{"version":"03f1c2cb8e18d7b6cd2b88af99811b01d8ac1fab87a20f8ffed672d13ed9980c","signature":"53e8271ed9ec8c8e197f90077dc475d6f396ff5825c657a7487d66d2ddefe4a2"},{"version":"252cadf2f292a0f1f83aac871ffd7ffa01cfe9c7807807144649829819d58262","signature":"a0ea78430d431cd60ba7c60e0439e68449416c6da4557a0ed0c13eddcadb7c1f"},{"version":"ad803b7559d94d5b3aa47c3d12e50f160570bd082fbd9dfc568bb3d87eca0ae7","signature":"77abb38bc75a33e9579138dcede7ccc1f9ec7dc171a393006b6e6d61b45c107b"},{"version":"4f4298257de3ba34e67e753e4170760dffc8a2347eb4a4cdb1ab57001b6c0c14","signature":"ed155403e75660e5ced33e5ae18d4daa1f1e8c2603d24a239abc799435dbf124"},{"version":"9bb37e327d06bc032fd19496949828741d049792eb598a1dad7200b156d09d8d","signature":"e344f651d02986596c33a8b4e9495750044f4e7a0acbbf4f752f56852a0d7d90"},{"version":"9c9305bf3e87395bb0b6c350aa5749729a36e30eccb43dc070d07bcd936c02d9","signature":"29950ef2a517a9254a91a91495daa98e8ce0f926ef299ba891e8a28eca3de850"},{"version":"2b5d58380a771126fa8356618483a9f7b7afa6688640813417c73b7203af5215","signature":"03ba45b7629990069bdb3c77e7511afaf92b40d2fbe1984999aaaa54fc5141b1"},{"version":"508998755e135c164b5184a681301cc43ea0d0fcb253b8efa16f6512b93d1946","signature":"14e44b8e788764b288fe9423718096ddf1ec6bc61b42a7d1fb4b9f686d7fee9d"},{"version":"482723066279e20ce7aca2864a689eb5c3f186990f8cc0dd8663b01d60eb146f","signature":"310ac9fe27916d254d7c9d7ab0f7174e9fb169888177a405ec614382f85377db"},{"version":"d4da759d307bb6260e81216042f0ef2948e99e1f7af94c528ca34379e707ed81","signature":"02da24189a7d8ca30727640c73cf352d386cb2feb7916caa0e42b12b13dd1231"},{"version":"eca7bae77c5d0c16336acb1e0c399de5a6a6d4658bbf9824594de9016d7fd2cb","signature":"77e1ff7c51adbe877c27fcd84c64cd1b288ade8110da4dd0927657f5bfab1052"},{"version":"751764bb94219b4ce8f5475dc35d3de2e432fea01a0c9610cd7f69ad05e398c6","impliedFormat":1},{"version":"f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","impliedFormat":1},{"version":"a4a39b5714adfcadd3bbea6698ca2e942606d833bde62ad5fb6ec55f5e438ff8","impliedFormat":1},{"version":"bbc1d029093135d7d9bfa4b38cbf8761db505026cc458b5e9c8b74f4000e5e75","impliedFormat":1},{"version":"1f68ab0e055994eb337b67aa87d2a15e0200951e9664959b3866ee6f6b11a0fe","impliedFormat":1}],"root":[[72,89]],"options":{"composite":true,"declaration":true,"declarationMap":true,"esModuleInterop":true,"module":1,"outDir":"./dist","rootDir":"./src","skipLibCheck":true,"sourceMap":true,"strict":true,"target":9},"referencedMap":[[94,1],[93,2],[92,3],[71,4],[63,5],[70,6],[64,7],[67,8],[60,4],[62,9],[69,10],[61,11],[72,12],[73,12],[74,12],[75,12],[76,12],[77,12],[78,12],[79,12],[80,12],[89,13],[87,14],[86,12],[81,12],[84,12],[83,12],[82,12],[88,15],[85,12]],"latestChangedDtsFile":"./dist/whmcs.d.ts","version":"5.9.3"} \ No newline at end of file