release: prepare v0.9.7

This commit is contained in:
BigBodyCobain
2026-05-01 22:55:04 -06:00
parent ea457f27da
commit 28b3bd5ebf
670 changed files with 187060 additions and 14006 deletions
@@ -0,0 +1,487 @@
#!/usr/bin/env node
import fs from 'node:fs/promises';
import path from 'node:path';
const HELP_TEXT = `
ShadowBroker DM root health Prometheus exporter
Usage:
node scripts/mesh/export-dm-root-health-prometheus.mjs [--stdout] [--output PATH] [--base-url URL] [--health-path PATH]
Environment:
SB_DM_ROOT_BASE_URL=http://127.0.0.1:8000
SB_DM_ROOT_HEALTH_PATH=/api/wormhole/dm/root-health
SB_DM_ROOT_AUTH_HEADER=X-Admin-Key: change-me
SB_DM_ROOT_AUTH_COOKIE=operator_session=...
SB_DM_ROOT_TIMEOUT_MS=10000
SB_DM_ROOT_PROMETHEUS_OUTPUT=/var/lib/node_exporter/textfile_collector/shadowbroker_dm_root.prom
Flags:
--stdout Print Prometheus metrics to stdout
--output PATH Override SB_DM_ROOT_PROMETHEUS_OUTPUT
--base-url URL Override SB_DM_ROOT_BASE_URL
--health-path PATH Override SB_DM_ROOT_HEALTH_PATH
--help Show this text
Exit codes:
0 = export succeeded
2 = fetch or payload validation failed
`.trim();
function parseArgs(argv) {
const parsed = {};
for (let index = 0; index < argv.length; index += 1) {
const current = String(argv[index] || '').trim();
if (!current) continue;
if (current === '--stdout') {
parsed.stdout = true;
continue;
}
if (current === '--help' || current === '-h') {
parsed.help = true;
continue;
}
if (
(current === '--output' || current === '--base-url' || current === '--health-path') &&
index + 1 < argv.length
) {
parsed[current.slice(2).replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())] =
String(argv[index + 1] || '').trim();
index += 1;
}
}
return parsed;
}
function normalizeUrl(baseUrl, healthPath) {
const base = String(baseUrl || 'http://127.0.0.1:8000').trim().replace(/\/+$/, '');
const pathValue = String(healthPath || '/api/wormhole/dm/root-health').trim();
if (!pathValue) {
return `${base}/api/wormhole/dm/root-health`;
}
return pathValue.startsWith('http://') || pathValue.startsWith('https://')
? pathValue
: `${base}/${pathValue.replace(/^\/+/, '')}`;
}
function parseHeader(rawValue) {
const raw = String(rawValue || '').trim();
if (!raw) return null;
const separator = raw.indexOf(':');
if (separator <= 0) return null;
const name = raw.slice(0, separator).trim();
const value = raw.slice(separator + 1).trim();
if (!name || !value) return null;
return [name, value];
}
function safeInt(value, fallback = 0) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return fallback;
return Math.trunc(numeric);
}
function boolGauge(value) {
return value ? 1 : 0;
}
function metricEscape(value) {
return String(value ?? '')
.replace(/\\/g, '\\\\')
.replace(/\n/g, '\\n')
.replace(/"/g, '\\"');
}
function labelsText(labels) {
const entries = Object.entries(labels || {}).filter(([, value]) => String(value ?? '').length > 0);
if (!entries.length) return '';
return `{${entries.map(([key, value]) => `${key}="${metricEscape(value)}"`).join(',')}}`;
}
function appendMetric(lines, name, help, type, value, labels = undefined) {
lines.push(`# HELP ${name} ${help}`);
lines.push(`# TYPE ${name} ${type}`);
lines.push(`${name}${labelsText(labels)} ${value}`);
}
function stateCode(value, mapping, fallback) {
const key = String(value || '').trim().toLowerCase();
if (Object.prototype.hasOwnProperty.call(mapping, key)) {
return mapping[key];
}
return fallback;
}
function buildMetrics(payload, errorDetail = '') {
const checkedAt = safeInt(payload?.checked_at || Math.floor(Date.now() / 1000), Math.floor(Date.now() / 1000));
const summaryState = String(payload?.state || '').trim().toLowerCase();
const healthState = String(payload?.health_state || '').trim().toLowerCase();
const monitorState = String(payload?.monitoring?.state || '').trim().toLowerCase();
const witnessState = String(payload?.witness?.state || '').trim().toLowerCase();
const transparencyState = String(payload?.transparency?.state || '').trim().toLowerCase();
const nextAction = String(payload?.next_action || '').trim();
const alerts = Array.isArray(payload?.alerts) ? payload.alerts.filter((item) => item && typeof item === 'object') : [];
const summaryStateCode = stateCode(
summaryState,
{ local_cached_only: 0, current_external: 1, stale_external: 2 },
-1,
);
const healthStateCode = stateCode(
healthState,
{ ok: 0, warning: 1, stale: 2, error: 3 },
-1,
);
const monitorStateCode = stateCode(
monitorState,
{ ok: 0, warning: 1, critical: 2 },
-1,
);
const witnessStateCode = stateCode(
witnessState,
{ not_configured: 0, descriptors_only: 1, current: 2, stale: 3, error: 4 },
-1,
);
const transparencyStateCode = stateCode(
transparencyState,
{ not_configured: 0, current: 1, stale: 2, error: 3 },
-1,
);
const lines = [];
appendMetric(
lines,
'shadowbroker_dm_root_health_scrape_success',
'Whether the DM root health scrape succeeded.',
'gauge',
errorDetail ? 0 : 1,
);
appendMetric(
lines,
'shadowbroker_dm_root_checked_at_unixtime',
'Unix timestamp for the most recent DM root health check represented in this export.',
'gauge',
checkedAt,
);
appendMetric(
lines,
'shadowbroker_dm_root_summary_state_code',
'DM root operator summary state (0=local_cached_only, 1=current_external, 2=stale_external, -1=unknown).',
'gauge',
summaryStateCode,
);
appendMetric(
lines,
'shadowbroker_dm_root_health_state_code',
'DM root rolled-up health state (0=ok, 1=warning, 2=stale, 3=error, -1=unknown).',
'gauge',
healthStateCode,
);
appendMetric(
lines,
'shadowbroker_dm_root_monitor_state_code',
'Monitoring severity for DM root health (0=ok, 1=warning, 2=critical, -1=unknown).',
'gauge',
monitorStateCode,
);
appendMetric(
lines,
'shadowbroker_dm_root_strong_trust_blocked',
'Whether strong DM trust is currently blocked by external assurance state.',
'gauge',
boolGauge(Boolean(payload?.strong_trust_blocked)),
);
appendMetric(
lines,
'shadowbroker_dm_root_external_assurance_current',
'Whether configured external witness and transparency assurances are both current.',
'gauge',
boolGauge(Boolean(payload?.external_assurance_current)),
);
appendMetric(
lines,
'shadowbroker_dm_root_requires_attention',
'Whether DM root external assurance currently requires operator attention.',
'gauge',
boolGauge(Boolean(payload?.requires_attention)),
);
appendMetric(
lines,
'shadowbroker_dm_root_independent_quorum_met',
'Whether the current witness state satisfies independent quorum.',
'gauge',
boolGauge(Boolean(payload?.independent_quorum_met)),
);
appendMetric(
lines,
'shadowbroker_dm_root_alert_count',
'Number of active DM root health alerts.',
'gauge',
safeInt(payload?.alert_count, 0),
);
appendMetric(
lines,
'shadowbroker_dm_root_blocking_alert_count',
'Number of active blocking DM root health alerts.',
'gauge',
safeInt(payload?.blocking_alert_count, 0),
);
appendMetric(
lines,
'shadowbroker_dm_root_warning_alert_count',
'Number of active warning-level DM root health alerts.',
'gauge',
safeInt(payload?.warning_alert_count, 0),
);
appendMetric(
lines,
'shadowbroker_dm_root_witness_state_code',
'Witness operator state (0=not_configured, 1=descriptors_only, 2=current, 3=stale, 4=error, -1=unknown).',
'gauge',
witnessStateCode,
);
appendMetric(
lines,
'shadowbroker_dm_root_witness_health_state_code',
'Witness health state (0=ok, 1=warning, 2=stale, 3=error, -1=unknown).',
'gauge',
stateCode(String(payload?.witness?.health_state || '').trim().toLowerCase(), { ok: 0, warning: 1, stale: 2, error: 3 }, -1),
);
appendMetric(
lines,
'shadowbroker_dm_root_witness_age_seconds',
'Age in seconds of the current external witness package.',
'gauge',
safeInt(payload?.witness?.age_s, 0),
);
appendMetric(
lines,
'shadowbroker_dm_root_witness_warning_window_seconds',
'Configured warning threshold for external witness freshness.',
'gauge',
safeInt(payload?.witness?.warning_window_s, 0),
);
appendMetric(
lines,
'shadowbroker_dm_root_witness_freshness_window_seconds',
'Configured maximum freshness window for external witness material.',
'gauge',
safeInt(payload?.witness?.freshness_window_s, 0),
);
appendMetric(
lines,
'shadowbroker_dm_root_witness_reacquire_required',
'Whether external witness receipt reacquisition is currently required.',
'gauge',
boolGauge(Boolean(payload?.witness?.reacquire_required)),
);
appendMetric(
lines,
'shadowbroker_dm_root_witness_manifest_matches_current',
'Whether the external witness material matches the current manifest fingerprint.',
'gauge',
boolGauge(Boolean(payload?.witness?.manifest_matches_current)),
);
appendMetric(
lines,
'shadowbroker_dm_root_witness_independent_quorum_met',
'Whether the witness side independently satisfies quorum.',
'gauge',
boolGauge(Boolean(payload?.witness?.independent_quorum_met)),
);
appendMetric(
lines,
'shadowbroker_dm_root_transparency_state_code',
'Transparency operator state (0=not_configured, 1=current, 2=stale, 3=error, -1=unknown).',
'gauge',
transparencyStateCode,
);
appendMetric(
lines,
'shadowbroker_dm_root_transparency_health_state_code',
'Transparency health state (0=ok, 1=warning, 2=stale, 3=error, -1=unknown).',
'gauge',
stateCode(String(payload?.transparency?.health_state || '').trim().toLowerCase(), { ok: 0, warning: 1, stale: 2, error: 3 }, -1),
);
appendMetric(
lines,
'shadowbroker_dm_root_transparency_age_seconds',
'Age in seconds of the current external transparency ledger readback.',
'gauge',
safeInt(payload?.transparency?.age_s, 0),
);
appendMetric(
lines,
'shadowbroker_dm_root_transparency_warning_window_seconds',
'Configured warning threshold for external transparency freshness.',
'gauge',
safeInt(payload?.transparency?.warning_window_s, 0),
);
appendMetric(
lines,
'shadowbroker_dm_root_transparency_freshness_window_seconds',
'Configured maximum freshness window for external transparency readback.',
'gauge',
safeInt(payload?.transparency?.freshness_window_s, 0),
);
appendMetric(
lines,
'shadowbroker_dm_root_transparency_verification_required',
'Whether transparency verification refresh is currently required.',
'gauge',
boolGauge(Boolean(payload?.transparency?.verification_required)),
);
appendMetric(
lines,
'shadowbroker_dm_root_summary_info',
'State labels for the current DM root operator summary.',
'gauge',
1,
{
summary_state: summaryState || 'unknown',
health_state: healthState || 'unknown',
monitor_state: monitorState || 'unknown',
witness_state: witnessState || 'unknown',
transparency_state: transparencyState || 'unknown',
},
);
if (nextAction) {
appendMetric(
lines,
'shadowbroker_dm_root_next_action_info',
'Suggested next DM root operator action.',
'gauge',
1,
{ action: nextAction },
);
}
if (errorDetail) {
appendMetric(
lines,
'shadowbroker_dm_root_health_scrape_error_info',
'Reason for the most recent DM root health scrape failure.',
'gauge',
1,
{ reason: errorDetail },
);
}
for (const alert of alerts) {
const code = String(alert?.code || '').trim();
if (!code) continue;
appendMetric(
lines,
'shadowbroker_dm_root_alert_active',
'Active DM root health alerts.',
'gauge',
1,
{
code,
severity: String(alert?.severity || '').trim().toLowerCase() || 'unknown',
target: String(alert?.target || '').trim().toLowerCase() || 'dm_root',
blocking: boolGauge(Boolean(alert?.blocking)).toString(),
},
);
}
return `${lines.join('\n')}\n`;
}
async function fetchHealth(config) {
const headers = { Accept: 'application/json' };
const authHeader = parseHeader(config.authHeader);
if (authHeader) {
headers[authHeader[0]] = authHeader[1];
}
if (config.authCookie) {
headers.Cookie = config.authCookie;
}
const controller = new AbortController();
const timeout = globalThis.setTimeout(() => controller.abort(), config.timeoutMs);
try {
const response = await fetch(config.url, {
method: 'GET',
headers,
signal: controller.signal,
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || payload?.ok === false) {
const detail = String(payload?.detail || payload?.message || `http_${response.status}`).trim();
throw new Error(detail || 'dm_root_health_failed');
}
return payload;
} finally {
globalThis.clearTimeout(timeout);
}
}
async function writeMetrics(outputPath, text) {
const resolved = path.resolve(outputPath);
await fs.mkdir(path.dirname(resolved), { recursive: true });
const tempPath = `${resolved}.tmp`;
await fs.writeFile(tempPath, text, 'utf8');
await fs.rename(tempPath, resolved);
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
console.log(HELP_TEXT);
return;
}
const config = {
url: normalizeUrl(args.baseUrl || process.env.SB_DM_ROOT_BASE_URL, args.healthPath || process.env.SB_DM_ROOT_HEALTH_PATH),
authHeader: process.env.SB_DM_ROOT_AUTH_HEADER || '',
authCookie: process.env.SB_DM_ROOT_AUTH_COOKIE || '',
timeoutMs: Math.max(1000, safeInt(process.env.SB_DM_ROOT_TIMEOUT_MS, 10000)),
output: String(args.output || process.env.SB_DM_ROOT_PROMETHEUS_OUTPUT || '').trim(),
stdout: Boolean(args.stdout),
};
let metricsText = '';
let exitCode = 0;
try {
const payload = await fetchHealth(config);
metricsText = buildMetrics(payload);
} catch (error) {
const detail = String(error?.message || 'dm_root_health_fetch_failed').trim() || 'dm_root_health_fetch_failed';
metricsText = buildMetrics(
{
checked_at: Math.floor(Date.now() / 1000),
state: 'stale_external',
health_state: 'error',
monitoring: { state: 'critical' },
strong_trust_blocked: true,
requires_attention: true,
alert_count: 1,
blocking_alert_count: 1,
warning_alert_count: 0,
alerts: [
{
code: 'dm_root_health_scrape_failed',
severity: 'error',
target: 'dm_root',
blocking: true,
},
],
witness: {},
transparency: {},
},
detail,
);
exitCode = 2;
}
if (config.output) {
await writeMetrics(config.output, metricsText);
}
if (config.stdout || !config.output) {
process.stdout.write(metricsText);
}
if (exitCode) {
process.exit(exitCode);
}
}
await main();
+343
View File
@@ -0,0 +1,343 @@
#!/usr/bin/env node
import fs from 'node:fs/promises';
import path from 'node:path';
const HELP_TEXT = `
ShadowBroker DM root health poller
Usage:
node scripts/mesh/poll-dm-root-health-alerts.mjs [--once] [--base-url URL] [--alerts-path PATH]
Environment:
SB_DM_ROOT_BASE_URL=http://127.0.0.1:8000
SB_DM_ROOT_ALERTS_PATH=/api/wormhole/dm/root-health/alerts
SB_DM_ROOT_AUTH_HEADER=X-Admin-Key: change-me
SB_DM_ROOT_AUTH_COOKIE=operator_session=...
SB_DM_ROOT_INTERVAL_S=60
SB_DM_ROOT_TIMEOUT_MS=10000
SB_DM_ROOT_STATE_FILE=data/dm_root_health_bridge_state.json
SB_DM_ROOT_WARNING_WEBHOOK_URL=https://hooks.slack.example/services/...
SB_DM_ROOT_CRITICAL_WEBHOOK_URL=https://events.pagerduty.example/v2/enqueue
Flags:
--once Poll one time and exit with status 0/1/2
--base-url URL Override SB_DM_ROOT_BASE_URL
--alerts-path PATH Override SB_DM_ROOT_ALERTS_PATH
--help Show this text
Exit codes for --once:
0 = ok
1 = warning
2 = critical or fetch failure
`.trim();
function parseArgs(argv) {
const parsed = {};
for (let index = 0; index < argv.length; index += 1) {
const current = String(argv[index] || '').trim();
if (!current) continue;
if (current === '--once') {
parsed.once = true;
continue;
}
if (current === '--help' || current === '-h') {
parsed.help = true;
continue;
}
if ((current === '--base-url' || current === '--alerts-path') && index + 1 < argv.length) {
parsed[current.slice(2).replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())] =
String(argv[index + 1] || '').trim();
index += 1;
}
}
return parsed;
}
function normalizeUrl(baseUrl, alertsPath) {
const base = String(baseUrl || 'http://127.0.0.1:8000').trim().replace(/\/+$/, '');
const pathValue = String(alertsPath || '/api/wormhole/dm/root-health/alerts').trim();
if (!pathValue) {
return `${base}/api/wormhole/dm/root-health/alerts`;
}
return pathValue.startsWith('http://') || pathValue.startsWith('https://')
? pathValue
: `${base}/${pathValue.replace(/^\/+/, '')}`;
}
function parseHeader(rawValue) {
const raw = String(rawValue || '').trim();
if (!raw) return null;
const separator = raw.indexOf(':');
if (separator <= 0) return null;
const name = raw.slice(0, separator).trim();
const value = raw.slice(separator + 1).trim();
if (!name || !value) return null;
return [name, value];
}
function severityFromPayload(payload) {
const state = String(payload?.state || '').trim().toLowerCase();
if (state === 'critical') return 'critical';
if (state === 'warning') return 'warning';
if (state === 'ok') return 'ok';
if (payload?.page_required) return 'critical';
if (payload?.ticket_required) return 'warning';
return 'critical';
}
function buildFingerprint(payload) {
return JSON.stringify({
severity: severityFromPayload(payload),
state: String(payload?.state || '').trim().toLowerCase(),
page_required: Boolean(payload?.page_required),
ticket_required: Boolean(payload?.ticket_required),
active_alert_codes: Array.isArray(payload?.active_alert_codes) ? payload.active_alert_codes : [],
next_action: String(payload?.next_action || '').trim(),
alert_count: Number(payload?.alert_count || 0),
blocking_alert_count: Number(payload?.blocking_alert_count || 0),
warning_alert_count: Number(payload?.warning_alert_count || 0),
});
}
function buildSummary(payload) {
const severity = severityFromPayload(payload);
const activeAlertCodes = Array.isArray(payload?.active_alert_codes)
? payload.active_alert_codes.map((value) => String(value || '').trim()).filter(Boolean)
: [];
return {
severity,
state: String(payload?.state || '').trim().toLowerCase() || 'critical',
checkedAt: Number(payload?.checked_at || 0),
pageRequired: Boolean(payload?.page_required),
ticketRequired: Boolean(payload?.ticket_required),
recommendedCheckIntervalS: Number(payload?.recommended_check_interval_s || 60),
nextAction: String(payload?.next_action || '').trim(),
primaryAlert: String(payload?.primary_alert || '').trim(),
activeAlertCodes,
alertCount: Number(payload?.alert_count || 0),
blockingAlertCount: Number(payload?.blocking_alert_count || 0),
warningAlertCount: Number(payload?.warning_alert_count || 0),
fingerprint: buildFingerprint(payload),
raw: payload,
};
}
function failureSummary(detail) {
const message = String(detail || '').trim() || 'dm_root_health_poll_failed';
return {
severity: 'critical',
state: 'critical',
checkedAt: Math.floor(Date.now() / 1000),
pageRequired: true,
ticketRequired: true,
recommendedCheckIntervalS: 60,
nextAction: 'check_root_health_endpoint',
primaryAlert: message,
activeAlertCodes: ['dm_root_health_poll_failed'],
alertCount: 1,
blockingAlertCount: 1,
warningAlertCount: 0,
fingerprint: JSON.stringify({ severity: 'critical', error: message }),
raw: {
ok: false,
state: 'critical',
primary_alert: message,
active_alert_codes: ['dm_root_health_poll_failed'],
next_action: 'check_root_health_endpoint',
page_required: true,
ticket_required: true,
recommended_check_interval_s: 60,
},
};
}
async function loadStateFile(stateFile) {
if (!stateFile) return {};
try {
const raw = await fs.readFile(stateFile, 'utf8');
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}
async function writeStateFile(stateFile, value) {
if (!stateFile) return;
const targetPath = path.resolve(stateFile);
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.writeFile(targetPath, JSON.stringify(value, null, 2));
}
async function fetchAlerts(config) {
const headers = { Accept: 'application/json' };
const authHeader = parseHeader(config.authHeader);
if (authHeader) {
headers[authHeader[0]] = authHeader[1];
}
if (config.authCookie) {
headers.Cookie = config.authCookie;
}
const controller = new AbortController();
const timeout = globalThis.setTimeout(() => controller.abort(), config.timeoutMs);
try {
const response = await fetch(config.url, {
method: 'GET',
headers,
signal: controller.signal,
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || payload?.ok === false) {
const detail = String(payload?.detail || payload?.message || `http_${response.status}`).trim();
throw new Error(detail || 'dm_root_health_alerts_failed');
}
return buildSummary(payload);
} finally {
globalThis.clearTimeout(timeout);
}
}
async function postWebhook(targetUrl, summary, config) {
if (!targetUrl) {
return { delivered: false, reason: 'no_target' };
}
const headers = {
'Content-Type': 'application/json',
Accept: 'application/json',
};
const payload = {
source: 'shadowbroker_dm_root_health_bridge',
sent_at: new Date().toISOString(),
severity: summary.severity,
monitoring_state: summary.state,
page_required: summary.pageRequired,
ticket_required: summary.ticketRequired,
primary_alert: summary.primaryAlert,
next_action: summary.nextAction,
active_alert_codes: summary.activeAlertCodes,
alert_count: summary.alertCount,
blocking_alert_count: summary.blockingAlertCount,
warning_alert_count: summary.warningAlertCount,
checked_at: summary.checkedAt,
raw: summary.raw,
};
const controller = new AbortController();
const timeout = globalThis.setTimeout(() => controller.abort(), config.timeoutMs);
try {
const response = await fetch(targetUrl, {
method: 'POST',
headers,
body: JSON.stringify(payload),
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`webhook_http_${response.status}`);
}
return { delivered: true, reason: 'sent' };
} finally {
globalThis.clearTimeout(timeout);
}
}
async function maybeDeliverWebhook(summary, config) {
if (summary.severity === 'ok') {
return { delivered: false, reason: 'ok_state' };
}
const state = await loadStateFile(config.stateFile);
if (
String(state.last_fingerprint || '') === summary.fingerprint &&
String(state.last_severity || '') === summary.severity
) {
return { delivered: false, reason: 'duplicate' };
}
const targetUrl =
summary.severity === 'critical' ? config.criticalWebhookUrl : config.warningWebhookUrl;
const delivered = await postWebhook(targetUrl, summary, config);
if (delivered.delivered) {
await writeStateFile(config.stateFile, {
last_checked_at: summary.checkedAt,
last_severity: summary.severity,
last_fingerprint: summary.fingerprint,
last_target: targetUrl,
});
}
return delivered;
}
function printSummary(summary, webhookResult) {
const prefix = summary.severity.toUpperCase().padEnd(8, ' ');
const alerts = summary.activeAlertCodes.length > 0 ? summary.activeAlertCodes.join(',') : 'none';
const nextAction = summary.nextAction || 'none';
const webhookNote = webhookResult?.reason ? ` webhook=${webhookResult.reason}` : '';
console.log(
`[${prefix}] state=${summary.state} page=${summary.pageRequired} ticket=${summary.ticketRequired} ` +
`alerts=${alerts} next_action=${nextAction}${webhookNote}`,
);
}
function exitCodeForSeverity(severity) {
if (severity === 'ok') return 0;
if (severity === 'warning') return 1;
return 2;
}
async function sleep(ms) {
return new Promise((resolve) => {
globalThis.setTimeout(resolve, ms);
});
}
async function pollOnce(config) {
try {
const summary = await fetchAlerts(config);
const webhookResult = await maybeDeliverWebhook(summary, config);
printSummary(summary, webhookResult);
return summary;
} catch (error) {
const summary = failureSummary(error instanceof Error ? error.message : 'dm_root_health_poll_failed');
const webhookResult = await maybeDeliverWebhook(summary, config).catch(() => ({
delivered: false,
reason: 'webhook_failed',
}));
printSummary(summary, webhookResult);
return summary;
}
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
console.log(HELP_TEXT);
return;
}
const config = {
once: Boolean(args.once),
url: normalizeUrl(args.baseUrl || process.env.SB_DM_ROOT_BASE_URL, args.alertsPath || process.env.SB_DM_ROOT_ALERTS_PATH),
authHeader: process.env.SB_DM_ROOT_AUTH_HEADER || '',
authCookie: process.env.SB_DM_ROOT_AUTH_COOKIE || '',
intervalMs: Math.max(5, Number(process.env.SB_DM_ROOT_INTERVAL_S || 60)) * 1000,
timeoutMs: Math.max(1000, Number(process.env.SB_DM_ROOT_TIMEOUT_MS || 10000)),
stateFile: process.env.SB_DM_ROOT_STATE_FILE || '',
warningWebhookUrl: process.env.SB_DM_ROOT_WARNING_WEBHOOK_URL || '',
criticalWebhookUrl: process.env.SB_DM_ROOT_CRITICAL_WEBHOOK_URL || '',
};
if (config.once) {
const summary = await pollOnce(config);
process.exitCode = exitCodeForSeverity(summary.severity);
return;
}
while (true) {
const summary = await pollOnce(config);
const nextDelayMs = Math.max(
5000,
Number(summary.recommendedCheckIntervalS || config.intervalMs / 1000) * 1000,
);
await sleep(nextDelayMs || config.intervalMs);
}
}
await main();
@@ -0,0 +1,496 @@
#!/usr/bin/env node
import fs from 'node:fs/promises';
import path from 'node:path';
import crypto from 'node:crypto';
const PROTOCOL_VERSION = 'infonet/2';
const NETWORK_ID = 'sb-testnet-0';
const NODE_ID_PREFIX = '!sb_';
const NODE_ID_HEX_LEN = 32;
const MANIFEST_WITNESS_EVENT_TYPE = 'stable_dm_root_manifest_witness';
const MANIFEST_WITNESS_TYPE = 'stable_dm_root_manifest_witness';
const EXTERNAL_WITNESS_IMPORT_TYPE = 'stable_dm_root_manifest_external_witness_import';
const EXTERNAL_WITNESS_IDENTITY_TYPE = 'stable_dm_root_manifest_external_witness_identity';
const ROOT_DISTRIBUTION_PATH = '/api/wormhole/dm/root-distribution';
const HELP_TEXT = `
ShadowBroker external root witness package publisher
Usage:
node scripts/mesh/publish-external-root-witness-package.mjs --init-witness PATH
node scripts/mesh/publish-external-root-witness-package.mjs --descriptor-only --witness-file PATH [--output PATH]
node scripts/mesh/publish-external-root-witness-package.mjs --witness-file PATH [--output PATH]
Environment:
SB_DM_ROOT_BASE_URL=http://127.0.0.1:8000
SB_DM_ROOT_DISTRIBUTION_PATH=/api/wormhole/dm/root-distribution
SB_DM_ROOT_AUTH_HEADER=X-Admin-Key: change-me
SB_DM_ROOT_AUTH_COOKIE=operator_session=...
SB_DM_ROOT_TIMEOUT_MS=10000
SB_DM_ROOT_WITNESS_IDENTITY_FILE=./ops/witness-a.identity.json
SB_DM_ROOT_WITNESS_OUTPUT=./ops/root_witness_import.json
SB_DM_ROOT_WITNESS_LABEL=witness-a
SB_DM_ROOT_WITNESS_SOURCE_SCOPE=https_publish
SB_DM_ROOT_WITNESS_SOURCE_LABEL=witness-a
SB_DM_ROOT_WITNESS_INDEPENDENCE_GROUP=independent_witness_a
Flags:
--init-witness PATH Generate a new external witness identity file and exit
--witness-file PATH Load the external witness identity file to publish from
--descriptor-only Emit descriptors only, without manifest_fingerprint or signed receipts
--stdout Print the generated package to stdout
--output PATH Write the generated package JSON to PATH
--base-url URL Override SB_DM_ROOT_BASE_URL
--distribution-path PATH Override SB_DM_ROOT_DISTRIBUTION_PATH
--label VALUE Override witness descriptor label
--source-scope VALUE Override source_scope in the published import package
--source-label VALUE Override source_label in the published import package
--independence-group VAL Override witness independence group
--help Show this text
Typical flow:
1. Run --init-witness once on the external witness host.
2. Publish a descriptor-only package so the backend can import the external descriptor and republish the manifest.
3. Publish a full signed receipt package after the current manifest policy includes this external witness.
`.trim();
function parseArgs(argv) {
const parsed = {};
for (let index = 0; index < argv.length; index += 1) {
const current = String(argv[index] || '').trim();
if (!current) continue;
if (current === '--descriptor-only' || current === '--stdout') {
parsed[current.slice(2).replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())] = true;
continue;
}
if (current === '--help' || current === '-h') {
parsed.help = true;
continue;
}
if (
(
current === '--init-witness' ||
current === '--witness-file' ||
current === '--output' ||
current === '--base-url' ||
current === '--distribution-path' ||
current === '--label' ||
current === '--source-scope' ||
current === '--source-label' ||
current === '--independence-group'
) &&
index + 1 < argv.length
) {
parsed[current.slice(2).replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())] =
String(argv[index + 1] || '').trim();
index += 1;
}
}
return parsed;
}
function nowSeconds() {
return Math.floor(Date.now() / 1000);
}
function stableJson(value) {
if (Array.isArray(value)) {
return `[${value.map((item) => stableJson(item)).join(',')}]`;
}
if (value && typeof value === 'object') {
const entries = Object.entries(value)
.filter(([, entryValue]) => entryValue !== undefined)
.sort(([left], [right]) => left.localeCompare(right));
return `{${entries
.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJson(entryValue)}`)
.join(',')}}`;
}
return JSON.stringify(value);
}
function safeInt(value, fallback = 0) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return fallback;
return Math.trunc(numeric);
}
function normalizeUrl(baseUrl, routePath) {
const base = String(baseUrl || 'http://127.0.0.1:8000').trim().replace(/\/+$/, '');
const pathValue = String(routePath || ROOT_DISTRIBUTION_PATH).trim();
if (!pathValue) {
return `${base}${ROOT_DISTRIBUTION_PATH}`;
}
return pathValue.startsWith('http://') || pathValue.startsWith('https://')
? pathValue
: `${base}/${pathValue.replace(/^\/+/, '')}`;
}
function parseHeader(rawValue) {
const raw = String(rawValue || '').trim();
if (!raw) return null;
const separator = raw.indexOf(':');
if (separator <= 0) return null;
const name = raw.slice(0, separator).trim();
const value = raw.slice(separator + 1).trim();
if (!name || !value) return null;
return [name, value];
}
function toBase64Url(input) {
return Buffer.from(input)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '');
}
function fromBase64(value) {
return Buffer.from(String(value || '').trim(), 'base64');
}
function deriveNodeId(publicKeyBase64) {
const digest = crypto.createHash('sha256').update(fromBase64(publicKeyBase64)).digest('hex');
return `${NODE_ID_PREFIX}${digest.slice(0, NODE_ID_HEX_LEN)}`;
}
function buildWitnessDescriptor(identity, overrides = {}) {
return {
scope: 'root_witness',
label: String(overrides.label || identity.label || '').trim(),
node_id: String(identity.node_id || '').trim(),
public_key: String(identity.public_key || '').trim(),
public_key_algo: 'Ed25519',
management_scope: 'external',
independence_group: String(
overrides.independenceGroup || identity.independence_group || 'external_witness'
)
.trim()
.toLowerCase(),
};
}
function witnessPolicyFingerprint(policy) {
const normalizedWitnesses = Array.isArray(policy?.witnesses)
? policy.witnesses.map((item) => ({
scope: String(item?.scope || 'root_witness'),
label: String(item?.label || ''),
node_id: String(item?.node_id || '').trim(),
public_key: String(item?.public_key || '').trim(),
public_key_algo: String(item?.public_key_algo || 'Ed25519'),
management_scope: String(item?.management_scope || 'local').trim().toLowerCase(),
independence_group: String(item?.independence_group || '')
.trim()
.toLowerCase(),
}))
: [];
const canonical = {
type: String(policy?.type || 'stable_dm_root_manifest_witness_policy'),
policy_version: safeInt(policy?.policy_version, 1),
threshold: safeInt(policy?.threshold, 0),
witnesses: normalizedWitnesses,
};
return crypto.createHash('sha256').update(stableJson(canonical), 'utf8').digest('hex');
}
function manifestFingerprintForEnvelope(manifest) {
const canonical = {
type: String(manifest?.type || 'stable_dm_root_manifest'),
event_type: String(manifest?.event_type || 'stable_dm_root_manifest'),
node_id: String(manifest?.node_id || '').trim(),
public_key: String(manifest?.public_key || '').trim(),
public_key_algo: String(manifest?.public_key_algo || 'Ed25519'),
protocol_version: String(manifest?.protocol_version || PROTOCOL_VERSION),
sequence: safeInt(manifest?.sequence, 0),
payload: manifest?.payload && typeof manifest.payload === 'object' ? { ...manifest.payload } : {},
signature: String(manifest?.signature || '').trim(),
};
return crypto.createHash('sha256').update(stableJson(canonical), 'utf8').digest('hex');
}
function buildSignaturePayload({ eventType, nodeId, sequence, payload }) {
return [
PROTOCOL_VERSION,
NETWORK_ID,
eventType,
String(nodeId || '').trim(),
String(safeInt(sequence, 0)),
stableJson(payload && typeof payload === 'object' ? payload : {}),
].join('|');
}
function buildWitnessPayload(manifest) {
const payload = manifest?.payload && typeof manifest.payload === 'object' ? { ...manifest.payload } : {};
const witnessPolicy = payload?.witness_policy && typeof payload.witness_policy === 'object' ? payload.witness_policy : {};
return {
manifest_type: String(manifest?.type || 'stable_dm_root_manifest'),
manifest_event_type: String(manifest?.event_type || 'stable_dm_root_manifest'),
manifest_fingerprint: manifestFingerprintForEnvelope(manifest),
root_fingerprint: String(payload?.root_fingerprint || '').trim().toLowerCase(),
root_node_id: String(payload?.root_node_id || '').trim(),
generation: safeInt(payload?.generation, 0),
issued_at: safeInt(payload?.issued_at, 0),
expires_at: safeInt(payload?.expires_at, 0),
policy_version: safeInt(payload?.policy_version, 1),
witness_policy_fingerprint: witnessPolicyFingerprint(witnessPolicy),
witness_threshold: safeInt(witnessPolicy?.threshold, 0),
};
}
function createPrivateKeyFromIdentity(identity) {
const privateKeyRaw = fromBase64(identity.private_key);
const publicKeyRaw = fromBase64(identity.public_key);
if (privateKeyRaw.length !== 32 || publicKeyRaw.length !== 32) {
throw new Error('external witness identity keys must be raw Ed25519 base64');
}
return crypto.createPrivateKey({
key: {
crv: 'Ed25519',
d: toBase64Url(privateKeyRaw),
kty: 'OKP',
x: toBase64Url(publicKeyRaw),
},
format: 'jwk',
});
}
function generateWitnessIdentity(overrides = {}) {
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
const privateJwk = privateKey.export({ format: 'jwk' });
const publicJwk = publicKey.export({ format: 'jwk' });
const publicKeyRaw = Buffer.from(String(publicJwk.x || '').replace(/-/g, '+').replace(/_/g, '/'), 'base64');
const privateKeyRaw = Buffer.from(String(privateJwk.d || '').replace(/-/g, '+').replace(/_/g, '/'), 'base64');
const publicKeyBase64 = publicKeyRaw.toString('base64');
return {
type: EXTERNAL_WITNESS_IDENTITY_TYPE,
schema_version: 1,
created_at: nowSeconds(),
updated_at: nowSeconds(),
node_id: deriveNodeId(publicKeyBase64),
public_key: publicKeyBase64,
public_key_algo: 'Ed25519',
private_key: privateKeyRaw.toString('base64'),
label: String(overrides.label || 'external-witness').trim(),
management_scope: 'external',
independence_group: String(overrides.independenceGroup || 'external_witness').trim().toLowerCase(),
sequence: 0,
};
}
async function readJsonFile(filePath) {
const raw = await fs.readFile(path.resolve(filePath), 'utf8');
return JSON.parse(raw);
}
async function writeJsonFile(filePath, value) {
const target = path.resolve(filePath);
await fs.mkdir(path.dirname(target), { recursive: true });
await fs.writeFile(target, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}
async function loadWitnessIdentity(filePath) {
const parsed = await readJsonFile(filePath);
if (!parsed || typeof parsed !== 'object') {
throw new Error('external witness identity file root must be an object');
}
const identity = { ...parsed };
if (String(identity.type || EXTERNAL_WITNESS_IDENTITY_TYPE) !== EXTERNAL_WITNESS_IDENTITY_TYPE) {
throw new Error('external witness identity type invalid');
}
if (safeInt(identity.schema_version, 0) <= 0) {
throw new Error('external witness identity schema_version required');
}
if (String(identity.public_key_algo || 'Ed25519') !== 'Ed25519') {
throw new Error('external witness identity public_key_algo must be Ed25519');
}
if (!String(identity.public_key || '').trim() || !String(identity.private_key || '').trim()) {
throw new Error('external witness identity keys required');
}
const derivedNodeId = deriveNodeId(identity.public_key);
if (!String(identity.node_id || '').trim()) {
identity.node_id = derivedNodeId;
}
if (String(identity.node_id || '').trim() !== derivedNodeId) {
throw new Error('external witness identity node_id does not match public_key');
}
identity.sequence = Math.max(0, safeInt(identity.sequence, 0));
identity.label = String(identity.label || '').trim();
identity.independence_group = String(identity.independence_group || 'external_witness')
.trim()
.toLowerCase();
return identity;
}
async function fetchDistribution(config) {
const headers = { Accept: 'application/json' };
const authHeader = parseHeader(config.authHeader);
if (authHeader) {
headers[authHeader[0]] = authHeader[1];
}
if (config.authCookie) {
headers.Cookie = config.authCookie;
}
const controller = new AbortController();
const timeout = globalThis.setTimeout(() => controller.abort(), config.timeoutMs);
try {
const response = await fetch(config.url, {
method: 'GET',
headers,
signal: controller.signal,
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || payload?.ok === false) {
const detail = String(payload?.detail || payload?.message || `http_${response.status}`).trim();
throw new Error(detail || 'root_distribution_fetch_failed');
}
return payload;
} finally {
globalThis.clearTimeout(timeout);
}
}
function buildImportPackage({ identity, descriptor, sourceScope, sourceLabel, descriptorOnly, distribution }) {
const packageBase = {
type: EXTERNAL_WITNESS_IMPORT_TYPE,
schema_version: 1,
source_scope: String(sourceScope || 'external_publish').trim().toLowerCase(),
source_label: String(sourceLabel || descriptor.label || identity.label || '').trim(),
exported_at: nowSeconds(),
descriptors: [descriptor],
};
if (descriptorOnly) {
return packageBase;
}
const manifest = distribution?.manifest && typeof distribution.manifest === 'object' ? distribution.manifest : null;
if (!manifest) {
throw new Error('current root-distribution manifest required');
}
const manifestFingerprint =
String(distribution?.manifest_fingerprint || '').trim().toLowerCase() || manifestFingerprintForEnvelope(manifest);
const policyWitnesses = Array.isArray(distribution?.witness_policy?.witnesses)
? distribution.witness_policy.witnesses
: Array.isArray(manifest?.payload?.witness_policy?.witnesses)
? manifest.payload.witness_policy.witnesses
: [];
const declaredWitness = policyWitnesses.find(
(item) =>
String(item?.node_id || '').trim() === identity.node_id &&
String(item?.public_key || '').trim() === identity.public_key,
);
if (!declaredWitness) {
throw new Error(
'external witness is not declared in the current manifest policy; import a descriptor-only package and let the backend republish before generating receipts',
);
}
const nextSequence = Math.max(1, safeInt(identity.sequence, 0) + 1);
const witnessPayload = buildWitnessPayload(manifest);
const signaturePayload = buildSignaturePayload({
eventType: MANIFEST_WITNESS_EVENT_TYPE,
nodeId: identity.node_id,
sequence: nextSequence,
payload: witnessPayload,
});
const signature = crypto.sign(null, Buffer.from(signaturePayload, 'utf8'), createPrivateKeyFromIdentity(identity)).toString('hex');
identity.sequence = nextSequence;
identity.updated_at = nowSeconds();
return {
...packageBase,
manifest_fingerprint: manifestFingerprint,
witnesses: [
{
type: MANIFEST_WITNESS_TYPE,
event_type: MANIFEST_WITNESS_EVENT_TYPE,
node_id: identity.node_id,
public_key: identity.public_key,
public_key_algo: 'Ed25519',
protocol_version: PROTOCOL_VERSION,
sequence: nextSequence,
payload: witnessPayload,
signature,
identity_scope: 'root_witness',
},
],
};
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
console.log(HELP_TEXT);
return;
}
const label = String(args.label || process.env.SB_DM_ROOT_WITNESS_LABEL || 'external-witness').trim();
const independenceGroup = String(
args.independenceGroup || process.env.SB_DM_ROOT_WITNESS_INDEPENDENCE_GROUP || 'external_witness',
)
.trim()
.toLowerCase();
if (args.initWitness) {
const identity = generateWitnessIdentity({ label, independenceGroup });
await writeJsonFile(args.initWitness, identity);
console.log(`external witness identity written to ${path.resolve(args.initWitness)}`);
return;
}
const witnessFile = String(args.witnessFile || process.env.SB_DM_ROOT_WITNESS_IDENTITY_FILE || '').trim();
if (!witnessFile) {
console.error('external witness identity file required; use --witness-file PATH or --init-witness PATH');
process.exit(2);
}
const identity = await loadWitnessIdentity(witnessFile);
if (label) {
identity.label = label;
}
if (independenceGroup) {
identity.independence_group = independenceGroup;
}
const descriptor = buildWitnessDescriptor(identity, {
label,
independenceGroup,
});
const descriptorOnly = Boolean(args.descriptorOnly);
const sourceScope = String(args.sourceScope || process.env.SB_DM_ROOT_WITNESS_SOURCE_SCOPE || 'external_publish').trim();
const sourceLabel = String(args.sourceLabel || process.env.SB_DM_ROOT_WITNESS_SOURCE_LABEL || descriptor.label).trim();
let distribution = null;
if (!descriptorOnly) {
distribution = await fetchDistribution({
url: normalizeUrl(args.baseUrl || process.env.SB_DM_ROOT_BASE_URL, args.distributionPath || process.env.SB_DM_ROOT_DISTRIBUTION_PATH),
authHeader: process.env.SB_DM_ROOT_AUTH_HEADER || '',
authCookie: process.env.SB_DM_ROOT_AUTH_COOKIE || '',
timeoutMs: Math.max(1000, safeInt(process.env.SB_DM_ROOT_TIMEOUT_MS, 10000)),
});
}
const packageDocument = buildImportPackage({
identity,
descriptor,
sourceScope,
sourceLabel,
descriptorOnly,
distribution,
});
await writeJsonFile(witnessFile, identity);
const outputPath = String(args.output || process.env.SB_DM_ROOT_WITNESS_OUTPUT || '').trim();
if (outputPath) {
await writeJsonFile(outputPath, packageDocument);
}
if (args.stdout || !outputPath) {
process.stdout.write(`${JSON.stringify(packageDocument, null, 2)}\n`);
}
}
await main().catch((error) => {
console.error(String(error?.message || error || 'external witness package publish failed').trim());
process.exit(2);
});
@@ -0,0 +1,209 @@
#!/usr/bin/env node
import path from 'node:path';
import { spawn } from 'node:child_process';
const HELP_TEXT = `
ShadowBroker DM root deployment smoke
Usage:
node scripts/mesh/smoke-dm-root-deployment-flow.mjs [--keep] [--workspace PATH] [--base-url URL] [--require-current-external]
Environment:
SB_DM_ROOT_BASE_URL=http://127.0.0.1:8000
SB_DM_ROOT_AUTH_HEADER=X-Admin-Key: change-me
SB_DM_ROOT_AUTH_COOKIE=operator_session=...
SB_DM_ROOT_TIMEOUT_MS=10000
SB_DM_ROOT_DEPLOYMENT_SMOKE_WORKSPACE=.smoke/dm-root-deployment
What it does:
1. Runs the external witness bootstrap smoke.
2. Runs the transparency publication smoke.
3. Fetches /api/wormhole/dm/root-health.
4. Prints one rolled-up result for the current deployment state.
Flags:
--keep Keep the smoke workspace instead of deleting it
--workspace PATH Override SB_DM_ROOT_DEPLOYMENT_SMOKE_WORKSPACE
--base-url URL Override SB_DM_ROOT_BASE_URL
--require-current-external Fail unless root-health ends in current_external with strong trust unblocked
--help Show this text
`.trim();
function parseArgs(argv) {
const parsed = {};
for (let index = 0; index < argv.length; index += 1) {
const current = String(argv[index] || '').trim();
if (!current) continue;
if (current === '--keep' || current === '--require-current-external') {
parsed[current.slice(2).replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())] = true;
continue;
}
if (current === '--help' || current === '-h') {
parsed.help = true;
continue;
}
if ((current === '--workspace' || current === '--base-url') && index + 1 < argv.length) {
parsed[current.slice(2).replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())] =
String(argv[index + 1] || '').trim();
index += 1;
}
}
return parsed;
}
function normalizeUrl(baseUrl, routePath) {
const base = String(baseUrl || 'http://127.0.0.1:8000').trim().replace(/\/+$/, '');
return `${base}/${String(routePath || '').replace(/^\/+/, '')}`;
}
function parseHeader(rawValue) {
const raw = String(rawValue || '').trim();
if (!raw) return null;
const separator = raw.indexOf(':');
if (separator <= 0) return null;
const name = raw.slice(0, separator).trim();
const value = raw.slice(separator + 1).trim();
if (!name || !value) return null;
return [name, value];
}
function safeInt(value, fallback = 0) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return fallback;
return Math.trunc(numeric);
}
async function requestJson({ method, url, authHeader, authCookie, timeoutMs }) {
const headers = { Accept: 'application/json' };
const parsedAuth = parseHeader(authHeader);
if (parsedAuth) {
headers[parsedAuth[0]] = parsedAuth[1];
}
if (authCookie) {
headers.Cookie = authCookie;
}
const controller = new AbortController();
const timeout = globalThis.setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
method,
headers,
signal: controller.signal,
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || payload?.ok === false) {
const detail = String(payload?.detail || payload?.message || `http_${response.status}`).trim();
throw new Error(detail || `${method.toLowerCase()}_${url}_failed`);
}
return payload;
} finally {
globalThis.clearTimeout(timeout);
}
}
async function spawnNodeScript(scriptPath, args, env) {
await new Promise((resolve, reject) => {
const child = spawn(process.execPath, [scriptPath, ...args], {
stdio: 'inherit',
env,
});
child.on('error', reject);
child.on('exit', (code) => {
if (code === 0) {
resolve();
return;
}
reject(new Error(`${path.basename(scriptPath)} exited ${code ?? 1}`));
});
});
}
function assert(condition, detail) {
if (!condition) {
throw new Error(detail);
}
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
console.log(HELP_TEXT);
return;
}
const rootDir = process.cwd();
const workspace = path.resolve(
String(args.workspace || process.env.SB_DM_ROOT_DEPLOYMENT_SMOKE_WORKSPACE || '.smoke/dm-root-deployment').trim(),
);
const baseUrl = String(args.baseUrl || process.env.SB_DM_ROOT_BASE_URL || 'http://127.0.0.1:8000').trim();
const timeoutMs = Math.max(1000, safeInt(process.env.SB_DM_ROOT_TIMEOUT_MS, 10000));
const authHeader = process.env.SB_DM_ROOT_AUTH_HEADER || '';
const authCookie = process.env.SB_DM_ROOT_AUTH_COOKIE || '';
const witnessSmokeScript = path.resolve(rootDir, 'scripts/mesh/smoke-external-root-witness-flow.mjs');
const transparencySmokeScript = path.resolve(rootDir, 'scripts/mesh/smoke-root-transparency-publication-flow.mjs');
const witnessWorkspace = path.join(workspace, 'witness');
const transparencyWorkspace = path.join(workspace, 'transparency');
const childEnv = {
...process.env,
SB_DM_ROOT_BASE_URL: baseUrl,
SB_DM_ROOT_AUTH_HEADER: authHeader,
SB_DM_ROOT_AUTH_COOKIE: authCookie,
SB_DM_ROOT_TIMEOUT_MS: String(timeoutMs),
};
console.log('1/3 run external witness bootstrap smoke');
await spawnNodeScript(
witnessSmokeScript,
['--workspace', witnessWorkspace, '--base-url', baseUrl, ...(args.keep ? ['--keep'] : [])],
childEnv,
);
console.log('2/3 run transparency publication smoke');
await spawnNodeScript(
transparencySmokeScript,
['--workspace', transparencyWorkspace, '--base-url', baseUrl, ...(args.keep ? ['--keep'] : [])],
childEnv,
);
console.log('3/3 fetch rolled-up DM root health');
const health = await requestJson({
method: 'GET',
url: normalizeUrl(baseUrl, '/api/wormhole/dm/root-health'),
authHeader,
authCookie,
timeoutMs,
});
if (args.requireCurrentExternal) {
assert(String(health?.state || '').trim() === 'current_external', 'dm root health did not reach current_external');
assert(String(health?.health_state || '').trim() === 'ok', 'dm root health did not reach ok');
assert(!Boolean(health?.strong_trust_blocked), 'strong DM trust is still blocked');
}
const summary = {
ok: true,
workspace,
require_current_external: Boolean(args.requireCurrentExternal),
state: String(health?.state || '').trim(),
health_state: String(health?.health_state || '').trim(),
strong_trust_blocked: Boolean(health?.strong_trust_blocked),
external_assurance_current: Boolean(health?.external_assurance_current),
requires_attention: Boolean(health?.requires_attention),
monitoring_state: String(health?.monitoring?.state || '').trim(),
monitoring_status_line: String(health?.monitoring?.status_line || '').trim(),
next_action: String(health?.next_action || '').trim(),
witness_state: String(health?.witness?.state || '').trim(),
witness_health_state: String(health?.witness?.health_state || '').trim(),
transparency_state: String(health?.transparency?.state || '').trim(),
transparency_health_state: String(health?.transparency?.health_state || '').trim(),
};
console.log(JSON.stringify(summary, null, 2));
}
await main().catch((error) => {
console.error(String(error?.message || error || 'dm root deployment smoke failed').trim());
process.exit(2);
});
@@ -0,0 +1,283 @@
#!/usr/bin/env node
import fs from 'node:fs/promises';
import path from 'node:path';
import { spawn } from 'node:child_process';
const HELP_TEXT = `
ShadowBroker external root witness flow smoke
Usage:
node scripts/mesh/smoke-external-root-witness-flow.mjs [--keep] [--workspace PATH] [--base-url URL]
Environment:
SB_DM_ROOT_BASE_URL=http://127.0.0.1:8000
SB_DM_ROOT_AUTH_HEADER=X-Admin-Key: change-me
SB_DM_ROOT_AUTH_COOKIE=operator_session=...
SB_DM_ROOT_TIMEOUT_MS=10000
SB_DM_ROOT_SMOKE_WORKSPACE=.smoke/external-root-witness-flow
SB_DM_ROOT_WITNESS_LABEL=witness-a
SB_DM_ROOT_WITNESS_SOURCE_SCOPE=https_publish
SB_DM_ROOT_WITNESS_SOURCE_LABEL=witness-a
SB_DM_ROOT_WITNESS_INDEPENDENCE_GROUP=independent_witness_a
What it does:
1. Generate a fresh external witness identity.
2. Publish and import a descriptor-only package.
3. Trigger root-distribution republish and verify the new witness is in policy.
4. Publish and import a full signed witness receipt package.
5. Verify external witness receipts are current in root-distribution.
Flags:
--keep Keep the smoke workspace instead of deleting it
--workspace PATH Override SB_DM_ROOT_SMOKE_WORKSPACE
--base-url URL Override SB_DM_ROOT_BASE_URL
--help Show this text
`.trim();
function parseArgs(argv) {
const parsed = {};
for (let index = 0; index < argv.length; index += 1) {
const current = String(argv[index] || '').trim();
if (!current) continue;
if (current === '--keep') {
parsed.keep = true;
continue;
}
if (current === '--help' || current === '-h') {
parsed.help = true;
continue;
}
if ((current === '--workspace' || current === '--base-url') && index + 1 < argv.length) {
parsed[current.slice(2).replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())] =
String(argv[index + 1] || '').trim();
index += 1;
}
}
return parsed;
}
function normalizeUrl(baseUrl, routePath) {
const base = String(baseUrl || 'http://127.0.0.1:8000').trim().replace(/\/+$/, '');
const pathValue = String(routePath || '').trim();
if (!pathValue) {
return base;
}
return `${base}/${pathValue.replace(/^\/+/, '')}`;
}
function parseHeader(rawValue) {
const raw = String(rawValue || '').trim();
if (!raw) return null;
const separator = raw.indexOf(':');
if (separator <= 0) return null;
const name = raw.slice(0, separator).trim();
const value = raw.slice(separator + 1).trim();
if (!name || !value) return null;
return [name, value];
}
function safeInt(value, fallback = 0) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return fallback;
return Math.trunc(numeric);
}
async function requestJson({ method, url, authHeader, authCookie, timeoutMs, body }) {
const headers = { Accept: 'application/json' };
const parsedAuth = parseHeader(authHeader);
if (parsedAuth) {
headers[parsedAuth[0]] = parsedAuth[1];
}
if (authCookie) {
headers.Cookie = authCookie;
}
if (body !== undefined) {
headers['Content-Type'] = 'application/json';
}
const controller = new AbortController();
const timeout = globalThis.setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
signal: controller.signal,
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || payload?.ok === false) {
const detail = String(payload?.detail || payload?.message || `http_${response.status}`).trim();
throw new Error(detail || `${method.toLowerCase()}_${url}_failed`);
}
return payload;
} finally {
globalThis.clearTimeout(timeout);
}
}
async function spawnNodeScript(scriptPath, args, env) {
await new Promise((resolve, reject) => {
const child = spawn(process.execPath, [scriptPath, ...args], {
stdio: 'inherit',
env,
});
child.on('error', reject);
child.on('exit', (code) => {
if (code === 0) {
resolve();
return;
}
reject(new Error(`${path.basename(scriptPath)} exited ${code ?? 1}`));
});
});
}
function assert(condition, detail) {
if (!condition) {
throw new Error(detail);
}
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
console.log(HELP_TEXT);
return;
}
const rootDir = process.cwd();
const workspace = path.resolve(
String(args.workspace || process.env.SB_DM_ROOT_SMOKE_WORKSPACE || '.smoke/external-root-witness-flow').trim(),
);
const baseUrl = String(args.baseUrl || process.env.SB_DM_ROOT_BASE_URL || 'http://127.0.0.1:8000').trim();
const timeoutMs = Math.max(1000, safeInt(process.env.SB_DM_ROOT_TIMEOUT_MS, 10000));
const authHeader = process.env.SB_DM_ROOT_AUTH_HEADER || '';
const authCookie = process.env.SB_DM_ROOT_AUTH_COOKIE || '';
const label = String(process.env.SB_DM_ROOT_WITNESS_LABEL || 'witness-a').trim();
const sourceScope = String(process.env.SB_DM_ROOT_WITNESS_SOURCE_SCOPE || 'https_publish').trim();
const sourceLabel = String(process.env.SB_DM_ROOT_WITNESS_SOURCE_LABEL || label).trim();
const independenceGroup = String(
process.env.SB_DM_ROOT_WITNESS_INDEPENDENCE_GROUP || 'independent_witness_a',
)
.trim()
.toLowerCase();
const identityPath = path.join(workspace, 'witness.identity.json');
const descriptorPath = path.join(workspace, 'root_witness_descriptor.json');
const receiptPath = path.join(workspace, 'root_witness_receipt.json');
const publisherScript = path.resolve(rootDir, 'scripts/mesh/publish-external-root-witness-package.mjs');
await fs.mkdir(workspace, { recursive: true });
const childEnv = {
...process.env,
SB_DM_ROOT_BASE_URL: baseUrl,
SB_DM_ROOT_AUTH_HEADER: authHeader,
SB_DM_ROOT_AUTH_COOKIE: authCookie,
SB_DM_ROOT_TIMEOUT_MS: String(timeoutMs),
SB_DM_ROOT_WITNESS_LABEL: label,
SB_DM_ROOT_WITNESS_SOURCE_SCOPE: sourceScope,
SB_DM_ROOT_WITNESS_SOURCE_LABEL: sourceLabel,
SB_DM_ROOT_WITNESS_INDEPENDENCE_GROUP: independenceGroup,
};
try {
console.log('1/5 init external witness identity');
await spawnNodeScript(publisherScript, ['--init-witness', identityPath], childEnv);
console.log('2/5 publish and import descriptor-only package');
await spawnNodeScript(
publisherScript,
['--descriptor-only', '--witness-file', identityPath, '--output', descriptorPath],
childEnv,
);
const descriptorImport = await requestJson({
method: 'POST',
url: normalizeUrl(baseUrl, '/api/wormhole/dm/root-witnesses/import-config'),
authHeader,
authCookie,
timeoutMs,
body: { path: descriptorPath },
});
assert(descriptorImport?.ok === true, 'descriptor-only import failed');
console.log('3/5 trigger republish and verify the external witness is declared in policy');
const identity = JSON.parse(await fs.readFile(identityPath, 'utf8'));
const distributionAfterDescriptor = await requestJson({
method: 'GET',
url: normalizeUrl(baseUrl, '/api/wormhole/dm/root-distribution'),
authHeader,
authCookie,
timeoutMs,
});
const policyWitnesses = Array.isArray(distributionAfterDescriptor?.witness_policy?.witnesses)
? distributionAfterDescriptor.witness_policy.witnesses
: [];
const declared = policyWitnesses.some(
(item) =>
String(item?.node_id || '').trim() === String(identity?.node_id || '').trim() &&
String(item?.public_key || '').trim() === String(identity?.public_key || '').trim(),
);
assert(declared, 'external witness was not declared in the republished manifest policy');
console.log('4/5 publish and import the full signed external witness receipt package');
await spawnNodeScript(
publisherScript,
['--witness-file', identityPath, '--output', receiptPath],
childEnv,
);
const receiptImport = await requestJson({
method: 'POST',
url: normalizeUrl(baseUrl, '/api/wormhole/dm/root-witnesses/import-config'),
authHeader,
authCookie,
timeoutMs,
body: { path: receiptPath },
});
assert(receiptImport?.ok === true, 'external witness receipt import failed');
console.log('5/5 verify current root-distribution state');
const distributionFinal = await requestJson({
method: 'GET',
url: normalizeUrl(baseUrl, '/api/wormhole/dm/root-distribution'),
authHeader,
authCookie,
timeoutMs,
});
assert(Boolean(distributionFinal?.external_witness_receipts_current), 'external witness receipts did not become current');
assert(safeInt(distributionFinal?.external_witness_receipt_count, 0) >= 1, 'external witness receipt count did not increase');
const health = await requestJson({
method: 'GET',
url: normalizeUrl(baseUrl, '/api/wormhole/dm/root-health'),
authHeader,
authCookie,
timeoutMs,
});
const summary = {
ok: true,
workspace,
manifest_fingerprint: String(distributionFinal?.manifest_fingerprint || '').trim().toLowerCase(),
witness_policy_fingerprint: String(distributionFinal?.witness_policy_fingerprint || '').trim().toLowerCase(),
external_witness_receipt_count: safeInt(distributionFinal?.external_witness_receipt_count, 0),
witness_count: safeInt(distributionFinal?.witness_count, 0),
witness_domain_count: safeInt(distributionFinal?.witness_domain_count, 0),
witness_independent_quorum_met: Boolean(distributionFinal?.witness_independent_quorum_met),
witness_operator_state: String(distributionFinal?.external_witness_operator_state || '').trim(),
health_summary_state: String(health?.state || '').trim(),
health_state: String(health?.health_state || '').trim(),
health_next_action: String(health?.next_action || '').trim(),
};
console.log(JSON.stringify(summary, null, 2));
} finally {
if (!args.keep) {
await fs.rm(workspace, { recursive: true, force: true });
}
}
}
await main().catch((error) => {
console.error(String(error?.message || error || 'external witness flow smoke failed').trim());
process.exit(2);
});
@@ -0,0 +1,210 @@
#!/usr/bin/env node
import fs from 'node:fs/promises';
import path from 'node:path';
const HELP_TEXT = `
ShadowBroker root transparency publication smoke
Usage:
node scripts/mesh/smoke-root-transparency-publication-flow.mjs [--keep] [--workspace PATH] [--base-url URL]
Environment:
SB_DM_ROOT_BASE_URL=http://127.0.0.1:8000
SB_DM_ROOT_AUTH_HEADER=X-Admin-Key: change-me
SB_DM_ROOT_AUTH_COOKIE=operator_session=...
SB_DM_ROOT_TIMEOUT_MS=10000
SB_DM_ROOT_TRANSPARENCY_SMOKE_WORKSPACE=.smoke/root-transparency-publication
SB_DM_ROOT_TRANSPARENCY_MAX_RECORDS=64
What it does:
1. Fetch the current root transparency record.
2. Publish the transparency ledger to a chosen local file through the operator endpoint.
3. Read the published ledger back through the published-ledger endpoint.
4. Verify binding and chain fingerprints match the live transparency state.
Flags:
--keep Keep the smoke workspace instead of deleting it
--workspace PATH Override SB_DM_ROOT_TRANSPARENCY_SMOKE_WORKSPACE
--base-url URL Override SB_DM_ROOT_BASE_URL
--help Show this text
`.trim();
function parseArgs(argv) {
const parsed = {};
for (let index = 0; index < argv.length; index += 1) {
const current = String(argv[index] || '').trim();
if (!current) continue;
if (current === '--keep') {
parsed.keep = true;
continue;
}
if (current === '--help' || current === '-h') {
parsed.help = true;
continue;
}
if ((current === '--workspace' || current === '--base-url') && index + 1 < argv.length) {
parsed[current.slice(2).replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())] =
String(argv[index + 1] || '').trim();
index += 1;
}
}
return parsed;
}
function normalizeUrl(baseUrl, routePath) {
const base = String(baseUrl || 'http://127.0.0.1:8000').trim().replace(/\/+$/, '');
return `${base}/${String(routePath || '').replace(/^\/+/, '')}`;
}
function parseHeader(rawValue) {
const raw = String(rawValue || '').trim();
if (!raw) return null;
const separator = raw.indexOf(':');
if (separator <= 0) return null;
const name = raw.slice(0, separator).trim();
const value = raw.slice(separator + 1).trim();
if (!name || !value) return null;
return [name, value];
}
function safeInt(value, fallback = 0) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return fallback;
return Math.trunc(numeric);
}
async function requestJson({ method, url, authHeader, authCookie, timeoutMs, body }) {
const headers = { Accept: 'application/json' };
const parsedAuth = parseHeader(authHeader);
if (parsedAuth) {
headers[parsedAuth[0]] = parsedAuth[1];
}
if (authCookie) {
headers.Cookie = authCookie;
}
if (body !== undefined) {
headers['Content-Type'] = 'application/json';
}
const controller = new AbortController();
const timeout = globalThis.setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
signal: controller.signal,
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || payload?.ok === false) {
const detail = String(payload?.detail || payload?.message || `http_${response.status}`).trim();
throw new Error(detail || `${method.toLowerCase()}_${url}_failed`);
}
return payload;
} finally {
globalThis.clearTimeout(timeout);
}
}
function assert(condition, detail) {
if (!condition) {
throw new Error(detail);
}
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
console.log(HELP_TEXT);
return;
}
const workspace = path.resolve(
String(
args.workspace || process.env.SB_DM_ROOT_TRANSPARENCY_SMOKE_WORKSPACE || '.smoke/root-transparency-publication',
).trim(),
);
const baseUrl = String(args.baseUrl || process.env.SB_DM_ROOT_BASE_URL || 'http://127.0.0.1:8000').trim();
const timeoutMs = Math.max(1000, safeInt(process.env.SB_DM_ROOT_TIMEOUT_MS, 10000));
const authHeader = process.env.SB_DM_ROOT_AUTH_HEADER || '';
const authCookie = process.env.SB_DM_ROOT_AUTH_COOKIE || '';
const maxRecords = Math.max(1, safeInt(process.env.SB_DM_ROOT_TRANSPARENCY_MAX_RECORDS, 64));
const ledgerPath = path.join(workspace, 'root_transparency_ledger.json');
await fs.mkdir(workspace, { recursive: true });
try {
console.log('1/4 fetch current root transparency state');
const current = await requestJson({
method: 'GET',
url: normalizeUrl(baseUrl, '/api/wormhole/dm/root-transparency'),
authHeader,
authCookie,
timeoutMs,
});
assert(Boolean(current?.record_fingerprint), 'current root transparency record fingerprint missing');
assert(Boolean(current?.binding_fingerprint), 'current root transparency binding fingerprint missing');
console.log('2/4 publish transparency ledger to a local file through the operator endpoint');
const published = await requestJson({
method: 'POST',
url: normalizeUrl(baseUrl, '/api/wormhole/dm/root-transparency/ledger/publish'),
authHeader,
authCookie,
timeoutMs,
body: { path: ledgerPath, max_records: maxRecords },
});
assert(Boolean(published?.path), 'published transparency ledger path missing');
assert(Boolean(published?.chain_fingerprint), 'published transparency chain fingerprint missing');
console.log('3/4 read the published ledger back through the published-ledger endpoint');
const publishedReadback = await requestJson({
method: 'GET',
url: `${normalizeUrl(baseUrl, '/api/wormhole/dm/root-transparency/ledger/published')}?path=${encodeURIComponent(ledgerPath)}`,
authHeader,
authCookie,
timeoutMs,
});
assert(Boolean(publishedReadback?.chain_fingerprint), 'published ledger readback chain fingerprint missing');
assert(Boolean(publishedReadback?.head_binding_fingerprint), 'published ledger readback head binding missing');
console.log('4/4 verify the exported ledger matches live transparency state');
assert(
String(publishedReadback.chain_fingerprint || '').trim().toLowerCase() ===
String(published.chain_fingerprint || '').trim().toLowerCase(),
'published ledger chain fingerprint mismatch',
);
assert(
String(publishedReadback.head_binding_fingerprint || '').trim().toLowerCase() ===
String(current.binding_fingerprint || '').trim().toLowerCase(),
'published ledger binding fingerprint does not match current transparency binding',
);
assert(
String(publishedReadback.current_record_fingerprint || '').trim().toLowerCase() ===
String(current.record_fingerprint || '').trim().toLowerCase(),
'published ledger head record does not match current transparency record',
);
const ledgerStat = await fs.stat(ledgerPath);
const summary = {
ok: true,
workspace,
ledger_path: ledgerPath,
ledger_size_bytes: ledgerStat.size,
record_fingerprint: String(current.record_fingerprint || '').trim().toLowerCase(),
binding_fingerprint: String(current.binding_fingerprint || '').trim().toLowerCase(),
chain_fingerprint: String(publishedReadback.chain_fingerprint || '').trim().toLowerCase(),
record_count: safeInt(publishedReadback.record_count, 0),
};
console.log(JSON.stringify(summary, null, 2));
} finally {
if (!args.keep) {
await fs.rm(workspace, { recursive: true, force: true });
}
}
}
await main().catch((error) => {
console.error(String(error?.message || error || 'root transparency publication smoke failed').trim());
process.exit(2);
});
@@ -0,0 +1,350 @@
#!/usr/bin/env node
import fs from 'node:fs/promises';
import path from 'node:path';
import { spawn } from 'node:child_process';
const HELP_TEXT = `
ShadowBroker DM root external assurance sync
Usage:
node scripts/mesh/sync-dm-root-external-assurance.mjs [--base-url URL] [--witness-file PATH] [--publish-transparency]
Environment:
SB_DM_ROOT_BASE_URL=http://127.0.0.1:8000
SB_DM_ROOT_AUTH_HEADER=X-Admin-Key: change-me
SB_DM_ROOT_AUTH_COOKIE=operator_session=...
SB_DM_ROOT_TIMEOUT_MS=10000
SB_DM_ROOT_WITNESS_IDENTITY_FILE=./ops/witness-a.identity.json
SB_DM_ROOT_WITNESS_LABEL=witness-a
SB_DM_ROOT_WITNESS_SOURCE_SCOPE=https_publish
SB_DM_ROOT_WITNESS_SOURCE_LABEL=witness-a
SB_DM_ROOT_WITNESS_INDEPENDENCE_GROUP=independent_witness_a
SB_DM_ROOT_TRANSPARENCY_PUBLISH_PATH=./published/root_transparency_ledger.json
SB_DM_ROOT_TRANSPARENCY_MAX_RECORDS=64
What it does:
1. Creates an external witness identity if the configured file is missing.
2. Imports a descriptor-only witness package if the external witness is not yet declared in policy.
3. Imports a full signed witness receipt package once the current manifest policy allows it.
4. Optionally publishes the transparency ledger to a chosen file path.
5. Prints the final DM root health summary.
Flags:
--base-url URL Override SB_DM_ROOT_BASE_URL
--witness-file PATH Override SB_DM_ROOT_WITNESS_IDENTITY_FILE
--publish-transparency Publish the transparency ledger using SB_DM_ROOT_TRANSPARENCY_PUBLISH_PATH
--help Show this text
`.trim();
function parseArgs(argv) {
const parsed = {};
for (let index = 0; index < argv.length; index += 1) {
const current = String(argv[index] || '').trim();
if (!current) continue;
if (current === '--publish-transparency') {
parsed.publishTransparency = true;
continue;
}
if (current === '--help' || current === '-h') {
parsed.help = true;
continue;
}
if ((current === '--base-url' || current === '--witness-file') && index + 1 < argv.length) {
parsed[current.slice(2).replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())] =
String(argv[index + 1] || '').trim();
index += 1;
}
}
return parsed;
}
function normalizeUrl(baseUrl, routePath) {
const base = String(baseUrl || 'http://127.0.0.1:8000').trim().replace(/\/+$/, '');
return `${base}/${String(routePath || '').replace(/^\/+/, '')}`;
}
function parseHeader(rawValue) {
const raw = String(rawValue || '').trim();
if (!raw) return null;
const separator = raw.indexOf(':');
if (separator <= 0) return null;
const name = raw.slice(0, separator).trim();
const value = raw.slice(separator + 1).trim();
if (!name || !value) return null;
return [name, value];
}
function safeInt(value, fallback = 0) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return fallback;
return Math.trunc(numeric);
}
async function requestJson({ method, url, authHeader, authCookie, timeoutMs, body }) {
const headers = { Accept: 'application/json' };
const parsedAuth = parseHeader(authHeader);
if (parsedAuth) {
headers[parsedAuth[0]] = parsedAuth[1];
}
if (authCookie) {
headers.Cookie = authCookie;
}
if (body !== undefined) {
headers['Content-Type'] = 'application/json';
}
const controller = new AbortController();
const timeout = globalThis.setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
signal: controller.signal,
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || payload?.ok === false) {
const detail = String(payload?.detail || payload?.message || `http_${response.status}`).trim();
throw new Error(detail || `${method.toLowerCase()}_${url}_failed`);
}
return payload;
} finally {
globalThis.clearTimeout(timeout);
}
}
async function spawnNode(scriptPath, args, env, { expectJson = false } = {}) {
return await new Promise((resolve, reject) => {
let stdout = '';
let stderr = '';
const child = spawn(process.execPath, [scriptPath, ...args], {
env,
stdio: ['ignore', 'pipe', 'pipe'],
});
child.stdout.on('data', (chunk) => {
stdout += String(chunk || '');
process.stdout.write(chunk);
});
child.stderr.on('data', (chunk) => {
stderr += String(chunk || '');
process.stderr.write(chunk);
});
child.on('error', reject);
child.on('exit', (code) => {
if (code !== 0) {
reject(new Error(stderr.trim() || `${path.basename(scriptPath)} exited ${code ?? 1}`));
return;
}
if (!expectJson) {
resolve(undefined);
return;
}
try {
resolve(JSON.parse(stdout));
} catch {
reject(new Error(`failed to parse JSON output from ${path.basename(scriptPath)}`));
}
});
});
}
function witnessDeclaredInPolicy(distribution, identity) {
const witnesses = Array.isArray(distribution?.witness_policy?.witnesses)
? distribution.witness_policy.witnesses
: [];
return witnesses.some(
(item) =>
String(item?.node_id || '').trim() === String(identity?.node_id || '').trim() &&
String(item?.public_key || '').trim() === String(identity?.public_key || '').trim(),
);
}
async function readJsonFile(filePath) {
const raw = await fs.readFile(path.resolve(filePath), 'utf8');
return JSON.parse(raw);
}
async function exists(filePath) {
try {
await fs.access(path.resolve(filePath));
return true;
} catch {
return false;
}
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
console.log(HELP_TEXT);
return;
}
const rootDir = process.cwd();
const publisherScript = path.resolve(rootDir, 'scripts/mesh/publish-external-root-witness-package.mjs');
const baseUrl = String(args.baseUrl || process.env.SB_DM_ROOT_BASE_URL || 'http://127.0.0.1:8000').trim();
const witnessFile = String(args.witnessFile || process.env.SB_DM_ROOT_WITNESS_IDENTITY_FILE || '').trim();
const timeoutMs = Math.max(1000, safeInt(process.env.SB_DM_ROOT_TIMEOUT_MS, 10000));
const authHeader = process.env.SB_DM_ROOT_AUTH_HEADER || '';
const authCookie = process.env.SB_DM_ROOT_AUTH_COOKIE || '';
const transparencyPublishPath = String(process.env.SB_DM_ROOT_TRANSPARENCY_PUBLISH_PATH || '').trim();
const transparencyMaxRecords = Math.max(1, safeInt(process.env.SB_DM_ROOT_TRANSPARENCY_MAX_RECORDS, 64));
if (!witnessFile) {
throw new Error('witness identity file required via --witness-file or SB_DM_ROOT_WITNESS_IDENTITY_FILE');
}
const childEnv = {
...process.env,
SB_DM_ROOT_BASE_URL: baseUrl,
SB_DM_ROOT_AUTH_HEADER: authHeader,
SB_DM_ROOT_AUTH_COOKIE: authCookie,
SB_DM_ROOT_TIMEOUT_MS: String(timeoutMs),
};
const actions = [];
if (!(await exists(witnessFile))) {
console.log('creating external witness identity');
await spawnNode(publisherScript, ['--init-witness', witnessFile], childEnv);
actions.push('witness_identity_created');
}
const identity = await readJsonFile(witnessFile);
let distribution = await requestJson({
method: 'GET',
url: normalizeUrl(baseUrl, '/api/wormhole/dm/root-distribution'),
authHeader,
authCookie,
timeoutMs,
});
if (!witnessDeclaredInPolicy(distribution, identity)) {
console.log('importing descriptor-only external witness package');
const descriptorMaterial = await spawnNode(
publisherScript,
['--descriptor-only', '--witness-file', witnessFile, '--stdout'],
childEnv,
{ expectJson: true },
);
await requestJson({
method: 'POST',
url: normalizeUrl(baseUrl, '/api/wormhole/dm/root-witnesses/import'),
authHeader,
authCookie,
timeoutMs,
body: { material: descriptorMaterial },
});
actions.push('descriptor_imported');
distribution = await requestJson({
method: 'GET',
url: normalizeUrl(baseUrl, '/api/wormhole/dm/root-distribution'),
authHeader,
authCookie,
timeoutMs,
});
}
if (witnessDeclaredInPolicy(distribution, identity) && !Boolean(distribution?.external_witness_receipts_current)) {
console.log('importing full external witness receipt package');
const receiptMaterial = await spawnNode(
publisherScript,
['--witness-file', witnessFile, '--stdout'],
childEnv,
{ expectJson: true },
);
await requestJson({
method: 'POST',
url: normalizeUrl(baseUrl, '/api/wormhole/dm/root-witnesses/import'),
authHeader,
authCookie,
timeoutMs,
body: { material: receiptMaterial },
});
actions.push('receipt_imported');
distribution = await requestJson({
method: 'GET',
url: normalizeUrl(baseUrl, '/api/wormhole/dm/root-distribution'),
authHeader,
authCookie,
timeoutMs,
});
}
let transparency = null;
if (args.publishTransparency) {
if (!transparencyPublishPath) {
throw new Error('SB_DM_ROOT_TRANSPARENCY_PUBLISH_PATH required when --publish-transparency is used');
}
console.log('publishing transparency ledger');
transparency = await requestJson({
method: 'POST',
url: normalizeUrl(baseUrl, '/api/wormhole/dm/root-transparency/ledger/publish'),
authHeader,
authCookie,
timeoutMs,
body: {
path: transparencyPublishPath,
max_records: transparencyMaxRecords,
},
});
actions.push('transparency_published');
}
const health = await requestJson({
method: 'GET',
url: normalizeUrl(baseUrl, '/api/wormhole/dm/root-health'),
authHeader,
authCookie,
timeoutMs,
});
const summary = {
ok: true,
actions,
state: String(health?.state || '').trim(),
health_state: String(health?.health_state || '').trim(),
strong_trust_blocked: Boolean(health?.strong_trust_blocked),
external_assurance_current: Boolean(health?.external_assurance_current),
requires_attention: Boolean(health?.requires_attention),
next_action: String(health?.next_action || '').trim(),
witness: {
state: String(health?.witness?.state || '').trim(),
health_state: String(health?.witness?.health_state || '').trim(),
source_ref: String(health?.witness?.source_ref || '').trim(),
age_s: safeInt(health?.witness?.age_s, 0),
reacquire_required: Boolean(health?.witness?.reacquire_required),
independent_quorum_met: Boolean(health?.witness?.independent_quorum_met),
},
transparency: {
state: String(health?.transparency?.state || '').trim(),
health_state: String(health?.transparency?.health_state || '').trim(),
source_ref: String(health?.transparency?.source_ref || '').trim(),
age_s: safeInt(health?.transparency?.age_s, 0),
verification_required: Boolean(health?.transparency?.verification_required),
},
distribution: {
manifest_fingerprint: String(distribution?.manifest_fingerprint || '').trim().toLowerCase(),
witness_policy_fingerprint: String(distribution?.witness_policy_fingerprint || '').trim().toLowerCase(),
external_witness_receipt_count: safeInt(distribution?.external_witness_receipt_count, 0),
external_witness_receipts_current: Boolean(distribution?.external_witness_receipts_current),
witness_count: safeInt(distribution?.witness_count, 0),
witness_domain_count: safeInt(distribution?.witness_domain_count, 0),
},
};
if (transparency) {
summary.transparency_publish = {
path: String(transparency?.path || '').trim(),
chain_fingerprint: String(transparency?.chain_fingerprint || '').trim().toLowerCase(),
head_binding_fingerprint: String(transparency?.head_binding_fingerprint || '').trim().toLowerCase(),
record_count: safeInt(transparency?.record_count, 0),
};
}
console.log(JSON.stringify(summary, null, 2));
}
await main().catch((error) => {
console.error(String(error?.message || error || 'dm root external assurance sync failed').trim());
process.exit(2);
});
+237
View File
@@ -0,0 +1,237 @@
param(
[string]$NodeA = "http://127.0.0.1:8001",
[string]$NodeB = "http://127.0.0.1:8002",
[string]$AdminKey = "dm-test-node-local-admin-key-00000001"
)
$ErrorActionPreference = "Stop"
$Root = Resolve-Path (Join-Path $PSScriptRoot "..")
$RuntimeRoot = Join-Path $Root ".runtime\dm-two-node"
$ReportPath = Join-Path $RuntimeRoot "two-node-selftest.json"
$Headers = @{ "X-Admin-Key" = $AdminKey }
function Invoke-Json(
[string]$Method,
[string]$Uri,
[object]$Body = $null,
[switch]$Admin
) {
$args = @{
Method = $Method
Uri = $Uri
TimeoutSec = 30
}
if ($Admin) {
$args.Headers = $Headers
}
if ($null -ne $Body) {
$args.Body = ($Body | ConvertTo-Json -Depth 100)
$args.ContentType = "application/json"
}
return Invoke-RestMethod @args
}
function Assert-Ok([object]$Result, [string]$Step) {
if (-not $Result -or -not [bool]$Result.ok) {
$detail = if ($Result -and $Result.detail) { [string]$Result.detail } else { "no detail" }
throw "$Step failed: $detail"
}
}
function Register-DmNode([string]$BaseUrl, [string]$Label) {
$registered = Invoke-Json "Post" "$BaseUrl/api/wormhole/dm/register-key" -Admin
Assert-Ok $registered "$Label key registration"
if (-not [bool]$registered.prekeys_ok -or -not $registered.prekey_detail -or -not $registered.prekey_detail.bundle) {
throw "$Label prekey registration failed"
}
return [pscustomobject]@{
label = $Label
base = $BaseUrl
node_id = [string]$registered.node_id
dh_pub_key = [string]$registered.dh_pub_key
prekey_bundle = $registered.prekey_detail.bundle
registered = $registered
}
}
function Try-Compose(
[object]$Sender,
[object]$Receiver,
[string]$Plaintext
) {
$body = @{
peer_id = $Receiver.node_id
peer_dh_pub = $Receiver.dh_pub_key
plaintext = $Plaintext
local_alias = $Sender.label
remote_alias = $Receiver.label
}
return Invoke-Json "Post" "$($Sender.base)/api/wormhole/dm/compose" $body
}
function Decrypt-OnReceiver(
[object]$Receiver,
[object]$Sender,
[object]$Envelope
) {
$body = @{
peer_id = $Sender.node_id
ciphertext = $Envelope.ciphertext
nonce = $Envelope.nonce
format = $Envelope.format
local_alias = $Receiver.label
remote_alias = $Sender.label
session_welcome = $Envelope.session_welcome
}
return Invoke-Json "Post" "$($Receiver.base)/api/wormhole/dm/decrypt" $body -Admin
}
function Search-PlaintextInNodeData([string]$Needle) {
$hits = @()
foreach ($nodeName in @("node-a", "node-b")) {
$dataPath = Join-Path $RuntimeRoot "$nodeName\backend\data"
if (-not (Test-Path $dataPath)) {
continue
}
$matches = Get-ChildItem -Path $dataPath -Recurse -File -ErrorAction SilentlyContinue |
Select-String -Pattern ([regex]::Escape($Needle)) -SimpleMatch -ErrorAction SilentlyContinue
foreach ($match in @($matches)) {
$hits += [pscustomobject]@{
node = $nodeName
path = $match.Path
line = $match.LineNumber
}
}
}
return @($hits)
}
New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null
$healthA = Invoke-Json "Get" "$NodeA/api/health"
$healthB = Invoke-Json "Get" "$NodeB/api/health"
$nodeAState = Register-DmNode $NodeA "node-a"
$nodeBState = Register-DmNode $NodeB "node-b"
Invoke-Json "Post" "$NodeA/api/wormhole/dm/reset" @{ peer_id = $nodeBState.node_id } -Admin | Out-Null
Invoke-Json "Post" "$NodeB/api/wormhole/dm/reset" @{ peer_id = $nodeAState.node_id } -Admin | Out-Null
$inviteA = Invoke-Json "Get" "$NodeA/api/wormhole/dm/invite"
$inviteB = Invoke-Json "Get" "$NodeB/api/wormhole/dm/invite"
$inviteImportBIntoA = Invoke-Json "Post" "$NodeA/api/wormhole/dm/invite/import" @{
invite = $inviteB.invite
alias = "node-b"
} -Admin
Assert-Ok $inviteImportBIntoA "node-a import node-b signed invite"
$inviteImportAIntoB = Invoke-Json "Post" "$NodeB/api/wormhole/dm/invite/import" @{
invite = $inviteA.invite
alias = "node-a"
} -Admin
Assert-Ok $inviteImportAIntoB "node-b import node-a signed invite"
$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$messageAB = "dm-two-node-a-to-b-$timestamp"
$messageBA = "dm-two-node-b-to-a-$timestamp"
# Keep the actual round-trip below a clean first session so the receiver gets a
# fresh welcome every time.
Invoke-Json "Post" "$NodeA/api/wormhole/dm/reset" @{ peer_id = $nodeBState.node_id } -Admin | Out-Null
Invoke-Json "Post" "$NodeB/api/wormhole/dm/reset" @{ peer_id = $nodeAState.node_id } -Admin | Out-Null
$composeAB = Try-Compose $nodeAState $nodeBState $messageAB
Assert-Ok $composeAB "node-a compose to node-b"
$decryptAB = Decrypt-OnReceiver $nodeBState $nodeAState $composeAB
Assert-Ok $decryptAB "node-b decrypt from node-a"
if ([string]$decryptAB.plaintext -ne $messageAB) {
throw "node-b decrypted unexpected plaintext"
}
$composeBA = Try-Compose $nodeBState $nodeAState $messageBA
Assert-Ok $composeBA "node-b compose to node-a"
$decryptBA = Decrypt-OnReceiver $nodeAState $nodeBState $composeBA
Assert-Ok $decryptBA "node-a decrypt from node-b"
if ([string]$decryptBA.plaintext -ne $messageBA) {
throw "node-a decrypted unexpected plaintext"
}
$plaintextHits = @()
$plaintextHits += Search-PlaintextInNodeData $messageAB
$plaintextHits += Search-PlaintextInNodeData $messageBA
$report = [pscustomobject]@{
ok = $true
checked_at = $timestamp
nodes = @{
node_a = @{
url = $NodeA
id = $nodeAState.node_id
health_status = $healthA.status
}
node_b = @{
url = $NodeB
id = $nodeBState.node_id
health_status = $healthB.status
}
}
first_contact = @{
node_a_to_node_b = @{
local = "node-a"
remote = "node-b"
trust_level = [string]$inviteImportBIntoA.trust_level
invite_attested = [bool]$inviteImportBIntoA.invite_attested
}
node_b_to_node_a = @{
local = "node-b"
remote = "node-a"
trust_level = [string]$inviteImportAIntoB.trust_level
invite_attested = [bool]$inviteImportAIntoB.invite_attested
}
invite_export_ok = ([bool]$inviteA.ok -and [bool]$inviteB.ok)
invite_import_node_b_into_node_a = @{
ok = [bool]$inviteImportBIntoA.ok
detail = [string]$inviteImportBIntoA.detail
}
invite_import_node_a_into_node_b = @{
ok = [bool]$inviteImportAIntoB.ok
detail = [string]$inviteImportAIntoB.detail
}
}
message_round_trip = @{
node_a_to_node_b = @{
compose_ok = [bool]$composeAB.ok
decrypt_ok = [bool]$decryptAB.ok
format = [string]$composeAB.format
has_session_welcome = [bool]$composeAB.session_welcome
ciphertext_contains_plaintext = ([string]$composeAB.ciphertext).Contains($messageAB)
}
node_b_to_node_a = @{
compose_ok = [bool]$composeBA.ok
decrypt_ok = [bool]$decryptBA.ok
format = [string]$composeBA.format
has_session_welcome = [bool]$composeBA.session_welcome
ciphertext_contains_plaintext = ([string]$composeBA.ciphertext).Contains($messageBA)
}
}
privacy_storage_check = @{
plaintext_found_in_node_data = ($plaintextHits.Count -gt 0)
hits = @($plaintextHits)
}
limits = @(
"This proves two separate localhost backend processes can perform MLS DM compose/decrypt both ways.",
"It proves signed invite import can resolve invite-scoped prekeys over the peer-authenticated local test lane.",
"It does not prove RNS/Tor/relay delivery because this local runtime intentionally disables those transports."
)
}
$report | ConvertTo-Json -Depth 100 | Set-Content -Path $ReportPath -Encoding UTF8
Write-Host ""
Write-Host "DM two-node selftest passed."
Write-Host "A -> B: $($composeAB.format), decrypted by node-b."
Write-Host "B -> A: $($composeBA.format), decrypted by node-a."
Write-Host "Plaintext in node data: $($report.privacy_storage_check.plaintext_found_in_node_data)"
Write-Host "Invite import A<-B: $($report.first_contact.invite_import_node_b_into_node_a.ok) $($report.first_contact.invite_import_node_b_into_node_a.detail)"
Write-Host "Invite import B<-A: $($report.first_contact.invite_import_node_a_into_node_b.ok) $($report.first_contact.invite_import_node_a_into_node_b.detail)"
Write-Host "Report: $ReportPath"
+191
View File
@@ -0,0 +1,191 @@
param(
[int]$NodeAPort = 8001,
[int]$NodeBPort = 8002,
[switch]$NoSync
)
$ErrorActionPreference = "Stop"
$Root = Resolve-Path (Join-Path $PSScriptRoot "..")
$SourceBackend = Join-Path $Root "backend"
$RuntimeRoot = Join-Path $Root ".runtime\dm-two-node"
$PidFile = Join-Path $RuntimeRoot "pids.json"
function Resolve-SharedPython {
$marker = Join-Path $SourceBackend ".venv-dir"
$candidates = @()
if (Test-Path $marker) {
$raw = (Get-Content $marker -ErrorAction SilentlyContinue | Select-Object -First 1).Trim()
if ($raw) {
$venvDir = if ([System.IO.Path]::IsPathRooted($raw)) { $raw } else { Join-Path $SourceBackend $raw }
$candidates += Join-Path $venvDir "Scripts\python.exe"
}
}
$candidates += @(
(Join-Path $SourceBackend "venv\Scripts\python.exe"),
(Join-Path $SourceBackend "venv-repair\Scripts\python.exe")
)
$candidates += Get-ChildItem -Path $SourceBackend -Directory -Filter "venv-repair*" -ErrorAction SilentlyContinue |
ForEach-Object { Join-Path $_.FullName "Scripts\python.exe" }
foreach ($candidate in $candidates) {
if ($candidate -and (Test-Path $candidate)) {
return (Resolve-Path $candidate).Path
}
}
throw "Could not find an existing backend Python venv. Start the normal backend once first, then rerun this script."
}
function Stop-PortIfListening([int]$Port) {
$listeners = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue
foreach ($listener in $listeners) {
if ($listener.OwningProcess) {
Stop-Process -Id $listener.OwningProcess -Force -ErrorAction SilentlyContinue
}
}
}
function Sync-RuntimeBackend([string]$NodeName) {
$nodeRoot = Join-Path $RuntimeRoot $NodeName
$destBackend = Join-Path $nodeRoot "backend"
New-Item -ItemType Directory -Force -Path $nodeRoot | Out-Null
if (-not $NoSync) {
New-Item -ItemType Directory -Force -Path $destBackend | Out-Null
$excludeDirs = @(
"data",
"node_modules",
"venv",
".venv",
"venv-repair",
"venv-repair-*",
".venv-repair",
".pytest_cache",
".ruff_cache",
"__pycache__",
"build",
"backend.egg-info",
"tests",
"timemachine",
"sb-custody-verify-*"
)
$excludeFiles = @(".env", "*.pyc", "*.pyo", "*.log", "test_*.py")
$args = @(
$SourceBackend,
$destBackend,
"/MIR",
"/R:1",
"/W:1",
"/NFL",
"/NDL",
"/NJH",
"/NJS",
"/NP",
"/XD"
) + $excludeDirs + @("/XF") + $excludeFiles
& robocopy @args | Out-Null
if ($LASTEXITCODE -gt 7) {
throw "robocopy failed for $NodeName with exit code $LASTEXITCODE"
}
foreach ($runtimeOnlyDir in @("tests", "timemachine", ".pytest_cache", ".ruff_cache", "__pycache__")) {
$path = Join-Path $destBackend $runtimeOnlyDir
if (Test-Path $path) {
Remove-Item -LiteralPath $path -Recurse -Force -ErrorAction SilentlyContinue
}
}
Get-ChildItem -Path $destBackend -Filter "test_*.py" -File -ErrorAction SilentlyContinue |
Remove-Item -Force -ErrorAction SilentlyContinue
}
$dataDir = Join-Path $destBackend "data"
New-Item -ItemType Directory -Force -Path $dataDir | Out-Null
'{"enabled":true,"updated_at":0}' | Set-Content -Path (Join-Path $dataDir "node.json") -Encoding ASCII
return $destBackend
}
function Write-NodeRunner(
[string]$NodeName,
[string]$BackendDir,
[string]$Python,
[int]$Port,
[int]$PeerPort
) {
$nodeRoot = Split-Path $BackendDir -Parent
$logPath = Join-Path $nodeRoot "backend-$Port.log"
$runner = Join-Path $nodeRoot "run-$Port.cmd"
$peer = "http://127.0.0.1:$PeerPort"
$privacyCore = Join-Path $Root "privacy-core\target\release\privacy_core.dll"
if (-not (Test-Path $privacyCore)) {
$privacyCore = Join-Path $Root "privacy-core\debug\privacy_core.dll"
}
if (-not (Test-Path $privacyCore)) {
throw "Could not find privacy-core DLL under privacy-core\target\release or privacy-core\debug."
}
$content = @"
@echo off
set SB_TEST_NODE_NAME=$NodeName
set SB_TEST_NODE_URL=http://127.0.0.1:$Port
set ADMIN_KEY=dm-test-node-local-admin-key-00000001
set MESH_SELF_PEER_URL=http://127.0.0.1:$Port
set MESH_PEER_PUSH_SECRET=dm-test-two-node-peer-push-secret-00000001
set MESH_ONLY=true
set MESH_NODE_MODE=participant
set MESH_BOOTSTRAP_DISABLED=true
set MESH_MQTT_ENABLED=false
set MESH_RNS_ENABLED=false
set MESH_ARTI_ENABLED=false
set MESH_DM_SECURE_MODE=true
set MESH_PRIVATE_RELEASE_APPROVAL_ENABLE=true
set MESH_DM_RELAY_AUTO_RELOAD=true
set MESH_RELAY_PEERS=$peer
set PRIVACY_CORE_LIB=$privacyCore
set PYTHONPATH=$BackendDir
cd /d "$BackendDir"
"$Python" -m uvicorn main:app --host 127.0.0.1 --port $Port --timeout-keep-alive 120
"@
$content | Set-Content -Path $runner -Encoding ASCII
return @{ Runner = $runner; Log = $logPath }
}
function Start-TestNode([string]$NodeName, [int]$Port, [int]$PeerPort, [string]$Python) {
Stop-PortIfListening $Port
$backendDir = Sync-RuntimeBackend $NodeName
$runnerInfo = Write-NodeRunner $NodeName $backendDir $Python $Port $PeerPort
$cmd = "/c `"`"$($runnerInfo.Runner)`" > `"$($runnerInfo.Log)`" 2>&1`""
$process = Start-Process -FilePath "cmd.exe" -ArgumentList $cmd -PassThru -WindowStyle Minimized
return @{
node = $NodeName
port = $Port
pid = $process.Id
backend = $backendDir
data = Join-Path $backendDir "data"
log = $runnerInfo.Log
peer = "http://127.0.0.1:$PeerPort"
}
}
New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null
$python = Resolve-SharedPython
$nodes = @(
Start-TestNode "node-a" $NodeAPort $NodeBPort $python
Start-TestNode "node-b" $NodeBPort $NodeAPort $python
)
$payload = @{
started_at = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
shared_python = $python
nodes = $nodes
}
$payload | ConvertTo-Json -Depth 5 | Set-Content -Path $PidFile -Encoding UTF8
Write-Host ""
Write-Host "DM two-node test runtime started without copying dependencies."
Write-Host "Shared Python: $python"
foreach ($node in $nodes) {
Write-Host "$($node.node): http://127.0.0.1:$($node.port)"
Write-Host " data: $($node.data)"
Write-Host " log: $($node.log)"
}
Write-Host ""
Write-Host "Stop with: powershell -ExecutionPolicy Bypass -File scripts\stop-dm-test-nodes.ps1"
+37
View File
@@ -0,0 +1,37 @@
param(
[int[]]$Ports = @(8001, 8002)
)
$ErrorActionPreference = "Continue"
$Root = Resolve-Path (Join-Path $PSScriptRoot "..")
$RuntimeRoot = Join-Path $Root ".runtime\dm-two-node"
$PidFile = Join-Path $RuntimeRoot "pids.json"
if (Test-Path $PidFile) {
try {
$payload = Get-Content $PidFile -Raw | ConvertFrom-Json
foreach ($node in @($payload.nodes)) {
if ($node.pid) {
Stop-Process -Id ([int]$node.pid) -Force -ErrorAction SilentlyContinue
}
if ($node.port) {
$Ports += [int]$node.port
}
}
} catch {
Write-Host "Could not parse $PidFile; falling back to port cleanup."
}
}
foreach ($port in ($Ports | Select-Object -Unique)) {
$listeners = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
foreach ($listener in $listeners) {
if ($listener.OwningProcess) {
Write-Host "Stopping PID $($listener.OwningProcess) on port $port"
Stop-Process -Id $listener.OwningProcess -Force -ErrorAction SilentlyContinue
}
}
}
Write-Host "DM test nodes stopped."