Harden infonet control surfaces

This commit is contained in:
BigBodyCobain
2026-05-18 11:22:38 -06:00
parent 25a98a9869
commit 11ea345518
30 changed files with 1810 additions and 276 deletions
@@ -10,7 +10,7 @@ const mocks = vi.hoisted(() => ({
buildMailboxClaims: vi.fn(async () => []),
countDmMailboxes: vi.fn(async () => ({ ok: true, count: 0 })),
ensureRegisteredDmKey: vi.fn(async () => ({ dhPubKey: 'local-dh', dhAlgo: 'X25519' })),
fetchDmPublicKey: vi.fn(async () => ({ dh_pub_key: 'peer-dh', dh_algo: 'X25519' })),
fetchDmPublicKey: vi.fn(async () => ({ agent_id: '!sb_peer', dh_pub_key: 'peer-dh', dh_algo: 'X25519' })),
pollDmMailboxes: vi.fn(async () => ({ ok: true, messages: [] })),
sendDmMessage: vi.fn(async () => ({ ok: true, transport: 'relay' })),
sendOffLedgerConsentMessage: vi.fn(async () => ({ ok: true, transport: 'relay' })),
@@ -252,7 +252,7 @@ describe('MessagesView first-contact trust UX', () => {
mocks.pollDmMailboxes.mockResolvedValue({ ok: true, messages: [] });
mocks.countDmMailboxes.mockResolvedValue({ ok: true, count: 0 });
mocks.ensureRegisteredDmKey.mockResolvedValue({ dhPubKey: 'local-dh', dhAlgo: 'X25519' });
mocks.fetchDmPublicKey.mockResolvedValue({ dh_pub_key: 'peer-dh', dh_algo: 'X25519' });
mocks.fetchDmPublicKey.mockResolvedValue({ agent_id: '!sb_peer', dh_pub_key: 'peer-dh', dh_algo: 'X25519' });
mocks.sendOffLedgerConsentMessage.mockResolvedValue({ ok: true, transport: 'relay' });
mocks.canUseWormholeBootstrap.mockResolvedValue(false);
mocks.exportWormholeDmInvite.mockResolvedValue({
@@ -334,8 +334,9 @@ describe('MessagesView first-contact trust UX', () => {
await openComposeForRecipient('!sb_invited', 'hello to pinned peer');
expect(screen.queryByText('Unverified First Contact')).not.toBeInTheDocument();
expect(await screen.findByText('ROOT LOCAL QUORUM')).toBeInTheDocument();
expect(await screen.findByText(/Local quorum root rootabcd\.\.123456/i)).toBeInTheDocument();
expect(screen.queryByText('ROOT LOCAL QUORUM')).not.toBeInTheDocument();
expect(screen.queryByText(/Local quorum root rootabcd\.\.123456/i)).not.toBeInTheDocument();
expect(screen.queryByText(/Fingerprint/i)).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Send Secure Mail' })).toBeEnabled();
});
@@ -375,7 +376,34 @@ describe('MessagesView first-contact trust UX', () => {
expect(screen.queryByText(/still warming up/i)).not.toBeInTheDocument();
}, 10000);
it('does not flatten witness policy not met into a generic witnessed root label', async () => {
it('repairs the local sending key before sending instead of surfacing backend key jargon', async () => {
contactsState = {
'!sb_pinned': {
alias: 'Pinned Peer',
blocked: false,
trust_level: 'invite_pinned',
dhPubKey: 'peer-dh',
remotePrekeyFingerprint: 'abcdef123456',
},
};
mocks.ensureRegisteredDmKey
.mockResolvedValueOnce({ ok: true, dhPubKey: '', dhAlgo: 'X25519', detail: 'Missing DH public key' })
.mockResolvedValueOnce({ ok: true, dhPubKey: 'local-dh-repaired', dhAlgo: 'X25519' });
mocks.sendDmMessage.mockResolvedValueOnce({ ok: true, transport: 'relay' });
renderMessagesView();
await openComposeForRecipient('!sb_pinned', 'hello after repair');
fireEvent.click(screen.getByRole('button', { name: 'Send Secure Mail' }));
await waitFor(() => expect(mocks.ensureRegisteredDmKey).toHaveBeenCalledTimes(2));
await waitFor(() => expect(mocks.sendDmMessage).toHaveBeenCalled());
expect(await screen.findByText(/Mail delivered to Pinned Peer/i)).toBeInTheDocument();
expect(screen.queryByText(/Local DM key is unavailable/i)).not.toBeInTheDocument();
expect(screen.queryByText(/Missing DH public key/i)).not.toBeInTheDocument();
});
it('shows saved contacts without witness-policy implementation detail', async () => {
contactsState = {
'!sb_policy': {
alias: 'Policy Peer',
@@ -404,10 +432,39 @@ describe('MessagesView first-contact trust UX', () => {
renderMessagesView();
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText(/Witness-policy root rootpoli\.\.123456/i)).toBeInTheDocument();
expect(await screen.findByText('Saved Contact')).toBeInTheDocument();
expect(screen.queryByText(/Witness-policy root rootpoli\.\.123456/i)).not.toBeInTheDocument();
expect(screen.queryByText(/Witnessed root rootpoli\.\.123456/i)).not.toBeInTheDocument();
});
it('hydrates Wormhole contacts on first load even when a local browser identity exists', async () => {
let wormholeIdentityResolved = false;
contactsState = {
'!sb_saved': {
alias: 'Saved Person',
blocked: false,
trust_level: 'invite_pinned',
invitePinnedPrekeyLookupHandle: 'handle-saved',
invitePinnedTrustFingerprint: 'savedfingerprint123456',
},
};
mocks.isWormholeSecureRequired.mockResolvedValue(true);
mocks.fetchWormholeIdentity.mockImplementation(async () => {
wormholeIdentityResolved = true;
return { node_id: '!sb_local', public_key: 'local-pub' };
});
mocks.hydrateWormholeContacts.mockImplementation(async () =>
wormholeIdentityResolved ? contactsState : {},
);
renderMessagesView();
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText('Saved Person')).toBeInTheDocument();
expect(screen.queryByText(/No approved secure contacts yet/i)).not.toBeInTheDocument();
expect(mocks.fetchWormholeIdentity).toHaveBeenCalled();
});
it('shows an import-invite shortcut for unpinned contacts in the contact list', async () => {
contactsState = {
'!sb_unpinned': {
@@ -426,7 +483,7 @@ describe('MessagesView first-contact trust UX', () => {
expect(screen.getByLabelText(/Local Alias/i)).toHaveValue('!sb_unpinned');
});
it('surfaces pending contact requests in the contact list with approve and deny actions', async () => {
it('surfaces pending contact requests in a top-level requests tab with approve and deny actions', async () => {
localStorage.setItem(
'sb_infonet_mailbox_v1:!sb_local',
JSON.stringify({
@@ -464,7 +521,7 @@ describe('MessagesView first-contact trust UX', () => {
});
renderMessagesView();
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
fireEvent.click(await screen.findByRole('button', { name: /REQUESTS/i }));
expect(await screen.findByText('Contact Requests')).toBeInTheDocument();
expect(await screen.findByText('1 pending')).toBeInTheDocument();
@@ -576,13 +633,13 @@ describe('MessagesView first-contact trust UX', () => {
expect(
await screen.findByText(
/Import or re-import a signed invite before sending a contact request; legacy direct lookup is disabled\./i,
/This contact needs their full contact address once before messages can be sent/i,
),
).toBeInTheDocument();
expect(mocks.fetchDmPublicKey).not.toHaveBeenCalled();
});
it('announces attested invite imports as INVITE PINNED', async () => {
it('announces attested invite imports as a saved contact', async () => {
mocks.importWormholeDmInvite.mockResolvedValueOnce({
ok: true,
peer_id: '!sb_attested',
@@ -595,32 +652,34 @@ describe('MessagesView first-contact trust UX', () => {
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText("Paste Someone's Address")).toBeInTheDocument();
fireEvent.change(screen.getByPlaceholderText(/Paste the full text copied/i), {
fireEvent.change(screen.getByPlaceholderText(/Paste a short address/i), {
target: { value: JSON.stringify({ invite: { event_type: 'dm_invite', payload: {} } }) },
});
fireEvent.click(screen.getByRole('button', { name: 'Import Address' }));
expect(
await screen.findByText(/INVITE PINNED for !sb_attested \(invitefp\.\.tested\)\./i),
).toBeInTheDocument();
expect(await screen.findByText(/Contact saved: !sb_attested\./i)).toBeInTheDocument();
expect(await screen.findByText('Saved Contact')).toBeInTheDocument();
expect(screen.queryByText(/INVITE PINNED for/i)).not.toBeInTheDocument();
});
it('generates and copies the full signed public address instead of the lookup handle', async () => {
it('automatically creates a share address and keeps copy actions simple', async () => {
renderMessagesView();
fireEvent.click(await screen.findByRole('button', { name: 'Generate Address' }));
await waitFor(() => expect(mocks.writeClipboard).toHaveBeenCalled());
const copied = String(mocks.writeClipboard.mock.calls[0][0] || '');
expect(copied).toContain('"type": "shadowbroker.infonet.dm.invite"');
expect(copied).toContain('"prekey_lookup_handle": "handle-123"');
expect(copied).not.toBe('handle-123');
expect(await screen.findByText(/Generated and copied/i)).toBeInTheDocument();
expect(await screen.findByText(/Contact address ready/i)).toBeInTheDocument();
expect(await screen.findByText('handle-123')).toBeInTheDocument();
expect(screen.getByText(/Signed invite ready/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Copy Short Address/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Copy Full Address/i })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /Copy Short Address/i }));
await waitFor(() => expect(mocks.writeClipboard).toHaveBeenCalledWith('handle-123'));
const copied = String(mocks.writeClipboard.mock.calls.at(-1)?.[0] || '');
expect(copied).toBe('handle-123');
expect(screen.queryByText(/shadowbroker\.infonet\.dm\.invite/i)).not.toBeInTheDocument();
});
it('does not advertise legacy handle-only addresses as copyable public addresses', async () => {
it('does not advertise legacy handle-only addresses as copyable contact addresses', async () => {
localStorage.setItem(
'sb_infonet_dm_addresses_v1:!sb_local',
JSON.stringify({
@@ -641,25 +700,33 @@ describe('MessagesView first-contact trust UX', () => {
renderMessagesView();
expect(await screen.findByText(/Generate an address, then send it to someone/i)).toBeInTheDocument();
expect(await screen.findByText(/Contact address ready/i)).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText('Legacy handle')).toBeInTheDocument();
expect(screen.getByText('Address unavailable locally.')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Copy' })).toBeDisabled();
expect(screen.getAllByRole('button', { name: 'Copy Short' }).some((button) => !button.hasAttribute('disabled'))).toBe(true);
expect(screen.getAllByRole('button', { name: 'Copy Full' }).some((button) => button.hasAttribute('disabled'))).toBe(true);
});
it('explains raw lookup handles instead of showing a JSON parser error', async () => {
it('sends a contact request from a short address instead of requiring JSON', async () => {
renderMessagesView();
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText("Paste Someone's Address")).toBeInTheDocument();
fireEvent.change(screen.getByPlaceholderText(/Paste the full text copied/i), {
fireEvent.change(screen.getByPlaceholderText(/Paste a short address/i), {
target: { value: 'f0eee9e9ccf849bcb2d86c0d7a1e0669c75be4e05533b0f6c67' },
});
fireEvent.click(screen.getByRole('button', { name: 'Send Request' }));
expect(await screen.findByText(/only a short address ID/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Import Address' })).toBeDisabled();
await waitFor(() => expect(mocks.sendOffLedgerConsentMessage).toHaveBeenCalled());
expect(await screen.findByText(/Contact request sent to/i)).toBeInTheDocument();
expect(mocks.fetchDmPublicKey).toHaveBeenCalledWith(
'http://localhost:8000',
'',
'f0eee9e9ccf849bcb2d86c0d7a1e0669c75be4e05533b0f6c67',
);
expect(mocks.sendOffLedgerConsentMessage).toHaveBeenCalled();
expect(screen.queryByText(/Unexpected number in JSON/i)).not.toBeInTheDocument();
expect(mocks.importWormholeDmInvite).not.toHaveBeenCalled();
});
@@ -675,7 +742,7 @@ describe('MessagesView first-contact trust UX', () => {
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText("Paste Someone's Address")).toBeInTheDocument();
const addressField = screen.getByPlaceholderText(/Paste the full text copied/i);
const addressField = screen.getByPlaceholderText(/Paste a short address/i);
fireEvent.paste(addressField, {
clipboardData: {
getData: () => signedAddress,
@@ -687,7 +754,7 @@ describe('MessagesView first-contact trust UX', () => {
fireEvent.click(screen.getByRole('button', { name: 'Advanced Details' }));
expect(screen.getByLabelText('Raw copied public address')).toHaveValue(signedAddress);
expect(screen.getByLabelText('Raw copied contact address')).toHaveValue(signedAddress);
});
it('imports a copied address without waiting for secure mail warm-up', async () => {
@@ -710,17 +777,113 @@ describe('MessagesView first-contact trust UX', () => {
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText("Paste Someone's Address")).toBeInTheDocument();
fireEvent.change(screen.getByPlaceholderText(/Paste the full text copied/i), {
fireEvent.change(screen.getByPlaceholderText(/Paste a short address/i), {
target: { value: JSON.stringify({ invite: { event_type: 'dm_invite', payload: {} } }) },
});
fireEvent.click(screen.getByRole('button', { name: 'Import Address' }));
expect(await screen.findByText(/INVITE PINNED for !sb_now \(invitefp-now\)\./i)).toBeInTheDocument();
expect(await screen.findByText(/Contact saved: !sb_now\./i)).toBeInTheDocument();
expect(mocks.importWormholeDmInvite).toHaveBeenCalled();
expect(screen.queryByText(/Secure mail is still warming up/i)).not.toBeInTheDocument();
});
it('announces compat invite imports as TOFU PINNED with backend detail', async () => {
it('saves pending-delivery contacts without showing prekey jargon', async () => {
mocks.importWormholeDmInvite.mockResolvedValueOnce({
ok: true,
peer_id: '!sb_pending',
trust_fingerprint: 'invitefp-pending',
trust_level: 'invite_pinned',
pending_prekey: true,
detail: 'Contact saved.',
contact: {
alias: 'Pending Person',
blocked: false,
trust_level: 'invite_pinned',
invitePinnedPrekeyLookupHandle: 'handle-pending',
invitePinnedTrustFingerprint: 'invitefp-pending',
dhPubKey: '',
},
});
renderMessagesView();
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText("Paste Someone's Address")).toBeInTheDocument();
fireEvent.change(screen.getByPlaceholderText(/Paste a short address/i), {
target: { value: JSON.stringify({ invite: { event_type: 'dm_invite', payload: {} } }) },
});
fireEvent.click(screen.getByRole('button', { name: 'Import Address' }));
expect(await screen.findByText(/Contact saved: Pending Person\./i)).toBeInTheDocument();
expect(await screen.findByText('Saved Contact')).toBeInTheDocument();
expect(screen.queryByText(/prekey/i)).not.toBeInTheDocument();
});
it('saves mail locally when a saved contact is not reachable yet', async () => {
contactsState = {
'!sb_pending': {
alias: 'Pending Person',
blocked: false,
trust_level: 'invite_pinned',
invitePinnedPrekeyLookupHandle: 'handle-pending',
invitePinnedTrustFingerprint: 'invitefp-pending',
dhPubKey: '',
},
};
mocks.fetchDmPublicKey.mockResolvedValueOnce({ agent_id: '!sb_pending', dh_pub_key: '', dh_algo: 'X25519' });
renderMessagesView();
await openComposeForRecipient('!sb_pending', 'hello when ready');
fireEvent.click(screen.getByRole('button', { name: 'Send Secure Mail' }));
expect(await screen.findByText(/Mail is saved locally and will send automatically when the contact is reachable/i)).toBeInTheDocument();
expect(mocks.sendOffLedgerConsentMessage).not.toHaveBeenCalled();
expect(screen.queryByText(/delivery key has not reached/i)).not.toBeInTheDocument();
});
it('removes an approved contact immediately from the visible contact list', async () => {
contactsState = {
'!sb_remove': {
alias: 'Remove Me',
blocked: false,
trust_level: 'invite_pinned',
invitePinnedTrustFingerprint: 'removefingerprint123456',
dhPubKey: 'peer-dh',
},
};
mocks.removeContact.mockImplementation((peerId: string) => {
delete contactsState[peerId];
});
renderMessagesView();
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText('Remove Me')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Remove' }));
expect(await screen.findByText(/Removed contact: Remove Me\./i)).toBeInTheDocument();
expect(screen.queryByText('Remove Me')).not.toBeInTheDocument();
});
it('explains unresolved address delivery without exposing backend jargon', async () => {
mocks.importWormholeDmInvite.mockRejectedValueOnce(new Error('peer prekey lookup unavailable'));
renderMessagesView();
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText("Paste Someone's Address")).toBeInTheDocument();
fireEvent.change(screen.getByPlaceholderText(/Paste a short address/i), {
target: { value: JSON.stringify({ invite: { event_type: 'dm_invite', payload: {} } }) },
});
fireEvent.click(screen.getByRole('button', { name: 'Import Address' }));
expect(await screen.findByText(/This address is valid, but contact delivery is not ready on this node yet/i)).toBeInTheDocument();
expect(screen.queryByText('peer prekey lookup unavailable')).not.toBeInTheDocument();
expect(screen.queryByText(/sender prekey/i)).not.toBeInTheDocument();
});
it('announces compat invite imports as a saved contact without backend detail', async () => {
mocks.importWormholeDmInvite.mockResolvedValueOnce({
ok: true,
peer_id: '!sb_compat',
@@ -734,17 +897,16 @@ describe('MessagesView first-contact trust UX', () => {
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText("Paste Someone's Address")).toBeInTheDocument();
fireEvent.change(screen.getByPlaceholderText(/Paste the full text copied/i), {
fireEvent.change(screen.getByPlaceholderText(/Paste a short address/i), {
target: { value: JSON.stringify({ invite: { event_type: 'dm_invite', payload: {} } }) },
});
fireEvent.click(screen.getByRole('button', { name: 'Import Address' }));
expect(await screen.findByText(/Contact saved: !sb_compat\./i)).toBeInTheDocument();
expect(screen.queryByText(/TOFU PINNED for/i)).not.toBeInTheDocument();
expect(
await screen.findByText(/TOFU PINNED for !sb_compat \(invitefp\.\.compat\)\./i),
).toBeInTheDocument();
expect(
screen.getByText(/legacy invite imported as tofu_pinned; SAS verification required before first contact/i),
).toBeInTheDocument();
screen.queryByText(/legacy invite imported as tofu_pinned; SAS verification required before first contact/i),
).not.toBeInTheDocument();
});
it('surfaces stable root continuity breaks on invite re-import', async () => {
@@ -783,7 +945,7 @@ describe('MessagesView first-contact trust UX', () => {
fireEvent.click(screen.getByRole('button', { name: 'CONTACTS' }));
expect(await screen.findByText("Paste Someone's Address")).toBeInTheDocument();
fireEvent.change(screen.getByPlaceholderText(/Paste the full text copied/i), {
fireEvent.change(screen.getByPlaceholderText(/Paste a short address/i), {
target: { value: JSON.stringify({ invite: { event_type: 'dm_invite', payload: {} } }) },
});
fireEvent.click(screen.getByRole('button', { name: 'Import Address' }));
@@ -1357,7 +1357,9 @@ describe('wormholeIdentityClient strict profile hints', () => {
}),
);
expect(controlPlaneJson).toHaveBeenCalledWith('/api/wormhole/dm/root-health');
expect(controlPlaneJson).toHaveBeenCalledWith('/api/wormhole/dm/root-health', {
requireAdminSession: false,
});
});
it('prepares the interactive lane through the configured wormhole runtime and bootstraps identity state', async () => {
@@ -7,6 +7,7 @@
* - /api/tools/* (Sprint 1C addition)
* - /api/wormhole/* (pre-existing, regression)
* - /api/settings/* (pre-existing, regression)
* - /api/layers, /api/ais/feed, /api/ai/agent-actions
*
* Also verifies that:
* - non-sensitive mesh paths (e.g. mesh/events) do NOT receive injected key
@@ -272,6 +273,77 @@ describe('proxy admin-key injection coverage', () => {
expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBe(ADMIN_KEY);
});
it('POST /api/layers with valid session injects X-Admin-Key', async () => {
const cookie = await mintSession(ADMIN_KEY);
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ status: 'ok' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const req = new NextRequest('http://localhost/api/layers', {
method: 'POST',
body: JSON.stringify({ layers: { aircraft: true } }),
headers: { cookie, 'Content-Type': 'application/json' },
});
const res = await proxyPost(req, {
params: Promise.resolve({ path: ['layers'] }),
});
expect(res.status).toBe(200);
expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBe(ADMIN_KEY);
});
it('POST /api/ais/feed with valid session injects X-Admin-Key', async () => {
const cookie = await mintSession(ADMIN_KEY);
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ status: 'ok' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const req = new NextRequest('http://localhost/api/ais/feed', {
method: 'POST',
body: JSON.stringify({ msgs: [] }),
headers: { cookie, 'Content-Type': 'application/json' },
});
const res = await proxyPost(req, {
params: Promise.resolve({ path: ['ais', 'feed'] }),
});
expect(res.status).toBe(200);
expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBe(ADMIN_KEY);
});
it('GET /api/ai/agent-actions with valid session injects X-Admin-Key', async () => {
const cookie = await mintSession(ADMIN_KEY);
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ ok: true, actions: [] }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const req = new NextRequest('http://localhost/api/ai/agent-actions', {
method: 'GET',
headers: { cookie },
});
const res = await proxyGet(req, {
params: Promise.resolve({ path: ['ai', 'agent-actions'] }),
});
expect(res.status).toBe(200);
expect(capturedHeaders(fetchMock).get('X-Admin-Key')).toBe(ADMIN_KEY);
});
// -------------------------------------------------------------------------
// Non-sensitive mesh paths must NOT receive injected admin key
// -------------------------------------------------------------------------
+3
View File
@@ -64,6 +64,9 @@ function isSensitiveProxyPath(pathSegments: string[]): boolean {
if (joined === 'refresh') return true;
if (joined === 'debug-latest') return true;
if (joined === 'system/update') return true;
if (joined === 'layers') return true;
if (joined === 'ais/feed') return true;
if (joined === 'ai/agent-actions') return true;
if (pathSegments[0] === 'settings') return true;
if (joined === 'mesh/infonet/ingest') return true;
if (joined === 'mesh/meshtastic/send') return true;
+2 -2
View File
@@ -203,8 +203,8 @@ export default function Dashboard() {
uap_sightings: true,
// Biosurveillance
wastewater: true,
// CrowdThreat
crowdthreat: true,
// CrowdThreat is operator opt-in only.
crowdthreat: false,
// Shodan
shodan_overlay: false,
// AI Intel
File diff suppressed because it is too large Load Diff
@@ -7,16 +7,17 @@ import { fetchInfonetNodeStatusSnapshot } from '@/mesh/controlPlaneStatusClient'
interface Stats {
meshtastic: number;
aprs: number;
infonetNodes: number;
ledgerNodes: number;
infonetEvents: number;
syncPeers: number;
seedPeers: number;
nodeEnabled: boolean;
syncOutcome: string;
}
const EMPTY: Stats = {
meshtastic: 0, aprs: 0, infonetNodes: 0, infonetEvents: 0,
syncPeers: 0, nodeEnabled: false, syncOutcome: 'offline',
meshtastic: 0, aprs: 0, ledgerNodes: 0, infonetEvents: 0,
syncPeers: 0, seedPeers: 0, nodeEnabled: false, syncOutcome: 'offline',
};
export default function NetworkStats() {
@@ -32,22 +33,21 @@ export default function NetworkStats() {
fetchInfonetNodeStatusSnapshot(true).catch(() => null),
]);
if (!alive) return;
const knownNodes = Number(infonet?.known_nodes || 0);
const authorNodes = Number(infonet?.author_nodes ?? infonet?.known_nodes ?? 0);
const registeredNodes = Number(infonet?.registered_nodes || 0);
const syncPeerCount = Number(infonet?.bootstrap?.sync_peer_count || 0);
const defaultSyncPeerCount = Number(infonet?.bootstrap?.default_sync_peer_count || 0);
const lastPeerUrl = String(infonet?.sync_runtime?.last_peer_url || '').trim();
const visibleInfonetNodes = Math.max(
knownNodes,
syncPeerCount,
defaultSyncPeerCount,
lastPeerUrl ? 1 : 0,
const seedPeerCount = Number(
infonet?.bootstrap?.bootstrap_seed_peer_count
?? infonet?.bootstrap?.default_sync_peer_count
?? 0,
);
setStats({
meshtastic: Number(channelsRes?.total_live || channelsRes?.total_nodes || meshRes?.signal_counts?.meshtastic || 0),
aprs: Number(meshRes?.signal_counts?.aprs || 0),
infonetNodes: visibleInfonetNodes,
ledgerNodes: Math.max(authorNodes, registeredNodes),
infonetEvents: Number(infonet?.total_events || 0),
syncPeers: syncPeerCount,
seedPeers: seedPeerCount,
nodeEnabled: Boolean(infonet?.node_enabled),
syncOutcome: String(infonet?.sync_runtime?.last_outcome || 'offline').toLowerCase(),
});
@@ -74,11 +74,21 @@ export default function NetworkStats() {
<span className="text-gray-700">|</span>
<span>APRS <span className={stats.aprs > 0 ? 'text-green-400' : 'text-gray-600'}>{stats.aprs.toLocaleString()}</span></span>
<span className="text-gray-700">|</span>
<span>INFONET NODES <span className="text-white">{stats.infonetNodes}</span></span>
<span title="Distinct identities this node has seen on the accepted Infonet ledger. This is not a live user count.">
LEDGER NODES <span className="text-white">{stats.ledgerNodes}</span>
</span>
<span className="text-gray-700">|</span>
<span>EVENTS <span className="text-white">{stats.infonetEvents}</span></span>
<span className="text-gray-700">|</span>
<span>PEERS <span className="text-white">{stats.syncPeers}</span></span>
<span title="Configured peers this node pulls from. Usually this is just the seed unless another device is added as a sync peer.">
SYNC PEERS <span className="text-white">{stats.syncPeers}</span>
</span>
{stats.seedPeers > stats.syncPeers ? (
<>
<span className="text-gray-700">|</span>
<span title="Bootstrap seed peers available from config or manifest.">SEEDS <span className="text-white">{stats.seedPeers}</span></span>
</>
) : null}
</div>
);
}
@@ -305,6 +305,42 @@ function createPublicMeshAddress(): string {
return `!${fallback.toString(16).padStart(8, '0')}`;
}
function errorMessage(err: unknown, fallback: string = 'unknown error'): string {
if (err instanceof Error && err.message) return err.message;
if (typeof err === 'string' && err.trim()) return err.trim();
if (typeof err === 'object' && err !== null && 'message' in err) {
const message = String((err as { message?: unknown }).message || '').trim();
if (message) return message;
}
return fallback;
}
function describeMeshChatControlError(raw: string): string {
const message = String(raw || '').trim();
if (!message) return 'MeshChat could not update the local control plane.';
if (
message === 'control_plane_request_failed:530' ||
message === 'HTTP 530' ||
message.includes('control_plane_request_failed:530')
) {
return 'The local control plane did not complete the lane switch. Check that the backend is running and reachable, then try Mesh again.';
}
if (
message === 'control_plane_request_failed:502' ||
message === 'HTTP 502' ||
/Backend unavailable/i.test(message)
) {
return 'The frontend cannot reach the backend right now. Start or restart the backend, then try Mesh again.';
}
if (message === 'admin_session_required' || /local operator access only/i.test(message)) {
return 'This control action needs a local operator session. Open Settings or Node controls once so the app can authorize local changes, then try Mesh again.';
}
if (message.startsWith('{') || message.startsWith('<')) {
return 'MeshChat could not update the local control plane. Check the backend log for the upstream error.';
}
return message;
}
function describeGateCompatConsentRequired(): string {
return 'Local gate runtime is unavailable for this room.';
}
@@ -507,8 +543,13 @@ export function useMeshChatController({
body: JSON.stringify(body),
});
if (!res.ok) {
const detail = await res.text().catch(() => '');
throw new Error(detail || `HTTP ${res.status}`);
const data = await res.clone().json().catch(() => null) as
| { detail?: unknown; message?: unknown; error?: unknown }
| null;
const detail =
String(data?.detail || data?.message || data?.error || '').trim() ||
(await res.text().catch(() => '')).trim();
throw new Error(describeMeshChatControlError(detail || `HTTP ${res.status}`));
}
const data = (await res.json()) as MeshMqttSettings;
applyMeshMqttSettings(data);
@@ -528,7 +569,7 @@ export function useMeshChatController({
setMeshMqttStatusText(status);
return { ok: true as const, text: status, data };
} catch (err) {
const text = err instanceof Error ? err.message : 'MQTT settings update failed';
const text = describeMeshChatControlError(errorMessage(err, 'MQTT settings update failed'));
setMeshMqttStatusText(text);
return { ok: false as const, text };
} finally {
@@ -4222,7 +4263,14 @@ export function useMeshChatController({
);
const disablePrivateNodeForPublicMesh = useCallback(async () => {
await setInfonetNodeEnabled(false);
try {
await setInfonetNodeEnabled(false);
} catch (err) {
console.warn(
'[mesh] private node pre-disable failed before public Mesh activation; MQTT enable will retry lane isolation',
err,
);
}
}, []);
const disableWormholeForPublicMesh = useCallback(async () => {
@@ -4287,10 +4335,7 @@ export function useMeshChatController({
}
return { ok: true as const, text: successText };
} catch (err) {
const message =
typeof err === 'object' && err !== null && 'message' in err
? String((err as { message?: string }).message)
: 'unknown error';
const message = describeMeshChatControlError(errorMessage(err));
const errorText =
message === 'browser_identity_blocked_secure_mode'
? 'Mesh key creation is blocked while Wormhole secure mode is active. Turn Wormhole off first if you want a separate public mesh key.'
@@ -4345,10 +4390,7 @@ export function useMeshChatController({
setMeshQuickStatus(null);
return { ok: true as const, text };
} catch (err) {
const message =
typeof err === 'object' && err !== null && 'message' in err
? String((err as { message?: string }).message)
: 'unknown error';
const message = describeMeshChatControlError(errorMessage(err));
const text = `Could not turn MeshChat on: ${message}`;
setIdentityWizardStatus({ type: 'err', text });
setMeshQuickStatus({ type: 'err', text });
+5 -1
View File
@@ -49,8 +49,12 @@ export async function controlPlaneJson<T>(
const fallback =
res.status === 429
? 'control_plane_rate_limited'
: res.status === 530
? 'local_control_plane_unavailable'
: res.status === 502
? 'backend_unavailable'
: `control_plane_request_failed:${res.status || 'unknown'}`;
throw new Error(data?.detail || data?.message || fallback);
throw new Error(data?.detail || data?.message || data?.error || fallback);
}
return data as T;
}
@@ -58,6 +58,8 @@ export interface InfonetNodeStatusSnapshot {
total_events?: number;
active_events?: number;
known_nodes?: number;
author_nodes?: number;
registered_nodes?: number;
chain_size_kb?: number;
head_hash?: string;
unsigned_events?: number;
+1
View File
@@ -66,6 +66,7 @@ function callWorker(payload: Omit<WorkerRequest, 'id'> & Record<string, unknown>
async function callWormhole(path: string, body: Record<string, unknown>): Promise<string> {
const data = await controlPlaneJson<{ result?: string }>(path, {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
+13 -8
View File
@@ -1554,6 +1554,7 @@ function normalizeContactMap(input: Record<string, Contact> | Record<string, unk
async function persistContactToWormhole(peerId: string, contact: Contact): Promise<void> {
await controlPlaneJson('/api/wormhole/dm/contact', {
method: 'PUT',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
peer_id: peerId,
@@ -1565,6 +1566,7 @@ async function persistContactToWormhole(peerId: string, contact: Contact): Promi
async function deleteContactFromWormhole(peerId: string): Promise<void> {
await controlPlaneJson(`/api/wormhole/dm/contact/${encodeURIComponent(peerId)}`, {
method: 'DELETE',
requireAdminSession: false,
});
}
@@ -1601,17 +1603,20 @@ export async function hydrateWormholeContacts(force: boolean = false): Promise<R
if (!force && contactsHydration) {
return contactsHydration;
}
contactsHydration = controlPlaneJson<{ ok: boolean; contacts: Record<string, unknown> }>(
'/api/wormhole/dm/contacts',
)
.then((data) => {
contactCache = normalizeContactMap(data.contacts || {});
return contactCache;
})
.catch(() => contactCache);
contactsHydration = hydrateWormholeContactsFromNode().catch(() => contactCache);
return contactsHydration;
}
export async function hydrateWormholeContactsFromNode(): Promise<Record<string, Contact>> {
const data = await controlPlaneJson<{ ok: boolean; contacts: Record<string, unknown> }>(
'/api/wormhole/dm/contacts',
{ requireAdminSession: false },
);
contactCache = normalizeContactMap(data.contacts || {});
contactsHydration = Promise.resolve(contactCache);
return contactCache;
}
function getStoredContacts(): Record<string, Contact> {
if (!shouldUseWormholeContacts() && !contactsHydration && typeof window !== 'undefined') {
void hydrateWormholeContacts();
@@ -13,6 +13,7 @@ export async function bootstrapEncryptAccessRequest(peerId: string, plaintext: s
await ensureWormholeReadyForSecureAction('bootstrap_encrypt');
const data = await controlPlaneJson<{ result: string }>('/api/wormhole/dm/bootstrap-encrypt', {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
peer_id: peerId,
@@ -26,6 +27,7 @@ export async function bootstrapDecryptAccessRequest(senderId: string, ciphertext
await ensureWormholeReadyForSecureAction('bootstrap_decrypt');
const data = await controlPlaneJson<{ result: string }>('/api/wormhole/dm/bootstrap-decrypt', {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sender_id: senderId,
+24 -3
View File
@@ -102,6 +102,8 @@ export interface WormholeDmInviteImportResult {
trust_fingerprint: string;
trust_level: string;
detail?: string;
pending_prekey?: boolean;
prekey_detail?: string;
contact: Record<string, unknown>;
}
@@ -1091,7 +1093,9 @@ export function getWormholeDmInviteImportErrorResult(
}
export async function fetchWormholeDmRootHealth(): Promise<WormholeDmRootHealth> {
return controlPlaneJson<WormholeDmRootHealth>('/api/wormhole/dm/root-health');
return controlPlaneJson<WormholeDmRootHealth>('/api/wormhole/dm/root-health', {
requireAdminSession: false,
});
}
export async function bootstrapWormholeIdentity(): Promise<WormholeIdentity> {
@@ -1759,7 +1763,8 @@ export async function registerWormholeDmKey(): Promise<WormholeIdentity & { ok:
return controlPlaneJson<WormholeIdentity & { ok: boolean; detail?: string }>(
'/api/wormhole/dm/register-key',
{
method: 'POST',
method: 'POST',
requireAdminSession: false,
},
);
}
@@ -1771,6 +1776,7 @@ export async function issueWormholeDmSenderToken(
): Promise<WormholeDmSenderToken> {
return controlPlaneJson<WormholeDmSenderToken>('/api/wormhole/dm/sender-token', {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipient_id: recipientId,
@@ -1788,6 +1794,7 @@ export async function issueWormholeDmSenderTokens(
): Promise<WormholeDmSenderTokenBatch> {
return controlPlaneJson<WormholeDmSenderTokenBatch>('/api/wormhole/dm/sender-token', {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipient_id: recipientId,
@@ -1815,6 +1822,7 @@ export async function openWormholeSenderSeal(
): Promise<WormholeOpenedSeal> {
return controlPlaneJson<WormholeOpenedSeal>('/api/wormhole/dm/open-seal', {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sender_seal: senderSeal,
@@ -1833,6 +1841,7 @@ export async function buildWormholeSenderSeal(
): Promise<WormholeBuiltSeal> {
return controlPlaneJson<WormholeBuiltSeal>('/api/wormhole/dm/build-seal', {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipient_id: recipientId,
@@ -1850,6 +1859,7 @@ export async function deriveWormholeDeadDropTokenPair(
): Promise<WormholeDeadDropTokenPair> {
return controlPlaneJson<WormholeDeadDropTokenPair>('/api/wormhole/dm/dead-drop-token', {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
peer_id: peerId,
@@ -1865,6 +1875,7 @@ export async function issueWormholePairwiseAlias(
): Promise<WormholePairwiseAlias> {
return controlPlaneJson<WormholePairwiseAlias>('/api/wormhole/dm/pairwise-alias', {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
peer_id: peerId,
@@ -1880,6 +1891,7 @@ export async function rotateWormholePairwiseAlias(
): Promise<WormholeRotatedPairwiseAlias> {
return controlPlaneJson<WormholeRotatedPairwiseAlias>('/api/wormhole/dm/pairwise-alias/rotate', {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
peer_id: peerId,
@@ -1895,6 +1907,7 @@ export async function deriveWormholeDeadDropTokens(
): Promise<WormholeDeadDropTokensBatch> {
return controlPlaneJson<WormholeDeadDropTokensBatch>('/api/wormhole/dm/dead-drop-tokens', {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contacts,
@@ -1911,6 +1924,7 @@ export async function deriveWormholeSasPhrase(
): Promise<WormholeSasPhrase> {
return controlPlaneJson<WormholeSasPhrase>('/api/wormhole/dm/sas', {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
peer_id: peerId,
@@ -1929,6 +1943,7 @@ export async function confirmWormholeSasVerification(
): Promise<WormholeSasConfirmResult> {
return controlPlaneJson<WormholeSasConfirmResult>('/api/wormhole/dm/sas/confirm', {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
peer_id: peerId,
@@ -1944,6 +1959,7 @@ export async function acknowledgeWormholeSasFingerprint(
): Promise<WormholeSasConfirmResult> {
return controlPlaneJson<WormholeSasConfirmResult>('/api/wormhole/dm/sas/acknowledge', {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
peer_id: peerId,
@@ -1959,6 +1975,7 @@ export async function recoverWormholeSasRootContinuity(
): Promise<WormholeSasConfirmResult> {
return controlPlaneJson<WormholeSasConfirmResult>('/api/wormhole/dm/sas/recover-root', {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
peer_id: peerId,
@@ -1970,7 +1987,9 @@ export async function recoverWormholeSasRootContinuity(
}
export async function listWormholeDmContacts(): Promise<WormholeDmContactsResponse> {
return controlPlaneJson<WormholeDmContactsResponse>('/api/wormhole/dm/contacts');
return controlPlaneJson<WormholeDmContactsResponse>('/api/wormhole/dm/contacts', {
requireAdminSession: false,
});
}
export async function putWormholeDmContact(
@@ -1979,6 +1998,7 @@ export async function putWormholeDmContact(
): Promise<{ ok: boolean; peer_id: string; contact: Record<string, unknown> }> {
return controlPlaneJson('/api/wormhole/dm/contact', {
method: 'PUT',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
peer_id: peerId,
@@ -1992,6 +2012,7 @@ export async function deleteWormholeDmContact(
): Promise<{ ok: boolean; peer_id: string; deleted: boolean }> {
return controlPlaneJson(`/api/wormhole/dm/contact/${encodeURIComponent(peerId)}`, {
method: 'DELETE',
requireAdminSession: false,
});
}