Files
p2ns/docs/plugins/README.md
T
2026-05-28 12:39:38 -04:00

1088 lines
32 KiB
Markdown

# 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('<h1>Hello World!</h1>');
} 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(`
<!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
```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.