Files
p2ns/docs/plugins
2026-05-28 01:14:13 -04:00
..
2025-12-17 20:05:50 -05:00
2025-12-17 20:05:50 -05:00
2026-05-27 22:15:33 -04:00
2026-05-27 22:15:33 -04:00
2026-05-28 01:14:13 -04:00
2026-05-27 22:15:33 -04:00
2025-12-17 20:05:50 -05:00
2026-05-27 23:08:45 -04:00
2025-12-17 20:05:50 -05:00
2026-05-27 22:15:33 -04:00
2026-05-27 22:15:33 -04:00
2026-05-27 22:23:30 -04:00

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

SDK Features

Example Plugins

Quick Start

  1. Create a plugin directory structure:

    mkdir -p plugin-sites/my.custom.domain/www
    
  2. Create config.json in your plugin directory:

    {
      "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"
    }
    
  3. Create index.js (optional - only if you need dynamic handling):

    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');
      }
    };
    
  4. Add static files to www/ directory (HTML, CSS, JS, images, etc.)

  5. 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:

{
  "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:

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/apiapi)
  • 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

// 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 peer channels
const peerChannels = sdk.state.peerChannels;

// Get peer metrics
const peerMetrics = sdk.state.peerMetrics;

// Get peer history
const peerHistory = sdk.state.peerHistory;

Plugin Information

// 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

// 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

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 automatically broadcast to the admin interface and displayed in real-time in the plugin's log terminal. Logs are also written to app.log in the plugin directory.

Core Operations

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

const sdk = require('../../includes/plugins/sdk');

module.exports = {
  async handler(req, res) {
    if (req.url === '/') {
      res.writeHead(200, { 'Content-Type': 'text/html' });
      res.end('<h1>Hello World!</h1>');
    } else {
      res.writeHead(404);
      res.end('Not Found');
    }
  }
};
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(`
        <!DOCTYPE html>
        <html>
        <head><title>P2P Site</title></head>
        <body>
          <h1>P2P Site Dashboard</h1>
          <h2>Domains: ${domains.length}</h2>
          <h2>Peers: ${peers.length}</h2>
          <h2>Metrics</h2>
          <pre>${JSON.stringify(metrics, null, 2)}</pre>
        </body>
        </html>
      `);
      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

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):

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)
  • sdk.channels - P2P communication channels (see PLUGIN_CHANNELS.md)
  • sdk.drives - Hyperdrive distributed file system (see HYPERDRIVE.md)

Admin Panel Registration

Plugins can register actions and settings that appear in the admin interface:

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 the SDK logging functions instead of console.log (logs appear in admin interface)
  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 are displayed in real-time in the plugin's log terminal
  • Logs are automatically captured from sdk.log calls
  • Logs are also written to app.log in the plugin directory
  • Log terminals are hidden by default and appear when you start/stop/restart a plugin

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.