# P2NS Plugin System The P2NS plugin system allows you to create custom internal websites. Each internal domain is automatically discovered from plugins in `plugin-sites/{domain}/` that have a valid `config.json` file. The system will automatically register any plugin with a valid `config.json` as an internal domain. **Note:** `p2ns.admin` is always treated as an internal domain, even without a plugin. ## Plugin Documentation Index This directory contains comprehensive documentation for the P2NS plugin system: ### Core Documentation - **[README.md](README.md)** - This file: Complete plugin system guide - **[PLUGIN_SDK.md](PLUGIN_SDK.md)** - Complete Plugin SDK API reference ### SDK Features - **[HYPERDB.md](HYPERDB.md)** - HyperDB database integration for plugins - **[HYPERDRIVE.md](HYPERDRIVE.md)** - Hyperdrive distributed file system for plugins - **[PLUGIN_CHANNELS.md](PLUGIN_CHANNELS.md)** - Plugin peer protocols via protomux-rpc ### Example Plugins - **[example.plugin.md](example.plugin.md)** - Example Plugin template documentation - **[global.profile.md](global.profile.md)** - Global Profile plugin documentation - **[domain.consensus.md](domain.consensus.md)** - Domain Consensus plugin documentation - **[peer.directory.md](peer.directory.md)** - Peer Directory plugin documentation - **[peer.visualize.md](peer.visualize.md)** - Peer Visualize plugin documentation - **[file.drop.md](file.drop.md)** - File Drop plugin documentation - **[peer.paste.md](peer.paste.md)** - Peer Paste plugin documentation - **[vis-network-migration.md](vis-network-migration.md)** - Migration guide for visualization libraries ### Related Documentation - **[../RESTAPI.md](../RESTAPI.md)** - REST API endpoints including plugin management - **[../EXAMPLES.md](../EXAMPLES.md)** - API usage examples ## Quick Start 1. **Create a plugin directory structure**: ```bash mkdir -p plugin-sites/my.custom.domain/www ``` 3. **Create `config.json`** in your plugin directory: ```json { "name": "my.custom.domain", "version": "1.0.0", "description": "My custom plugin", "author": "Your Name", "domain": "my.custom.domain", "homepage": "https://example.com", "license": "MIT", "dependencies": {}, "www": "www" } ``` 4. **Create `index.js`** (optional - only if you need dynamic handling): ```javascript const sdk = require('../../includes/plugins/sdk'); module.exports = { async handler(req, res) { const { path } = sdk.router.parseRequest(req); // Handle API endpoint if (path === 'api/hello') { return sdk.router.json(res, { message: 'Hello!' }); } // Return false to fall back to static file serving return false; }, async onInit() { sdk.log.info('my.custom.domain', 'Plugin initialized'); }, async onShutdown() { sdk.log.info('my.custom.domain', 'Plugin shutting down'); } }; ``` 5. **Add static files to `www/` directory** (HTML, CSS, JS, images, etc.) 6. **Restart P2NS** - your plugin will be automatically loaded! ## Plugin Structure Plugins use a document root structure with a `www/` directory for static files and optional `index.js` for dynamic handling. ### Directory Structure ``` plugin-sites/ {domain}/ ├── config.json # Plugin metadata (required) ├── index.js # Plugin handler (optional) └── www/ # Document root for static files ├── index.html ├── css/ ├── js/ └── assets/ ``` ### config.json The `config.json` file contains plugin metadata: ```json { "name": "plugin-name", "version": "1.0.0", "description": "Plugin description", "author": "Author name", "homepage": "https://example.com", "license": "MIT", "dependencies": {}, "www": "www" } ``` **Required fields:** - `name` - Plugin name (defaults to directory name) - `version` - Plugin version (defaults to "1.0.0") **Optional fields:** - `description` - Plugin description - `author` - Author name - `homepage` - Homepage URL - `license` - License identifier - `dependencies` - Dependency object (for future use) - `www` - Document root directory name (defaults to "www") ### Plugin Handler (index.js) The `index.js` file is optional. If present, it must export an object with the following interface: **Optional:** - **`handler(req, res)`** - Function that handles HTTP requests - `req` - Node.js HTTP request object - `res` - Node.js HTTP response object - Return `false` to fall back to static file serving from `www/` - Return `true` or nothing if request was handled - **`onInit()`** - Called when the plugin is loaded - Use this to initialize resources, set up timers, etc. - **`onShutdown()`** - Called when the plugin is unloaded - Use this to clean up resources, close connections, etc. **Note:** If no `index.js` exists but `www/` directory exists, the plugin will serve static files only. ### Router Utilities The SDK provides router utilities to simplify request handling: ```javascript const sdk = require('../../includes/plugins/sdk'); module.exports = { async handler(req, res) { // Parse request to get path and query parameters const { path, query } = sdk.router.parseRequest(req); // Handle API endpoint if (path === 'api/data') { const data = { message: 'Hello from plugin' }; return sdk.router.json(res, data); } // Handle text response if (path === 'status') { return sdk.router.text(res, 'OK', 200); } // Handle errors if (path === 'error') { return sdk.router.error(res, 'Something went wrong', 500); } // Handle 404 if (path === 'missing') { return sdk.router.notFound(res, 'Resource not found'); } // Return false to fall back to static file serving return false; } }; ``` **Router Methods:** - `sdk.router.parseRequest(req)` - Parse request to extract `{ path, query, method }` - Automatically handles domain prefixes (e.g., `/peer.directory/api` → `api`) - `sdk.router.json(res, data, status = 200)` - Send JSON response - `sdk.router.text(res, text, status = 200, contentType = 'text/plain')` - Send text response - `sdk.router.error(res, message, status = 500)` - Send error response as JSON - `sdk.router.notFound(res, message = 'Not Found')` - Send 404 response ## Plugin SDK The SDK provides access to P2NS system functionality: ### State Access ```javascript // Get connected peers count const peerCount = sdk.state.connectedPeers; // Get list of peer IDs const peerIds = sdk.state.peerIds; // Get local peer ID (Hyperswarm public key) const localPeerId = sdk.state.localPeerId; // Get dnsPass instance (may be null if not initialized) const dnsPass = sdk.state.dnsPass; // Get domain to IP mapping const ipMap = sdk.state.domainToIPMap; // Get IP for a specific domain const ip = sdk.state.getIPForDomain('example.tld'); // Get all active Holesail connections const holesails = sdk.state.holesails; // Get swarm peer channels (plugin P2P uses sdk.channels, not this map) const peerChannels = sdk.state.peerChannels; // Get peer metrics const peerMetrics = sdk.state.peerMetrics; // Get peer history const peerHistory = sdk.state.peerHistory; ``` ### Plugin Information ```javascript // Get plugin configuration const config = sdk.plugin.getConfig(); // Get specific config value const version = sdk.plugin.getConfigValue('version'); // Get all domains that have plugins loaded const pluginDomains = sdk.plugin.getAllPluginDomains(); // Get www directory path const wwwDir = sdk.plugin.getWwwDir(); // Get plugin directory path const pluginDir = sdk.plugin.getPluginDir(); ``` ### DNS Operations ```javascript // Get hash for a domain const hash = await sdk.dns.getHashForDomain('example.tld'); // Get all DNS entries const entries = await sdk.dns.getAllEntries(); // Get consensus state for a domain const consensus = await sdk.dns.getConsensusState('example.tld'); // Get consensus metrics const metrics = sdk.dns.getConsensusMetrics(); ``` ### Logging ```javascript sdk.log.debug('my-plugin', 'Debug message'); sdk.log.info('my-plugin', 'Info message'); sdk.log.warn('my-plugin', 'Warning message'); sdk.log.error('my-plugin', 'Error message'); ``` **Note:** Plugin logs are written to `logs/plugins.log`, broadcast as `plugin-log` WebSocket messages (per-plugin terminal in admin), and appended to `plugin-sites/{domain}/app.log`. ### Core Operations ```javascript // Get all entries const entries = await sdk.core.getAllEntries(); // Get hash for domain const hash = await sdk.core.getHashForDomain('example.tld'); // Get consensus state const state = await sdk.core.getConsensusState('example.tld'); ``` ### Domain Management ```javascript // Add a new domain await sdk.domains.addDomain('example.tld', 'hs://hash123', [ { name: 'web', port: 80, protocol: 'tcp' } ]); // Remove a domain await sdk.domains.removeDomain('example.tld'); // Vote for a specific claimant await sdk.domains.voteForDomain('example.tld', 'claimant-id'); // Get complete domain information const info = await sdk.domains.getDomainInfo('example.tld'); // List all domains const domains = await sdk.domains.listDomains(); // Get domain IP const ip = sdk.domains.getDomainIP('example.tld'); // Create interface for domain const ip = await sdk.domains.createInterfaceForDomain('example.tld'); ``` ### Holesail Operations ```javascript // Create a Holesail server const server = await sdk.holesail.createServer({ name: 'my-server', port: 8080, host: '0.0.0.0', secure: true, udp: false }); // List all servers const servers = sdk.holesail.listServers(); // Get server info const server = sdk.holesail.getServer('server-id'); // Restart a server await sdk.holesail.restartServer('server-id'); // Stop a server await sdk.holesail.stopServer('server-id'); // Remove a server await sdk.holesail.removeServer('server-id'); // Create a Holesail client const client = await sdk.holesail.createClient({ domain: 'example.tld', key: 'hs://hash123', port: 8080, protocol: 'tcp' }); // List all clients const clients = sdk.holesail.listClients(); // Get client info const client = sdk.holesail.getClient('client-id'); // Restart a client await sdk.holesail.restartClient('client-id'); // Stop a client await sdk.holesail.stopClient('client-id'); // Remove a client await sdk.holesail.removeClient('client-id'); ``` ### Peer Management ```javascript // Get peer information const peerInfo = sdk.peers.getPeerInfo('peer-id'); // Get peer metrics const metrics = sdk.peers.getPeerMetrics('peer-id'); // Get peer history const history = sdk.peers.getPeerHistory('peer-id'); // Block a peer await sdk.peers.blockPeer('peer-id'); // Unblock a peer await sdk.peers.unblockPeer('peer-id'); // Check if peer is blocked const isBlocked = sdk.peers.isPeerBlocked('peer-id'); // List all blocked peers const blocked = sdk.peers.getBlockedPeers(); ``` ### Certificate Management ```javascript // Get certificate for domain const cert = await sdk.certificates.getCertificate('example.tld'); // Create certificate for domain const cert = sdk.certificates.createCertificate('example.tld', [ { type: 2, value: 'example.tld' }, { type: 7, value: '127.0.0.1' } ]); // List all certificates const certs = await sdk.certificates.listCertificates(); // Get root CA info const rootCA = sdk.certificates.getRootCA(); ``` ### Local DNS Management ```javascript // Add a local DNS record await sdk.localDns.addRecord({ name: 'example.tld', type: 'A', class: 'IN', ttl: 3600, address: '192.168.1.1' }); // Remove DNS record(s) await sdk.localDns.removeRecord('example.tld', 'A'); // List all records const records = sdk.localDns.listRecords(); // Get specific record(s) const records = sdk.localDns.getRecord('example.tld', 'A'); // Update a record await sdk.localDns.updateRecord(0, { name: 'example.tld', type: 'A', class: 'IN', ttl: 7200, address: '192.168.1.2' }); ``` ### Interface Management ```javascript // List all interfaces const interfaces = sdk.interfaces.listInterfaces(); // Get interface for domain const iface = sdk.interfaces.getInterfaceForDomain('example.tld'); // Create interface for domain const ip = await sdk.interfaces.createInterface('example.tld'); // Remove interface for domain await sdk.interfaces.removeInterface('example.tld'); ``` ### Metrics and Monitoring ```javascript // Get system-wide metrics const metrics = sdk.metrics.getSystemMetrics(); // Get peer metrics const peerMetrics = sdk.metrics.getPeerMetrics(); // Get domain metrics const domainMetrics = sdk.metrics.getDomainMetrics('example.tld'); // Get Holesail metrics const holesailMetrics = sdk.metrics.getHolesailMetrics(); // Get consensus metrics const consensusMetrics = sdk.metrics.getConsensusMetrics(); // Get process metrics const processMetrics = await sdk.metrics.getProcessMetrics(); // Get historical data const historical = sdk.metrics.getHistoricalData('requests', 100); ``` ### WebSocket Support ```javascript // Broadcast message to all WebSocket clients sdk.websocket.broadcast({ type: 'update', data: 'something changed' }); // Get WebSocket client count const count = sdk.websocket.getClientCount(); ``` ### Configuration Access ```javascript // Get all configuration const config = sdk.config.getConfig(); // Get specific config value const port = sdk.config.getConfigValue('INTERNAL_PORT'); // Get environment variable const nodeEnv = sdk.config.getEnvironmentVariable('NODE_ENV'); // Validate configuration const validated = sdk.config.validateConfig(); ``` ### Backup and Restore ```javascript // Create a manual backup const backupPath = await sdk.backup.createBackup(); // List all backups const backups = await sdk.backup.listBackups(); // Restore from backup await sdk.backup.restoreBackup('backup-2025-12-02.tar.gz'); // Get backup information const info = await sdk.backup.getBackupInfo('backup-2025-12-02.tar.gz'); ``` ### Subscription Management ```javascript // Add subscription await sdk.subscriptions.addSubscription( 'example.tld', 'web', 'hs://hash123', 80, 'tcp' ); // Remove subscription await sdk.subscriptions.removeSubscription('example.tld', 'web'); // List all subscriptions const subscriptions = await sdk.subscriptions.listSubscriptions(); // Get subscriptions for domain const subs = await sdk.subscriptions.getSubscriptionsForDomain('example.tld'); ``` ### HTTP Client Utilities ```javascript // Make HTTP request const response = await sdk.http.request('https://api.example.com/data', { method: 'GET', headers: { 'Authorization': 'Bearer token' } }); // GET request const response = await sdk.http.get('https://api.example.com/data'); // POST request const response = await sdk.http.post('https://api.example.com/data', { key: 'value' }); // PUT request const response = await sdk.http.put('https://api.example.com/data', { key: 'value' }); // DELETE request const response = await sdk.http.delete('https://api.example.com/data'); ``` ### File System Utilities ```javascript // Get plugin directory const pluginDir = sdk.fs.getPluginDir(); // Read file (within plugin directory) const content = await sdk.fs.readFile('data.json'); // Write file (within plugin directory) await sdk.fs.writeFile('data.json', JSON.stringify({ key: 'value' })); // List directory const files = await sdk.fs.readDir('assets'); // Check if file exists const exists = await sdk.fs.exists('config.json'); ``` ### Event System ```javascript // Subscribe to system events sdk.events.on('domain-added', (data) => { console.log('Domain added:', data.domain); }); // Subscribe once sdk.events.once('peer-connected', (data) => { console.log('Peer connected:', data.peerId); }); // Unsubscribe sdk.events.off('domain-added', handler); // Emit custom event sdk.events.emit('custom-event', { data: 'value' }); ``` ### Enhanced Utilities ```javascript // Check if DNS service is initialized const isReady = sdk.utils.isDNSReady(); // Get internal domains list const domains = await sdk.utils.getInternalDomains(); // Check if a domain is internal const isInternal = await sdk.utils.isInternalDomain('example.tld'); // Wait for DNS to be ready await sdk.utils.waitForDNSReady(30000); // Wait for peer to connect await sdk.utils.waitForPeer('peer-id', 30000); // Retry with exponential backoff const result = await sdk.utils.retry(async () => { return await someOperation(); }, { maxRetries: 3, initialDelay: 1000 }); // Debounce function const debouncedFn = sdk.utils.debounce(() => { console.log('Called after delay'); }, 1000); // Throttle function const throttledFn = sdk.utils.throttle(() => { console.log('Called at most once per delay'); }, 1000); // Sleep/delay await sdk.utils.sleep(5000); // Wait 5 seconds // Format Holesail hash const formatted = sdk.utils.formatHash('hash123'); // Returns 'hs://hash123' // Parse Holesail hash const parsed = sdk.utils.parseHash('hs://hash123'); // Returns 'hash123' ``` ### Security and Validation ```javascript // Validate domain name if (sdk.security.validateDomain('example.tld')) { // Domain is valid } // Validate Holesail hash if (sdk.security.validateHash('hs://hash123')) { // Hash is valid } // Sanitize user input const safe = sdk.security.sanitizeInput(userInput); // Check permission if (sdk.security.checkPermission('add-domain')) { // Operation allowed } ``` ## Request Handling Order The system handles requests in the following order: 1. **Static files from `www/` directory** - Files are served directly from the `www/` directory 2. **Plugin handler** - If a static file is not found, the plugin's `handler()` function is called 3. **404 Not Found** - If neither static files nor handler handle the request This means you can: - Serve static HTML/CSS/JS from `www/` without any handler code - Use a handler for dynamic routes while serving static assets from `www/` - Return `false` from handler to fall back to static file serving ## Static File Serving Static files are automatically served from the `www/` directory. Just place your HTML, CSS, JS, and other assets in the `www/` subdirectory. Example structure: ``` plugin-sites/my.custom.domain/ ├── config.json ├── index.js # Optional handler └── www/ # Document root ├── index.html ├── styles.css ├── app.js └── images/ └── logo.png ``` Files in `www/` are served with appropriate MIME types based on file extensions. ## Examples ### Simple HTML Response ```javascript const sdk = require('../../includes/plugins/sdk'); module.exports = { async handler(req, res) { if (req.url === '/') { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end('

