feat(web): complete capability-driven instance modules

This commit is contained in:
chick
2026-07-17 21:48:20 +08:00
parent 0c84e8c88e
commit 5eb8680393
10 changed files with 2074 additions and 0 deletions
+71
View File
@@ -6,15 +6,22 @@ import { createEventStreamClient, type EventStreamClient } from './events/event-
import { FleetPage, type FleetDataSource, type FleetSnapshot } from './fleet/fleet-page.js';
import { createFleetApiDataSource } from './fleet/fleet-api-data-source.js';
import { AutomationModule, type AutomationDataSource } from './instances/automation-module.js';
import { CallsModule, type CallsDataSource } from './instances/calls-module.js';
import { CellularModule, type CellularDataSource } from './instances/cellular-module.js';
import {
DeviceNetworkModule,
type DeviceNetworkDataSource,
} from './instances/device-network-module.js';
import { EsimModule, type EsimDataSource } from './instances/esim-module.js';
import { OverviewSystemPage, type OverviewDataSource } from './instances/overview-system.js';
import { InstanceEditor, type InstanceDataSource } from './instances/instance-crud.js';
import { MessagesModule, type MessagesDataSource } from './instances/messages-module.js';
import {
NotificationsModule,
type NotificationsDataSource,
} from './instances/notifications-module.js';
import { OtaModule, type OtaDataSource } from './instances/ota-module.js';
import {
InstanceDetail,
INSTANCE_MODULE_LABELS,
@@ -74,6 +81,10 @@ export interface AppShellProps {
deviceNetworkDataSource?: DeviceNetworkDataSource;
messagesDataSource?: MessagesDataSource;
callsDataSource?: CallsDataSource;
esimDataSource?: EsimDataSource;
notificationsDataSource?: NotificationsDataSource;
automationDataSource?: AutomationDataSource;
otaDataSource?: OtaDataSource;
eventStreamClient?: EventStreamClient;
}
@@ -146,6 +157,10 @@ function Page({
deviceNetworkDataSource,
messagesDataSource,
callsDataSource,
esimDataSource,
notificationsDataSource,
automationDataSource,
otaDataSource,
fleetRefreshSignal,
detailRefreshSignal,
}: {
@@ -161,6 +176,10 @@ function Page({
deviceNetworkDataSource: DeviceNetworkDataSource | undefined;
messagesDataSource: MessagesDataSource | undefined;
callsDataSource: CallsDataSource | undefined;
esimDataSource: EsimDataSource | undefined;
notificationsDataSource: NotificationsDataSource | undefined;
automationDataSource: AutomationDataSource | undefined;
otaDataSource: OtaDataSource | undefined;
fleetRefreshSignal: number;
detailRefreshSignal: number;
}): ReactNode {
@@ -259,6 +278,50 @@ function Page({
),
}
: {})}
{...(module === 'esim' && instance
? {
moduleContent: (
<EsimModule
instance={instance}
{...(esimDataSource ? { dataSource: esimDataSource } : {})}
refreshSignal={detailRefreshSignal}
/>
),
}
: {})}
{...(module === 'notifications' && instance
? {
moduleContent: (
<NotificationsModule
instance={instance}
{...(notificationsDataSource ? { dataSource: notificationsDataSource } : {})}
refreshSignal={detailRefreshSignal}
/>
),
}
: {})}
{...(module === 'automation' && instance
? {
moduleContent: (
<AutomationModule
instance={instance}
{...(automationDataSource ? { dataSource: automationDataSource } : {})}
refreshSignal={detailRefreshSignal}
/>
),
}
: {})}
{...(module === 'ota' && instance
? {
moduleContent: (
<OtaModule
instance={instance}
{...(otaDataSource ? { dataSource: otaDataSource } : {})}
refreshSignal={detailRefreshSignal}
/>
),
}
: {})}
/>
);
}
@@ -294,6 +357,10 @@ export function AppShell({
deviceNetworkDataSource,
messagesDataSource,
callsDataSource,
esimDataSource,
notificationsDataSource,
automationDataSource,
otaDataSource,
eventStreamClient,
}: AppShellProps) {
const defaultEventStreamClient = useMemo(() => createEventStreamClient(), []);
@@ -351,6 +418,10 @@ export function AppShell({
deviceNetworkDataSource={deviceNetworkDataSource}
messagesDataSource={messagesDataSource}
callsDataSource={callsDataSource}
esimDataSource={esimDataSource}
notificationsDataSource={notificationsDataSource}
automationDataSource={automationDataSource}
otaDataSource={otaDataSource}
fleetRefreshSignal={refresh.fleet}
detailRefreshSignal={refresh.detail}
/>
+27
View File
@@ -104,4 +104,31 @@ export {
type MessagesSnapshot,
} from './instances/messages-module.js';
export {
AutomationModule,
sanitizeAutomationSnapshot,
type AutomationDataSource,
type AutomationModuleProps,
type AutomationSnapshot,
} from './instances/automation-module.js';
export {
EsimModule,
type EsimDataSource,
type EsimModuleProps,
type EsimSnapshot,
} from './instances/esim-module.js';
export {
NotificationsModule,
sanitizeNotificationsSnapshot,
type NotificationsDataSource,
type NotificationsModuleProps,
type NotificationsSnapshot,
} from './instances/notifications-module.js';
export {
OtaModule,
type OtaDataSource,
type OtaModuleProps,
type OtaSnapshot,
} from './instances/ota-module.js';
export const webWorkspaceReady = true;
@@ -0,0 +1,196 @@
// @vitest-environment jsdom
import { cleanup, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { InstanceContext } from '../app-shell.js';
import {
AutomationModule,
type AutomationDataSource,
type AutomationSnapshot,
} from './automation-module.js';
afterEach(cleanup);
const owner: InstanceContext = {
id: 'alpha',
name: 'Alpha',
origin: 'https://alpha.example',
status: 'online',
authentication: 'authenticated',
freshness: 'fresh',
};
const snapshot: AutomationSnapshot = {
observedAt: '2026-07-17T13:00:00Z',
status: { state: 'healthy', scheduler: 'active', workers: 'available' },
tasks: { total: 12, enabled: 9, disabled: 3, running: 2, succeeded: 7, failed: 1 },
};
function deferredSource() {
let resolve!: (value: unknown) => void;
const load = vi.fn<AutomationDataSource['load']>(
(_instanceId, _signal) =>
new Promise((done) => {
void _instanceId;
void _signal;
resolve = done;
}),
);
return { source: { load }, load, resolve: (value: unknown) => resolve(value) };
}
describe('isolated Automation read module', () => {
it('loads through the injected exact-owner source and renders only aggregate task status', async () => {
const pending = deferredSource();
render(<AutomationModule instance={owner} dataSource={pending.source} />);
expect(screen.getByRole('status', { name: 'Automation loading status' })).toBeTruthy();
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
pending.resolve(snapshot);
const counts = await screen.findByRole('region', { name: 'Task counts' });
expect(within(counts).getByText('12')).toBeTruthy();
expect(within(counts).getByText('9')).toBeTruthy();
expect(screen.getByRole('region', { name: 'Automation status' }).textContent).toContain(
'healthy',
);
expect(screen.queryByRole('region', { name: 'Safe labels' })).toBeNull();
expect(screen.getByText(/Observed 2026-07-17T13:00:00Z/)).toBeTruthy();
});
it('omits labels and never exposes task config, endpoints, tokens, phones, messages, payloads, logs, or operations', async () => {
const unsafe = {
...snapshot,
endpoint: 'https://secret.example/run',
token: 'secret-token',
phoneNumber: '+1-555-0100',
message: 'private message content',
payload: 'private payload',
rawLogs: 'private raw log',
tasks: { ...snapshot.tasks, config: 'secret cron config', endpoint: '/execute' },
status: { ...snapshot.status, token: 'nested-token', lastMessage: 'nested-message' },
labels: ['Daily jobs'],
};
render(<AutomationModule instance={owner} dataSource={{ load: async () => unsafe }} />);
expect(await screen.findByText(/read-only aggregate view/i)).toBeTruthy();
expect(document.body.textContent).not.toMatch(
/Daily jobs|secret\.example|secret-token|555|private message|private payload|private raw log|cron config|execute|nested-token|nested-message/i,
);
expect(screen.queryByRole('button')).toBeNull();
expect(screen.getByText(/read-only aggregate view/i)).toBeTruthy();
});
it('copies and bounds enums, counts, and timestamps while dropping all labels', async () => {
const unsafe = {
observedAt: 'scheduler-token@example.test',
status: { state: 'state-secret', scheduler: 'scheduler-secret', workers: ['worker-secret'] },
tasks: { total: -1, enabled: 1.5, disabled: 2, running: Number.POSITIVE_INFINITY, failed: 3 },
labels: [
'Safe 1',
'Safe 2',
'Safe 3',
'Safe 4',
'Safe 5',
'Safe 6',
'Safe 7',
'Safe 8',
'Safe 9',
'token@example.test',
'<script>secret</script>',
'x'.repeat(33),
42,
],
};
render(<AutomationModule instance={owner} dataSource={{ load: async () => unsafe }} />);
await screen.findByRole('region', { name: 'Task counts' });
expect(within(screen.getByRole('region', { name: 'Task counts' })).getByText('2')).toBeTruthy();
expect(within(screen.getByRole('region', { name: 'Task counts' })).getByText('3')).toBeTruthy();
expect(document.body.textContent).not.toMatch(
/scheduler-token|state-secret|scheduler-secret|worker-secret|Safe [1-9]|script|Infinity|1\.5|-1/,
);
expect(screen.queryByText(/^Observed /)).toBeNull();
unsafe.labels[0] = 'Mutated';
unsafe.tasks.disabled = 99;
expect(screen.queryByText('Mutated')).toBeNull();
expect(screen.queryByText('99')).toBeNull();
});
it('fails closed without authentication or an injected source', () => {
const load = vi.fn<AutomationDataSource['load']>().mockResolvedValue(snapshot);
const { rerender } = render(
<AutomationModule
instance={{ ...owner, authentication: 'auth-required' }}
dataSource={{ load }}
/>,
);
expect(screen.getByRole('alert').textContent).toMatch(/authentication is required/i);
expect(load).not.toHaveBeenCalled();
rerender(<AutomationModule instance={owner} />);
expect(screen.getByRole('status', { name: 'Automation unavailable' }).textContent).toMatch(
/no safe Automation read data source.*will not invent.*production endpoint/i,
);
});
it('uses fixed errors, retries, and retains only the same-owner snapshot on refresh failure', async () => {
const user = userEvent.setup();
const load = vi
.fn<AutomationDataSource['load']>()
.mockResolvedValueOnce(snapshot)
.mockRejectedValueOnce(new Error('secret endpoint token'))
.mockResolvedValueOnce({ ...snapshot, tasks: { ...snapshot.tasks, total: 13 } });
const source = { load };
const { rerender } = render(
<AutomationModule instance={owner} dataSource={source} refreshSignal={0} />,
);
const counts = await screen.findByRole('region', { name: 'Task counts' });
expect(within(counts).getByText('12')).toBeTruthy();
rerender(<AutomationModule instance={owner} dataSource={source} refreshSignal={1} />);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toMatch(/showing the last known Automation data/i);
expect(alert.textContent).not.toMatch(/secret endpoint token/i);
expect(
within(screen.getByRole('region', { name: 'Task counts' })).getByText('12'),
).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'Retry loading Automation' }));
expect(
await within(screen.getByRole('region', { name: 'Task counts' })).findByText('13'),
).toBeTruthy();
});
it('aborts superseded reads and fences late data from another owner', async () => {
const alpha = deferredSource();
const bravo = deferredSource();
const load = vi.fn<AutomationDataSource['load']>((id, signal) =>
id === 'alpha' ? alpha.source.load(id, signal) : bravo.source.load(id, signal),
);
const source = { load };
const { rerender } = render(<AutomationModule instance={owner} dataSource={source} />);
const alphaSignal = load.mock.calls[0]?.[1];
rerender(
<AutomationModule instance={{ ...owner, id: 'bravo', name: 'Bravo' }} dataSource={source} />,
);
expect(alphaSignal?.aborted).toBe(true);
alpha.resolve({ ...snapshot, tasks: { total: 41 } });
bravo.resolve({ ...snapshot, tasks: { total: 42 } });
expect(await screen.findByText('42')).toBeTruthy();
expect(screen.queryByText('41')).toBeNull();
});
it('turns a synchronous source throw into the fixed initial-load error', async () => {
render(
<AutomationModule
instance={owner}
dataSource={{
load: () => {
throw new Error('private automation token');
},
}}
/>,
);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('Automation data could not be loaded.');
expect(alert.textContent).not.toContain('private automation token');
});
});
@@ -0,0 +1,296 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
import type { InstanceContext } from '../app-shell.js';
/** Aggregate task counts only; per-task records and configuration are intentionally unsupported. */
export interface AutomationTaskCounts {
readonly total?: number | null;
readonly enabled?: number | null;
readonly disabled?: number | null;
readonly running?: number | null;
readonly queued?: number | null;
readonly succeeded?: number | null;
readonly failed?: number | null;
}
/** Bounded high-level status labels only, never status payloads or raw logs. */
export type AutomationState = 'healthy' | 'degraded' | 'failed' | 'disabled' | 'unknown';
export type AutomationScheduler = 'active' | 'idle' | 'paused' | 'disabled' | 'unavailable';
export type AutomationWorkers = 'available' | 'busy' | 'degraded' | 'disabled' | 'unavailable';
export interface AutomationStatus {
readonly state?: AutomationState | null;
readonly scheduler?: AutomationScheduler | null;
readonly workers?: AutomationWorkers | null;
}
export interface AutomationSnapshot {
readonly observedAt?: string;
readonly status: AutomationStatus;
readonly tasks: AutomationTaskCounts;
}
export interface AutomationDataSource {
/** Injected by the authenticated owner; this isolated module defines no production endpoint. */
load(instanceId: string, signal: AbortSignal): Promise<unknown>;
}
export interface AutomationModuleProps {
readonly instance: InstanceContext;
readonly dataSource?: AutomationDataSource;
readonly refreshSignal?: unknown;
}
type ReadState =
| { kind: 'idle'; ownerId: string }
| { kind: 'loading'; ownerId: string; snapshot?: AutomationSnapshot }
| { kind: 'ready'; ownerId: string; snapshot: AutomationSnapshot }
| { kind: 'error'; ownerId: string; snapshot?: AutomationSnapshot };
const SAFE_LOAD_ERROR = 'Automation data could not be loaded.';
const STATES = new Set<AutomationState>(['healthy', 'degraded', 'failed', 'disabled', 'unknown']);
const SCHEDULERS = new Set<AutomationScheduler>([
'active',
'idle',
'paused',
'disabled',
'unavailable',
]);
const WORKERS = new Set<AutomationWorkers>([
'available',
'busy',
'degraded',
'disabled',
'unavailable',
]);
const TASK_COUNT_KEYS = [
'total',
'enabled',
'disabled',
'running',
'queued',
'succeeded',
'failed',
] as const;
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function safeTimestamp(value: unknown): string | undefined {
if (
typeof value !== 'string' ||
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value)
)
return undefined;
const parsed = new Date(value);
if (!Number.isFinite(parsed.getTime())) return undefined;
const canonical = parsed.toISOString();
return canonical === value || canonical === value.replace(/Z$/, '.000Z') ? value : undefined;
}
function boundedEnum<T extends string>(
value: unknown,
allowed: ReadonlySet<T>,
): T | null | undefined {
if (value === null) return null;
return typeof value === 'string' && allowed.has(value as T) ? (value as T) : undefined;
}
/** Copy and validate untrusted source data before it can enter React state. */
export function sanitizeAutomationSnapshot(value: unknown): AutomationSnapshot {
const source = isRecord(value) ? value : {};
const rawStatus = isRecord(source.status) ? source.status : {};
const state = boundedEnum(rawStatus.state, STATES);
const scheduler = boundedEnum(rawStatus.scheduler, SCHEDULERS);
const workers = boundedEnum(rawStatus.workers, WORKERS);
const status: AutomationStatus = {
...(state !== undefined ? { state } : {}),
...(scheduler !== undefined ? { scheduler } : {}),
...(workers !== undefined ? { workers } : {}),
};
const rawTasks = isRecord(source.tasks) ? source.tasks : {};
const tasks: Record<string, number | null> = {};
for (const key of TASK_COUNT_KEYS) {
const count = rawTasks[key];
if (count === null || (Number.isSafeInteger(count) && (count as number) >= 0))
tasks[key] = count as number | null;
}
const observedAt = safeTimestamp(source.observedAt);
return {
...(observedAt ? { observedAt } : {}),
status,
tasks,
};
}
function Section({ label, children }: { label: string; children: ReactNode }) {
return (
<section className="automation-card" aria-label={label}>
<h2>{label}</h2>
{children}
</section>
);
}
function Fields({
values,
}: {
values: readonly (readonly [string, string | number | null | undefined])[];
}) {
const supplied = values.filter(([, value]) => value !== undefined);
if (!supplied.length) return <p>No aggregate status was supplied.</p>;
return (
<dl>
{supplied.map(([label, value]) => (
<div key={label}>
<dt>{label}</dt>
<dd>{value == null ? 'Unavailable' : String(value)}</dd>
</div>
))}
</dl>
);
}
function SnapshotView({ snapshot }: { snapshot: AutomationSnapshot }) {
const status = snapshot.status;
const tasks = snapshot.tasks;
return (
<>
{snapshot.observedAt ? <p>Observed {snapshot.observedAt}</p> : null}
<p>Read-only aggregate view; task details and operational controls are excluded.</p>
<div className="automation-grid">
<Section label="Automation status">
<Fields
values={[
['State', status.state],
['Scheduler', status.scheduler],
['Workers', status.workers],
]}
/>
</Section>
<Section label="Task counts">
<Fields
values={[
['Total', tasks.total],
['Enabled', tasks.enabled],
['Disabled', tasks.disabled],
['Running', tasks.running],
['Queued', tasks.queued],
['Succeeded', tasks.succeeded],
['Failed', tasks.failed],
]}
/>
</Section>
</div>
</>
);
}
export function AutomationModule({ instance, dataSource, refreshSignal }: AutomationModuleProps) {
const requestFence = useRef(0);
const [retry, setRetry] = useState(0);
const [state, setState] = useState<ReadState>({ kind: 'idle', ownerId: instance.id });
useEffect(() => {
const request = ++requestFence.current;
const ownerId = instance.id;
const controller = new AbortController();
if (instance.authentication !== 'authenticated' || !dataSource) {
setState({ kind: 'idle', ownerId });
return () => controller.abort();
}
setState((current) => ({
kind: 'loading',
ownerId,
...(current.ownerId === ownerId && current.kind !== 'idle' && current.snapshot
? { snapshot: current.snapshot }
: {}),
}));
let read: Promise<unknown>;
try {
read = dataSource.load(ownerId, controller.signal);
} catch (reason: unknown) {
read = Promise.reject(reason);
}
void read.then(
(snapshot) => {
if (request === requestFence.current && !controller.signal.aborted)
setState({ kind: 'ready', ownerId, snapshot: sanitizeAutomationSnapshot(snapshot) });
},
(_reason: unknown) => {
void _reason;
if (request === requestFence.current && !controller.signal.aborted)
setState((current) => ({
kind: 'error',
ownerId,
...(current.ownerId === ownerId && current.kind === 'loading' && current.snapshot
? { snapshot: current.snapshot }
: {}),
}));
},
);
return () => controller.abort();
}, [dataSource, instance.authentication, instance.id, refreshSignal, retry]);
if (instance.authentication !== 'authenticated')
return (
<div className="state-panel state-error" role="alert">
Authentication is required before Automation data can be read for this instance.
</div>
);
if (!dataSource)
return (
<div className="state-panel" role="status" aria-label="Automation unavailable">
No safe Automation read data source is available. This console will not invent or call an
uncontracted production endpoint.
</div>
);
const retained =
state.ownerId === instance.id && (state.kind === 'loading' || state.kind === 'error')
? state.snapshot
: undefined;
if ((state.kind === 'idle' || state.kind === 'loading') && !retained)
return (
<p role="status" aria-label="Automation loading status">
Loading Automation
</p>
);
if (state.kind === 'error' && !retained)
return (
<div className="state-panel state-error" role="alert">
<p>Unable to load Automation: {SAFE_LOAD_ERROR}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading Automation
</button>
</div>
);
const snapshot =
state.kind === 'ready' && state.ownerId === instance.id ? state.snapshot : retained;
if (!snapshot) return null;
return (
<div className="automation-module">
{state.kind === 'loading' ? <p role="status">Refreshing Automation</p> : null}
{state.kind === 'error' ? (
<div className="state-panel state-error" role="alert">
<p>Refresh failed; showing the last known Automation data.</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading Automation
</button>
</div>
) : null}
{instance.freshness !== 'fresh' ? (
<p className="state-panel" role="status" aria-label="Automation freshness">
Automation data is {instance.freshness}; verify freshness before relying on these values.
</p>
) : null}
<SnapshotView snapshot={snapshot} />
</div>
);
}
+203
View File
@@ -0,0 +1,203 @@
// @vitest-environment jsdom
import { cleanup, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { InstanceContext } from '../app-shell.js';
import { EsimModule, type EsimDataSource, type EsimSnapshot } from './esim-module.js';
afterEach(cleanup);
const owner: InstanceContext = {
id: 'alpha',
name: 'Alpha',
origin: 'https://alpha.example',
status: 'online',
authentication: 'authenticated',
freshness: 'fresh',
};
const snapshot: EsimSnapshot = {
profileCount: 4,
enabledProfileCount: 2,
lpacStatus: 'available',
workMode: 'idle',
labels: ['Provisioned', 'Enabled'],
};
function deferredSource() {
let resolve!: (value: EsimSnapshot) => void;
const load = vi.fn<EsimDataSource['load']>(() => new Promise((done) => (resolve = done)));
return { source: { load }, load, resolve: (value: EsimSnapshot) => resolve(value) };
}
describe('isolated safe eSIM read module', () => {
it('loads only through the injected source and presents safe aggregates', async () => {
const pending = deferredSource();
render(<EsimModule instance={owner} dataSource={pending.source} />);
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
pending.resolve(snapshot);
const section = await screen.findByRole('region', { name: 'eSIM safe summary' });
expect(within(section).getByText('4')).toBeTruthy();
expect(within(section).getByText('2')).toBeTruthy();
expect(within(section).getByText('available')).toBeTruthy();
expect(within(section).getByText('idle')).toBeTruthy();
expect(within(section).getByText('Provisioned')).toBeTruthy();
expect(screen.queryByRole('button')).toBeNull();
});
it('has no endpoint fallback and requires the exact authenticated owner', async () => {
const load = vi.fn<EsimDataSource['load']>().mockResolvedValue(snapshot);
const { rerender } = render(<EsimModule instance={owner} dataSource={{ load }} />);
expect(await screen.findByText('Provisioned')).toBeTruthy();
rerender(
<EsimModule
instance={{
...owner,
id: 'bravo',
authentication: 'auth-required',
status: 'auth-required',
}}
dataSource={{ load }}
/>,
);
expect(screen.getByRole('alert').textContent).toMatch(/authentication is required/i);
expect(screen.queryByText('Provisioned')).toBeNull();
expect(load).toHaveBeenCalledTimes(1);
rerender(<EsimModule instance={owner} />);
expect(screen.getByRole('status', { name: 'eSIM unavailable' }).textContent).toMatch(
/no safe esim read data source.*will not invent or call.*endpoint/i,
);
});
it('enforces a runtime presentation allowlist against untrusted extra and invalid values', async () => {
const hostile = {
...snapshot,
profileCount: -1,
enabledProfileCount: Number.NaN,
lpacStatus: '8901-sensitive',
workMode: '<img src=x onerror=alert(1)>',
labels: ['Enabled', 'secret-profile-name', '<script>alert(1)</script>'],
eid: 'EID-secret',
iccid: 'ICCID-secret',
imsi: 'IMSI-secret',
msisdn: 'MSISDN-secret',
smsc: 'SMSC-secret',
smdp: 'SM-DP-secret',
matchingId: 'matching-secret',
confirmationCode: 'confirmation-secret',
imei: 'IMEI-secret',
isdpAid: 'ISDP-AID-secret',
providerData: { secret: 'raw-provider-secret' },
} as unknown as EsimSnapshot;
render(<EsimModule instance={owner} dataSource={{ load: async () => hostile }} />);
expect(await screen.findByText('Enabled')).toBeTruthy();
for (const forbidden of [
'EID-secret',
'ICCID-secret',
'IMSI-secret',
'MSISDN-secret',
'SMSC-secret',
'SM-DP-secret',
'matching-secret',
'confirmation-secret',
'IMEI-secret',
'ISDP-AID-secret',
'raw-provider-secret',
'secret-profile-name',
])
expect(document.body.textContent).not.toContain(forbidden);
expect(document.querySelector('img')).toBeNull();
expect(document.querySelector('script')).toBeNull();
expect(screen.getAllByText('Unavailable')).toHaveLength(4);
});
it('retains same-owner data during refresh and uses fixed safe errors with retry', async () => {
const user = userEvent.setup();
const load = vi
.fn<EsimDataSource['load']>()
.mockResolvedValueOnce(snapshot)
.mockRejectedValueOnce(new Error('EID-secret upstream failure'))
.mockResolvedValueOnce({ ...snapshot, profileCount: 5 });
const source = { load };
const { rerender } = render(
<EsimModule instance={owner} dataSource={source} refreshSignal={0} />,
);
expect(await screen.findByText('Provisioned')).toBeTruthy();
rerender(<EsimModule instance={owner} dataSource={source} refreshSignal={1} />);
expect(
await screen.findByText(/Refresh failed; showing the last known eSIM summary/i),
).toBeTruthy();
expect(document.body.textContent).not.toContain('EID-secret');
expect(screen.getByText('4')).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'Retry loading eSIM data' }));
expect(await screen.findByText('5')).toBeTruthy();
});
it('shows a fixed initial error without leaking rejection details', async () => {
render(
<EsimModule
instance={owner}
dataSource={{
load: async () => {
throw 'private code';
},
}}
/>,
);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('eSIM data could not be loaded.');
expect(alert.textContent).not.toContain('private code');
});
it('aborts superseded reads and fences late prior-owner results', async () => {
const alpha = deferredSource();
const bravo = deferredSource();
const load = vi.fn<EsimDataSource['load']>((id, signal) =>
id === 'alpha' ? alpha.source.load(id, signal) : bravo.source.load(id, signal),
);
const source = { load };
const { rerender } = render(<EsimModule instance={owner} dataSource={source} />);
const alphaSignal = load.mock.calls[0]?.[1];
rerender(<EsimModule instance={{ ...owner, id: 'bravo' }} dataSource={source} />);
expect(alphaSignal?.aborted).toBe(true);
alpha.resolve({ ...snapshot, labels: ['Error'] });
bravo.resolve({ ...snapshot, labels: ['Pending'] });
expect(await screen.findByText('Pending')).toBeTruthy();
expect(screen.queryByText('Error')).toBeNull();
});
it('caps labels at eight and defensively sanitizes unknown and null payloads', async () => {
const labels = Array.from(
{ length: 12 },
(_, index) =>
(['Provisioned', 'Enabled', 'Disabled', 'Pending', 'Error'] as const)[index % 5],
);
const { rerender } = render(
<EsimModule instance={owner} dataSource={{ load: async () => ({ labels }) }} />,
);
const summary = await screen.findByRole('region', { name: 'eSIM safe summary' });
expect(within(summary).getAllByRole('listitem')).toHaveLength(8);
rerender(
<EsimModule instance={owner} dataSource={{ load: async () => null }} refreshSignal={1} />,
);
expect(await screen.findByText('No safe labels were supplied.')).toBeTruthy();
expect(screen.getAllByText('Unavailable')).toHaveLength(4);
});
it('turns a synchronous source throw into the fixed initial-load error', async () => {
render(
<EsimModule
instance={owner}
dataSource={{
load: () => {
throw new Error('private eSIM identifier');
},
}}
/>,
);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('eSIM data could not be loaded.');
expect(alert.textContent).not.toContain('private eSIM identifier');
});
});
+251
View File
@@ -0,0 +1,251 @@
import { useEffect, useRef, useState } from 'react';
import type { InstanceContext } from '../app-shell.js';
/** Bounded status vocabularies are the only strings this module may present. */
export type EsimLpacStatus = 'available' | 'unavailable' | 'degraded' | 'unknown';
export type EsimWorkMode = 'idle' | 'working' | 'disabled' | 'unknown';
export type EsimSafeLabel = 'Provisioned' | 'Enabled' | 'Disabled' | 'Pending' | 'Error';
/**
* Deliberately aggregate-only. Subscriber, device, provisioning, provider, and profile
* identifiers have no place in this contract.
*/
export interface EsimSnapshot {
readonly profileCount?: number | null;
readonly enabledProfileCount?: number | null;
readonly lpacStatus?: EsimLpacStatus | null;
readonly workMode?: EsimWorkMode | null;
readonly labels?: readonly EsimSafeLabel[];
}
export interface EsimDataSource {
/** Supplied by the authenticated owner; this module defines and calls no endpoint. */
load(instanceId: string, signal: AbortSignal): Promise<unknown>;
}
export interface EsimModuleProps {
readonly instance: InstanceContext;
readonly dataSource?: EsimDataSource;
readonly refreshSignal?: unknown;
}
type SafeSnapshot = {
profileCount?: number;
enabledProfileCount?: number;
lpacStatus?: EsimLpacStatus;
workMode?: EsimWorkMode;
labels: EsimSafeLabel[];
};
type ReadState =
| { kind: 'idle'; ownerId: string }
| { kind: 'loading'; ownerId: string; snapshot?: SafeSnapshot }
| { kind: 'ready'; ownerId: string; snapshot: SafeSnapshot }
| { kind: 'error'; ownerId: string; snapshot?: SafeSnapshot };
const LPAC_STATUSES = new Set<EsimLpacStatus>(['available', 'unavailable', 'degraded', 'unknown']);
const WORK_MODES = new Set<EsimWorkMode>(['idle', 'working', 'disabled', 'unknown']);
const SAFE_LABELS = new Set<EsimSafeLabel>([
'Provisioned',
'Enabled',
'Disabled',
'Pending',
'Error',
]);
const SAFE_LOAD_ERROR = 'eSIM data could not be loaded.';
const MAX_SAFE_LABELS = 8;
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function safeCount(value: unknown): number | undefined {
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
}
/** Copy only explicitly permitted values; never pass an injected object through to rendering. */
function sanitizeSnapshot(value: unknown): SafeSnapshot {
const candidate = isRecord(value) ? value : {};
const profileCount = safeCount(candidate.profileCount);
const enabledProfileCount = safeCount(candidate.enabledProfileCount);
const lpacStatus = LPAC_STATUSES.has(candidate.lpacStatus as EsimLpacStatus)
? (candidate.lpacStatus as EsimLpacStatus)
: undefined;
const workMode = WORK_MODES.has(candidate.workMode as EsimWorkMode)
? (candidate.workMode as EsimWorkMode)
: undefined;
const labels = Array.isArray(candidate.labels)
? candidate.labels
.filter(
(label): label is EsimSafeLabel =>
typeof label === 'string' && SAFE_LABELS.has(label as EsimSafeLabel),
)
.slice(0, MAX_SAFE_LABELS)
: [];
return {
...(profileCount === undefined ? {} : { profileCount }),
...(enabledProfileCount === undefined ? {} : { enabledProfileCount }),
...(lpacStatus === undefined ? {} : { lpacStatus }),
...(workMode === undefined ? {} : { workMode }),
labels,
};
}
function display(value: number | string | undefined): string {
return value === undefined ? 'Unavailable' : String(value);
}
function SnapshotView({ snapshot }: { snapshot: SafeSnapshot }) {
return (
<section className="esim-card" aria-label="eSIM safe summary">
<h2>eSIM safe summary</h2>
<dl>
<div>
<dt>Profile count</dt>
<dd>{display(snapshot.profileCount)}</dd>
</div>
<div>
<dt>Enabled profile count</dt>
<dd>{display(snapshot.enabledProfileCount)}</dd>
</div>
<div>
<dt>lpac status</dt>
<dd>{display(snapshot.lpacStatus)}</dd>
</div>
<div>
<dt>Work mode</dt>
<dd>{display(snapshot.workMode)}</dd>
</div>
</dl>
<h3>Safe labels</h3>
{snapshot.labels.length ? (
<ul>
{snapshot.labels.map((label, index) => (
<li key={`${label}-${index}`}>{label}</li>
))}
</ul>
) : (
<p>No safe labels were supplied.</p>
)}
</section>
);
}
export function EsimModule({ instance, dataSource, refreshSignal }: EsimModuleProps) {
const requestOwner = useRef(0);
const [retry, setRetry] = useState(0);
const [state, setState] = useState<ReadState>({ kind: 'idle', ownerId: instance.id });
useEffect(() => {
const request = ++requestOwner.current;
const ownerId = instance.id;
const controller = new AbortController();
if (instance.authentication !== 'authenticated' || !dataSource) {
setState({ kind: 'idle', ownerId });
return () => controller.abort();
}
setState((current) => ({
kind: 'loading',
ownerId,
...(current.ownerId === ownerId &&
(current.kind === 'ready' || current.kind === 'error') &&
current.snapshot
? { snapshot: current.snapshot }
: {}),
}));
let read: Promise<unknown>;
try {
read = dataSource.load(ownerId, controller.signal);
} catch (reason: unknown) {
read = Promise.reject(reason);
}
void read.then(
(value) => {
if (request === requestOwner.current && !controller.signal.aborted) {
setState({ kind: 'ready', ownerId, snapshot: sanitizeSnapshot(value) });
}
},
(_reason: unknown) => {
void _reason;
if (request === requestOwner.current && !controller.signal.aborted) {
setState((current) => ({
kind: 'error',
ownerId,
...(current.ownerId === ownerId && current.kind === 'loading' && current.snapshot
? { snapshot: current.snapshot }
: {}),
}));
}
},
);
return () => controller.abort();
}, [dataSource, instance.authentication, instance.id, refreshSignal, retry]);
if (instance.authentication !== 'authenticated') {
return (
<div className="state-panel state-error" role="alert">
Authentication is required before eSIM data can be read for this instance.
</div>
);
}
if (!dataSource) {
return (
<div className="state-panel" role="status" aria-label="eSIM unavailable">
No safe eSIM read data source is available. This console will not invent or call an
uncontracted production endpoint.
</div>
);
}
const retained =
state.ownerId === instance.id && (state.kind === 'loading' || state.kind === 'error')
? state.snapshot
: undefined;
if ((state.kind === 'idle' || state.kind === 'loading') && !retained) {
return (
<p role="status" aria-label="eSIM loading status">
Loading eSIM summary
</p>
);
}
if (state.kind === 'error' && !retained) {
return (
<div className="state-panel state-error" role="alert">
<p>Unable to load eSIM data: {SAFE_LOAD_ERROR}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading eSIM data
</button>
</div>
);
}
const snapshot =
state.kind === 'ready' && state.ownerId === instance.id ? state.snapshot : retained;
if (!snapshot) return null;
return (
<div className="esim-module">
{state.kind === 'loading' ? <p role="status">Refreshing eSIM summary</p> : null}
{state.kind === 'error' ? (
<div className="state-panel state-error" role="alert">
<p>Refresh failed; showing the last known eSIM summary.</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading eSIM data
</button>
</div>
) : null}
{instance.freshness !== 'fresh' ? (
<p className="state-panel" role="status" aria-label="eSIM freshness">
eSIM data is {instance.freshness}; verify freshness before relying on these values.
</p>
) : null}
<SnapshotView snapshot={snapshot} />
</div>
);
}
@@ -0,0 +1,247 @@
// @vitest-environment jsdom
import { cleanup, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { InstanceContext } from '../app-shell.js';
import {
NotificationsModule,
type NotificationsDataSource,
type NotificationsSnapshot,
} from './notifications-module.js';
afterEach(cleanup);
const owner: InstanceContext = {
id: 'alpha',
name: 'Alpha',
origin: 'https://alpha.example',
status: 'online',
authentication: 'authenticated',
freshness: 'fresh',
};
const snapshot: NotificationsSnapshot = {
observedAt: '2026-07-17T12:00:00Z',
channels: {
status: 'healthy',
total: 5,
enabled: 4,
disabled: 1,
healthy: 3,
degraded: 1,
failed: 0,
endpoint: 'https://private.example/hook',
destination: '+15550000123',
token: 'channel-secret',
config: { provider: 'private-provider' },
},
queue: {
status: 'processing',
total: 18,
pending: 4,
processing: 2,
delivered: 11,
failed: 1,
payload: 'private queue payload',
destination: 'private@example.test',
},
logs: {
status: 'available',
total: 30,
info: 20,
warning: 7,
error: 3,
rawLogs: ['private raw log line'],
phoneNumber: '+15550000456',
},
};
function deferredSource() {
let resolve!: (value: unknown) => void;
let reject!: (reason: unknown) => void;
const load = vi.fn<NotificationsDataSource['load']>(
(_instanceId, _signal) =>
new Promise((done, fail) => {
void _instanceId;
void _signal;
resolve = done;
reject = fail;
}),
);
return {
source: { load },
load,
resolve: (value: unknown) => resolve(value),
reject: (reason: unknown) => reject(reason),
};
}
describe('Notifications isolated safe read module', () => {
it('reads only through the injected exact-owner source and renders allowlisted aggregates', async () => {
const pending = deferredSource();
render(<NotificationsModule instance={owner} dataSource={pending.source} />);
expect(screen.getByRole('status', { name: 'Notifications loading status' })).toBeTruthy();
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
pending.resolve(snapshot);
const channels = await screen.findByRole('region', { name: 'Channel aggregate' });
expect(within(channels).getByText('healthy')).toBeTruthy();
expect(within(channels).getByText('5')).toBeTruthy();
expect(
within(screen.getByRole('region', { name: 'Queue aggregate' })).getByText('18'),
).toBeTruthy();
expect(
within(screen.getByRole('region', { name: 'Log aggregate' })).getByText('30'),
).toBeTruthy();
expect(screen.getByText(/Observed 2026-07-17T12:00:00Z/)).toBeTruthy();
});
it('never exposes polymorphic config, endpoints, destinations, tokens, phone numbers, payloads, raw logs, or actions', async () => {
render(<NotificationsModule instance={owner} dataSource={{ load: async () => snapshot }} />);
expect(await screen.findByText(/aggregate counts and status only/i)).toBeTruthy();
const text = document.body.textContent ?? '';
for (const secret of [
'https://private.example/hook',
'+15550000123',
'channel-secret',
'private-provider',
'private queue payload',
'private@example.test',
'private raw log line',
'+15550000456',
])
expect(text).not.toContain(secret);
expect(
screen.queryByText(/endpoint|destination|token|phone number|payload|raw log/i),
).toBeNull();
expect(screen.queryByRole('button')).toBeNull();
expect(screen.queryByRole('link')).toBeNull();
});
it('fails closed without an injected source or authenticated owner', async () => {
const load = vi.fn<NotificationsDataSource['load']>().mockResolvedValue(snapshot);
const { rerender } = render(<NotificationsModule instance={owner} />);
expect(screen.getByRole('status', { name: 'Notifications unavailable' }).textContent).toMatch(
/no safe notifications read data source.*uncontracted production endpoint/i,
);
rerender(
<NotificationsModule
instance={{ ...owner, authentication: 'auth-required' }}
dataSource={{ load }}
/>,
);
expect(screen.getByRole('alert').textContent).toMatch(/authentication is required/i);
expect(load).not.toHaveBeenCalled();
});
it('clears prior-owner data, aborts replaced reads, and fences late responses', async () => {
const alpha = deferredSource();
const bravo = deferredSource();
const load = vi.fn<NotificationsDataSource['load']>((id, signal) =>
id === 'alpha' ? alpha.source.load(id, signal) : bravo.source.load(id, signal),
);
const source = { load };
const { rerender } = render(<NotificationsModule instance={owner} dataSource={source} />);
const alphaSignal = load.mock.calls[0]?.[1];
rerender(
<NotificationsModule
instance={{ ...owner, id: 'bravo', name: 'Bravo' }}
dataSource={source}
/>,
);
expect(alphaSignal?.aborted).toBe(true);
alpha.resolve(snapshot);
bravo.resolve({
...snapshot,
channels: { ...snapshot.channels, status: 'degraded', total: 9 },
});
expect(await screen.findByText('degraded')).toBeTruthy();
expect(screen.queryByText('healthy')).toBeNull();
});
it('uses fixed errors and retains only same-owner last-good data across refresh failure and retry', async () => {
const user = userEvent.setup();
const load = vi
.fn<NotificationsDataSource['load']>()
.mockResolvedValueOnce(snapshot)
.mockRejectedValueOnce(new Error('secret endpoint and token'))
.mockResolvedValueOnce({ ...snapshot, channels: { status: 'available', total: 6 } });
const source = { load };
const { rerender } = render(
<NotificationsModule instance={owner} dataSource={source} refreshSignal={0} />,
);
expect(await screen.findByText('healthy')).toBeTruthy();
rerender(<NotificationsModule instance={owner} dataSource={source} refreshSignal={1} />);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toMatch(/refresh failed; showing the last known notifications data/i);
expect(alert.textContent).not.toContain('secret endpoint');
expect(screen.getByText('healthy')).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'Retry loading Notifications' }));
expect(
await within(screen.getByRole('region', { name: 'Channel aggregate' })).findByText(
'available',
),
).toBeTruthy();
});
it('copies a bounded safe snapshot and drops malformed nested data and nominal-field secrets', async () => {
const unsafe = {
observedAt: 'api-token@example.test',
channels: {
status: 'channel-secret',
total: -1,
enabled: 1.5,
disabled: 2,
healthy: Number.POSITIVE_INFINITY,
},
queue: ['private payload'],
logs: { status: { token: 'nested-secret' }, total: Number.MAX_SAFE_INTEGER + 1, error: 3 },
};
render(<NotificationsModule instance={owner} dataSource={{ load: async () => unsafe }} />);
const logs = await screen.findByRole('region', { name: 'Log aggregate' });
expect(within(logs).getByText('3')).toBeTruthy();
expect(
within(screen.getByRole('region', { name: 'Channel aggregate' })).getByText('2'),
).toBeTruthy();
expect(document.body.textContent).not.toMatch(
/api-token|channel-secret|private payload|nested-secret|Infinity|1\.5|-1/,
);
expect(screen.queryByText(/^Observed /)).toBeNull();
unsafe.channels.disabled = 99;
expect(screen.queryByText('99')).toBeNull();
});
it('uses a fixed initial-load error without rejection details', async () => {
render(
<NotificationsModule
instance={owner}
dataSource={{ load: async () => Promise.reject(new Error('raw log and private token')) }}
/>,
);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('Notifications data could not be loaded.');
expect(alert.textContent).not.toContain('private token');
});
it('turns a synchronous source throw into the fixed initial-load error', async () => {
render(
<NotificationsModule
instance={owner}
dataSource={{
load: () => {
throw new Error('private notification token');
},
}}
/>,
);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('Notifications data could not be loaded.');
expect(alert.textContent).not.toContain('private notification token');
});
});
@@ -0,0 +1,343 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
import type { InstanceContext } from '../app-shell.js';
/** Values permitted at the Notifications presentation boundary. */
export type NotificationStatus =
| 'healthy'
| 'degraded'
| 'failed'
| 'available'
| 'unavailable'
| 'idle'
| 'processing'
| 'paused';
export type NotificationAggregateValue = NotificationStatus | number | null;
/** Aggregate channel counts/status only. All source-specific details are ignored. */
export interface NotificationChannelAggregate {
readonly status?: NotificationStatus | null;
readonly total?: number | null;
readonly enabled?: number | null;
readonly disabled?: number | null;
readonly healthy?: number | null;
readonly degraded?: number | null;
readonly failed?: number | null;
readonly [sourceField: string]: unknown;
}
/** Aggregate queue counts/status only. Individual jobs and their payloads are unsupported. */
export interface NotificationQueueAggregate {
readonly status?: NotificationStatus | null;
readonly total?: number | null;
readonly pending?: number | null;
readonly processing?: number | null;
readonly delivered?: number | null;
readonly failed?: number | null;
readonly [sourceField: string]: unknown;
}
/** Aggregate log counts/status only. Raw records are unsupported. */
export interface NotificationLogAggregate {
readonly status?: NotificationStatus | null;
readonly total?: number | null;
readonly info?: number | null;
readonly warning?: number | null;
readonly error?: number | null;
readonly [sourceField: string]: unknown;
}
export interface NotificationsSnapshot {
readonly observedAt?: string;
readonly channels: NotificationChannelAggregate;
readonly queue: NotificationQueueAggregate;
readonly logs: NotificationLogAggregate;
}
export interface NotificationsDataSource {
/** Supplied by the authenticated owner; this module defines and calls no network endpoint. */
load(instanceId: string, signal: AbortSignal): Promise<unknown>;
}
export interface NotificationsModuleProps {
readonly instance: InstanceContext;
readonly dataSource?: NotificationsDataSource;
/** Change this owner-provided value to request another read. */
readonly refreshSignal?: unknown;
}
type OwnedSnapshot = { readonly ownerId: string; readonly value: NotificationsSnapshot };
type ReadState =
| { kind: 'idle' }
| { kind: 'loading'; snapshot?: OwnedSnapshot }
| { kind: 'ready'; snapshot: OwnedSnapshot }
| { kind: 'error'; snapshot?: OwnedSnapshot };
const SAFE_LOAD_ERROR = 'Notifications data could not be loaded.';
const NOTIFICATION_STATUSES = new Set<NotificationStatus>([
'healthy',
'degraded',
'failed',
'available',
'unavailable',
'idle',
'processing',
'paused',
]);
const COUNT_KEYS = {
channels: ['total', 'enabled', 'disabled', 'healthy', 'degraded', 'failed'],
queue: ['total', 'pending', 'processing', 'delivered', 'failed'],
logs: ['total', 'info', 'warning', 'error'],
} as const;
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function safeTimestamp(value: unknown): string | undefined {
if (
typeof value !== 'string' ||
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value)
)
return undefined;
const parsed = new Date(value);
if (!Number.isFinite(parsed.getTime())) return undefined;
const canonical = parsed.toISOString();
return canonical === value || canonical === value.replace(/Z$/, '.000Z') ? value : undefined;
}
function sanitizeAggregate(
value: unknown,
countKeys: readonly string[],
): Record<string, NotificationAggregateValue> {
const source = isRecord(value) ? value : {};
const safe: Record<string, NotificationAggregateValue> = {};
if (source.status === null) safe.status = null;
else if (
typeof source.status === 'string' &&
NOTIFICATION_STATUSES.has(source.status as NotificationStatus)
)
safe.status = source.status as NotificationStatus;
for (const key of countKeys) {
const count = source[key];
if (count === null || (Number.isSafeInteger(count) && (count as number) >= 0))
safe[key] = count as number | null;
}
return safe;
}
/** Copy and validate untrusted source data before it can enter React state. */
export function sanitizeNotificationsSnapshot(value: unknown): NotificationsSnapshot {
const source = isRecord(value) ? value : {};
const observedAt = safeTimestamp(source.observedAt);
return {
...(observedAt ? { observedAt } : {}),
channels: sanitizeAggregate(source.channels, COUNT_KEYS.channels),
queue: sanitizeAggregate(source.queue, COUNT_KEYS.queue),
logs: sanitizeAggregate(source.logs, COUNT_KEYS.logs),
};
}
const CHANNEL_FIELDS = [
['Status', 'status'],
['Total', 'total'],
['Enabled', 'enabled'],
['Disabled', 'disabled'],
['Healthy', 'healthy'],
['Degraded', 'degraded'],
['Failed', 'failed'],
] as const;
const QUEUE_FIELDS = [
['Status', 'status'],
['Total', 'total'],
['Pending', 'pending'],
['Processing', 'processing'],
['Delivered', 'delivered'],
['Failed', 'failed'],
] as const;
const LOG_FIELDS = [
['Status', 'status'],
['Total', 'total'],
['Info', 'info'],
['Warning', 'warning'],
['Error', 'error'],
] as const;
function Section({ label, children }: { label: string; children: ReactNode }) {
return (
<section className="notifications-card" aria-label={label}>
<h2>{label}</h2>
{children}
</section>
);
}
function AggregateFields({
aggregate,
fields,
}: {
aggregate: Readonly<Record<string, unknown>>;
fields: readonly (readonly [label: string, key: string])[];
}) {
// Constant-key projection is the security boundary: never enumerate or spread source objects.
const supplied = fields.filter(([, key]) => aggregate[key] !== undefined);
if (!supplied.length) return <p>No aggregate status was supplied.</p>;
return (
<dl>
{supplied.map(([label, key]) => {
const value = aggregate[key] as NotificationAggregateValue;
return (
<div key={key}>
<dt>{label}</dt>
<dd>{value == null ? 'Unavailable' : String(value)}</dd>
</div>
);
})}
</dl>
);
}
function SnapshotView({ snapshot }: { snapshot: NotificationsSnapshot }) {
return (
<>
{snapshot.observedAt ? <p>Observed {snapshot.observedAt}</p> : null}
<p>Aggregate counts and status only; notification details and contents are excluded.</p>
<div className="notifications-grid">
<Section label="Channel aggregate">
<AggregateFields aggregate={snapshot.channels} fields={CHANNEL_FIELDS} />
</Section>
<Section label="Queue aggregate">
<AggregateFields aggregate={snapshot.queue} fields={QUEUE_FIELDS} />
</Section>
<Section label="Log aggregate">
<AggregateFields aggregate={snapshot.logs} fields={LOG_FIELDS} />
</Section>
</div>
</>
);
}
export function NotificationsModule({
instance,
dataSource,
refreshSignal,
}: NotificationsModuleProps) {
const requestFence = useRef(0);
const [retry, setRetry] = useState(0);
const [state, setState] = useState<ReadState>({ kind: 'idle' });
useEffect(() => {
const request = ++requestFence.current;
const ownerId = instance.id;
const controller = new AbortController();
if (instance.authentication !== 'authenticated' || !dataSource) {
setState({ kind: 'idle' });
return () => controller.abort();
}
setState((current) => {
const retained =
current.kind !== 'idle' && current.snapshot?.ownerId === ownerId
? current.snapshot
: undefined;
return { kind: 'loading', ...(retained ? { snapshot: retained } : {}) };
});
let read: Promise<unknown>;
try {
read = dataSource.load(ownerId, controller.signal);
} catch (reason: unknown) {
read = Promise.reject(reason);
}
void read.then(
(value) => {
if (request === requestFence.current && !controller.signal.aborted) {
setState({
kind: 'ready',
snapshot: { ownerId, value: sanitizeNotificationsSnapshot(value) },
});
}
},
(_reason: unknown) => {
void _reason;
if (request === requestFence.current && !controller.signal.aborted) {
setState((current) => {
const retained =
current.kind === 'loading' && current.snapshot?.ownerId === ownerId
? current.snapshot
: undefined;
return { kind: 'error', ...(retained ? { snapshot: retained } : {}) };
});
}
},
);
return () => controller.abort();
}, [dataSource, instance.authentication, instance.id, refreshSignal, retry]);
if (instance.authentication !== 'authenticated') {
return (
<div className="state-panel state-error" role="alert">
Authentication is required before Notifications data can be read for this instance.
</div>
);
}
if (!dataSource) {
return (
<div className="state-panel" role="status" aria-label="Notifications unavailable">
No safe Notifications read data source is available. This console will not invent or call an
uncontracted production endpoint.
</div>
);
}
const snapshot =
state.kind !== 'idle' && state.snapshot?.ownerId === instance.id ? state.snapshot : undefined;
if ((state.kind === 'idle' || state.kind === 'loading') && !snapshot) {
return (
<p role="status" aria-label="Notifications loading status">
Loading Notifications
</p>
);
}
if (state.kind === 'error' && !snapshot) {
return (
<div className="state-panel state-error" role="alert">
<p>Unable to load Notifications: {SAFE_LOAD_ERROR}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading Notifications
</button>
</div>
);
}
if (!snapshot) return null;
return (
<div className="notifications-module">
{state.kind === 'loading' ? <p role="status">Refreshing Notifications</p> : null}
{state.kind === 'error' ? (
<div className="state-panel state-error" role="alert">
<p>Refresh failed; showing the last known Notifications data.</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading Notifications
</button>
</div>
) : null}
{instance.freshness !== 'fresh' ? (
<p className="state-panel" role="status" aria-label="Notifications freshness">
Notifications data is {instance.freshness}; verify freshness before relying on these
values.
</p>
) : null}
<SnapshotView snapshot={snapshot.value} />
</div>
);
}
+172
View File
@@ -0,0 +1,172 @@
// @vitest-environment jsdom
import { cleanup, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { InstanceContext } from '../app-shell.js';
import { OtaModule, type OtaDataSource, type OtaSnapshot } from './ota-module.js';
afterEach(cleanup);
const owner: InstanceContext = {
id: 'alpha',
name: 'Alpha',
origin: 'https://alpha.example',
status: 'online',
authentication: 'authenticated',
freshness: 'fresh',
};
const snapshot: OtaSnapshot = {
currentVersion: 'v2.4.1-build.7',
status: 'downloading',
progressPercent: 42,
updateAvailable: true,
};
function deferredSource() {
let resolve!: (value: OtaSnapshot) => void;
const load = vi.fn<OtaDataSource['load']>(
(_instanceId, _signal) =>
new Promise((done) => {
void _instanceId;
void _signal;
resolve = done;
}),
);
return { source: { load }, load, resolve: (value: OtaSnapshot) => resolve(value) };
}
describe('isolated safe OTA read module', () => {
it('reads only through the injected exact-owner source and renders allowlisted OTA metadata', async () => {
const pending = deferredSource();
render(<OtaModule instance={owner} dataSource={pending.source} />);
expect(screen.getByRole('status', { name: 'OTA loading status' })).toBeTruthy();
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
pending.resolve(snapshot);
const summary = await screen.findByRole('region', { name: 'OTA safe summary' });
expect(within(summary).getByText('v2.4.1-build.7')).toBeTruthy();
expect(within(summary).getByText('downloading')).toBeTruthy();
expect(within(summary).getByText('42%')).toBeTruthy();
expect(within(summary).getByText('Available')).toBeTruthy();
});
it('strictly projects safe values and rejects URLs, proxy prefixes, notes, logs, and file paths', async () => {
const unsafe = {
...snapshot,
currentVersion: 'https://private.example/releases/secret',
status: 'proxy:/admin/ota',
endpoint: 'https://private.example/ota',
proxyPrefix: '/api/proxy/alpha',
releaseNotes: 'private release notes',
rawLogs: ['private raw OTA log'],
filePath: '/var/lib/private/update.bin',
uploadPath: '/tmp/private.bin',
} as unknown as OtaSnapshot;
render(<OtaModule instance={owner} dataSource={{ load: async () => unsafe }} />);
const summary = await screen.findByRole('region', { name: 'OTA safe summary' });
expect(within(summary).getAllByText('Unavailable')).toHaveLength(2);
expect(document.body.textContent).not.toMatch(
/private\.example|admin\/ota|api\/proxy|private release notes|private raw OTA log|var\/lib|tmp\/private/i,
);
expect(screen.queryByRole('link')).toBeNull();
expect(screen.queryByRole('button')).toBeNull();
});
it('offers no endpoint or apply, cancel, or upload operations', async () => {
render(<OtaModule instance={owner} dataSource={{ load: async () => snapshot }} />);
expect(await screen.findByText('v2.4.1-build.7')).toBeTruthy();
expect(screen.getByText(/read-only OTA metadata/i)).toBeTruthy();
expect(screen.queryByText(/endpoint/i)).toBeNull();
expect(screen.queryByRole('button', { name: /apply|cancel|upload/i })).toBeNull();
expect(screen.queryByRole('textbox')).toBeNull();
});
it('fails closed without an injected source or authenticated owner', () => {
const load = vi.fn<OtaDataSource['load']>().mockResolvedValue(snapshot);
const { rerender } = render(<OtaModule instance={owner} />);
expect(screen.getByRole('status', { name: 'OTA unavailable' }).textContent).toMatch(
/no safe OTA read data source.*will not invent.*production endpoint/i,
);
rerender(
<OtaModule instance={{ ...owner, authentication: 'auth-required' }} dataSource={{ load }} />,
);
expect(screen.getByRole('alert').textContent).toMatch(/authentication is required/i);
expect(load).not.toHaveBeenCalled();
});
it('aborts superseded reads, clears the old owner, and fences late results', async () => {
const alpha = deferredSource();
const bravo = deferredSource();
const load = vi.fn<OtaDataSource['load']>((id, signal) =>
id === 'alpha' ? alpha.source.load(id, signal) : bravo.source.load(id, signal),
);
const source = { load };
const { rerender } = render(<OtaModule instance={owner} dataSource={source} />);
const alphaSignal = load.mock.calls[0]?.[1];
rerender(<OtaModule instance={{ ...owner, id: 'bravo', name: 'Bravo' }} dataSource={source} />);
expect(alphaSignal?.aborted).toBe(true);
alpha.resolve({ ...snapshot, currentVersion: 'v1.0.0-alpha' });
bravo.resolve({ ...snapshot, currentVersion: 'v3.0.0-bravo' });
expect(await screen.findByText('v3.0.0-bravo')).toBeTruthy();
expect(screen.queryByText('v1.0.0-alpha')).toBeNull();
});
it('retains only same-owner last-good data on refresh failure and retries with fixed errors', async () => {
const user = userEvent.setup();
const load = vi
.fn<OtaDataSource['load']>()
.mockResolvedValueOnce(snapshot)
.mockRejectedValueOnce(new Error('secret URL /var/private/update.bin'))
.mockResolvedValueOnce({ ...snapshot, currentVersion: 'v2.4.2' });
const source = { load };
const { rerender } = render(
<OtaModule instance={owner} dataSource={source} refreshSignal={0} />,
);
expect(await screen.findByText('v2.4.1-build.7')).toBeTruthy();
rerender(<OtaModule instance={owner} dataSource={source} refreshSignal={1} />);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toMatch(/refresh failed; showing the last known OTA data/i);
expect(alert.textContent).not.toMatch(/secret URL|var\/private/i);
expect(screen.getByText('v2.4.1-build.7')).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'Retry loading OTA data' }));
expect(await screen.findByText('v2.4.2')).toBeTruthy();
});
it('uses a fixed initial-load error without rejection details', async () => {
render(
<OtaModule
instance={owner}
dataSource={{ load: async () => Promise.reject(new Error('private release URL')) }}
/>,
);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('OTA data could not be loaded.');
expect(alert.textContent).not.toContain('private release URL');
});
it('accepts only bounded semver-like versions and handles unknown or null payloads', async () => {
const opaque = 'release-token-0123456789abcdef';
const { rerender } = render(
<OtaModule
instance={owner}
dataSource={{ load: async () => ({ currentVersion: opaque }) }}
/>,
);
let summary = await screen.findByRole('region', { name: 'OTA safe summary' });
expect(within(summary).getAllByText('Unavailable')).toHaveLength(4);
expect(document.body.textContent).not.toContain(opaque);
rerender(
<OtaModule instance={owner} dataSource={{ load: async () => null }} refreshSignal={1} />,
);
summary = await screen.findByRole('region', { name: 'OTA safe summary' });
expect(within(summary).getAllByText('Unavailable')).toHaveLength(4);
});
});
+268
View File
@@ -0,0 +1,268 @@
import { useEffect, useRef, useState } from 'react';
import type { InstanceContext } from '../app-shell.js';
export type OtaStatus =
| 'idle'
| 'checking'
| 'up-to-date'
| 'available'
| 'downloading'
| 'verifying'
| 'installing'
| 'rebooting'
| 'completed'
| 'failed'
| 'unknown';
/**
* The complete OTA presentation contract. Unknown source properties are deliberately tolerated at
* the type boundary but are never copied into render state.
*/
export interface OtaSnapshot {
readonly currentVersion?: string | null;
readonly status?: OtaStatus | null;
readonly progressPercent?: number | null;
readonly updateAvailable?: boolean | null;
readonly [sourceField: string]: unknown;
}
export interface OtaDataSource {
/** Supplied by the authenticated owner; this module defines and calls no network endpoint. */
load(instanceId: string, signal: AbortSignal): Promise<unknown>;
}
export interface OtaModuleProps {
readonly instance: InstanceContext;
readonly dataSource?: OtaDataSource;
/** Change this owner-provided value to request another read. */
readonly refreshSignal?: unknown;
}
type SafeOtaSnapshot = {
readonly currentVersion?: string;
readonly status?: OtaStatus;
readonly progressPercent?: number;
readonly updateAvailable?: boolean;
};
type ReadState =
| { kind: 'idle'; ownerId: string }
| { kind: 'loading'; ownerId: string; snapshot?: SafeOtaSnapshot }
| { kind: 'ready'; ownerId: string; snapshot: SafeOtaSnapshot }
| { kind: 'error'; ownerId: string; snapshot?: SafeOtaSnapshot };
const SAFE_LOAD_ERROR = 'OTA data could not be loaded.';
const OTA_STATUSES = new Set<OtaStatus>([
'idle',
'checking',
'up-to-date',
'available',
'downloading',
'verifying',
'installing',
'rebooting',
'completed',
'failed',
'unknown',
]);
/** Permit only a bounded semver-like version, never an arbitrary opaque source token. */
function safeVersion(value: unknown): string | undefined {
if (typeof value !== 'string' || value.length > 80) return undefined;
return /^v?(?:0|[1-9]\d{0,5})\.(?:0|[1-9]\d{0,5})\.(?:0|[1-9]\d{0,5})(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.test(
value,
)
? value
: undefined;
}
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function sanitizeSnapshot(value: unknown): SafeOtaSnapshot {
const source = isRecord(value) ? value : {};
const currentVersion = safeVersion(source.currentVersion);
const status = OTA_STATUSES.has(source.status as OtaStatus)
? (source.status as OtaStatus)
: undefined;
const progressPercent =
typeof source.progressPercent === 'number' &&
Number.isFinite(source.progressPercent) &&
source.progressPercent >= 0 &&
source.progressPercent <= 100
? source.progressPercent
: undefined;
const updateAvailable =
typeof source.updateAvailable === 'boolean' ? source.updateAvailable : undefined;
// Constant-key primitive projection is the security boundary. Never enumerate or retain source.
return {
...(currentVersion === undefined ? {} : { currentVersion }),
...(status === undefined ? {} : { status }),
...(progressPercent === undefined ? {} : { progressPercent }),
...(updateAvailable === undefined ? {} : { updateAvailable }),
};
}
function display(value: string | undefined): string {
return value ?? 'Unavailable';
}
function SnapshotView({ snapshot }: { snapshot: SafeOtaSnapshot }) {
return (
<section className="ota-card" aria-label="OTA safe summary">
<h2>OTA safe summary</h2>
<p>Read-only OTA metadata: update operations and source-specific details are excluded.</p>
<dl>
<div>
<dt>Current version</dt>
<dd>{display(snapshot.currentVersion)}</dd>
</div>
<div>
<dt>Status</dt>
<dd>{display(snapshot.status)}</dd>
</div>
<div>
<dt>Progress</dt>
<dd>
{snapshot.progressPercent === undefined
? 'Unavailable'
: `${snapshot.progressPercent}%`}
</dd>
</div>
<div>
<dt>Update availability</dt>
<dd>
{snapshot.updateAvailable === undefined
? 'Unavailable'
: snapshot.updateAvailable
? 'Available'
: 'No update available'}
</dd>
</div>
</dl>
</section>
);
}
export function OtaModule({ instance, dataSource, refreshSignal }: OtaModuleProps) {
const requestFence = useRef(0);
const [retry, setRetry] = useState(0);
const [state, setState] = useState<ReadState>({ kind: 'idle', ownerId: instance.id });
useEffect(() => {
const request = ++requestFence.current;
const ownerId = instance.id;
const controller = new AbortController();
if (instance.authentication !== 'authenticated' || !dataSource) {
setState({ kind: 'idle', ownerId });
return () => controller.abort();
}
setState((current) => ({
kind: 'loading',
ownerId,
...(current.ownerId === ownerId &&
(current.kind === 'ready' || current.kind === 'error') &&
current.snapshot
? { snapshot: current.snapshot }
: {}),
}));
let read: Promise<unknown>;
try {
read = dataSource.load(ownerId, controller.signal);
} catch (reason: unknown) {
read = Promise.reject(reason);
}
void read.then(
(value) => {
if (request === requestFence.current && !controller.signal.aborted) {
setState({ kind: 'ready', ownerId, snapshot: sanitizeSnapshot(value) });
}
},
(_reason: unknown) => {
void _reason;
if (request === requestFence.current && !controller.signal.aborted) {
setState((current) => ({
kind: 'error',
ownerId,
...(current.ownerId === ownerId && current.kind === 'loading' && current.snapshot
? { snapshot: current.snapshot }
: {}),
}));
}
},
);
return () => controller.abort();
}, [dataSource, instance.authentication, instance.id, refreshSignal, retry]);
if (instance.authentication !== 'authenticated') {
return (
<div className="state-panel state-error" role="alert">
Authentication is required before OTA data can be read for this instance.
</div>
);
}
if (!dataSource) {
return (
<div className="state-panel" role="status" aria-label="OTA unavailable">
No safe OTA read data source is available. This console will not invent or call an
uncontracted production endpoint.
</div>
);
}
const retained =
state.ownerId === instance.id && (state.kind === 'loading' || state.kind === 'error')
? state.snapshot
: undefined;
if ((state.kind === 'idle' || state.kind === 'loading') && !retained) {
return (
<p role="status" aria-label="OTA loading status">
Loading OTA data
</p>
);
}
if (state.kind === 'error' && !retained) {
return (
<div className="state-panel state-error" role="alert">
<p>Unable to load OTA data: {SAFE_LOAD_ERROR}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading OTA data
</button>
</div>
);
}
const snapshot =
state.kind === 'ready' && state.ownerId === instance.id ? state.snapshot : retained;
if (!snapshot) return null;
return (
<div className="ota-module">
{state.kind === 'loading' ? <p role="status">Refreshing OTA data</p> : null}
{state.kind === 'error' ? (
<div className="state-panel state-error" role="alert">
<p>Refresh failed; showing the last known OTA data.</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading OTA data
</button>
</div>
) : null}
{instance.freshness !== 'fresh' ? (
<p className="state-panel" role="status" aria-label="OTA freshness">
OTA data is {instance.freshness}; verify freshness before relying on these values.
</p>
) : null}
<SnapshotView snapshot={snapshot} />
</div>
);
}