1088 lines
32 KiB
Markdown
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.
|
|
|