Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 113 additions & 3 deletions apps/desktop/e2e/config-canary.e2e.mts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ import { join } from 'node:path';
import { noop } from 'foxts/noop';
import { wait } from 'foxts/wait';
import type { ElectronApplication } from 'playwright-core';
import type { DistServer, ServerMode } from './config-canary/dist-server.mts';
import { startDistServer, waitForRequest } from './config-canary/dist-server.mts';
import type { DistServer, EmergencyServerMode, ServerMode } from './config-canary/dist-server.mts';
import {
startDistServer,
startEmergencyServer,
waitForRequest,
} from './config-canary/dist-server.mts';
import {
buildDesktopWithBootstrap,
EMERGENCY_PORT,
generateTlsMaterial,
launchApp,
PORT,
Expand All @@ -21,18 +26,21 @@ import { baseline, canary, rollback, rollForward } from './config-canary/fixture
import type { ConfigStateFile } from './config-canary/state-file.mts';
import {
readConfigState,
readEmergencyState,
waitForConfigState,
writeCorruptConfigState,
} from './config-canary/state-file.mts';

const REJECTION_SETTLE_MS = 1000;

let mode: ServerMode = 'offline';
let emergencyMode: EmergencyServerMode = 'offline';

interface Harness {
app: ElectronApplication | null;
readonly caCert: string;
readonly dist: DistServer;
readonly emergency: DistServer;
readonly home: string;
readonly userData: string;
}
Expand Down Expand Up @@ -199,6 +207,106 @@ async function driveScenarios(harness: Harness): Promise<void> {
assertAccepted(state, baseline);
console.log('PASS baseline republication recovers after corrupt-state reset');
});

emergencyMode = 'kill-switch';
harness.emergency.requests.length = 0;
await withLaunch(harness, 'offline', async () => {
const app = assertApp(harness);
const boundary = await refreshBoundary(app);
assert.equal(boundary.report.normal, 'error');
assert.equal(boundary.report.emergency, 'updated');
Comment on lines +216 to +217

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This asserts a transient refresh status that races the app's own background emergency refresh. startDesktopConfigRefresh() runs at app-ready before createDesktopWindow() (apps/desktop/src/main/index.ts:75) and schedules an emergency refresh 1s later (FIRST_EMERGENCY_REFRESH_DELAY_MS, apps/desktop/src/main/config.ts:38), while launchApp only waits for body to be visible. If that background refresh lands first it accepts the document and stores the ETag, so this explicit refresh() sends If-None-Match, the new server 304s (dist-server.mts:186), and report.emergency is 'not-modified'. Same exposure at lines 237 and 262.

const emergency = boundary.info.emergency;
assert(emergency);
assert.deepEqual(emergency.disabledFeatures, ['feature.aiAssist']);
assert.equal(emergency.emergencyVersion, '1');
await waitForRequest(
harness.dist,
(request) => request.path === baseline.pointerPath && request.status === 'disconnected',
);
await waitForRequest(
harness.emergency,
(request) => request.mode === 'kill-switch' && request.status === 200,
);
console.log('PASS emergency origin activates kill switch during main-channel outage');
});

emergencyMode = 'forced-minimum';
harness.emergency.requests.length = 0;
await withLaunch(harness, 'offline', async () => {
const boundary = await refreshBoundary(assertApp(harness));
assert.equal(boundary.report.emergency, 'updated');
const emergency = boundary.info.emergency;
assert(emergency);
assert.equal(emergency.emergencyVersion, '2');
assert.equal(emergency.forceMinVersion, '2.4.0');
console.log('PASS newer emergency state replaces the persisted kill switch');
});

emergencyMode = 'offline';
harness.emergency.requests.length = 0;
await withLaunch(harness, 'offline', async () => {
const boundary = await refreshBoundary(assertApp(harness));
assert.equal(boundary.report.emergency, 'error');
const emergency = boundary.info.emergency;
assert(emergency);
assert.equal(emergency.emergencyVersion, '2');
assert.equal(emergency.forceMinVersion, '2.4.0');
console.log('PASS forced minimum survives reconstructed runtime and emergency outage');
});

emergencyMode = 'release';
harness.emergency.requests.length = 0;
let releaseRaw = '';
await withLaunch(harness, 'offline', async () => {
const boundary = await refreshBoundary(assertApp(harness));
assert.equal(boundary.report.emergency, 'updated');
const emergency = boundary.info.emergency;
assert(emergency);
assert.deepEqual(emergency.disabledFeatures, []);
assert.equal(emergency.emergencyVersion, '3');
releaseRaw = (await readEmergencyState(harness.home))?.raw ?? '';
assert(releaseRaw);
console.log('PASS explicit newer release clears emergency restrictions');
});

