v0.9.6: InfoNet hashchain, Wormhole gate encryption, mesh reputation, 16 community contributors

Gate messages now propagate via the Infonet hashchain as encrypted blobs — every node syncs them
through normal chain sync while only Gate members with MLS keys can decrypt. Added mesh reputation
system, peer push workers, voluntary Wormhole opt-in for node participation, fork recovery,
killwormhole scripts, obfuscated terminology, and hardened the self-updater to protect encryption
keys and chain state during updates.

New features: Shodan search, train tracking, Sentinel Hub imagery, 8 new intelligence layers,
CCTV expansion to 11,000+ cameras across 6 countries, Mesh Terminal CLI, prediction markets,
desktop-shell scaffold, and comprehensive mesh test suite (215 frontend + backend tests passing).

Community contributors: @wa1id, @AlborzNazari, @adust09, @Xpirix, @imqdcr, @csysp, @suranyami,
@chr0n1x, @johan-martensson, @singularfailure, @smithbh, @OrfeoTerkuci, @deuza, @tm-const,
@Elhard1, @ttulttul
This commit is contained in:
anoracleofra-code
2026-03-26 05:58:04 -06:00
parent d363013742
commit 668ce16dc7
363 changed files with 170430 additions and 23203 deletions
@@ -0,0 +1,91 @@
import { describe, expect, it } from 'vitest';
import {
gateEnvelopeDisplayText,
gateEnvelopeState,
isEncryptedGateEnvelope,
} from '@/mesh/gateEnvelope';
import { normalizePayload } from '@/mesh/meshProtocol';
import { validateEventPayload } from '@/mesh/meshSchema';
describe('gate envelope protocol', () => {
it('normalizes encrypted gate-message payloads', () => {
expect(
normalizePayload('gate_message', {
gate: 'Finance',
epoch: '2',
ciphertext: 'opaque',
nonce: 'nonce-2',
sender_ref: 'persona-fin-1',
}),
).toEqual({
gate: 'finance',
epoch: 2,
ciphertext: 'opaque',
nonce: 'nonce-2',
sender_ref: 'persona-fin-1',
format: 'g1',
});
});
it('accepts encrypted gate-message envelopes and rejects plaintext ones', () => {
expect(
validateEventPayload('gate_message', {
gate: 'finance',
epoch: 2,
ciphertext: 'opaque',
nonce: 'nonce-2',
sender_ref: 'persona-fin-1',
format: 'g1',
}),
).toEqual({ ok: true });
expect(
validateEventPayload('gate_message', {
gate: 'finance',
message: 'plaintext',
}),
).toEqual({ ok: false, reason: 'Payload is not normalized' });
});
});
describe('gate envelope display', () => {
it('detects encrypted gate messages and shows placeholders honestly', () => {
const encrypted = {
event_type: 'gate_message',
gate: 'finance',
epoch: 2,
ciphertext: 'opaque',
nonce: 'nonce-2',
sender_ref: 'persona-fin-1',
};
expect(isEncryptedGateEnvelope(encrypted)).toBe(true);
expect(gateEnvelopeState(encrypted)).toBe('locked');
expect(gateEnvelopeDisplayText(encrypted)).toBe('ENCRYPTED GATE MESSAGE - KEY UNAVAILABLE');
expect(
gateEnvelopeState({
...encrypted,
decrypted_message: 'decoded text',
}),
).toBe('decrypted');
expect(
gateEnvelopeDisplayText({
...encrypted,
decrypted_message: 'decoded text',
}),
).toBe('decoded text');
expect(
gateEnvelopeState({
event_type: 'gate_notice',
message: 'legacy plaintext',
}),
).toBe('plaintext');
expect(
gateEnvelopeDisplayText({
event_type: 'gate_notice',
message: 'legacy plaintext',
}),
).toBe('legacy plaintext');
});
});
@@ -0,0 +1,158 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const deadDropTokensForContacts = vi.fn();
const mailboxClaimToken = vi.fn();
const mailboxDecoySharedToken = vi.fn();
vi.mock('@/mesh/meshDeadDrop', () => ({
deadDropToken: vi.fn(),
deadDropTokensForContacts,
}));
vi.mock('@/mesh/meshMailbox', () => ({
mailboxClaimToken,
mailboxDecoySharedToken,
}));
vi.mock('@/mesh/meshIdentity', () => ({
deriveSenderSealKey: vi.fn(),
ensureDhKeysFresh: vi.fn(),
deriveSharedKey: vi.fn(),
encryptDM: vi.fn(),
getDHAlgo: vi.fn(() => 'X25519'),
getNodeIdentity: vi.fn(() => ({ nodeId: '!self', publicKey: 'pub' })),
getPublicKeyAlgo: vi.fn(() => 'Ed25519'),
nextSequence: vi.fn(() => 1),
verifyNodeIdBindingFromPublicKey: vi.fn(async () => true),
}));
vi.mock('@/mesh/wormholeIdentityClient', () => ({
buildWormholeSenderSeal: vi.fn(),
getActiveSigningContext: vi.fn(async () => null),
isWormholeSecureRequired: vi.fn(async () => false),
issueWormholeDmSenderToken: vi.fn(),
issueWormholeDmSenderTokens: vi.fn(),
registerWormholeDmKey: vi.fn(),
signRawMeshMessage: vi.fn(),
signMeshEvent: vi.fn(),
}));
vi.mock('@/mesh/meshSchema', () => ({
validateEventPayload: vi.fn(() => ({ ok: true, reason: 'ok' })),
}));
describe('mailbox claim privacy padding', () => {
beforeEach(() => {
vi.resetModules();
vi.unstubAllEnvs();
vi.stubEnv('NEXT_PUBLIC_ENABLE_RFC2A_CLAIM_SHAPE', '1');
deadDropTokensForContacts.mockReset();
mailboxClaimToken.mockReset();
mailboxDecoySharedToken.mockReset();
mailboxClaimToken.mockImplementation(async (type: string) => `${type}-token`);
mailboxDecoySharedToken.mockImplementation(async (index: number) => `decoy-${index}`);
});
function buildSharedTokens(count: number): string[] {
return Array.from({ length: count }, (_, index) => `shared-${index + 1}`);
}
it('uses bucketed shared-claim envelopes across multiple contact counts', async () => {
const mod = await import('@/mesh/meshDmClient');
for (const testCase of [
{ realSharedClaims: 0, expectedSharedClaims: 3, expectedTotalClaims: 5 },
{ realSharedClaims: 1, expectedSharedClaims: 3, expectedTotalClaims: 5 },
{ realSharedClaims: 3, expectedSharedClaims: 3, expectedTotalClaims: 5 },
{ realSharedClaims: 4, expectedSharedClaims: 6, expectedTotalClaims: 8 },
{ realSharedClaims: 7, expectedSharedClaims: 12, expectedTotalClaims: 14 },
{ realSharedClaims: 25, expectedSharedClaims: 30, expectedTotalClaims: 32 },
{ realSharedClaims: 30, expectedSharedClaims: 30, expectedTotalClaims: 32 },
]) {
deadDropTokensForContacts.mockResolvedValue(buildSharedTokens(testCase.realSharedClaims));
const claims = await mod.buildMailboxClaims({});
expect(claims.slice(0, 2)).toEqual([
{ type: 'self', token: 'self-token' },
{ type: 'requests', token: 'requests-token' },
]);
expect(claims.filter((claim) => claim.type === 'shared')).toHaveLength(
testCase.expectedSharedClaims,
);
expect(claims).toHaveLength(testCase.expectedTotalClaims);
}
});
it('falls back to the legacy shared-claim floor when the experiment is disabled', async () => {
vi.resetModules();
vi.stubEnv('NEXT_PUBLIC_ENABLE_RFC2A_CLAIM_SHAPE', '0');
deadDropTokensForContacts.mockResolvedValue(['shared-1', 'shared-2', 'shared-3', 'shared-4']);
const mod = await import('@/mesh/meshDmClient');
const claims = await mod.buildMailboxClaims({});
const sharedClaims = claims.filter((claim) => claim.type === 'shared');
expect(mod.MAILBOX_SHARED_CLAIM_SHAPE_VERSION).toBe('legacy-floor-v1');
expect(sharedClaims).toEqual([
{ type: 'shared', token: 'shared-1' },
{ type: 'shared', token: 'shared-2' },
{ type: 'shared', token: 'shared-3' },
{ type: 'shared', token: 'shared-4' },
]);
expect(claims).toHaveLength(6);
});
it('deduplicates real shared tokens before filling the bucketed envelope', async () => {
deadDropTokensForContacts.mockResolvedValue(['shared-real', 'shared-real']);
const mod = await import('@/mesh/meshDmClient');
const claims = await mod.buildMailboxClaims({
alice: { blocked: false, dhPubKey: 'dh-a' },
});
const sharedClaims = claims.filter((claim) => claim.type === 'shared');
expect(sharedClaims).toEqual([
{ type: 'shared', token: 'decoy-0' },
{ type: 'shared', token: 'shared-real' },
{ type: 'shared', token: 'decoy-1' },
]);
});
it('preserves every real shared token within the supported 30-claim shared range', async () => {
const realSharedTokens = buildSharedTokens(30);
deadDropTokensForContacts.mockResolvedValue(realSharedTokens);
const mod = await import('@/mesh/meshDmClient');
const claims = await mod.buildMailboxClaims({});
const sharedTokens = claims
.filter((claim) => claim.type === 'shared')
.map((claim) => claim.token);
expect(sharedTokens).toHaveLength(30);
expect(new Set(sharedTokens)).toEqual(new Set(realSharedTokens));
});
it('keeps decoy shared tokens distinct from real shared tokens', async () => {
const realSharedTokens = ['shared-1', 'shared-2', 'shared-3', 'shared-4'];
deadDropTokensForContacts.mockResolvedValue(realSharedTokens);
const mod = await import('@/mesh/meshDmClient');
const claims = await mod.buildMailboxClaims({});
const sharedTokens = claims
.filter((claim) => claim.type === 'shared')
.map((claim) => String(claim.token || ''));
const decoyTokens = sharedTokens.filter((token) => !realSharedTokens.includes(token));
expect(sharedTokens).toEqual([
'shared-1',
'decoy-0',
'shared-2',
'shared-3',
'decoy-1',
'shared-4',
]);
expect(decoyTokens).toEqual(['decoy-0', 'decoy-1']);
expect(decoyTokens.every((token) => !realSharedTokens.includes(token))).toBe(true);
});
});
@@ -0,0 +1,32 @@
import { readFileSync } from 'fs';
import path from 'path';
import { buildSignaturePayload, type JsonValue } from '@/mesh/meshProtocol';
type Fixture = {
name: string;
event_type: string;
node_id: string;
sequence: number;
payload: Record<string, JsonValue>;
expected: string;
};
describe('mesh canonical signature payloads', () => {
const cwd = process.cwd();
const fixturePath = cwd.endsWith('frontend')
? path.resolve(cwd, '..', 'docs', 'mesh', 'mesh-canonical-fixtures.json')
: path.resolve(cwd, 'docs', 'mesh', 'mesh-canonical-fixtures.json');
const fixtures = JSON.parse(readFileSync(fixturePath, 'utf-8')) as Fixture[];
for (const fixture of fixtures) {
it(`matches fixture: ${fixture.name}`, () => {
const result = buildSignaturePayload({
eventType: fixture.event_type,
nodeId: fixture.node_id,
sequence: fixture.sequence,
payload: fixture.payload,
});
expect(result).toBe(fixture.expected);
});
}
});
@@ -0,0 +1,244 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const controlPlaneJson = vi.fn();
const idbStore = new Map<string, unknown>();
vi.mock('@/lib/controlPlane', () => ({
controlPlaneJson,
}));
vi.mock('@/mesh/meshKeyStore', () => ({
getKey: vi.fn(async (id: string) => idbStore.get(id) ?? null),
setKey: vi.fn(async (id: string, key: unknown) => {
idbStore.set(id, key);
}),
deleteKey: vi.fn(async (id: string) => {
idbStore.delete(id);
}),
}));
async function flushStoragePersistence(): Promise<void> {
await Promise.resolve();
await Promise.resolve();
await new Promise((resolve) => setTimeout(resolve, 0));
}
async function waitForEncryptedContacts(): Promise<string | null> {
for (let i = 0; i < 20; i += 1) {
await flushStoragePersistence();
const stored =
sessionStorage.getItem('sb_mesh_contacts') || localStorage.getItem('sb_mesh_contacts');
if (typeof stored === 'string' && stored.startsWith('enc:')) {
return stored;
}
}
return sessionStorage.getItem('sb_mesh_contacts') || localStorage.getItem('sb_mesh_contacts');
}
function bufToBase64(buf: ArrayBuffer): string {
return btoa(String.fromCharCode(...new Uint8Array(buf)));
}
describe('meshIdentity contact storage hardening', () => {
beforeEach(() => {
vi.resetModules();
controlPlaneJson.mockReset();
idbStore.clear();
const makeStorage = () => {
const values = new Map<string, string>();
return {
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => void values.set(key, value),
removeItem: (key: string) => void values.delete(key),
clear: () => void values.clear(),
};
};
Object.defineProperty(globalThis, 'localStorage', {
value: makeStorage(),
configurable: true,
writable: true,
});
Object.defineProperty(globalThis, 'sessionStorage', {
value: makeStorage(),
configurable: true,
writable: true,
});
});
async function provisionLocalIdentity(mod: typeof import('@/mesh/meshIdentity')) {
localStorage.setItem('sb_mesh_pubkey', 'test-pub');
localStorage.setItem('sb_mesh_node_id', '!sb_contacts123456');
localStorage.setItem('sb_mesh_sovereignty_accepted', 'true');
const keyPair = (await crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: 'P-256' },
false,
['deriveKey', 'deriveBits'],
)) as CryptoKeyPair;
const publicRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey);
localStorage.setItem('sb_mesh_dh_pubkey', bufToBase64(publicRaw));
localStorage.setItem('sb_mesh_dh_algo', 'ECDH');
idbStore.set('sb_mesh_dh_priv', keyPair.privateKey);
}
it('hydrates secure-mode contacts from Wormhole and avoids localStorage persistence', async () => {
controlPlaneJson
.mockResolvedValueOnce({
ok: true,
contacts: {
alice: { blocked: false, dhPubKey: 'dh_a', sharedAlias: 'alias_a' },
},
})
.mockResolvedValueOnce({
ok: true,
peer_id: 'alice',
contact: { blocked: false, dhPubKey: 'dh_a2', sharedAlias: 'alias_a' },
});
const mod = await import('@/mesh/meshIdentity');
mod.setSecureModeCached(true);
const contacts = await mod.hydrateWormholeContacts(true);
expect(contacts.alice.dhPubKey).toBe('dh_a');
mod.addContact('alice', 'dh_a2');
await Promise.resolve();
expect(localStorage.getItem('sb_mesh_contacts')).toBeNull();
expect(mod.getContacts().alice.dhPubKey).toBe('dh_a2');
expect(controlPlaneJson).toHaveBeenLastCalledWith('/api/wormhole/dm/contact', expect.any(Object));
});
it('stores local contacts as encrypted ciphertext and hydrates them back', async () => {
const mod = await import('@/mesh/meshIdentity');
await provisionLocalIdentity(mod);
mod.addContact('alice', 'dh_a', 'Alice', 'X25519');
mod.updateContact('alice', {
remotePrekeyFingerprint: 'fp-1',
remotePrekeyObservedFingerprint: 'fp-1',
remotePrekeyPinnedAt: 111,
remotePrekeyLastSeenAt: 222,
remotePrekeySequence: 3,
remotePrekeySignedAt: 444,
remotePrekeyMismatch: false,
});
const stored = await waitForEncryptedContacts();
expect(String(stored ?? '')).toMatch(/^enc:/);
expect(String(stored ?? '')).not.toContain('"alice"');
expect(String(stored ?? '')).not.toContain('"dh_a"');
const hydrated = await mod.hydrateWormholeContacts(true);
expect(hydrated.alice.dhPubKey).toBe('dh_a');
expect(hydrated.alice.alias).toBe('Alice');
expect(hydrated.alice.remotePrekeyFingerprint).toBe('fp-1');
expect(hydrated.alice.remotePrekeyObservedFingerprint).toBe('fp-1');
expect(hydrated.alice.remotePrekeyPinnedAt).toBe(111);
expect(hydrated.alice.remotePrekeyLastSeenAt).toBe(222);
expect(hydrated.alice.remotePrekeySequence).toBe(3);
expect(hydrated.alice.remotePrekeySignedAt).toBe(444);
expect(hydrated.alice.remotePrekeyMismatch).toBe(false);
});
it('migrates legacy plaintext contacts to encrypted storage on first hydrate', async () => {
const mod = await import('@/mesh/meshIdentity');
await provisionLocalIdentity(mod);
localStorage.setItem(
'sb_mesh_contacts',
JSON.stringify({ alice: { blocked: false, dhPubKey: 'legacy_dh', alias: 'Legacy Alice' } }),
);
const hydrated = await mod.hydrateWormholeContacts(true);
expect(hydrated.alice.dhPubKey).toBe('legacy_dh');
expect(hydrated.alice.alias).toBe('Legacy Alice');
const stored = await waitForEncryptedContacts();
expect(String(stored ?? '')).toMatch(/^enc:/);
expect(String(stored ?? '')).not.toContain('legacy_dh');
});
it('encrypts identity-bound browser payloads under distinct info domains', async () => {
const mod = await import('@/mesh/meshIdentity');
await provisionLocalIdentity(mod);
const accessCipher = await mod.encryptIdentityBoundStoragePayload(
[{ sender_id: 'alice', timestamp: 1 }],
'SB-ACCESS-REQUESTS-STORAGE-V1',
);
expect(accessCipher).toMatch(/^enc:/);
expect(accessCipher).not.toContain('alice');
const decrypted = await mod.decryptIdentityBoundStoragePayload(
accessCipher,
'SB-ACCESS-REQUESTS-STORAGE-V1',
[],
);
expect(decrypted).toEqual([{ sender_id: 'alice', timestamp: 1 }]);
await expect(
mod.decryptIdentityBoundStoragePayload(
accessCipher,
'SB-PENDING-CONTACTS-STORAGE-V1',
[],
),
).rejects.toThrow();
});
it('treats unreadable encrypted contacts as empty and warns instead of crashing', async () => {
const mod = await import('@/mesh/meshIdentity');
await provisionLocalIdentity(mod);
localStorage.setItem('sb_mesh_contacts', 'enc:not-valid-ciphertext');
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
const hydrated = await mod.hydrateWormholeContacts(true);
expect(hydrated).toEqual({});
expect(warn).toHaveBeenCalledWith(
'[mesh] contact storage unreadable — treating as empty contacts',
expect.anything(),
);
warn.mockRestore();
});
it('purges browser-persisted contact graph when secure mode boundary is applied', async () => {
const mod = await import('@/mesh/meshIdentity');
localStorage.setItem(
'sb_mesh_contacts',
JSON.stringify({ bob: { blocked: false, sharedAlias: 'peer-b' } }),
);
mod.purgeBrowserContactGraph();
expect(localStorage.getItem('sb_mesh_contacts')).toBeNull();
expect(mod.getContacts()).toEqual({});
});
it('rotates the mailbox-claim secret when identity state is cleared', async () => {
const { mailboxClaimToken } = await import('@/mesh/meshMailbox');
const mod = await import('@/mesh/meshIdentity');
await provisionLocalIdentity(mod);
const first = await mailboxClaimToken('requests', '!sb_contacts123456');
const second = await mailboxClaimToken('requests', '!sb_contacts123456');
expect(second).toBe(first);
await mod.clearBrowserIdentityState();
localStorage.setItem('sb_mesh_pubkey', 'test-pub');
localStorage.setItem('sb_mesh_node_id', '!sb_contacts123456');
localStorage.setItem('sb_mesh_sovereignty_accepted', 'true');
const keyPair = (await crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: 'P-256' },
false,
['deriveKey', 'deriveBits'],
)) as CryptoKeyPair;
const publicRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey);
localStorage.setItem('sb_mesh_dh_pubkey', bufToBase64(publicRaw));
localStorage.setItem('sb_mesh_dh_algo', 'ECDH');
idbStore.set('sb_mesh_dh_priv', keyPair.privateKey);
const rotated = await mailboxClaimToken('requests', '!sb_contacts123456');
expect(rotated).not.toBe(first);
});
});
@@ -0,0 +1,85 @@
import { describe, expect, it } from 'vitest';
import {
allDmPeerIds,
buildAliasRotateMessage,
buildAccessGrantedMessage,
buildContactAcceptMessage,
buildContactDenyMessage,
buildContactOfferMessage,
mergeAliasHistory,
parseAliasRotateMessage,
parseAccessGrantedMessage,
parseDmConsentMessage,
preferredDmPeerId,
} from '@/mesh/meshDmConsent';
describe('mesh DM consent helpers', () => {
it('builds and parses access-granted payloads', () => {
const message = buildAccessGrantedMessage('dmx_alpha');
expect(parseAccessGrantedMessage(message)).toEqual({ shared_alias: 'dmx_alpha' });
});
it('builds and parses off-ledger contact offer payloads', () => {
const message = buildContactOfferMessage('dh_pub', 'X25519', '40.12,-105.27');
expect(parseDmConsentMessage(message)).toEqual({
kind: 'contact_offer',
dh_pub_key: 'dh_pub',
dh_algo: 'X25519',
geo_hint: '40.12,-105.27',
});
});
it('builds and parses off-ledger contact accept payloads', () => {
const message = buildContactAcceptMessage('dmx_pairwise');
expect(parseDmConsentMessage(message)).toEqual({
kind: 'contact_accept',
shared_alias: 'dmx_pairwise',
});
});
it('builds and parses off-ledger contact deny payloads', () => {
const message = buildContactDenyMessage('declined');
expect(parseDmConsentMessage(message)).toEqual({
kind: 'contact_deny',
reason: 'declined',
});
});
it('prefers the pairwise alias for shared DM routing', () => {
expect(preferredDmPeerId('node_public', { sharedAlias: 'dmx_pairwise' })).toBe('dmx_pairwise');
expect(preferredDmPeerId('node_public', { sharedAlias: '' })).toBe('node_public');
});
it('keeps both alias and public ids during the transition window', () => {
expect(allDmPeerIds('node_public', { sharedAlias: 'dmx_pairwise' })).toEqual([
'dmx_pairwise',
'node_public',
]);
expect(allDmPeerIds('node_public', { sharedAlias: 'node_public' })).toEqual(['node_public']);
});
it('builds and parses alias rotation control payloads', () => {
const message = buildAliasRotateMessage('dmx_next');
expect(parseAliasRotateMessage(message)).toEqual({ shared_alias: 'dmx_next' });
});
it('promotes pending alias after the grace window elapses', () => {
const now = Date.now();
expect(
preferredDmPeerId('node_public', {
sharedAlias: 'dmx_current',
pendingSharedAlias: 'dmx_next',
sharedAliasGraceUntil: now - 1,
}),
).toBe('dmx_next');
});
it('keeps alias history compact and unique', () => {
expect(mergeAliasHistory(['dmx_a', 'dmx_b', 'dmx_a', 'dmx_c', 'dmx_d'], 3)).toEqual([
'dmx_a',
'dmx_b',
'dmx_c',
]);
});
});
@@ -0,0 +1,286 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
type StoreRecord = Map<string, unknown>;
type DbRecord = {
version: number;
stores: Map<string, StoreRecord>;
};
const databases = new Map<string, DbRecord>();
const deletedDatabases: string[] = [];
const workerInstances: FakeWorker[] = [];
vi.mock('@/lib/controlPlane', () => ({
controlPlaneJson: vi.fn(),
}));
vi.mock('@/mesh/wormholeIdentityClient', () => ({
ensureWormholeReadyForSecureAction: vi.fn(async () => undefined),
isWormholeReady: vi.fn(async () => false),
}));
vi.mock('@/mesh/meshIdentity', () => ({
getDHAlgo: vi.fn(() => 'X25519'),
}));
function makeStorage() {
const values = new Map<string, string>();
return {
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => void values.set(key, value),
removeItem: (key: string) => void values.delete(key),
clear: () => void values.clear(),
get length() {
return values.size;
},
key: (_i: number) => null as string | null,
};
}
class FakeWorker {
onmessage: ((event: MessageEvent<{ id: string; ok: boolean; result?: string }>) => void) | null =
null;
terminated = false;
constructor() {
workerInstances.push(this);
}
postMessage(message: { id: string }) {
queueMicrotask(() => {
this.onmessage?.({
data: { id: message.id, ok: true, result: '' },
} as MessageEvent<{ id: string; ok: boolean; result?: string }>);
});
}
terminate() {
this.terminated = true;
}
}
function domStringList(record: DbRecord): DOMStringList {
return {
contains: (name: string) => record.stores.has(name),
item: (index: number) => Array.from(record.stores.keys())[index] ?? null,
get length() {
return record.stores.size;
},
} as DOMStringList;
}
function makeRequest<T>(
executor: (request: IDBRequest<T>) => void,
tx?: IDBTransaction,
): IDBRequest<T> {
const request = {} as IDBRequest<T>;
queueMicrotask(() => {
executor(request);
tx?.oncomplete?.(new Event('complete') as Event);
});
return request;
}
function makeObjectStore(record: DbRecord, name: string, tx: IDBTransaction): IDBObjectStore {
const store = record.stores.get(name);
if (!store) throw new Error(`missing object store ${name}`);
return {
get(key: IDBValidKey) {
return makeRequest((request) => {
(request as { result?: unknown }).result = store.get(String(key));
request.onsuccess?.(new Event('success') as Event);
}, tx);
},
put(value: unknown, key?: IDBValidKey) {
return makeRequest((request) => {
store.set(String(key ?? ''), value);
(request as { result?: unknown }).result = key;
request.onsuccess?.(new Event('success') as Event);
}, tx);
},
delete(key: IDBValidKey) {
return makeRequest((request) => {
store.delete(String(key));
request.onsuccess?.(new Event('success') as Event);
}, tx);
},
} as unknown as IDBObjectStore;
}
function makeTransaction(record: DbRecord): IDBTransaction {
const tx = {
oncomplete: null,
onerror: null,
onabort: null,
objectStore: (name: string) => makeObjectStore(record, name, tx as unknown as IDBTransaction),
} as unknown as IDBTransaction;
return tx;
}
function makeDb(name: string, record: DbRecord): IDBDatabase {
return {
name,
version: record.version,
objectStoreNames: domStringList(record),
createObjectStore(storeName: string) {
if (!record.stores.has(storeName)) {
record.stores.set(storeName, new Map());
}
return {} as IDBObjectStore;
},
transaction(storeName: string | string[]) {
return makeTransaction(record);
},
close() {
/* noop */
},
} as unknown as IDBDatabase;
}
function createFakeIndexedDb() {
return {
open(name: string, version?: number) {
const request = {} as IDBOpenDBRequest;
queueMicrotask(() => {
const resolvedVersion = Number(version || 1);
let record = databases.get(name);
const upgrading = !record || resolvedVersion > record.version;
if (!record) {
record = { version: resolvedVersion, stores: new Map() };
databases.set(name, record);
}
if (upgrading) {
record.version = resolvedVersion;
(request as { result?: IDBDatabase }).result = makeDb(name, record);
request.onupgradeneeded?.(new Event('upgradeneeded') as IDBVersionChangeEvent);
}
(request as { result?: IDBDatabase }).result = makeDb(name, record);
request.onsuccess?.(new Event('success') as Event);
});
return request;
},
deleteDatabase(name: string) {
const request = {} as IDBOpenDBRequest;
queueMicrotask(() => {
deletedDatabases.push(name);
databases.delete(name);
request.onsuccess?.(new Event('success') as Event);
});
return request;
},
};
}
function ensureStore(name: string, version: number, storeName: string): StoreRecord {
let record = databases.get(name);
if (!record) {
record = { version, stores: new Map() };
databases.set(name, record);
}
record.version = Math.max(record.version, version);
if (!record.stores.has(storeName)) {
record.stores.set(storeName, new Map());
}
return record.stores.get(storeName)!;
}
function getStoredValue(name: string, storeName: string, key: string): unknown {
return databases.get(name)?.stores.get(storeName)?.get(key);
}
describe('worker ratchet vault hardening', () => {
beforeEach(() => {
vi.resetModules();
databases.clear();
deletedDatabases.length = 0;
workerInstances.length = 0;
Object.defineProperty(globalThis, 'localStorage', {
value: makeStorage(),
configurable: true,
writable: true,
});
Object.defineProperty(globalThis, 'sessionStorage', {
value: makeStorage(),
configurable: true,
writable: true,
});
Object.defineProperty(globalThis, 'Worker', {
value: FakeWorker,
configurable: true,
writable: true,
});
Object.defineProperty(globalThis, 'indexedDB', {
value: createFakeIndexedDb(),
configurable: true,
writable: true,
});
});
it('persists worker ratchet state as an encrypted blob instead of raw state', async () => {
const mod = await import('@/mesh/meshDmWorkerVault');
const sample = {
alice: {
algo: 'X25519',
rk: 'root-key',
cks: 'send-chain',
ckr: 'recv-chain',
dhSelfPub: 'pub',
dhSelfPriv: 'private-material',
dhRemote: 'remote',
ns: 1,
nr: 2,
pn: 3,
skipped: { 'remote:1': 'mk' },
updated: 123,
},
};
await mod.writeWorkerRatchetStates(sample);
const raw = getStoredValue(mod.WORKER_RATCHET_DB, 'ratchet', 'state');
expect(typeof raw).toBe('string');
expect(String(raw)).not.toContain('dhSelfPriv');
expect(String(raw)).not.toContain('private-material');
const loaded = await mod.readWorkerRatchetStates();
expect(loaded).toEqual(sample);
});
it('migrates legacy plaintext worker state into encrypted storage on read', async () => {
const legacyStore = ensureStore('sb_mesh_dm_worker', 1, 'ratchet');
legacyStore.set('state', {
bob: {
algo: 'X25519',
rk: 'legacy-rk',
dhSelfPub: 'legacy-pub',
dhSelfPriv: 'legacy-private',
dhRemote: 'legacy-remote',
ns: 0,
nr: 0,
pn: 0,
updated: 999,
},
});
const mod = await import('@/mesh/meshDmWorkerVault');
const loaded = await mod.readWorkerRatchetStates();
const raw = getStoredValue(mod.WORKER_RATCHET_DB, 'ratchet', 'state');
expect(loaded.bob?.dhSelfPriv).toBe('legacy-private');
expect(typeof raw).toBe('string');
expect(String(raw)).not.toContain('legacy-private');
});
it('purgeBrowserDmState clears worker persistence and legacy browser copies', async () => {
localStorage.setItem('sb_mesh_dm_ratchet', 'legacy');
sessionStorage.setItem('sb_mesh_ratchet_telemetry', '{"seen":1}');
const mod = await import('@/mesh/meshDmWorkerClient');
await mod.purgeBrowserDmState();
expect(localStorage.getItem('sb_mesh_dm_ratchet')).toBeNull();
expect(sessionStorage.getItem('sb_mesh_ratchet_telemetry')).toBeNull();
expect(deletedDatabases).toContain('sb_mesh_dm_worker');
expect(workerInstances[0]?.terminated).toBe(true);
});
});
@@ -0,0 +1,112 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('@/lib/controlPlane', () => ({
controlPlaneJson: vi.fn(),
}));
vi.mock('@/mesh/meshKeyStore', () => ({
getKey: vi.fn().mockResolvedValue(null),
setKey: vi.fn().mockResolvedValue(undefined),
deleteKey: vi.fn().mockResolvedValue(undefined),
}));
describe('mesh identity storage separation', () => {
beforeEach(() => {
vi.resetModules();
const makeStorage = () => {
const values = new Map<string, string>();
return {
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => void values.set(key, value),
removeItem: (key: string) => void values.delete(key),
clear: () => void values.clear(),
};
};
Object.defineProperty(globalThis, 'localStorage', {
value: makeStorage(),
configurable: true,
writable: true,
});
Object.defineProperty(globalThis, 'sessionStorage', {
value: makeStorage(),
configurable: true,
writable: true,
});
});
it('keeps public browser identity separate from Wormhole descriptor cache', async () => {
const mod = await import('@/mesh/meshIdentity');
mod.cachePublicIdentity({
nodeId: '!sb_public',
publicKey: 'public-key',
publicKeyAlgo: 'Ed25519',
});
mod.cacheWormholeIdentityDescriptor({
nodeId: '!sb_wormhole',
publicKey: 'wormhole-key',
publicKeyAlgo: 'Ed25519',
});
expect(mod.getStoredNodeDescriptor()).toEqual({
nodeId: '!sb_public',
publicKey: 'public-key',
publicKeyAlgo: 'Ed25519',
});
expect(mod.getWormholeIdentityDescriptor()).toEqual({
nodeId: '!sb_wormhole',
publicKey: 'wormhole-key',
publicKeyAlgo: 'Ed25519',
});
});
it('clears browser public identity and Wormhole descriptor cache together on full reset', async () => {
const mod = await import('@/mesh/meshIdentity');
mod.cachePublicIdentity({
nodeId: '!sb_public',
publicKey: 'public-key',
publicKeyAlgo: 'Ed25519',
});
mod.cacheWormholeIdentityDescriptor({
nodeId: '!sb_wormhole',
publicKey: 'wormhole-key',
publicKeyAlgo: 'Ed25519',
});
await mod.clearBrowserIdentityState();
expect(mod.getStoredNodeDescriptor()).toBeNull();
expect(mod.getWormholeIdentityDescriptor()).toBeNull();
});
it('migrates legacy browser and Wormhole node ids to the current format', async () => {
const mod = await import('@/mesh/meshIdentity');
const publicKey = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
const currentNodeId = await mod.deriveNodeIdFromPublicKey(publicKey);
mod.cachePublicIdentity({
nodeId: '!sb_deadbeef',
publicKey,
publicKeyAlgo: 'Ed25519',
});
mod.cacheWormholeIdentityDescriptor({
nodeId: '!sb_deadbeef',
publicKey,
publicKeyAlgo: 'Ed25519',
});
await mod.migrateLegacyNodeIds();
expect(mod.getStoredNodeDescriptor()).toEqual({
nodeId: currentNodeId,
publicKey,
publicKeyAlgo: 'Ed25519',
});
expect(mod.getWormholeIdentityDescriptor()).toEqual({
nodeId: currentNodeId,
publicKey,
publicKeyAlgo: 'Ed25519',
});
});
});
@@ -0,0 +1,32 @@
import { readFileSync } from 'fs';
import path from 'path';
import { buildMerkleRoot, verifyMerkleProof } from '@/mesh/meshMerkle';
type Fixture = {
leaves: string[];
root: string;
proofs: Record<string, { hash: string; side: string }[]>;
};
describe('mesh merkle fixtures', () => {
const cwd = process.cwd();
const fixturePath = cwd.endsWith('frontend')
? path.resolve(cwd, '..', 'docs', 'mesh', 'mesh-merkle-fixtures.json')
: path.resolve(cwd, 'docs', 'mesh', 'mesh-merkle-fixtures.json');
const fixtures = JSON.parse(readFileSync(fixturePath, 'utf-8')) as Fixture;
it('builds the expected root', async () => {
const root = await buildMerkleRoot(fixtures.leaves);
expect(root).toBe(fixtures.root);
});
it('verifies provided proofs', async () => {
const root = fixtures.root;
for (const [idxStr, proof] of Object.entries(fixtures.proofs)) {
const idx = Number(idxStr);
const leaf = fixtures.leaves[idx];
const ok = await verifyMerkleProof(leaf, idx, proof, root);
expect(ok).toBe(true);
}
});
});
@@ -0,0 +1,96 @@
import { describe, expect, it } from 'vitest';
import {
buildDmTrustHint,
buildPrivateLaneHint,
dmTrustPrimaryActionLabel,
isFirstContactTrustOnly,
shortTrustFingerprint,
shouldAutoRevealSasForTrust,
} from '@/mesh/meshPrivacyHints';
describe('meshPrivacyHints', () => {
it('flags recent private-lane fallback as a danger hint', () => {
const hint = buildPrivateLaneHint({
activeTab: 'dms',
recentPrivateFallback: true,
recentPrivateFallbackReason: 'Tor transport failed and clearnet relay was used.',
dmTransportMode: 'relay',
});
expect(hint).toEqual(
expect.objectContaining({
severity: 'danger',
title: 'RECENT PRIVACY DOWNGRADE',
}),
);
expect(hint?.detail).toContain('clearnet relay');
});
it('flags remote prekey mismatch as a danger trust hint', () => {
const hint = buildDmTrustHint({
remotePrekeyMismatch: true,
});
expect(hint).toEqual(
expect.objectContaining({
severity: 'danger',
title: 'REMOTE PREKEY CHANGED',
}),
);
});
it('flags first-seen pinned contacts as TOFU until verified', () => {
const contact = {
remotePrekeyFingerprint: 'abc123',
remotePrekeyPinnedAt: 123,
verify_registry: false,
verify_inband: false,
verified: false,
};
expect(isFirstContactTrustOnly(contact)).toBe(true);
expect(buildDmTrustHint(contact)).toEqual(
expect.objectContaining({
severity: 'warn',
title: 'FIRST CONTACT (TOFU ONLY)',
}),
);
expect(buildDmTrustHint(contact)?.detail).toContain('not proof of sender identity');
expect(dmTrustPrimaryActionLabel(contact)).toBe('VERIFY SAS NOW');
expect(shouldAutoRevealSasForTrust(contact)).toBe(true);
});
it('auto-reveals SAS for trust hazards but keeps ordinary verified contacts quiet', () => {
expect(
shouldAutoRevealSasForTrust({
remotePrekeyMismatch: true,
}),
).toBe(true);
expect(
shouldAutoRevealSasForTrust({
verify_mismatch: true,
}),
).toBe(true);
expect(
shouldAutoRevealSasForTrust({
verified: true,
verify_inband: true,
verify_registry: true,
}),
).toBe(false);
expect(
dmTrustPrimaryActionLabel({
verified: true,
verify_inband: true,
verify_registry: true,
}),
).toBe('SHOW SAS');
});
it('shortens long trust fingerprints for display', () => {
expect(shortTrustFingerprint('abcdef0123456789fedcba9876543210')).toBe('abcdef01..543210');
expect(shortTrustFingerprint('abcd1234')).toBe('abcd1234');
expect(shortTrustFingerprint('')).toBe('unknown');
});
});
@@ -0,0 +1,175 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
// Track what gets stored in IndexedDB
const idbStore = new Map<string, unknown>();
const deletedDatabases: string[] = [];
vi.mock('@/lib/controlPlane', () => ({
controlPlaneJson: vi.fn(),
}));
vi.mock('@/mesh/meshKeyStore', () => ({
getKey: vi.fn(async (id: string) => idbStore.get(id) ?? null),
setKey: vi.fn(async (id: string, key: unknown) => {
idbStore.set(id, key);
}),
deleteKey: vi.fn(async (id: string) => {
idbStore.delete(id);
}),
}));
function makeStorage() {
const values = new Map<string, string>();
return {
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => void values.set(key, value),
removeItem: (key: string) => void values.delete(key),
clear: () => void values.clear(),
get length() {
return values.size;
},
key: (_i: number) => null as string | null,
};
}
describe('signing key storage hardening', () => {
beforeEach(() => {
vi.resetModules();
idbStore.clear();
deletedDatabases.length = 0;
Object.defineProperty(globalThis, 'localStorage', {
value: makeStorage(),
configurable: true,
writable: true,
});
Object.defineProperty(globalThis, 'sessionStorage', {
value: makeStorage(),
configurable: true,
writable: true,
});
Object.defineProperty(globalThis, 'indexedDB', {
value: {
deleteDatabase: vi.fn((name: string) => {
deletedDatabases.push(name);
const request = {} as IDBOpenDBRequest;
queueMicrotask(() => {
request.onsuccess?.(new Event('success') as Event);
});
return request;
}),
},
configurable: true,
writable: true,
});
});
it('getNodeIdentity returns identity even when privateKey is empty (post-migration)', async () => {
const mod = await import('@/mesh/meshIdentity');
// Simulate a state where the signing key has already been migrated:
// publicKey and nodeId exist, but privateKey does not.
localStorage.setItem('sb_mesh_pubkey', 'test-pub');
localStorage.setItem('sb_mesh_node_id', '!sb_abcd1234abcd1234');
localStorage.setItem('sb_mesh_sovereignty_accepted', 'true');
// No sb_mesh_privkey — simulates post-migration state
const identity = mod.getNodeIdentity();
expect(identity).not.toBeNull();
expect(identity!.publicKey).toBe('test-pub');
expect(identity!.nodeId).toBe('!sb_abcd1234abcd1234');
expect(identity!.privateKey).toBe('');
});
it('getNodeIdentity triggers eager migration and does not expose legacy privateKey', async () => {
const mod = await import('@/mesh/meshIdentity');
localStorage.setItem('sb_mesh_pubkey', 'test-pub');
localStorage.setItem('sb_mesh_node_id', '!sb_abcd1234abcd1234');
localStorage.setItem('sb_mesh_privkey', '{"fake":"jwk"}');
localStorage.setItem('sb_mesh_sovereignty_accepted', 'true');
const identity = mod.getNodeIdentity();
expect(identity).not.toBeNull();
expect(identity!.privateKey).toBe('');
// The eager migration fires asynchronously (void ensureSigningPrivateKey()).
// In this test environment crypto.subtle.importKey will fail on the fake JWK,
// but the extractable browser copy should still be scrubbed.
await new Promise((r) => setTimeout(r, 10));
expect(localStorage.getItem('sb_mesh_privkey')).toBeNull();
expect(identity!.publicKey).toBe('test-pub');
});
it('purgeBrowserSigningMaterial clears IndexedDB signing key', async () => {
const { deleteKey } = await import('@/mesh/meshKeyStore');
const mod = await import('@/mesh/meshIdentity');
idbStore.set('sb_mesh_sign_priv', 'mock-crypto-key');
localStorage.setItem('sb_mesh_privkey', '{"fake":"jwk"}');
localStorage.setItem('sb_mesh_sequence', '42');
await mod.purgeBrowserSigningMaterial();
expect(deleteKey).toHaveBeenCalledWith('sb_mesh_sign_priv');
expect(localStorage.getItem('sb_mesh_privkey')).toBeNull();
expect(localStorage.getItem('sb_mesh_sequence')).toBeNull();
});
it('clearBrowserIdentityState clears both DH and signing keys from IndexedDB', async () => {
const { deleteKey } = await import('@/mesh/meshKeyStore');
const mod = await import('@/mesh/meshIdentity');
localStorage.setItem('sb_mesh_pubkey', 'test-pub');
localStorage.setItem('sb_mesh_node_id', '!sb_test');
localStorage.setItem('sb_mesh_privkey', '{"fake":"jwk"}');
localStorage.setItem('sb_mesh_session_mode', 'true');
localStorage.setItem('sb_mesh_sovereignty_accepted', 'true');
localStorage.setItem('sb_dm_bundle_fingerprint', 'bundle-fp');
sessionStorage.setItem('sb_wormhole_desc_node_id', '!sb_gate');
sessionStorage.setItem('sb_mesh_dm_ratchet', 'encrypted');
sessionStorage.setItem('sb_mesh_ratchet_telemetry', '{"seen":1}');
await mod.clearBrowserIdentityState();
expect(deleteKey).toHaveBeenCalledWith('sb_mesh_dh_priv');
expect(deleteKey).toHaveBeenCalledWith('sb_mesh_sign_priv');
expect(localStorage.getItem('sb_mesh_pubkey')).toBeNull();
expect(localStorage.getItem('sb_mesh_privkey')).toBeNull();
expect(localStorage.getItem('sb_dm_bundle_fingerprint')).toBeNull();
expect(sessionStorage.getItem('sb_wormhole_desc_node_id')).toBeNull();
expect(sessionStorage.getItem('sb_mesh_dm_ratchet')).toBeNull();
expect(sessionStorage.getItem('sb_mesh_ratchet_telemetry')).toBeNull();
expect(deletedDatabases).toContain('sb_mesh_ratchet_crypto');
});
it('generateDHKeys fails closed when non-extractable DH key storage is unavailable', async () => {
const { setKey } = await import('@/mesh/meshKeyStore');
vi.mocked(setKey).mockRejectedValueOnce(new Error('idb unavailable'));
const mod = await import('@/mesh/meshIdentity');
await expect(mod.generateDHKeys()).rejects.toThrow('IndexedDB required for DH key storage');
expect(localStorage.getItem('sb_mesh_dh_privkey')).toBeNull();
expect(sessionStorage.getItem('sb_mesh_dh_privkey')).toBeNull();
});
it('signWithStoredKey is exported and throws when no key available', async () => {
const mod = await import('@/mesh/meshIdentity');
// No key in IndexedDB or localStorage
await expect(mod.signWithStoredKey('test message')).rejects.toThrow(
'No signing key available',
);
});
it('signEvent fails closed when only public identity metadata exists', async () => {
const mod = await import('@/mesh/meshIdentity');
sessionStorage.setItem('sb_mesh_pubkey', 'test-pub');
sessionStorage.setItem('sb_mesh_node_id', '!sb_abcd1234abcd1234');
sessionStorage.setItem('sb_mesh_sovereignty_accepted', 'true');
sessionStorage.setItem('sb_mesh_algo', 'Ed25519');
await expect(
mod.signEvent('message', '!sb_abcd1234abcd1234', 1, { message: 'hello' }),
).rejects.toThrow('No signing key available');
});
});
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest';
import {
getMeshTerminalWriteLockReason,
isMeshTerminalWriteCommand,
} from '@/lib/meshTerminalPolicy';
describe('mesh terminal policy', () => {
it('blocks sensitive terminal writes while anonymous mode is active', () => {
const reason = getMeshTerminalWriteLockReason({
wormholeRequired: true,
wormholeReady: true,
anonymousMode: true,
anonymousModeReady: true,
});
expect(reason).toContain('Anonymous Infonet mode');
expect(isMeshTerminalWriteCommand('dm', ['add', '!sb_test'])).toBe(true);
expect(isMeshTerminalWriteCommand('mesh', ['send', 'hello'])).toBe(true);
});
it('blocks sensitive terminal writes until Wormhole secure mode is ready', () => {
const reason = getMeshTerminalWriteLockReason({
wormholeRequired: true,
wormholeReady: false,
anonymousMode: false,
anonymousModeReady: false,
});
expect(reason).toContain('until Wormhole secure mode is ready');
expect(isMeshTerminalWriteCommand('gate', ['create', 'newsroom'])).toBe(true);
expect(isMeshTerminalWriteCommand('send', ['broadcast', 'hello'])).toBe(true);
});
it('keeps read-only terminal commands available', () => {
expect(isMeshTerminalWriteCommand('status', [])).toBe(false);
expect(isMeshTerminalWriteCommand('signals', ['10'])).toBe(false);
expect(isMeshTerminalWriteCommand('mesh', ['listen', '20'])).toBe(false);
expect(isMeshTerminalWriteCommand('messages', [])).toBe(false);
});
});
@@ -0,0 +1,224 @@
import {
getSenderRecoveryState,
REQUEST_V2_REDUCED_VERSION,
recoverSenderSealWithFallback,
requiresSenderRecovery,
shouldAllowRequestActions,
shouldKeepUnresolvedRequestVisible,
shouldPromoteRecoveredSenderForBootstrap,
shouldPromoteRecoveredSenderForKnownContact,
} from '@/mesh/requestSenderRecovery';
describe('requestSenderRecovery', () => {
it('only promotes a known-contact sender when the seal verified and the sender matches', () => {
expect(
shouldPromoteRecoveredSenderForKnownContact(
{ sender_id: 'alice', seal_verified: true },
'alice',
),
).toBe(true);
expect(
shouldPromoteRecoveredSenderForKnownContact(
{ sender_id: 'alice', seal_verified: false },
'alice',
),
).toBe(false);
expect(
shouldPromoteRecoveredSenderForKnownContact(
{ sender_id: 'mallory', seal_verified: true },
'alice',
),
).toBe(false);
});
it('only promotes a bootstrap-recovered sender when the seal verified', () => {
expect(
shouldPromoteRecoveredSenderForBootstrap({
sender_id: 'alice',
seal_verified: true,
}),
).toBe(true);
expect(
shouldPromoteRecoveredSenderForBootstrap({
sender_id: 'alice',
seal_verified: false,
}),
).toBe(false);
expect(shouldPromoteRecoveredSenderForBootstrap(null)).toBe(false);
});
it('prefers explicit request-v2 recovery markers over sealed-string inference', () => {
expect(
requiresSenderRecovery({
sender_id: 'opaque',
sender_seal: 'v3:test',
request_contract_version: REQUEST_V2_REDUCED_VERSION,
sender_recovery_required: true,
}),
).toBe(true);
expect(
getSenderRecoveryState({
sender_id: 'opaque',
sender_seal: 'v3:test',
request_contract_version: REQUEST_V2_REDUCED_VERSION,
sender_recovery_required: true,
}),
).toBe('pending');
expect(
requiresSenderRecovery({
sender_id: 'sealed:abcd',
sender_seal: 'v2:test',
}),
).toBe(true);
});
it('only allows request actions once canonical recovery reaches verified', () => {
expect(
shouldAllowRequestActions({
request_contract_version: REQUEST_V2_REDUCED_VERSION,
sender_recovery_required: true,
sender_recovery_state: 'verified',
}),
).toBe(true);
expect(
shouldAllowRequestActions({
request_contract_version: REQUEST_V2_REDUCED_VERSION,
sender_recovery_required: true,
sender_recovery_state: 'pending',
}),
).toBe(false);
expect(
shouldAllowRequestActions({
request_contract_version: REQUEST_V2_REDUCED_VERSION,
sender_recovery_required: true,
sender_recovery_state: 'failed',
}),
).toBe(false);
expect(
shouldAllowRequestActions({
request_contract_version: undefined,
sender_recovery_required: undefined,
sender_recovery_state: undefined,
}),
).toBe(true);
});
it('keeps only pending or failed canonical request-v2 mail visible in the unresolved inbox flow', () => {
expect(
shouldKeepUnresolvedRequestVisible({
delivery_class: 'request',
request_contract_version: REQUEST_V2_REDUCED_VERSION,
sender_recovery_required: true,
sender_recovery_state: 'pending',
}),
).toBe(true);
expect(
shouldKeepUnresolvedRequestVisible({
delivery_class: 'request',
request_contract_version: REQUEST_V2_REDUCED_VERSION,
sender_recovery_required: true,
sender_recovery_state: 'failed',
}),
).toBe(true);
expect(
shouldKeepUnresolvedRequestVisible({
delivery_class: 'request',
request_contract_version: REQUEST_V2_REDUCED_VERSION,
sender_recovery_required: true,
sender_recovery_state: 'verified',
}),
).toBe(false);
expect(
shouldKeepUnresolvedRequestVisible({
delivery_class: 'request',
request_contract_version: undefined,
sender_recovery_required: undefined,
sender_recovery_state: 'pending',
}),
).toBe(false);
expect(
shouldKeepUnresolvedRequestVisible({
delivery_class: 'shared',
request_contract_version: REQUEST_V2_REDUCED_VERSION,
sender_recovery_required: true,
sender_recovery_state: 'pending',
}),
).toBe(false);
});
it('prefers local recovery and only falls back to the helper on local failure', async () => {
const openLocal = vi.fn().mockResolvedValue({
sender_id: 'alice',
seal_verified: true,
});
const openHelper = vi.fn().mockResolvedValue({
sender_id: 'helper-alice',
seal_verified: true,
});
await expect(
recoverSenderSealWithFallback({
wormholeReady: true,
openLocal,
openHelper,
}),
).resolves.toEqual({ sender_id: 'alice', seal_verified: true });
expect(openLocal).toHaveBeenCalledTimes(1);
expect(openHelper).not.toHaveBeenCalled();
});
it('uses the helper only as fallback when local recovery cannot open the seal', async () => {
const openLocal = vi.fn().mockResolvedValue(null);
const openHelper = vi.fn().mockResolvedValue({
sender_id: 'alice',
seal_verified: true,
});
await expect(
recoverSenderSealWithFallback({
wormholeReady: true,
openLocal,
openHelper,
}),
).resolves.toEqual({ sender_id: 'alice', seal_verified: true });
expect(openLocal).toHaveBeenCalledTimes(1);
expect(openHelper).toHaveBeenCalledTimes(1);
});
it('does not invoke the helper when Wormhole fallback is unavailable', async () => {
const openLocal = vi.fn().mockResolvedValue(null);
const openHelper = vi.fn().mockResolvedValue({
sender_id: 'alice',
seal_verified: true,
});
await expect(
recoverSenderSealWithFallback({
wormholeReady: false,
openLocal,
openHelper,
}),
).resolves.toBeNull();
expect(openLocal).toHaveBeenCalledTimes(1);
expect(openHelper).not.toHaveBeenCalled();
});
it('treats helper failure as unresolved instead of promoting helper authority', async () => {
const openLocal = vi.fn().mockResolvedValue(null);
const openHelper = vi.fn().mockRejectedValue(new Error('helper_failed'));
await expect(
recoverSenderSealWithFallback({
wormholeReady: true,
openLocal,
openHelper,
}),
).resolves.toBeNull();
expect(openLocal).toHaveBeenCalledTimes(1);
expect(openHelper).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,40 @@
import {
ensureCanonicalRequestV2SenderSeal,
REQUEST_V2_SENDER_SEAL_VERSION_ERROR,
requiresCanonicalRequestV2SenderSeal,
} from '@/mesh/requestSenderSealPolicy';
describe('requestSenderSealPolicy', () => {
it('requires canonical v3 seals only for request-class sealed sender', () => {
expect(
requiresCanonicalRequestV2SenderSeal({
deliveryClass: 'request',
useSealedSender: true,
}),
).toBe(true);
expect(
requiresCanonicalRequestV2SenderSeal({
deliveryClass: 'request',
useSealedSender: false,
}),
).toBe(false);
expect(
requiresCanonicalRequestV2SenderSeal({
deliveryClass: 'shared',
useSealedSender: true,
}),
).toBe(false);
});
it('accepts v3 seals and rejects non-v3 seals for canonical request-v2 sender sealing', () => {
expect(ensureCanonicalRequestV2SenderSeal('v3:ephemeral:payload')).toBe(
'v3:ephemeral:payload',
);
expect(() => ensureCanonicalRequestV2SenderSeal('v2:legacy-payload')).toThrow(
REQUEST_V2_SENDER_SEAL_VERSION_ERROR,
);
expect(() => ensureCanonicalRequestV2SenderSeal('')).toThrow(
REQUEST_V2_SENDER_SEAL_VERSION_ERROR,
);
});
});
@@ -0,0 +1,178 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const controlPlaneJson = vi.fn();
const idbStore = new Map<string, unknown>();
vi.mock('@/lib/controlPlane', () => ({
controlPlaneJson,
}));
vi.mock('@/mesh/meshKeyStore', () => ({
getKey: vi.fn(async (id: string) => idbStore.get(id) ?? null),
setKey: vi.fn(async (id: string, key: unknown) => {
idbStore.set(id, key);
}),
deleteKey: vi.fn(async (id: string) => {
idbStore.delete(id);
}),
}));
function bufToBase64(buf: ArrayBuffer): string {
return btoa(String.fromCharCode(...new Uint8Array(buf)));
}
async function buildV3SealForRecipient(params: {
recipientPublicKey: CryptoKey;
recipientId: string;
msgId: string;
plaintext: string;
}) {
const { encryptDM } = await import('@/mesh/meshIdentity');
const { PROTOCOL_VERSION } = await import('@/mesh/meshProtocol');
const ephemeral = (await crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: 'P-256' },
true,
['deriveBits', 'deriveKey'],
)) as CryptoKeyPair;
const ephemeralPubRaw = await crypto.subtle.exportKey('raw', ephemeral.publicKey);
const ephemeralPub = bufToBase64(ephemeralPubRaw);
const secret = await crypto.subtle.deriveBits(
{ name: 'ECDH', public: params.recipientPublicKey },
ephemeral.privateKey,
256,
);
const salt = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(
`SB-SEAL-SALT|${params.recipientId}|${params.msgId}|${PROTOCOL_VERSION}|${ephemeralPub}`,
),
);
const hkdfKey = await crypto.subtle.importKey('raw', secret, 'HKDF', false, ['deriveKey']);
const sealKey = await crypto.subtle.deriveKey(
{
name: 'HKDF',
hash: 'SHA-256',
salt,
info: new TextEncoder().encode('SB-SENDER-SEAL-V3'),
},
hkdfKey,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt'],
);
const ciphertext = await encryptDM(params.plaintext, sealKey);
return `v3:${ephemeralPub}:${ciphertext}`;
}
describe('request sender seal recovery window', () => {
beforeEach(() => {
vi.resetModules();
controlPlaneJson.mockReset();
idbStore.clear();
const makeStorage = () => {
const values = new Map<string, string>();
return {
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => void values.set(key, value),
removeItem: (key: string) => void values.delete(key),
clear: () => void values.clear(),
};
};
Object.defineProperty(globalThis, 'localStorage', {
value: makeStorage(),
configurable: true,
writable: true,
});
Object.defineProperty(globalThis, 'sessionStorage', {
value: makeStorage(),
configurable: true,
writable: true,
});
});
it('opens a v3 sender seal with the immediately previous retained recipient key', async () => {
const mod = await import('@/mesh/meshIdentity');
const previousRecipient = (await crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: 'P-256' },
false,
['deriveBits', 'deriveKey'],
)) as CryptoKeyPair;
const currentRecipient = (await crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: 'P-256' },
false,
['deriveBits', 'deriveKey'],
)) as CryptoKeyPair;
idbStore.set('sb_mesh_dh_priv', currentRecipient.privateKey);
idbStore.set('sb_mesh_dh_prev_priv', previousRecipient.privateKey);
localStorage.setItem('sb_mesh_dh_algo', 'ECDH');
const plaintext = JSON.stringify({ sender_id: 'alice', msg_id: 'msg-rotation' });
const senderSeal = await buildV3SealForRecipient({
recipientPublicKey: previousRecipient.publicKey,
recipientId: '!sb_recipient',
msgId: 'msg-rotation',
plaintext,
});
await expect(
mod.decryptSenderSealPayloadLocally(senderSeal, '', '!sb_recipient', 'msg-rotation'),
).resolves.toBe(plaintext);
});
it('returns null when the prior retained recipient key is unavailable', async () => {
const mod = await import('@/mesh/meshIdentity');
const previousRecipient = (await crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: 'P-256' },
false,
['deriveBits', 'deriveKey'],
)) as CryptoKeyPair;
const currentRecipient = (await crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: 'P-256' },
false,
['deriveBits', 'deriveKey'],
)) as CryptoKeyPair;
idbStore.set('sb_mesh_dh_priv', currentRecipient.privateKey);
localStorage.setItem('sb_mesh_dh_algo', 'ECDH');
const senderSeal = await buildV3SealForRecipient({
recipientPublicKey: previousRecipient.publicKey,
recipientId: '!sb_recipient',
msgId: 'msg-rotation-miss',
plaintext: JSON.stringify({ sender_id: 'alice', msg_id: 'msg-rotation-miss' }),
});
await expect(
mod.decryptSenderSealPayloadLocally(senderSeal, '', '!sb_recipient', 'msg-rotation-miss'),
).resolves.toBeNull();
});
it('retains the current DH private key in the previous-key slot when rotating', async () => {
const mod = await import('@/mesh/meshIdentity');
const existing = (await crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: 'P-256' },
false,
['deriveBits', 'deriveKey'],
)) as CryptoKeyPair;
const originalGenerateKey = crypto.subtle.generateKey.bind(crypto.subtle);
const generateKeySpy = vi
.spyOn(crypto.subtle, 'generateKey')
.mockImplementation(((algorithm: AlgorithmIdentifier, extractable: boolean, keyUsages: KeyUsage[]) => {
if (algorithm === 'X25519') {
return Promise.reject(new Error('x25519_unavailable_for_test'));
}
return originalGenerateKey(algorithm, extractable, keyUsages);
}) as typeof crypto.subtle.generateKey);
idbStore.set('sb_mesh_dh_priv', existing.privateKey);
try {
await mod.generateDHKeys();
expect(idbStore.get('sb_mesh_dh_prev_priv')).toBe(existing.privateKey);
expect(idbStore.get('sb_mesh_dh_priv')).not.toBe(existing.privateKey);
} finally {
generateKeySpy.mockRestore();
}
});
});
@@ -0,0 +1,125 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const controlPlaneJson = vi.fn();
const getNodeIdentity = vi.fn<
() => { nodeId: string; publicKey: string; privateKey: string } | null
>(() => null);
const signEvent = vi.fn();
const signMessage = vi.fn();
const signWithStoredKey = vi.fn();
const isSecureModeCached = vi.fn(() => true);
const fetchWormholeSettings = vi.fn(async () => ({ enabled: true }));
const fetchWormholeState = vi.fn(async () => ({ ready: true }));
vi.mock('@/lib/controlPlane', () => ({
controlPlaneJson,
}));
vi.mock('@/mesh/meshIdentity', () => ({
cacheWormholeIdentityDescriptor: vi.fn(),
getNodeIdentity,
getPublicKeyAlgo: vi.fn(() => 'ed25519'),
isSecureModeCached,
purgeBrowserSigningMaterial: vi.fn(async () => {}),
setSecureModeCached: vi.fn(),
signEvent,
signMessage,
signWithStoredKey,
}));
vi.mock('@/mesh/meshProtocol', () => ({
PROTOCOL_VERSION: 'sb-test',
}));
vi.mock('@/mesh/wormholeClient', () => ({
fetchWormholeSettings,
fetchWormholeState,
}));
describe('wormholeIdentityClient strict profile hints', () => {
beforeEach(() => {
vi.resetModules();
controlPlaneJson.mockReset();
controlPlaneJson.mockResolvedValue({ ok: true });
getNodeIdentity.mockReset();
getNodeIdentity.mockReturnValue(null);
signEvent.mockReset();
signMessage.mockReset();
signWithStoredKey.mockReset();
isSecureModeCached.mockReset();
isSecureModeCached.mockReturnValue(true);
fetchWormholeSettings.mockReset();
fetchWormholeSettings.mockResolvedValue({ enabled: true });
fetchWormholeState.mockReset();
fetchWormholeState.mockResolvedValue({ ready: true });
});
it('applies strict gate_operator enforcement to gate persona and compose operations', async () => {
const mod = await import('@/mesh/wormholeIdentityClient');
await mod.listWormholeGatePersonas('infonet');
await mod.createWormholeGatePersona('infonet', 'persona-1');
await mod.activateWormholeGatePersona('infonet', 'persona-1');
await mod.clearWormholeGatePersona('infonet');
await mod.retireWormholeGatePersona('infonet', 'persona-1');
await mod.composeWormholeGateMessage('infonet', 'hello');
expect(controlPlaneJson).toHaveBeenNthCalledWith(
1,
'/api/wormhole/gate/infonet/personas',
expect.objectContaining({
capabilityIntent: 'wormhole_gate_persona',
sessionProfileHint: 'gate_operator',
enforceProfileHint: true,
}),
);
for (let i = 2; i <= 5; i += 1) {
expect(controlPlaneJson).toHaveBeenNthCalledWith(
i,
expect.any(String),
expect.objectContaining({
capabilityIntent: 'wormhole_gate_persona',
sessionProfileHint: 'gate_operator',
enforceProfileHint: true,
}),
);
}
expect(controlPlaneJson).toHaveBeenNthCalledWith(
6,
'/api/wormhole/gate/message/compose',
expect.objectContaining({
capabilityIntent: 'wormhole_gate_content',
sessionProfileHint: 'gate_operator',
enforceProfileHint: true,
}),
);
});
it('browser raw signing fails closed instead of falling back to legacy jwk signing', async () => {
fetchWormholeSettings.mockResolvedValue({ enabled: false });
fetchWormholeState.mockResolvedValue({ ready: false });
getNodeIdentity.mockReturnValue({
nodeId: '!sb_browser',
publicKey: 'browser-pub',
privateKey: '',
});
signWithStoredKey.mockRejectedValue(new Error('no key'));
const mod = await import('@/mesh/wormholeIdentityClient');
await expect(mod.signRawMeshMessage('payload')).rejects.toThrow(
'browser_signing_key_unavailable',
);
expect(signWithStoredKey).toHaveBeenCalledWith('payload');
expect(signMessage).not.toHaveBeenCalled();
});
it('keeps the cached secure boundary when wormhole settings fetch fails', async () => {
fetchWormholeSettings.mockRejectedValue(new Error('network down'));
isSecureModeCached.mockReturnValue(true);
const mod = await import('@/mesh/wormholeIdentityClient');
await expect(mod.isWormholeSecureRequired()).resolves.toBe(true);
});
});