diff --git a/.gitea/scripts/update-index.js b/.gitea/scripts/update-index.js new file mode 100644 index 0000000..23e5dcb --- /dev/null +++ b/.gitea/scripts/update-index.js @@ -0,0 +1,28 @@ +import { readdir, readFile, writeFile } from 'fs/promises'; +import { join } from 'path'; + +const pluginsDir = './plugins'; + +async function updateIndex() { + const files = await readdir(pluginsDir); + const pluginIds = files + .filter(f => f.endsWith('.json') && f !== 'index.json') + .map(f => f.replace('.json', '')) + .sort(); + + const index = { + version: '1.0.0', + updatedAt: new Date().toISOString(), + plugins: pluginIds + }; + + await writeFile( + join(pluginsDir, 'index.json'), + JSON.stringify(index, null, 2) + '\n' + ); + + console.log(`✅ Updated index.json with ${pluginIds.length} plugins`); + console.log('Plugins:', pluginIds.join(', ')); +} + +updateIndex().catch(console.error); diff --git a/.gitea/scripts/validate-pr.js b/.gitea/scripts/validate-pr.js index 55038f5..9096d4d 100644 --- a/.gitea/scripts/validate-pr.js +++ b/.gitea/scripts/validate-pr.js @@ -10,12 +10,93 @@ * 4. Plugin ID matches filename * 5. Author matches PR creator (for new plugins) * 6. Can only remove own plugins + * 7. Thumbnail requirements: png/gif/jpg, max 512x512, max 2MB */ import { readFile, access } from 'fs/promises'; import { join } from 'path'; +import https from 'https'; +import http from 'http'; const REQUIRED_FIELDS = ['id', 'name', 'version', 'description', 'author', 'repository']; +const MAX_THUMBNAIL_SIZE = 2 * 1024 * 1024; // 2MB +const MAX_THUMBNAIL_DIMENSIONS = 512; +const ALLOWED_IMAGE_FORMATS = ['png', 'gif', 'jpg', 'jpeg']; + +async function validateThumbnail(url) { + return new Promise((resolve, reject) => { + const protocol = url.startsWith('https') ? https : http; + + protocol.get(url, (res) => { + if (res.statusCode !== 200) { + return reject(new Error(`Thumbnail URL returned ${res.statusCode}`)); + } + + const chunks = []; + let size = 0; + + res.on('data', (chunk) => { + chunks.push(chunk); + size += chunk.length; + + if (size > MAX_THUMBNAIL_SIZE) { + res.destroy(); + reject(new Error(`Thumbnail exceeds 2MB limit (${(size / 1024 / 1024).toFixed(2)}MB)`)); + } + }); + + res.on('end', () => { + const buffer = Buffer.concat(chunks); + + // Check image format by magic bytes + const isPNG = buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4E && buffer[3] === 0x47; + const isJPG = buffer[0] === 0xFF && buffer[1] === 0xD8 && buffer[2] === 0xFF; + const isGIF = buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46; + + if (!isPNG && !isJPG && !isGIF) { + return reject(new Error('Thumbnail must be PNG, JPG, or GIF format')); + } + + // Check dimensions (simplified check - reads PNG/JPG headers) + let width, height; + + if (isPNG) { + width = buffer.readUInt32BE(16); + height = buffer.readUInt32BE(20); + } else if (isJPG) { + // Simplified JPEG dimension reading + let offset = 2; + while (offset < buffer.length) { + if (buffer[offset] !== 0xFF) break; + offset++; + const marker = buffer[offset]; + offset++; + + if (marker === 0xC0 || marker === 0xC2) { + height = buffer.readUInt16BE(offset + 3); + width = buffer.readUInt16BE(offset + 5); + break; + } + + const segmentLength = buffer.readUInt16BE(offset); + offset += segmentLength; + } + } else if (isGIF) { + width = buffer.readUInt16LE(6); + height = buffer.readUInt16LE(8); + } + + if (width > MAX_THUMBNAIL_DIMENSIONS || height > MAX_THUMBNAIL_DIMENSIONS) { + return reject(new Error(`Thumbnail dimensions ${width}x${height} exceed ${MAX_THUMBNAIL_DIMENSIONS}x${MAX_THUMBNAIL_DIMENSIONS}`)); + } + + resolve({ width, height, size, format: isPNG ? 'PNG' : isJPG ? 'JPG' : 'GIF' }); + }); + + res.on('error', reject); + }).on('error', reject); + }); +} const prAuthor = process.argv[2]; const changedFiles = process.argv[3]?.split('\n').filter(f => f.trim()) || []; @@ -56,12 +137,22 @@ for (const file of changedFiles) { const pluginId = file.replace('plugins/', '').replace('.json', ''); + // Filename must be prefixed with username + if (!pluginId.startsWith(`${prAuthor}-`)) { + errors.push(`❌ ${file}: Filename must start with your username "${prAuthor}-" (e.g., "${prAuthor}-my-plugin.json")`); + continue; + } + + // Adjust file paths based on working directory + const prFilePath = process.env.PR_FILES_DIR ? `${process.env.PR_FILES_DIR}/${file.replace('plugins/', '')}` : file; + const baseFilePath = file; + // Check if file was added or removed let isAdded = false; let isRemoved = false; try { - await access(file); + await access(prFilePath); isAdded = true; } catch { isRemoved = true; @@ -70,9 +161,32 @@ for (const file of changedFiles) { if (isAdded) { // Validate added/modified plugin try { - const content = await readFile(file, 'utf-8'); + const content = await readFile(prFilePath, 'utf-8'); const plugin = JSON.parse(content); + // Check if this plugin already exists on main + let isModification = false; + let originalAuthor = null; + + try { + const originalContent = await readFile(baseFilePath, 'utf-8'); + const originalPlugin = JSON.parse(originalContent); + isModification = true; + originalAuthor = originalPlugin.author; + } catch { + // File doesn't exist on main, this is a new plugin + isModification = false; + } + + // If modifying existing plugin, MUST match original author + if (isModification) { + if (originalAuthor !== prAuthor) { + errors.push(`❌ ${file}: Cannot modify plugin owned by "${originalAuthor}" (you are "${prAuthor}")`); + continue; + } + console.log(` ℹ️ Modifying existing plugin by ${originalAuthor}`); + } + // Check required fields for (const field of REQUIRED_FIELDS) { if (!plugin[field]) { @@ -90,7 +204,7 @@ for (const file of changedFiles) { errors.push(`❌ ${file}: Invalid version format: ${plugin.version}`); } - // Check author matches PR creator (for new files) + // Check author matches PR creator if (plugin.author !== prAuthor) { errors.push(`❌ ${file}: Author "${plugin.author}" must match PR creator "${prAuthor}"`); } @@ -104,6 +218,23 @@ for (const file of changedFiles) { } } + // Check thumbnail if provided (optional) + if (plugin.thumbnail) { + try { + new URL(plugin.thumbnail); + + // Validate thumbnail meets requirements + try { + const thumbInfo = await validateThumbnail(plugin.thumbnail); + console.log(` ✓ Thumbnail: ${thumbInfo.format} ${thumbInfo.width}x${thumbInfo.height} (${(thumbInfo.size / 1024).toFixed(1)}KB)`); + } catch (thumbErr) { + errors.push(`❌ ${file}: Thumbnail validation failed: ${thumbErr.message}`); + } + } catch { + errors.push(`❌ ${file}: Invalid thumbnail URL: ${plugin.thumbnail}`); + } + } + if (errors.length === 0) { added.push(pluginId); console.log(`✅ Valid plugin: ${plugin.name}`); @@ -114,10 +245,21 @@ for (const file of changedFiles) { } } else if (isRemoved) { - // For removed files, we can't validate ownership easily in CI - // Just track that it was removed - removed.push(pluginId); - console.log(`🗑️ Removed: ${pluginId}`); + // For removed files, check the original author from main branch + try { + const originalContent = await readFile(baseFilePath, 'utf-8'); + const originalPlugin = JSON.parse(originalContent); + + if (originalPlugin.author !== prAuthor) { + errors.push(`❌ ${file}: Cannot remove plugin owned by "${originalPlugin.author}" (you are "${prAuthor}")`); + continue; + } + + removed.push(pluginId); + console.log(`🗑️ Removed: ${pluginId} (owned by ${prAuthor})`); + } catch (err) { + errors.push(`❌ ${file}: Could not verify ownership for removal: ${err.message}`); + } } } diff --git a/.gitea/workflows/update-index.yml b/.gitea/workflows/update-index.yml index 0e1ed0e..f13f6ca 100644 --- a/.gitea/workflows/update-index.yml +++ b/.gitea/workflows/update-index.yml @@ -13,29 +13,13 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + - name: Generate index.json - run: | - cd plugins - echo '{' > index.json - echo ' "version": "1.0.0",' >> index.json - echo ' "updatedAt": "'$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")'",' >> index.json - echo ' "plugins": [' >> index.json - - first=true - for file in *.json; do - if [ "$file" != "index.json" ]; then - plugin_id="${file%.json}" - if [ "$first" = true ]; then - echo " \"$plugin_id\"" >> index.json - first=false - else - echo ", \"$plugin_id\"" >> index.json - fi - fi - done | sed '$ s/,$//' - - echo ' ]' >> index.json - echo '}' >> index.json + run: node .gitea/scripts/update-index.js - name: Commit index.json run: | diff --git a/.gitea/workflows/validate-pr.yml b/.gitea/workflows/validate-pr.yml index ff36ea0..c69ffd1 100644 --- a/.gitea/workflows/validate-pr.yml +++ b/.gitea/workflows/validate-pr.yml @@ -2,15 +2,21 @@ name: Validate Plugin PR on: pull_request: branches: [ main ] - paths: - - 'plugins/**' jobs: validate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Checkout base branch (main) for validation scripts + uses: actions/checkout@v3 with: + ref: main + path: base + + - name: Checkout PR branch for plugin files + uses: actions/checkout@v3 + with: + path: pr fetch-depth: 0 - name: Setup Node.js @@ -21,18 +27,72 @@ jobs: - name: Get changed files id: changed-files run: | + cd pr echo "files=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep '^plugins/' || echo '')" >> $GITHUB_OUTPUT - - name: Validate PR + - name: Block attempts to modify infrastructure run: | + cd pr + INFRA_CHANGES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E '^\.gitea/' || echo '') + if [ -n "$INFRA_CHANGES" ]; then + echo "❌ ERROR: PRs cannot modify .gitea/ directory files" + echo "Changed infrastructure files:" + echo "$INFRA_CHANGES" + echo "" + echo "Only plugins/ directory changes are allowed." + exit 1 + fi + + - name: Verify commit authors match PR creator + run: | + cd pr + echo "🔍 Verifying commit authors..." + PR_USER="${{ github.event.pull_request.user.login }}" + echo "PR created by: $PR_USER" + + # Get all commits in this PR + COMMITS=$(git log --format="%H" origin/${{ github.base_ref }}..HEAD) + + for commit in $COMMITS; do + AUTHOR=$(git show -s --format='%an' $commit) + EMAIL=$(git show -s --format='%ae' $commit) + echo " Commit $commit by $AUTHOR <$EMAIL>" + + # This is a warning, not a hard block, since git authors can be configured differently + # The real security comes from PR creator authentication + done + + echo "✓ PR created by authenticated user: $PR_USER" + echo "Note: Validation will check plugin ownership against this authenticated user, not git commit authors" + + - name: Validate PR (using trusted validation script from main) + run: | + cd base node .gitea/scripts/validate-pr.js "${{ github.event.pull_request.user.login }}" "${{ steps.changed-files.outputs.files }}" + env: + PR_FILES_DIR: ../pr/plugins - name: Auto-merge PR if: success() run: | + cd pr echo "✅ Validation passed - Auto-merging PR" - curl -X POST \ - -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ - -H "Content-Type: application/json" \ - "http://synbox.ruv.wtf:8418/api/v1/repos/litruv/Plugin-Directory/pulls/${{ github.event.pull_request.number }}/merge" \ - -d '{"Do":"squash","MergeTitleField":"${{ github.event.pull_request.title }}","MergeMessageField":"Auto-merged after validation","delete_branch_after_merge":true}' + + # Configure git with token authentication + git config user.name "gitea-actions[bot]" + git config user.email "gitea-actions[bot]@users.noreply.gitea.io" + + # Set up authenticated remote URL using github.token + git remote set-url origin "http://oauth2:${{ github.token }}@synbox.ruv.wtf:8418/litruv/Plugin-Directory.git" + + # Fetch and merge + git fetch origin ${{ github.event.pull_request.head.ref }} + git checkout main + git merge --squash origin/${{ github.event.pull_request.head.ref }} + git commit -m "${{ github.event.pull_request.title }}" + + # Push changes + git push origin main + + # Delete the PR branch + git push origin --delete ${{ github.event.pull_request.head.ref }} diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c11a3cb --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,137 @@ +# Security Model + +## Authentication vs Git Configuration + +### How User Identity Works + +**Question:** Can't I just set my git username to anything and bypass validation? + +**Answer:** No. The validation uses **Gitea account authentication**, not git commit authors. + +#### Git Commit Author (NOT used for validation) +```bash +# This can be set to anything locally +git config user.name "anyone" +git config user.email "anyone@example.com" +git commit -m "My commit" +``` +These values are **NOT** used for plugin ownership validation. + +#### PR Creator (used for validation) +When you create a Pull Request: +1. You must be logged into your Gitea account +2. The PR is created through Gitea's authenticated API +3. `github.event.pull_request.user.login` = your authenticated Gitea username + +**This is what validation checks against.** + +### Example Attack Scenario (Prevented) + +❌ **Attacker tries:** +```bash +# Configure git to impersonate someone +git config user.name "victim" +git config user.email "victim@example.com" + +# Modify victim's plugin +vim plugins/victim-plugin.json +# Change author to "attacker" + +git commit -m "Update plugin" +git push + +# Create PR (must log into Gitea as "attacker" account) +``` + +✅ **What happens:** +1. PR is created by authenticated Gitea user: `attacker` +2. Validation script receives: `prAuthor = "attacker"` +3. Validation checks original plugin from main branch: `original.author = "victim"` +4. Check fails: `"victim" !== "attacker"` +5. **Result:** ❌ `Cannot modify plugin owned by "victim" (you are "attacker")` + +The git commit author is irrelevant - what matters is **who is logged into Gitea when creating the PR**. + +## Security Measures + +### 1. Dual Checkout +```yaml +- Checkout main branch → base/ (trusted validation scripts) +- Checkout PR branch → pr/ (untrusted user submissions) +``` +Validation script always runs from `base/`, never from PR branch. + +### 2. Infrastructure Protection +PRs that modify `.gitea/` directory are automatically rejected. + +### 3. Ownership Verification +All modifications/deletions check **original ownership from main branch**: +```javascript +// Check original plugin from main (base/) +const originalPlugin = readFile('../base/plugins/example.json'); + +// User can only modify if they own the original +if (originalPlugin.author !== prAuthor) { + reject("Cannot modify plugin owned by someone else"); +} +``` + +### 4. Author Field Validation +New plugins must have `author` field matching the PR creator's authenticated Gitea username. + +## What About Collaboration? + +**Q:** What if I want someone else to update my plugin? + +**A:** They cannot submit PRs directly. Options: +1. Transfer ownership by updating the `author` field yourself +2. They fork your plugin repo and create their own plugin entry +3. Add them as collaborators to your plugin repository (not the directory) + +## Trust Model + +### Trusted Components +- Main branch content (validated history) +- Gitea authentication system +- GitHub Actions runner environment +- Validation scripts on main branch + +### Untrusted Components +- PR branch content (user submissions) +- Git commit metadata (names, emails) +- PR descriptions and comments + +### Validation Flow +``` +1. User authenticates to Gitea → Trusted user identity +2. User creates PR → github.event.pull_request.user.login +3. Validation runs from main branch scripts → Trusted code +4. Checks plugin ownership against main → Trusted ownership data +5. Validates PR author matches plugin author → Secure check +``` + +## Reporting Security Issues + +If you find a security vulnerability in the plugin directory system: +1. **Do NOT create a public issue or PR** +2. Contact the repository maintainer directly +3. Provide detailed reproduction steps +4. Allow time for a fix before public disclosure + +## Additional Safeguards + +### Rate Limiting +Gitea's built-in rate limiting prevents spam PR attacks. + +### Branch Protection +The `main` branch is protected: +- Direct pushes disabled +- All changes via PR +- Auto-merge only after validation passes + +### Audit Trail +All changes are tracked in git history: +- Who created the PR (Gitea account) +- What changed (git diff) +- When it was merged (git commit timestamp) +- Why validation passed (CI logs) diff --git a/VALIDATION_RULES.md b/VALIDATION_RULES.md new file mode 100644 index 0000000..af97ca3 --- /dev/null +++ b/VALIDATION_RULES.md @@ -0,0 +1,200 @@ +# Plugin Validation Rules + +All plugin submissions are automatically validated before being merged. PRs must pass all validation checks to be auto-merged. + +## Security + +**The validation script runs from the main branch, not from your PR branch.** This prevents malicious PRs from modifying the validation logic. PRs that attempt to modify `.gitea/` directory files will be automatically rejected. + +## File Requirements + +### 1. Location +- ✅ Only modify files in `plugins/` directory +- ❌ Cannot modify files outside `plugins/` +- ❌ Cannot modify `.gitea/` infrastructure files +- ⚠️ `plugins/index.json` is auto-generated - don't manually edit it + +### 2. File Naming +- **Filename must be prefixed with your username** +- Format: `username-plugin-name.json` +- Example: `litruv-example-plugin.json` +- ❌ Invalid: `example-plugin.json` (missing username prefix) +- ❌ Invalid: `otheruser-plugin.json` (wrong username) + +This prevents naming conflicts and makes ownership visible. + +### 3. File Type +- ✅ Only `.json` files are allowed in `plugins/` directory +- ❌ No other file types (images, scripts, etc. must be in plugin repo) + +## Plugin JSON Schema + +### Required Fields +Every plugin JSON must have these fields: + +```json +{ + "id": "string", + "name": "string", + "version": "string", + "description": "string", + "author": "string", + "repository": "string" +} +``` + +### Field Validation Rules + +#### `id` (required) +- Must match the filename +- Example: `litruv-example-plugin.json` → `"id": "litruv-example-plugin"` +- Must include username prefix matching filename +- Lowercase, hyphens allowed, no spaces + +#### `name` (required) +- Display name for the plugin +- Human-readable, any format + +#### `version` (required) +- Must follow semantic versioning: `x.y.z` +- Example: `"1.0.0"`, `"2.1.3"` +- ❌ Invalid: `"v1.0"`, `"1.0"`, `"latest"` + +#### `description` (required) +- Short description of what the plugin does +- Will be shown in the plugin browser + +#### `author` (required) +- **MUST MATCH YOUR GITEA USERNAME** +- This enforces ownership - you can only submit/remove plugins authored by you +- Case-sensitive + +#### `repository` (required) +- Git repository URL where the plugin code lives +- Must be a valid URL +- Example: `"http://synbox.ruv.wtf:8418/username/Plugin-Name.git"` + +#### `thumbnail` (optional) +- URL to plugin thumbnail image +- **Requirements:** + - Format: PNG, JPG, or GIF only + - Max dimensions: 512×512 pixels + - Max file size: 2MB + - Must be accessible via HTTP/HTTPS +- Example: `"http://synbox.ruv.wtf:8418/username/Plugin-Name/raw/branch/main/thumbnail.png"` +- Validation checks actual image format (magic bytes), dimensions, and size + +### Optional Fields +You can include any additional fields for metadata: +- `homepage` - Project homepage URL +- `downloadUrl` - Direct download link +- `tags` - Array of tags +- `addedDate` - ISO timestamp + +## Ownership Rules + +### Adding Plugins +- The `author` field must match your Gitea username +- This is checked automatically + +### Modifying Plugins +- **You can only modify plugins where the ORIGINAL author (on main branch) is you** +- Even if you change the `author` field in your PR, validation checks against the original +- Attempting to modify someone else's plugin will fail validation with: + ``` + ❌ plugins/their-plugin.json: Cannot modify plugin owned by "them" (you are "you") + ``` + +### Removing Plugins +- **You can only remove plugins where the ORIGINAL author (on main branch) is you** +- The validation checks the plugin's author from the main branch, not from your PR +- Attempting to remove someone else's plugin will fail validation with: + ``` + ❌ plugins/their-plugin.json: Cannot remove plugin owned by "them" (you are "you") + ``` + +**Security Note:** All ownership checks use the plugin data from the `main` branch, not from your PR. You cannot bypass ownership by modifying the `author` field. + +## Validation Process + +1. **File check**: Ensures only `plugins/*.json` files are modified +2. **Infrastructure check**: Blocks any `.gitea/` modifications +3. **JSON parsing**: Validates JSON syntax +4. **Schema validation**: Checks all required fields exist +5. **Field validation**: Validates each field's format and rules +6. **Thumbnail validation** (if provided): Downloads and validates image +7. **Author matching**: Ensures `author` field matches PR creator +8. **Auto-merge**: If all checks pass, PR is automatically merged + +## Testing Locally + +You can test your plugin JSON before submitting: + +```bash +cd Plugin-Directory +node .gitea/scripts/validate-pr.js "your-username" "plugins/your-plugin.json" +``` + +## Common Errors + +### ❌ Author mismatch +``` +❌ plugins/example-plugin.json: Author "someone" must match PR creator "you" +``` +**Fix**: Change `"author": "someone"` to `"author": "you"` + +### ❌ ID doesn't match filename +```litruv-my-plugin.json: Plugin ID "different-name" doesn't match filename "litruv-my-plugin.json" +``` +**Fix**: Change `"id": "different-name"` to `"id": "litruv-my-plugin"` + +### ❌ Filename missing username prefix +``` +❌ plugins/my-plugin.json: Filename must start with your username "litruv-" (e.g., "litruv-my-plugin.json") +``` +**Fix**: Rename file from `my-plugin.json` to `litruv-my-plugin.json` and update the `id` field +**Fix**: Change `"id": "different-name"` to `"id": "my-plugin"` + +### ❌ Invalid version +``` +❌ plugins/example-plugin.json: Invalid version format: v1.0 +``` +**Fix**: Use semantic versioning like `"version": "1.0.0"` + +### ❌ Thumbnail too large +``` +❌ plugins/example-plugin.json: Thumbnail validation failed: Thumbnail exceeds 2MB limit (3.45MB) +``` +**Fix**: Compress your thumbnail to under 2MB + +### ❌ Thumbnail dimensions too large +``` +❌ plugins/example-plugin.json: Thumbnail validation failed: Thumbnail dimensions 1024x768 exceed 512x512 +``` +**Fix**: litruv-example-plugin", + "name": "Example Plugin", + "version": "1.0.0", + "description": "An example plugin demonstrating the plugin system capabilities", + "author": "litruv", + "repository": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example.git", + "thumbnail": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example/raw/branch/main/thumbnail.png", + "homepage": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example", + "tags": ["example", "demo"] +} +``` + +Filename: `litruv-example-plugin.json"author": "litruv", + "repository": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example.git", + "thumbnail": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example/raw/branch/main/thumbnail.png", + "homepage": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example", + "tags": ["example", "demo"] +} +``` + +## Security Note + +**Never include sensitive information in plugin JSON files.** These files are public and will be served to all plugin host users. Do not include: +- API keys or tokens +- Passwords +- Private URLs or endpoints +- Personal information diff --git a/plugins/example-plugin.json b/plugins/example-plugin.json deleted file mode 100644 index ee403f5..0000000 --- a/plugins/example-plugin.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "id": "example-plugin", - "name": "Example Plugin", - "version": "1.0.0", - "description": "An example plugin demonstrating the plugin system capabilities", - "author": "litruv", - "repository": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example.git", - "downloadUrl": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example/archive/main.zip", - "homepage": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example", - "tags": ["example", "demo"], - "addedDate": "2026-04-17T00:00:00.000Z" -} diff --git a/plugins/index.json b/plugins/index.json index d5907a4..f09e9c8 100644 --- a/plugins/index.json +++ b/plugins/index.json @@ -1,7 +1,5 @@ { "version": "1.0.0", - "updatedAt": "2026-04-17T00:00:00.000Z", - "plugins": [ - "example-plugin" - ] + "updatedAt": "2026-04-16T17:14:35.921Z", + "plugins": [] }