emergencyMode = 'equivocation';
harness.emergency.requests.length = 0;
await withLaunch(harness, 'offline', async () => {
const boundary = await refreshBoundary(assertApp(harness));
assert.equal(boundary.report.emergency, 'error');
const emergency = boundary.info.emergency;
assert(emergency);
assert.deepEqual(emergency.disabledFeatures, []);
assert.equal(emergency.emergencyVersion, '3');
assert.equal((await readEmergencyState(harness.home))?.raw, releaseRaw);
console.log('PASS equal-version emergency equivocation cannot replace explicit release');
Comment on lines +276 to +282

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scenario passes without the equivocating document ever being delivered — report.emergency === 'error' and unchanged persisted bytes are equally consistent with a 404, a path typo, or a dead server, which is exactly what the two 'offline' scenarios around it assert. Since anti-equivocation is the security property being claimed, add the delivery proof the kill-switch scenario already uses at line 226: await waitForRequest(harness.emergency, (request) => request.mode === 'equivocation' && request.status === 200);. The requests.length = 0 reset on line 273 is already set up for it.

});

emergencyMode = 'offline';
harness.emergency.requests.length = 0;
await withLaunch(harness, 'offline', async () => {
const boundary = await refreshBoundary(assertApp(harness));
assert.equal(boundary.report.emergency, 'error');
const emergency = boundary.info.emergency;
assert(emergency);
assert.deepEqual(emergency.disabledFeatures, []);
assert.equal(emergency.emergencyVersion, '3');
assert.equal((await readEmergencyState(harness.home))?.raw, releaseRaw);
console.log('PASS explicit release remains sticky across reconstructed runtime and outage');
});
}

function assertApp(harness: Harness): ElectronApplication {
assert(harness.app);
return harness.app;
}

async function refreshBoundary(app: ElectronApplication) {
const page = await app.firstWindow();
return page.evaluate(async () => {
const report = await window.linkcodeConfig.refresh();
return { info: window.linkcodeConfig.snapshotInfo(), report };
});
}

async function main(): Promise<void> {
Expand All @@ -210,7 +318,8 @@ async function main(): Promise<void> {
const tls = generateTlsMaterial(scratch);
buildDesktopWithBootstrap();
const dist = await startDistServer(tls, PORT, () => mode);
harness = { app: null, caCert: tls.cert, dist, home, userData };
const emergency = await startEmergencyServer(tls, EMERGENCY_PORT, () => emergencyMode);
harness = { app: null, caCert: tls.cert, dist, emergency, home, userData };
await driveScenarios(harness);

console.log(
Expand All @@ -219,6 +328,7 @@ async function main(): Promise<void> {
} finally {
await harness?.app?.close().catch(noop);
harness?.dist.server.close();
harness?.emergency.server.close();
rmSync(scratch, { recursive: true, force: true });
}
}
Expand Down
59 changes: 58 additions & 1 deletion apps/desktop/e2e/config-canary/dist-server.mts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { PilotFixtureStep } from './fixture.mts';
import {
baseline,
canary,
emergencyBytes,
fixture,
pointerBytes,
rollback,
Expand All @@ -27,9 +28,16 @@ export type ServerMode =
| 'tampered-pointer'
| 'tampered-snapshot';

export type EmergencyServerMode =
| 'equivocation'
| 'forced-minimum'
| 'kill-switch'
| 'offline'
| 'release';

export interface DistRequest {
readonly ifNoneMatch: string | null;
readonly mode: ServerMode;
readonly mode: EmergencyServerMode | ServerMode;
readonly path: string;
readonly status: 200 | 304 | 404 | 'disconnected';
}
Expand Down Expand Up @@ -148,6 +156,55 @@ export function startDistServer(
});
}

export function startEmergencyServer(
tls: { cert: string; key: string },
port: number,
getMode: () => EmergencyServerMode,
): Promise<DistServer> {
const requests: DistRequest[] = [];
const server = createServer(
{ cert: readFileSync(tls.cert), key: readFileSync(tls.key) },
(request, response) => {
const mode = getMode();
const path = new URL(request.url ?? '/', `https://127.0.0.1:${port}`).pathname;
const header = request.headers['if-none-match'];
const ifNoneMatch = typeof header === 'string' ? header : null;
if (mode === 'offline') {
requests.push({ ifNoneMatch, mode, path, status: 'disconnected' });
request.socket.destroy();
return;
}
if (path !== '/v1/acme/desktop/emergency.json') {
requests.push({ ifNoneMatch, mode, path, status: 404 });
response.writeHead(404).end();
return;
}
const name =
mode === 'kill-switch' ? 'killSwitch' : mode === 'forced-minimum' ? 'forcedMinimum' : mode;
const bytes = emergencyBytes(name);
const etag = `"emergency-${mode}"`;
if (ifNoneMatch === etag) {
requests.push({ ifNoneMatch, mode, path, status: 304 });
response.writeHead(304, { etag }).end();
return;
}
requests.push({ ifNoneMatch, mode, path, status: 200 });
response
.writeHead(200, {
'cache-control': 'public, max-age=60, must-revalidate',
'content-length': bytes.byteLength,
'content-type': 'application/json',
etag,
})
.end(bytes);
},
);
return new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(port, '127.0.0.1', () => resolve({ requests, server }));
});
}