Hello World!

'); } else { res.writeHead(404); res.end('Not Found'); } } }; ``` ### Full-Featured P2P Site ```javascript const sdk = require('../../includes/plugins/sdk'); module.exports = { async handler(req, res) { const url = new URL(req.url, `https://${req.headers.host}`); // API endpoint to list domains if (url.pathname === '/api/domains' && req.method === 'GET') { const domains = await sdk.domains.listDomains(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ domains })); return; } // API endpoint to add domain if (url.pathname === '/api/domains' && req.method === 'POST') { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', async () => { try { const { domain, hash } = JSON.parse(body); await sdk.domains.addDomain(domain, hash); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true })); } catch (err) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: err.message })); } }); return; } // API endpoint to get peer info if (url.pathname.startsWith('/api/peers/') && req.method === 'GET') { const peerId = url.pathname.split('/api/peers/')[1]; const peerInfo = sdk.peers.getPeerInfo(peerId); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(peerInfo)); return; } // Serve HTML page if (url.pathname === '/') { const domains = await sdk.domains.listDomains(); const peers = sdk.state.peerIds.map(id => sdk.peers.getPeerInfo(id)); const metrics = sdk.metrics.getSystemMetrics(); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` P2P Site

P2P Site Dashboard

Domains: ${domains.length}

