BREAKING EXPERIMENTAL: Holepunch-native hard migration (RPC core, shared storage, SDK v2)

BREAKING: All nodes must upgrade together. Legacy p2ns.core-request string
messages are rejected; invite/consensus control plane uses protomux-rpc
(invite.request, invite.ack, invite.relay*, consensus.*). Shared Corestore
namespaces are the default for plugin DBs and drives; USE_SHARED_CORESTORE_NAMESPACES=false
is debug-only. Admin plugin actions with params are validated before run.

EXPERIMENTAL: End-to-end RPC invite path reuses existing handlers via adapters;
invite wire still ships on the invite channel. Multi-peer invite/relay
integration tests are not in CI yet (core-rpc-smoke only).

SDK & channels:
- channel-rpc.js, sdk.channels.rpc (register/request/event)
- core-rpc.js for p2ns.core; action-params + plugin route validation
- sdk.db.getCore/reopen, sdk.state.getPeerChannelSnapshot,
  sdk.metrics.getHolepunchStats (schema v1)

Runtime:
- p2ns.js: RPC-first core invite/consensus; Hyperswarm firewall/reconnect
- channel-manager: required protomux-rpc per peer
- db-shared-namespace-migration; drive-manager shared namespaces
- proxy-server: invite.request RPC for joiners

Plugins: file.drop, global.profile, peer.directory, domain.consensus,
peer.visualize, example.plugin (RPC demo); peer.directory UI polish

Also: CI/smoke scripts, diagnostics hardening, plugin config schema validation,
admin action param modal, docs/RFCS, package-lock + engines.node >= 18
This commit is contained in:
Raven Scott
2026-05-28 10:51:19 -04:00
parent 4b783801c5
commit ea9124c429
49 changed files with 6576 additions and 215 deletions
+99 -4
View File
@@ -2,6 +2,15 @@
let pluginsData = [];
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// Fetch plugins from API
async function fetchPlugins() {
try {
@@ -449,6 +458,89 @@ function renderSettingInput(domain, key, setting) {
}
}
function normalizeActionParamValue(param, rawValue) {
if (param.type === 'number') {
const num = Number(rawValue);
if (Number.isNaN(num)) {
throw new Error(`Parameter "${param.name}" must be a number`);
}
return num;
}
if (param.type === 'boolean') {
return rawValue === true || rawValue === 'true' || rawValue === '1';
}
return rawValue;
}
async function collectActionParameters(action) {
if (!action.params || action.params.length === 0) {
return {};
}
const formFields = action.params.map((param, index) => {
const inputId = `plugin-action-param-${index}`;
const type = param.type || 'string';
const required = param.required ? 'required' : '';
const placeholder = param.placeholder ? `placeholder="${escapeHtml(param.placeholder)}"` : '';
const defaultValue = param.default !== undefined ? String(param.default) : '';
const description = param.description
? `<p class="text-xs theme-text-tertiary mt-1">${escapeHtml(param.description)}</p>`
: '';
if (type === 'boolean') {
return `
<label class="block text-sm theme-text-primary mb-3">
<span class="block mb-1">${escapeHtml(param.label || param.name)}</span>
<select id="${inputId}" class="w-full p-2 theme-input rounded">
<option value="false" ${defaultValue === 'false' ? 'selected' : ''}>False</option>
<option value="true" ${defaultValue === 'true' ? 'selected' : ''}>True</option>
</select>
${description}
</label>
`;
}
return `
<label class="block text-sm theme-text-primary mb-3">
<span class="block mb-1">${escapeHtml(param.label || param.name)}${param.required ? ' *' : ''}</span>
<input id="${inputId}" type="${type === 'number' ? 'number' : 'text'}"
class="w-full p-2 theme-input rounded"
value="${escapeHtml(defaultValue)}"
${placeholder}
${required} />
${description}
</label>
`;
}).join('');
const confirmed = await window.ConfirmationModal.show({
title: action.label || action.name || 'Run Action',
message: `<div><p class="theme-text-secondary mb-3">${escapeHtml(action.description || 'Provide action parameters.')}</p><div>${formFields}</div></div>`,
type: 'info',
confirmText: 'Run Action',
cancelText: 'Cancel',
allowHTML: true,
focusConfirm: false
});
if (!confirmed) return null;
const params = {};
for (let i = 0; i < action.params.length; i++) {
const param = action.params[i];
const element = document.getElementById(`plugin-action-param-${i}`);
const rawValue = element ? element.value : '';
if (param.required && String(rawValue).trim() === '') {
throw new Error(`Parameter "${param.name}" is required`);
}
if (String(rawValue).trim() === '' && !param.required) {
continue;
}
params[param.name] = normalizeActionParamValue(param, rawValue);
}
return params;
}
// Execute a plugin action
async function executeAction(domain, actionName, action) {
if (!action) {
@@ -470,10 +562,13 @@ async function executeAction(domain, actionName, action) {
}
// Collect parameters if any
const params = {};
if (action.params && action.params.length > 0) {
// TODO: Show modal to collect parameters
// For now, execute with empty params
const params = await collectActionParameters(action);
if (params === null) {
if (button) {
button.disabled = false;
button.innerHTML = `${action.icon || '⚡'} ${action.label || actionName}`;
}
return;
}
const res = await fetch(`/api/plugins/${domain}/actions/${actionName}`, {