diff --git a/docs/plans/PLUGIN-DISTRIBUTION-SYSTEM.md b/docs/plans/PLUGIN-DISTRIBUTION-SYSTEM.md new file mode 100644 index 00000000..70bae0b2 --- /dev/null +++ b/docs/plans/PLUGIN-DISTRIBUTION-SYSTEM.md @@ -0,0 +1,692 @@ +# IGNY8 Plugin Distribution System + +**Created:** January 9, 2026 +**Version:** 1.0 +**Status:** Planning +**Scope:** WordPress, Shopify, Custom Site Integration Plugins + +--- + +## Overview + +This document outlines the architecture for distributing and auto-updating IGNY8 integration plugins across multiple platforms (WordPress, Shopify, Custom Sites). + +--- + +## 1. Directory Structure + +### 1.1 - Server Location + +``` +/data/app/igny8/ +├── backend/ +│ └── igny8_core/ +│ └── plugins/ # NEW: Plugin management +│ ├── __init__.py +│ ├── models.py # Plugin version tracking +│ ├── views.py # Download & update API +│ ├── serializers.py +│ ├── urls.py +│ └── utils.py # ZIP compression, versioning +├── plugins/ # NEW: Plugin source & distributions +│ ├── wordpress/ +│ │ ├── source/ # Development source files +│ │ │ └── igny8-wp-bridge/ +│ │ │ ├── igny8-wp-bridge.php +│ │ │ ├── includes/ +│ │ │ ├── assets/ +│ │ │ └── readme.txt +│ │ └── dist/ # Compiled/zipped releases +│ │ ├── igny8-wp-bridge-v1.0.0.zip +│ │ ├── igny8-wp-bridge-v1.0.1.zip +│ │ └── igny8-wp-bridge-latest.zip # Symlink to latest +│ ├── shopify/ # FUTURE +│ │ ├── source/ +│ │ └── dist/ +│ └── custom-site/ # FUTURE +│ ├── source/ +│ └── dist/ +└── docs/ + └── plans/ + └── PLUGIN-DISTRIBUTION-SYSTEM.md +``` + +### 1.2 - Why This Structure? + +| Location | Purpose | +|----------|---------| +| `/plugins/` | Separate from app code, easy to manage | +| `/plugins/{platform}/source/` | Development files, version controlled | +| `/plugins/{platform}/dist/` | Ready-to-download ZIP files | +| `/backend/igny8_core/plugins/` | Django app for API & version management | + +--- + +## 2. Database Models + +### 2.1 - Plugin Model + +```python +# backend/igny8_core/plugins/models.py + +class Plugin(models.Model): + """Represents a plugin type (WordPress, Shopify, etc.)""" + + PLATFORM_CHOICES = [ + ('wordpress', 'WordPress'), + ('shopify', 'Shopify'), + ('custom', 'Custom Site'), + ] + + name = models.CharField(max_length=100) # "IGNY8 WP Bridge" + slug = models.SlugField(unique=True) # "igny8-wp-bridge" + platform = models.CharField(max_length=20, choices=PLATFORM_CHOICES) + description = models.TextField() + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'plugins' + + +class PluginVersion(models.Model): + """Tracks each version of a plugin""" + + STATUS_CHOICES = [ + ('draft', 'Draft'), # In development + ('testing', 'Testing'), # Internal testing + ('staged', 'Staged'), # Ready, not yet pushed + ('released', 'Released'), # Available for download + ('update_ready', 'Update Ready'), # Push to installed sites + ('deprecated', 'Deprecated'), # Old version + ] + + plugin = models.ForeignKey(Plugin, on_delete=models.CASCADE, related_name='versions') + version = models.CharField(max_length=20) # "1.0.0", "1.0.1" + version_code = models.IntegerField() # 100, 101 (for comparison) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft') + + # File info + file_path = models.CharField(max_length=500) # Relative path in dist/ + file_size = models.IntegerField(default=0) # Bytes + checksum = models.CharField(max_length=64) # SHA256 for integrity + + # Release info + changelog = models.TextField(blank=True) + min_api_version = models.CharField(max_length=20, default='1.0') + + # Timestamps + created_at = models.DateTimeField(auto_now_add=True) + released_at = models.DateTimeField(null=True, blank=True) + + # Auto-update control + force_update = models.BooleanField(default=False) # Critical security fix + + class Meta: + db_table = 'plugin_versions' + unique_together = ['plugin', 'version'] + ordering = ['-version_code'] + + +class PluginInstallation(models.Model): + """Tracks where plugins are installed (per site)""" + + site = models.ForeignKey('Site', on_delete=models.CASCADE, related_name='plugin_installations') + plugin = models.ForeignKey(Plugin, on_delete=models.CASCADE) + current_version = models.ForeignKey(PluginVersion, on_delete=models.SET_NULL, null=True) + + # Installation status + is_active = models.BooleanField(default=True) + last_health_check = models.DateTimeField(null=True) + + # Update tracking + pending_update = models.ForeignKey( + PluginVersion, + on_delete=models.SET_NULL, + null=True, + related_name='pending_installations' + ) + update_notified_at = models.DateTimeField(null=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'plugin_installations' + unique_together = ['site', 'plugin'] +``` + +--- + +## 3. API Endpoints + +### 3.1 - Public Endpoints (No Auth Required) + +```python +# Download latest plugin (for initial installation) +GET /api/plugins/{slug}/download/ +# Returns: ZIP file download + +# Check for updates (called by installed plugins) +GET /api/plugins/{slug}/check-update/?current_version=1.0.0 +# Returns: { "update_available": true, "latest_version": "1.0.1", "download_url": "...", "changelog": "..." } +``` + +### 3.2 - Authenticated Endpoints + +```python +# Get download URL for user (tracks downloads) +GET /api/plugins/{slug}/download-url/ +# Returns: { "url": "...", "expires_at": "..." } + +# Report installation (called after WP plugin is activated) +POST /api/plugins/{slug}/register-installation/ +# Body: { "site_id": 123, "version": "1.0.0" } + +# Update installation status (health check) +POST /api/plugins/{slug}/health-check/ +# Body: { "site_id": 123, "version": "1.0.0", "status": "active" } +``` + +### 3.3 - Admin Endpoints + +```python +# List all plugin versions +GET /api/admin/plugins/ + +# Create new version +POST /api/admin/plugins/{slug}/versions/ +# Body: { "version": "1.0.1", "changelog": "...", "status": "draft" } + +# Upload plugin ZIP +POST /api/admin/plugins/{slug}/versions/{version}/upload/ +# Body: multipart/form-data with ZIP file + +# Release version (make available for download) +POST /api/admin/plugins/{slug}/versions/{version}/release/ + +# Push update to installed sites +POST /api/admin/plugins/{slug}/versions/{version}/push-update/ +# This sets status to 'update_ready' and notifies all installations +``` + +--- + +## 4. WordPress Plugin Update Mechanism + +### 4.1 - How WordPress Auto-Updates Work + +WordPress checks for plugin updates by calling a filter. Our plugin hooks into this: + +```php +// In igny8-wp-bridge.php + +class IGNY8_WP_Bridge { + + private $plugin_slug = 'igny8-wp-bridge'; + private $version = '1.0.0'; + private $api_url = 'https://app.igny8.com/api/plugins/'; + + public function __construct() { + // Check for updates + add_filter('pre_set_site_transient_update_plugins', [$this, 'check_for_updates']); + add_filter('plugins_api', [$this, 'plugin_info'], 10, 3); + } + + public function check_for_updates($transient) { + if (empty($transient->checked)) { + return $transient; + } + + // Call IGNY8 API to check for updates + $response = wp_remote_get($this->api_url . $this->plugin_slug . '/check-update/', [ + 'timeout' => 10, + 'headers' => [ + 'X-IGNY8-Site-ID' => get_option('igny8_site_id'), + 'X-IGNY8-API-Key' => get_option('igny8_api_key'), + ], + 'body' => [ + 'current_version' => $this->version, + ] + ]); + + if (is_wp_error($response)) { + return $transient; + } + + $data = json_decode(wp_remote_retrieve_body($response), true); + + if (!empty($data['update_available'])) { + $plugin_file = $this->plugin_slug . '/' . $this->plugin_slug . '.php'; + + $transient->response[$plugin_file] = (object) [ + 'slug' => $this->plugin_slug, + 'new_version' => $data['latest_version'], + 'package' => $data['download_url'], + 'url' => $data['info_url'] ?? '', + ]; + } + + return $transient; + } + + public function plugin_info($result, $action, $args) { + if ($action !== 'plugin_information' || $args->slug !== $this->plugin_slug) { + return $result; + } + + // Fetch plugin info from IGNY8 API + $response = wp_remote_get($this->api_url . $this->plugin_slug . '/info/'); + + if (is_wp_error($response)) { + return $result; + } + + $data = json_decode(wp_remote_retrieve_body($response), true); + + return (object) [ + 'name' => $data['name'], + 'slug' => $this->plugin_slug, + 'version' => $data['version'], + 'author' => 'IGNY8', + 'homepage' => 'https://igny8.com', + 'sections' => [ + 'description' => $data['description'], + 'changelog' => $data['changelog'], + ], + 'download_link' => $data['download_url'], + ]; + } +} +``` + +### 4.2 - Update Flow + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Plugin Update Flow │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. Admin marks version as "Update Ready" in IGNY8 Dashboard │ +│ │ │ +│ ▼ │ +│ 2. IGNY8 API returns update_available=true for check-update calls │ +│ │ │ +│ ▼ │ +│ 3. WordPress cron checks for updates (or user clicks "Check Now") │ +│ │ │ +│ ▼ │ +│ 4. WP Plugin calls IGNY8 API → Gets new version info │ +│ │ │ +│ ▼ │ +│ 5. WordPress shows "Update Available" in Plugins page │ +│ │ │ +│ ▼ │ +│ 6. User clicks Update (or auto-update if enabled) │ +│ │ │ +│ ▼ │ +│ 7. WordPress downloads ZIP from IGNY8 and installs │ +│ │ │ +│ ▼ │ +│ 8. Plugin activation hook reports new version to IGNY8 │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 5. Initial Download (ZIP Generation) + +### 5.1 - Build Script + +```bash +#!/bin/bash +# scripts/build-wp-plugin.sh + +VERSION=$1 +PLUGIN_NAME="igny8-wp-bridge" +SOURCE_DIR="/data/app/igny8/plugins/wordpress/source/${PLUGIN_NAME}" +DIST_DIR="/data/app/igny8/plugins/wordpress/dist" + +if [ -z "$VERSION" ]; then + echo "Usage: ./build-wp-plugin.sh " + exit 1 +fi + +# Create dist directory if not exists +mkdir -p "$DIST_DIR" + +# Create temp directory for packaging +TEMP_DIR=$(mktemp -d) +PACKAGE_DIR="${TEMP_DIR}/${PLUGIN_NAME}" + +# Copy source files +cp -r "$SOURCE_DIR" "$PACKAGE_DIR" + +# Update version in main plugin file +sed -i "s/Version: .*/Version: ${VERSION}/" "${PACKAGE_DIR}/${PLUGIN_NAME}.php" +sed -i "s/\$version = '.*'/\$version = '${VERSION}'/" "${PACKAGE_DIR}/${PLUGIN_NAME}.php" + +# Create ZIP +cd "$TEMP_DIR" +zip -r "${DIST_DIR}/${PLUGIN_NAME}-v${VERSION}.zip" "$PLUGIN_NAME" + +# Update latest symlink +ln -sf "${PLUGIN_NAME}-v${VERSION}.zip" "${DIST_DIR}/${PLUGIN_NAME}-latest.zip" + +# Calculate checksum +sha256sum "${DIST_DIR}/${PLUGIN_NAME}-v${VERSION}.zip" > "${DIST_DIR}/${PLUGIN_NAME}-v${VERSION}.sha256" + +# Cleanup +rm -rf "$TEMP_DIR" + +echo "Built: ${DIST_DIR}/${PLUGIN_NAME}-v${VERSION}.zip" +echo "Checksum: $(cat ${DIST_DIR}/${PLUGIN_NAME}-v${VERSION}.sha256)" +``` + +### 5.2 - Django Management Command + +```python +# backend/igny8_core/plugins/management/commands/build_plugin.py + +from django.core.management.base import BaseCommand +import subprocess +import hashlib +from plugins.models import Plugin, PluginVersion + +class Command(BaseCommand): + help = 'Build and register a new plugin version' + + def add_arguments(self, parser): + parser.add_argument('--plugin', type=str, required=True) + parser.add_argument('--version', type=str, required=True) + parser.add_argument('--changelog', type=str, default='') + parser.add_argument('--release', action='store_true') + + def handle(self, *args, **options): + plugin = Plugin.objects.get(slug=options['plugin']) + version = options['version'] + + # Run build script + result = subprocess.run( + ['./scripts/build-wp-plugin.sh', version], + capture_output=True, + text=True + ) + + if result.returncode != 0: + self.stderr.write(f"Build failed: {result.stderr}") + return + + # Calculate version code (1.0.1 -> 101) + parts = version.split('.') + version_code = int(parts[0]) * 10000 + int(parts[1]) * 100 + int(parts[2]) + + # Get file info + file_path = f"{plugin.slug}-v{version}.zip" + full_path = f"/data/app/igny8/plugins/wordpress/dist/{file_path}" + + with open(full_path, 'rb') as f: + checksum = hashlib.sha256(f.read()).hexdigest() + + import os + file_size = os.path.getsize(full_path) + + # Create version record + plugin_version, created = PluginVersion.objects.update_or_create( + plugin=plugin, + version=version, + defaults={ + 'version_code': version_code, + 'file_path': file_path, + 'file_size': file_size, + 'checksum': checksum, + 'changelog': options['changelog'], + 'status': 'released' if options['release'] else 'draft', + } + ) + + action = 'Created' if created else 'Updated' + self.stdout.write(f"{action} version {version} for {plugin.name}") +``` + +--- + +## 6. Frontend Integration + +### 6.1 - Download Button Component + +```tsx +// frontend/src/components/sites/PluginDownloadSection.tsx + +import { Button } from '@/components/ui'; +import { useQuery } from '@tanstack/react-query'; +import { api } from '@/services/api'; + +interface PluginInfo { + name: string; + slug: string; + version: string; + download_url: string; + file_size: number; + platform: string; +} + +export function PluginDownloadSection({ platform = 'wordpress' }: { platform: string }) { + const { data: plugin, isLoading } = useQuery({ + queryKey: ['plugin', platform], + queryFn: () => api.get(`/plugins/${platform}/latest/`).then(r => r.data), + }); + + const handleDownload = async () => { + if (!plugin) return; + + // Track download + await api.post(`/plugins/${plugin.slug}/track-download/`); + + // Trigger download + window.location.href = plugin.download_url; + }; + + if (isLoading) return
Loading...
; + if (!plugin) return null; + + return ( +
+
+
+ +
+
+

{plugin.name}

+

+ Download and install the plugin on your {platform === 'wordpress' ? 'WordPress' : platform} site +

+
+ Version {plugin.version} + {formatFileSize(plugin.file_size)} +
+
+ +
+
+ ); +} +``` + +### 6.2 - Admin Plugin Management Page (Future) + +``` +/admin/plugins # List all plugins +/admin/plugins/{slug} # Plugin details + versions +/admin/plugins/{slug}/versions # Version history +/admin/plugins/{slug}/upload # Upload new version +``` + +--- + +## 7. Security Considerations + +### 7.1 - Download Security + +| Measure | Implementation | +|---------|----------------| +| Signed URLs | Time-limited download URLs (15 min expiry) | +| Checksum | SHA256 verification in WP plugin | +| API Key Required | Downloads require valid site API key | +| Rate Limiting | Max 10 downloads/hour per site | + +### 7.2 - Update Security + +| Measure | Implementation | +|---------|----------------| +| HTTPS Only | All API calls over TLS | +| Version Signing | Checksum comparison before install | +| Rollback | Keep last 3 versions for emergency rollback | +| Staged Rollout | Option to release to % of installs first | + +--- + +## 8. Implementation Phases + +### Phase 1: Basic Infrastructure (Week 1) +- [ ] Create `/plugins/` directory structure +- [ ] Create Django `plugins` app with models +- [ ] Migrate existing WP plugin to `/plugins/wordpress/source/` +- [ ] Create build script for ZIP generation +- [ ] Implement download API endpoint + +### Phase 2: Frontend Integration (Week 1) +- [ ] Update Site Settings > Integrations download button +- [ ] Create plugin info API endpoint +- [ ] Add version display to download section + +### Phase 3: Update System (Week 2) +- [ ] Implement check-update API +- [ ] Add update hooks to WP plugin +- [ ] Create PluginInstallation tracking +- [ ] Build admin version management UI + +### Phase 4: Advanced Features (Week 3) +- [ ] Signed download URLs +- [ ] Version rollback support +- [ ] Update notifications in IGNY8 dashboard +- [ ] Staged rollout percentage + +### Phase 5: Additional Platforms (Future) +- [ ] Shopify plugin structure +- [ ] Custom site SDK/package +- [ ] Platform-specific update mechanisms + +--- + +## 9. File Serving Configuration + +### 9.1 - Nginx Configuration + +```nginx +# Add to nginx config for igny8 + +# Serve plugin downloads +location /plugins/download/ { + alias /data/app/igny8/plugins/; + + # Only allow ZIP files + location ~ \.zip$ { + add_header Content-Disposition 'attachment'; + add_header X-Content-Type-Options 'nosniff'; + } + + # Deny access to source files + location ~ /source/ { + deny all; + } +} +``` + +### 9.2 - Django URL (Proxied Downloads) + +```python +# Better approach: Django handles download with auth + +# backend/igny8_core/plugins/views.py + +from django.http import FileResponse +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny + +@api_view(['GET']) +@permission_classes([AllowAny]) +def download_plugin(request, slug): + plugin = get_object_or_404(Plugin, slug=slug, is_active=True) + latest = plugin.versions.filter(status__in=['released', 'update_ready']).first() + + if not latest: + return Response({'error': 'No version available'}, status=404) + + file_path = f'/data/app/igny8/plugins/{plugin.platform}/dist/{latest.file_path}' + + response = FileResponse( + open(file_path, 'rb'), + content_type='application/zip' + ) + response['Content-Disposition'] = f'attachment; filename="{latest.file_path}"' + return response +``` + +--- + +## 10. Monitoring & Analytics + +### 10.1 - Metrics to Track + +| Metric | Purpose | +|--------|---------| +| Downloads per version | Adoption rate | +| Active installations | Current reach | +| Update success rate | Quality indicator | +| Version distribution | Upgrade urgency | +| Health check failures | Support needs | + +### 10.2 - Admin Dashboard Widgets + +- **Plugin Downloads** - Daily/weekly download counts +- **Version Distribution** - Pie chart of installed versions +- **Update Adoption** - Time to 80% adoption after release +- **Installation Health** - Sites with outdated/failing plugins + +--- + +## Quick Reference + +### Commands + +```bash +# Build new version +python manage.py build_plugin --plugin=igny8-wp-bridge --version=1.0.1 --changelog="Bug fixes" + +# Release version (make downloadable) +python manage.py release_plugin --plugin=igny8-wp-bridge --version=1.0.1 + +# Push update to sites +python manage.py push_plugin_update --plugin=igny8-wp-bridge --version=1.0.1 +``` + +### API Quick Reference + +``` +GET /api/plugins/{slug}/download/ # Download latest ZIP +GET /api/plugins/{slug}/check-update/ # Check for updates +GET /api/plugins/{slug}/info/ # Plugin metadata +POST /api/plugins/{slug}/register/ # Register installation +POST /api/plugins/{slug}/health-check/ # Report status +``` + +--- + +**Document Owner:** IGNY8 Team +**Next Review:** After Phase 1 implementation