Peers: ${peers.length}

Metrics

${JSON.stringify(metrics, null, 2)}
`); return; } res.writeHead(404); res.end('Not Found'); }, async onInit() { sdk.log.info('my-plugin', 'Initializing plugin...'); // Subscribe to events sdk.events.on('domain-added', (data) => { sdk.log.info('my-plugin', `Domain added: ${data.domain}`); sdk.websocket.broadcast({ type: 'domain-added', domain: data.domain }); }); // Wait for DNS to be ready await sdk.utils.waitForDNSReady(); sdk.log.info('my-plugin', 'DNS service is ready'); }, async onShutdown() { sdk.log.info('my-plugin', 'Shutting down plugin...'); // Cleanup resources } }; ``` ### REST API with Full SDK ```javascript const sdk = require('../../includes/plugins/sdk'); module.exports = { async handler(req, res) { const url = new URL(req.url, `https://${req.headers.host}`); // Domains API if (url.pathname === '/api/domains' && req.method === 'GET') { const domains = await sdk.domains.listDomains(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ domains })); return; } if (url.pathname === '/api/domains' && req.method === 'POST') { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', async () => { try { const { domain, hash, clients } = JSON.parse(body); await sdk.domains.addDomain(domain, hash, clients); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true })); } catch (err) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: err.message })); } }); return; } // Holesail API if (url.pathname === '/api/holesail/servers' && req.method === 'GET') { const servers = sdk.holesail.listServers(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ servers })); return; } if (url.pathname === '/api/holesail/servers' && req.method === 'POST') { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', async () => { try { const opts = JSON.parse(body); const server = await sdk.holesail.createServer(opts); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(server)); } catch (err) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: err.message })); } }); return; } // Metrics API if (url.pathname === '/api/metrics' && req.method === 'GET') { const metrics = sdk.metrics.getSystemMetrics(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(metrics)); return; } res.writeHead(404); res.end('Not Found'); } }; ``` ### Using Existing Handlers You can also delegate to existing handlers if needed (e.g., for admin interface): ```javascript const path = require('path'); const { handleAdminRequest } = require(path.join(__dirname, '../../includes/admin')); module.exports = { async handler(req, res) { // Delegate to existing handler await handleAdminRequest(req, res); } }; ``` Note: The old `directory.js` handler has been removed. The `peer.directory` functionality is now provided by the `peer.directory` plugin in `plugin-sites/peer.directory/`. ## Complete SDK Reference The SDK provides comprehensive access to all P2NS functionality: - **`sdk.state`** - System state access (read-only) - **`sdk.dns`** - DNS operations and domain resolution - **`sdk.domains`** - Domain management (add, remove, vote, list) - **`sdk.holesail`** - Holesail server and client management - **`sdk.peers`** - Peer management and blocking - **`sdk.certificates`** - TLS certificate management - **`sdk.localDns`** - Local DNS record management - **`sdk.interfaces`** - Virtual network interface management - **`sdk.metrics`** - System metrics and monitoring - **`sdk.websocket`** - WebSocket broadcasting - **`sdk.config`** - Configuration access - **`sdk.backup`** - Backup and restore operations - **`sdk.subscriptions`** - Service subscription management - **`sdk.http`** - HTTP client utilities - **`sdk.fs`** - File system utilities (plugin directory only) - **`sdk.events`** - Event system for real-time updates - **`sdk.utils`** - Enhanced utility functions - **`sdk.security`** - Security and validation utilities - **`sdk.log`** - Logging utilities - **`sdk.core`** - Core P2NS operations - **`sdk.plugin`** - Plugin information and configuration access - **`sdk.router`** - HTTP request parsing and response helpers - **`sdk.admin`** - Admin panel registration (actions, settings) - **`sdk.db`** - HyperDB database operations (see [HYPERDB.md](HYPERDB.md)) - **`sdk.channels`** - Plugin protomux-rpc protocols (see [PLUGIN_CHANNELS.md](PLUGIN_CHANNELS.md)) - **`sdk.drives`** - Hyperdrive distributed file system (see [HYPERDRIVE.md](HYPERDRIVE.md)) ### Admin Panel Registration Plugins can register actions and settings that appear in the admin interface: ```javascript async onInit() { // Register an action that can be called from the admin panel sdk.admin.registerAction('reset-data', async (params) => { // Action handler sdk.log.info('my-plugin', 'Resetting data...'); // Perform action return { success: true, message: 'Data reset' }; }, { label: 'Reset Data', description: 'Reset all plugin data to defaults', icon: '🔄' }); // Register a setting sdk.admin.registerSetting('maxItems', { type: 'number', label: 'Maximum Items', description: 'Maximum number of items to display', default: 100, min: 1, max: 1000 }); // Register a boolean setting sdk.admin.registerSetting('enableFeature', { type: 'boolean', label: 'Enable Feature', description: 'Enable the new feature', default: false }); // Register a select setting sdk.admin.registerSetting('theme', { type: 'select', label: 'Theme', description: 'Choose a theme', default: 'light', options: [ { value: 'light', label: 'Light' }, { value: 'dark', label: 'Dark' }, { value: 'auto', label: 'Auto' } ] }); // Access saved settings const maxItems = await sdk.admin.getSetting('maxItems', 100); const allSettings = await sdk.admin.getAllSettings(); } // Access settings in your handler async handler(req, res) { const maxItems = await sdk.admin.getSetting('maxItems', 100); // Use setting value } ``` **Action Types:** - Actions are async functions that can be called from the admin panel - They receive a `params` object with any parameters - They should return a result object or throw an error **Setting Types:** - `string` - Text input - `number` - Number input (supports `min`, `max`) - `boolean` - Checkbox - `select` - Dropdown (requires `options` array) - `textarea` - Multi-line text input **Settings Persistence:** - Settings are automatically saved to `cache/plugin-settings/{domain}.json` - Settings persist across plugin restarts - Use `sdk.admin.getSetting(key, defaultValue)` to retrieve saved values - Use `sdk.admin.getAllSettings()` to get all saved settings ## Best Practices 1. **Error Handling**: Always wrap async operations in try-catch blocks 2. **Logging**: Use `sdk.log` instead of `console.log` (admin Plugins tab + `logs/plugins.log` and per-plugin `app.log`) 3. **Resource Cleanup**: Implement `onShutdown()` to clean up resources 4. **Security**: Validate and sanitize user input using `sdk.security` 5. **Performance**: Cache expensive operations when possible 6. **DNS Ready**: Check `sdk.utils.isDNSReady()` before DNS operations 7. **Events**: Use `sdk.events` for real-time updates instead of polling 8. **Retry Logic**: Use `sdk.utils.retry()` for unreliable operations 9. **File Safety**: Only use `sdk.fs` for file operations (restricted to plugin directory) 10. **Validation**: Always validate user input with `sdk.security.validateDomain()` etc. 11. **Admin Integration**: Register actions and settings in `onInit()` for better admin interface integration 12. **Settings**: Use `sdk.admin.getSetting()` to access saved settings in your handler ## Plugin Management Plugins can be managed via the admin interface at `https://p2ns.admin` in the Plugins tab: - **View Plugins**: See all installed plugins with their status, version, description, and features - **Start/Stop**: Start or stop plugins without restarting P2NS - **Restart**: Reload a plugin to apply code changes without restarting P2NS - **Actions**: Execute registered plugin actions directly from the admin interface - **Settings**: Configure plugin settings with a user-friendly form interface - **Logs**: View real-time logs for each plugin in an integrated terminal **Plugin Status:** - **Loaded**: Plugin is loaded and running - **Stopped**: Plugin is stopped and unloaded from memory - **Static**: Plugin only serves static files (no handler) **Logs:** - Plugin logs appear in the Plugins tab terminal and in `logs/plugins.log` (all plugins) plus `plugin-sites/{domain}/app.log` - Logs are captured from `sdk.log` calls only (not `console.log`) - Log terminals are hidden by default and appear when you start/stop/restart a plugin - System-wide split logs (core, proxy, HTTP proxy, DNS, Holesail) are in the admin **Logs** tab — see [README_LONGFORM.md](../README_LONGFORM.md#logging-and-debugging) ## Troubleshooting - **Plugin not loading**: Check that `config.json` exists with valid `name` and `version` fields. The plugin will be automatically discovered if it has a valid config.json. - **Module not found**: Ensure your `index.js` uses correct relative paths - **SDK errors**: Make sure DNS service is initialized before using DNS operations - **Static files not serving**: Ensure files are in the `www/` directory and have correct permissions - **Config not accessible**: Plugin config is only available when handling requests (via `sdk.plugin.getConfig()`) - **Logs not showing**: Ensure you're using `sdk.log` methods instead of `console.log`. Logs appear in the admin interface after starting/stopping/restarting a plugin. - **Settings not persisting**: Settings are saved to `cache/plugin-settings/{domain}.json`. Ensure the cache directory is writable. - **Actions not appearing**: Actions must be registered in `onInit()`. Ensure the plugin is loaded and `onInit()` completed successfully. ## Migration from Existing Handlers If you want to migrate an existing internal site (like `peer.directory`) to use the plugin system: 1. Copy the handler code to `plugin-sites/{domain}/index.js` 2. Adapt it to use the plugin SDK 3. The system will automatically use your plugin instead of the default handler The `handleAdminRequest` handler remains available for the admin interface. The old `directory.js` handler has been removed and replaced by the `peer.directory` plugin.