export function waitForRequest(
server: DistServer,
predicate: (request: DistRequest) => boolean,
Expand Down
7 changes: 4 additions & 3 deletions apps/desktop/e2e/config-canary/electron-app.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import { join, resolve as resolvePath } from 'node:path';
import type { ElectronApplication } from 'playwright-core';
import { _electron } from 'playwright-core';

import { fixture } from './fixture.mts';
import { emergencyFixture, fixture } from './fixture.mts';

const require = createRequire(import.meta.url);
const desktopDir = resolvePath(import.meta.dirname, '../..');
const electronBinary = require('electron') as unknown as string;

export const PORT = 44100 + (process.pid % 1000);
export const EMERGENCY_PORT = PORT + 1000;

export function generateTlsMaterial(directory: string): { cert: string; key: string } {
const key = join(directory, 'key.pem');
Expand Down Expand Up @@ -49,8 +50,8 @@ export function buildDesktopWithBootstrap(): void {
brandId: fixture.target.brandId,
channel: fixture.target.channel,
defaults: fixture.bootstrapDefaults,
emergencyEndpoint: null,
emergencyPublicKeys: {},
emergencyEndpoint: `https://127.0.0.1:${EMERGENCY_PORT}`,
emergencyPublicKeys: emergencyFixture.keys.emergency,
endpoint: `https://127.0.0.1:${PORT}`,
maximumSchemaVersion: fixture.maximumSchemaVersion,
publicKeys: fixture.keys,
Expand Down
22 changes: 22 additions & 0 deletions apps/desktop/e2e/config-canary/fixture.mts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,28 @@ interface PilotFixture {
readonly target: { brandId: string; channel: string; platform: string };
}

interface EmergencyFixture {
readonly documents: Readonly<
Record<
'equivocation' | 'forcedMinimum' | 'killSwitch' | 'release',
{ readonly document: Readonly<Record<string, unknown>> }
>
>;
readonly keys: { readonly emergency: Readonly<Record<string, string>> };
}

export const fixture = JSON.parse(
readFileSync(join(import.meta.dirname, '../fixtures/pilot-e2e-v1.json'), 'utf8'),
) as PilotFixture;
export const emergencyFixture = JSON.parse(
readFileSync(
join(
import.meta.dirname,
'../../../../packages/foundation/common/src/config/__fixtures__/emergency-handoff-v1.json',
),
'utf8',
),
) as EmergencyFixture;

function fixtureStep(name: PilotFixtureStep['name']): PilotFixtureStep {
const found = fixture.steps.find((step) => step.name === name);
Expand All @@ -45,6 +64,9 @@ export function pointerBytes(step: PilotFixtureStep): Buffer {
export function snapshotBytes(step: PilotFixtureStep): Buffer {
return Buffer.from(step.snapshotBase64Url, 'base64url');
}
export function emergencyBytes(name: keyof EmergencyFixture['documents']): Buffer {
return Buffer.from(JSON.stringify(emergencyFixture.documents[name].document), 'utf8');
}

// Same byte length keeps the JSON canonical while invalidating the Ed25519 signature.
export const tamperedPointer = Buffer.from(
Expand Down
19 changes: 15 additions & 4 deletions apps/desktop/e2e/config-canary/state-file.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { join } from 'node:path';
import { asyncRetry } from 'foxts/async-retry';

const STORAGE_KEY = 'linkcode-config:v1:normal:acme:desktop:canary';
const EMERGENCY_STORAGE_KEY = 'linkcode-config:v1:emergency:acme:desktop';

export interface ConfigState {
readonly highWater?: { readonly payloadSha256: string; readonly version: string };
Expand All @@ -17,19 +18,29 @@ export interface ConfigStateFile {
readonly value: ConfigState;
}

function statePath(home: string): string {
function statePath(home: string, storageKey: string): string {
return join(
home,
'.config',
'LinkCode Development',
'config',
`${Buffer.from(STORAGE_KEY).toString('base64url')}.json`,
`${Buffer.from(storageKey).toString('base64url')}.json`,
);
}

export async function readConfigState(home: string): Promise<ConfigStateFile | null> {
try {
const raw = await readFile(statePath(home), 'utf8');
const raw = await readFile(statePath(home, STORAGE_KEY), 'utf8');
return { raw, value: JSON.parse(raw) as ConfigState };
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
throw error;
}
}

export async function readEmergencyState(home: string): Promise<ConfigStateFile | null> {
try {
const raw = await readFile(statePath(home, EMERGENCY_STORAGE_KEY), 'utf8');
return { raw, value: JSON.parse(raw) as ConfigState };
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
Expand Down Expand Up @@ -58,5 +69,5 @@ export function waitForConfigState(
}

export function writeCorruptConfigState(home: string): Promise<void> {
return writeFile(statePath(home), '{"lkg":"corrupted', 'utf8');
return writeFile(statePath(home, STORAGE_KEY), '{"lkg":"corrupted', 'utf8');
}
Loading